mirror of https://github.com/mpolden/echoip
Merge pull request #8 from levelsoftware/enable-use-of-environment-variables-for-docker
Enable use of environment variables for docker
This commit is contained in:
commit
712e2166d5
|
@ -1,2 +1,3 @@
|
|||
Dockerfile
|
||||
Dockerfile.geoip
|
||||
/etc/systemd
|
||||
Dockerfile.geoip
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
.vscode/
|
||||
.idea/
|
||||
/bin/
|
||||
.envrc
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Build
|
||||
FROM golang:1.15-buster AS build
|
||||
WORKDIR /go/src/github.com/mpolden/echoip
|
||||
FROM golang:1.18-buster AS build
|
||||
WORKDIR /go/src/github.com/levelsoftware/echoip
|
||||
COPY . .
|
||||
|
||||
# Must build without cgo because libc is unavailable in runtime image
|
||||
|
@ -8,7 +8,7 @@ ENV GO111MODULE=on CGO_ENABLED=0
|
|||
RUN make
|
||||
|
||||
# Run
|
||||
FROM scratch
|
||||
FROM alpine
|
||||
EXPOSE 8080
|
||||
|
||||
COPY --from=build /go/bin/echoip /opt/echoip/
|
||||
|
|
8
Makefile
8
Makefile
|
@ -1,5 +1,5 @@
|
|||
DOCKER ?= docker
|
||||
DOCKER_IMAGE ?= mpolden/echoip
|
||||
DOCKER_IMAGE ?= levelsoftware/echoip
|
||||
OS := $(shell uname)
|
||||
ifeq ($(OS),Linux)
|
||||
TAR_OPTS := --wildcards
|
||||
|
@ -22,11 +22,11 @@ check-fmt:
|
|||
|
||||
lint: check-fmt vet
|
||||
|
||||
install: install-config
|
||||
install:
|
||||
go install ./...
|
||||
|
||||
install-config:
|
||||
sudo install -D etc/echoip/config.toml /etc/echoip/config.toml
|
||||
install -D etc/echoip/config.toml /etc/echoip/config.toml
|
||||
|
||||
databases := GeoLite2-City GeoLite2-Country GeoLite2-ASN
|
||||
|
||||
|
@ -51,7 +51,7 @@ docker-multiarch-builder:
|
|||
$(DOCKER) run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
docker-build:
|
||||
$(DOCKER) build -t $(DOCKER_IMAGE) .
|
||||
$(DOCKER) build -t $(DOCKER_IMAGE) .
|
||||
|
||||
docker-login:
|
||||
@echo "$(DOCKER_PASSWORD)" | $(DOCKER) login -u "$(DOCKER_USERNAME)" --password-stdin
|
||||
|
|
22
README.md
22
README.md
|
@ -132,6 +132,28 @@ CityFile = ""
|
|||
AsnFile = ""
|
||||
```
|
||||
|
||||
### Environment Variables for Configuration
|
||||
You can also use environment variables for configuration, most likely used for Docker.
|
||||
|
||||
```
|
||||
ECHOIP_LISTEN=":8080"
|
||||
ECHOIP_TEMPLATE_DIR="html/"
|
||||
ECHOIP_REDIS_URL="redis://localhost:6379"
|
||||
ECHOIP_DATABASE="ipstack"
|
||||
ECHOIP_TRUSTED_HEADERS="X-Real-IP,X-Forwaded-For"
|
||||
ECHOIP_IPSTACK_API_KEY="askdfj39sjdkf29dsjfk39sdfkj3"
|
||||
ECHOIP_GEOIP_COUNTRY_FILE="/full/path/to/file.db"
|
||||
ECHOIP_GEOIP_CITY_FILE="/full/path/to/file.db"
|
||||
ECHOIP_GEOIP_ASN_FILE="/full/path/to/file.db"
|
||||
ECHOIP_CACHE_TTL=3600
|
||||
ECHOIP_REVERSE_LOOKUP=true
|
||||
ECHOIP_PORT_LOOKUP=true
|
||||
ECHOIP_SHOW_SPONSOR=true
|
||||
ECHOIP_PROFILE=false
|
||||
ECHOIP_IPSTACK_USE_HTTPS=true
|
||||
ECHOIP_IPSTACK_ENABLE_SECURITY=true
|
||||
```
|
||||
|
||||
### Caching with Redis
|
||||
|
||||
You can connect EchoIP to a Redis client to cache each request per IP. You can configure the life of the key in `config.CacheTtl`.
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/levelsoftware/echoip/cache"
|
||||
"github.com/levelsoftware/echoip/config"
|
||||
"github.com/levelsoftware/echoip/http"
|
||||
"github.com/levelsoftware/echoip/iputil"
|
||||
"github.com/levelsoftware/echoip/iputil/geo"
|
||||
|
@ -33,61 +34,38 @@ func init() {
|
|||
log.SetFlags(log.Lshortfile)
|
||||
}
|
||||
|
||||
type IPStack struct {
|
||||
ApiKey string
|
||||
UseHttps bool
|
||||
EnableSecurity bool
|
||||
}
|
||||
|
||||
type GeoIP struct {
|
||||
CountryFile string
|
||||
CityFile string
|
||||
AsnFile string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Listen string
|
||||
TemplateDir string
|
||||
RedisUrl string
|
||||
CacheTtl int
|
||||
ReverseLookup bool
|
||||
PortLookup bool
|
||||
ShowSponsor bool
|
||||
TrustedHeaders []string
|
||||
|
||||
Database string
|
||||
Profile bool
|
||||
|
||||
IPStack IPStack
|
||||
GeoIP GeoIP
|
||||
}
|
||||
|
||||
func main() {
|
||||
file, err := os.Open("/etc/echoip/config.toml")
|
||||
runConfig, err := config.GetConfig()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("Error building configuration: %s", err)
|
||||
}
|
||||
|
||||
file, err := os.Open("/etc/echoip/config.toml")
|
||||
defer file.Close()
|
||||
|
||||
var config Config
|
||||
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Error opening config file (/etc/echoip/config.toml): %s", err)
|
||||
} else {
|
||||
var b []byte
|
||||
b, err = io.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Printf("Error reading config file (/etc/echoip/config.toml): %s", err)
|
||||
}
|
||||
|
||||
err = toml.Unmarshal(b, &config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
err = toml.Unmarshal(b, &runConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing config file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var parser parser.Parser
|
||||
if config.Database == "geoip" {
|
||||
if runConfig.Database == "geoip" {
|
||||
log.Print("Using GeoIP for IP database")
|
||||
geo, err := geo.Open(
|
||||
config.GeoIP.CountryFile,
|
||||
config.GeoIP.CityFile,
|
||||
config.GeoIP.AsnFile,
|
||||
runConfig.GeoIP.CountryFile,
|
||||
runConfig.GeoIP.CityFile,
|
||||
runConfig.GeoIP.AsnFile,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -95,27 +73,27 @@ func main() {
|
|||
parser = &geo
|
||||
}
|
||||
|
||||
if config.Database == "ipstack" {
|
||||
log.Print("Using GeoIP for IP database")
|
||||
if config.IPStack.EnableSecurity {
|
||||
log.Print("Enable Security Module ( Requires Professional Plus account )")
|
||||
if runConfig.Database == "ipstack" {
|
||||
log.Print("Using IP Stack for IP database")
|
||||
if runConfig.IPStack.EnableSecurity {
|
||||
log.Print("Enable IP Stack Security Module ( Requires Professional Plus account )")
|
||||
}
|
||||
enableSecurity := ipstackApi.ParamEnableSecurity(config.IPStack.EnableSecurity)
|
||||
apiKey := ipstackApi.ParamToken(config.IPStack.ApiKey)
|
||||
useHttps := ipstackApi.ParamUseHTTPS(config.IPStack.UseHttps)
|
||||
if config.IPStack.UseHttps {
|
||||
enableSecurity := ipstackApi.ParamEnableSecurity(runConfig.IPStack.EnableSecurity)
|
||||
apiKey := ipstackApi.ParamToken(runConfig.IPStack.ApiKey)
|
||||
useHttps := ipstackApi.ParamUseHTTPS(runConfig.IPStack.UseHttps)
|
||||
if runConfig.IPStack.UseHttps {
|
||||
log.Print("Use IP Stack HTTPS API ( Requires non-free account )")
|
||||
}
|
||||
if err := ipstackApi.Init(apiKey, enableSecurity, useHttps); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Error initializing IP Stack client: %s", err)
|
||||
}
|
||||
ips := ipstack.IPStack{}
|
||||
parser = &ips
|
||||
}
|
||||
|
||||
var serverCache cache.Cache
|
||||
if len(config.RedisUrl) > 0 {
|
||||
redisCache, err := cache.RedisCache(config.RedisUrl)
|
||||
if len(runConfig.RedisUrl) > 0 {
|
||||
redisCache, err := cache.RedisCache(runConfig.RedisUrl)
|
||||
serverCache = &redisCache
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -124,34 +102,35 @@ func main() {
|
|||
serverCache = &cache.Null{}
|
||||
}
|
||||
|
||||
server := http.New(parser, serverCache, config.CacheTtl, config.Profile)
|
||||
server.IPHeaders = config.TrustedHeaders
|
||||
server := http.New(parser, serverCache, runConfig.CacheTtl, runConfig.Profile)
|
||||
server.IPHeaders = runConfig.TrustedHeaders
|
||||
|
||||
if _, err := os.Stat(config.TemplateDir); err == nil {
|
||||
server.Template = config.TemplateDir
|
||||
if _, err := os.Stat(runConfig.TemplateDir); err == nil {
|
||||
server.Template = runConfig.TemplateDir
|
||||
} else {
|
||||
log.Printf("Not configuring default handler: Template not found: %s", config.TemplateDir)
|
||||
log.Printf("Not configuring default handler: Template not found: %s", runConfig.TemplateDir)
|
||||
}
|
||||
if config.ReverseLookup {
|
||||
if runConfig.ReverseLookup {
|
||||
log.Println("Enabling reverse lookup")
|
||||
server.LookupAddr = iputil.LookupAddr
|
||||
}
|
||||
if config.PortLookup {
|
||||
if runConfig.PortLookup {
|
||||
log.Println("Enabling port lookup")
|
||||
server.LookupPort = iputil.LookupPort
|
||||
}
|
||||
if config.ShowSponsor {
|
||||
if runConfig.ShowSponsor {
|
||||
log.Println("Enabling sponsor logo")
|
||||
server.Sponsor = config.ShowSponsor
|
||||
server.Sponsor = runConfig.ShowSponsor
|
||||
}
|
||||
if len(config.TrustedHeaders) > 0 {
|
||||
log.Printf("Trusting remote IP from header(s): %s", config.TrustedHeaders)
|
||||
if len(runConfig.TrustedHeaders) > 0 {
|
||||
log.Printf("Trusting remote IP from header(s): %s", runConfig.TrustedHeaders)
|
||||
}
|
||||
if config.Profile {
|
||||
if runConfig.Profile {
|
||||
log.Printf("Enabling profiling handlers")
|
||||
}
|
||||
log.Printf("Listening on http://%s", config.Listen)
|
||||
if err := server.ListenAndServe(config.Listen); err != nil {
|
||||
|
||||
log.Printf("Listening on http://%s", runConfig.Listen)
|
||||
if err := server.ListenAndServe(runConfig.Listen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IPStack struct {
|
||||
ApiKey string
|
||||
UseHttps bool
|
||||
EnableSecurity bool
|
||||
}
|
||||
|
||||
type GeoIP struct {
|
||||
CountryFile string
|
||||
CityFile string
|
||||
AsnFile string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Listen string
|
||||
TemplateDir string
|
||||
RedisUrl string
|
||||
CacheTtl int
|
||||
ReverseLookup bool
|
||||
PortLookup bool
|
||||
ShowSponsor bool
|
||||
TrustedHeaders []string
|
||||
|
||||
Database string
|
||||
Profile bool
|
||||
|
||||
IPStack IPStack
|
||||
GeoIP GeoIP
|
||||
}
|
||||
|
||||
func GetConfig() (Config, error) {
|
||||
defaultConfig := Config{
|
||||
Listen: getenv_string("ECHOIP_LISTEN", ":8080"),
|
||||
TemplateDir: getenv_string("ECHOIP_TEMPLATE_DIR", "html/"),
|
||||
RedisUrl: getenv_string("ECHOIP_REDIS_URL", ""),
|
||||
Database: getenv_string("ECHOIP_DATABASE", "geoip"),
|
||||
IPStack: IPStack{
|
||||
ApiKey: getenv_string("ECHOIP_IPSTACK_API_KEY", ""),
|
||||
},
|
||||
GeoIP: GeoIP{
|
||||
CountryFile: getenv_string("ECHOIP_GEOIP_COUNTRY_FILE", ""),
|
||||
CityFile: getenv_string("ECHOIP_GEOIP_CITY_FILE", ""),
|
||||
AsnFile: getenv_string("ECHOIP_GEOIP_ASN_FILE", ""),
|
||||
},
|
||||
}
|
||||
|
||||
cacheTtl, err := getenv_int("ECHOIP_CACHE_TTL", 3600)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.CacheTtl = cacheTtl
|
||||
|
||||
reverseLookup, err := getenv_bool("ECHOIP_REVERSE_LOOKUP", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.ReverseLookup = reverseLookup
|
||||
|
||||
portLookup, err := getenv_bool("ECHOIP_PORT_LOOKUP", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.PortLookup = portLookup
|
||||
|
||||
showSponsor, err := getenv_bool("ECHOIP_SHOW_SPONSOR", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.ShowSponsor = showSponsor
|
||||
|
||||
profile, err := getenv_bool("ECHOIP_PROFILE", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.Profile = profile
|
||||
|
||||
ipStackUseHttps, err := getenv_bool("ECHOIP_IPSTACK_USE_HTTPS", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.IPStack.UseHttps = ipStackUseHttps
|
||||
|
||||
ipStackEnableSecurity, err := getenv_bool("ECHOIP_IPSTACK_ENABLE_SECURITY", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.IPStack.EnableSecurity = ipStackEnableSecurity
|
||||
|
||||
trustedHeaders := getenv_string("ECHOIP_TRUSTED_HEADERS", "")
|
||||
defaultConfig.TrustedHeaders = strings.Split(trustedHeaders, ",")
|
||||
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
func getenv_int(key string, fallback int) (int, error) {
|
||||
value := os.Getenv(key)
|
||||
|
||||
if len(value) > 0 {
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return intValue, nil
|
||||
}
|
||||
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
func getenv_string(key string, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if len(value) == 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func getenv_bool(key string, fallback bool) (bool, error) {
|
||||
value := os.Getenv(key)
|
||||
|
||||
if len(value) > 0 {
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return boolValue, nil
|
||||
}
|
||||
|
||||
return fallback, nil
|
||||
}
|
|
@ -1,6 +1,14 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
echoip:
|
||||
image: hub.01a.in/echoip:latest
|
||||
environment:
|
||||
- ECHOIP_DATABASE="ipstack"
|
||||
- ECHOIP_IPSTACK_API_KEY="ipstack"
|
||||
ports:
|
||||
- '8080:8080'
|
||||
|
||||
cache:
|
||||
image: redis:6.2-alpine
|
||||
restart: always
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Listen = ":8080"
|
||||
TemplateDir = "html/index.html"
|
||||
TemplateDir = "html"
|
||||
RedisUrl = "redis://localhost:6379"
|
||||
CacheTtl = 3600 # in seconds
|
||||
ReverseLookup = true
|
||||
|
|
5
go.mod
5
go.mod
|
@ -18,6 +18,7 @@ require (
|
|||
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
)
|
||||
|
|
11
go.sum
11
go.sum
|
@ -128,15 +128,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -160,8 +162,9 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -174,8 +177,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
|
|
Loading…
Reference in New Issue