Merge branch 'master' into restrict-dest
This commit is contained in:
commit
4444e7821c
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
12
.github/workflows/master.yml
vendored
12
.github/workflows/master.yml
vendored
@ -31,13 +31,17 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Print builder available platforms
|
name: Print builder available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
- name: Dockerhub login
|
|
||||||
run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
-
|
-
|
||||||
name: Run Buildx
|
name: Dockerhub login
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Run Buildx for amd64, armv7, arm64 architectures
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \
|
--platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||||
--output "type=image,push=false" \
|
--output "type=image,push=false" \
|
||||||
--push \
|
--push \
|
||||||
--tag "$DOCKERHUB_REPOSITORY:latest" \
|
--tag "$DOCKERHUB_REPOSITORY:latest" \
|
||||||
|
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@ -18,17 +18,20 @@ jobs:
|
|||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1.1.0
|
uses: crazy-max/ghaction-docker-buildx@v1.1.0
|
||||||
- name: Release version
|
-
|
||||||
|
name: Release version
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV
|
||||||
- name: Print release version
|
-
|
||||||
|
name: Print release version
|
||||||
run: echo ${{ env.RELEASE_VERSION }}
|
run: echo ${{ env.RELEASE_VERSION }}
|
||||||
- name: Dockerhub login
|
-
|
||||||
|
name: Dockerhub login
|
||||||
run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
|
run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
-
|
-
|
||||||
name: Run Buildx
|
name: Run Buildx for amd64, armv7, arm64 architectures
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \
|
--platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||||
--output "type=image,push=false" \
|
--output "type=image,push=false" \
|
||||||
--push \
|
--push \
|
||||||
--tag "$DOCKERHUB_REPOSITORY:$RELEASE_VERSION" \
|
--tag "$DOCKERHUB_REPOSITORY:$RELEASE_VERSION" \
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ARG GOLANG_VERSION="1.16.5"
|
ARG GOLANG_VERSION="1.19.1"
|
||||||
|
|
||||||
FROM golang:$GOLANG_VERSION-alpine as builder
|
FROM golang:$GOLANG_VERSION-alpine as builder
|
||||||
RUN apk --no-cache add tzdata
|
RUN apk --no-cache add tzdata
|
||||||
@ -6,8 +6,6 @@ WORKDIR /go/src/github.com/serjs/socks5
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5
|
||||||
|
|
||||||
FROM scratch
|
FROM gcr.io/distroless/static:nonroot
|
||||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=builder /go/src/github.com/serjs/socks5/socks5 /
|
COPY --from=builder /go/src/github.com/serjs/socks5/socks5 /
|
||||||
ENTRYPOINT ["/socks5"]
|
ENTRYPOINT ["/socks5"]
|
||||||
|
12
Dockerfile.armv6
Normal file
12
Dockerfile.armv6
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
ARG GOLANG_VERSION="1.19.1"
|
||||||
|
|
||||||
|
FROM golang:$GOLANG_VERSION-alpine as builder
|
||||||
|
RUN apk --no-cache add tzdata
|
||||||
|
WORKDIR /go/src/github.com/serjs/socks5
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s' -o ./socks5
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /go/src/github.com/serjs/socks5/socks5 /
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
ENTRYPOINT ["/socks5"]
|
@ -26,6 +26,12 @@ Leave `PROXY_USER` and `PROXY_PASSWORD` empty for skip authentication options wh
|
|||||||
|PROXY_PORT|String|1080|Set listen port for application inside docker container|
|
|PROXY_PORT|String|1080|Set listen port for application inside docker container|
|
||||||
|ALLOWED_DEST_FQDN|String|EMPTY|Allowed destination address regular expression pattern. Default allows all.|
|
|ALLOWED_DEST_FQDN|String|EMPTY|Allowed destination address regular expression pattern. Default allows all.|
|
||||||
|TZ|String|UTC|Set Timezone like in many common Operation Systems|
|
|TZ|String|UTC|Set Timezone like in many common Operation Systems|
|
||||||
|
|ALLOWED_IPS|String|Empty|Set allowed IP's that can connect to proxy, separator `,`|
|
||||||
|
|
||||||
|
`ALLOWED_IPS` parameter is not included in `serjs/go-socks5-proxy` image.\
|
||||||
|
You can build your image with:
|
||||||
|
`docker-compose -f docker-compose.build.yml up -d`\
|
||||||
|
Just don't forget to set parameters in the `.env` file.
|
||||||
|
|
||||||
## Test running service
|
## Test running service
|
||||||
|
|
||||||
|
11
docker-compose.build.yml
Normal file
11
docker-compose.build.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
socks5-build:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: socks5-build
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "1080:1080"
|
||||||
|
restart: unless-stopped
|
7
go.mod
7
go.mod
@ -1,9 +1,10 @@
|
|||||||
module github.com/serjs/socks5-server
|
module github.com/serjs/socks5-server
|
||||||
|
|
||||||
go 1.16
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible
|
github.com/caarlos0/env/v6 v6.10.1
|
||||||
golang.org/x/net v0.0.0-20180406214816-61147c48b25b // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/net v0.1.0 // indirect
|
||||||
|
8
go.sum
8
go.sum
@ -1,6 +1,6 @@
|
|||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible h1:FjzBjKb2W4ggJbyACel2jbTWSGqCpmu9YqXwcpUp+3M=
|
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
|
||||||
github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
|
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
||||||
golang.org/x/net v0.0.0-20180406214816-61147c48b25b h1:7rskAFQwNXGW6AD8E/6y0LDHW5mT9rsLD7ViLVFfh5w=
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
13
server.go
13
server.go
@ -2,10 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
"github.com/caarlos0/env"
|
"github.com/caarlos0/env/v6"
|
||||||
)
|
)
|
||||||
|
|
||||||
type params struct {
|
type params struct {
|
||||||
@ -13,6 +14,7 @@ type params struct {
|
|||||||
Password string `env:"PROXY_PASSWORD" envDefault:""`
|
Password string `env:"PROXY_PASSWORD" envDefault:""`
|
||||||
Port string `env:"PROXY_PORT" envDefault:"1080"`
|
Port string `env:"PROXY_PORT" envDefault:"1080"`
|
||||||
AllowedDestFqdn string `env:"ALLOWED_DEST_FQDN" envDefault:""`
|
AllowedDestFqdn string `env:"ALLOWED_DEST_FQDN" envDefault:""`
|
||||||
|
AllowedIPs []string `env:"ALLOWED_IPS" envSeparator:"," envDefault:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -45,6 +47,15 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set IP whitelist
|
||||||
|
if len(cfg.AllowedIPs) > 0 {
|
||||||
|
whitelist := make([]net.IP, len(cfg.AllowedIPs))
|
||||||
|
for i, ip := range cfg.AllowedIPs {
|
||||||
|
whitelist[i] = net.ParseIP(ip)
|
||||||
|
}
|
||||||
|
server.SetIPWhitelist(whitelist)
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Start listening proxy service on port %s\n", cfg.Port)
|
log.Printf("Start listening proxy service on port %s\n", cfg.Port)
|
||||||
if err := server.ListenAndServe("tcp", ":"+cfg.Port); err != nil {
|
if err := server.ListenAndServe("tcp", ":"+cfg.Port); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
32
vendor/github.com/armon/go-socks5/socks5.go
generated
vendored
32
vendor/github.com/armon/go-socks5/socks5.go
generated
vendored
@ -55,6 +55,7 @@ type Config struct {
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
config *Config
|
config *Config
|
||||||
authMethods map[uint8]Authenticator
|
authMethods map[uint8]Authenticator
|
||||||
|
isIPAllowed func(net.IP) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Server and potentially returns an error
|
// New creates a new Server and potentially returns an error
|
||||||
@ -93,6 +94,11 @@ func New(conf *Config) (*Server, error) {
|
|||||||
server.authMethods[a.GetCode()] = a
|
server.authMethods[a.GetCode()] = a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default IP whitelist function
|
||||||
|
server.isIPAllowed = func(ip net.IP) bool {
|
||||||
|
return true // default allow all IPs
|
||||||
|
}
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,11 +123,37 @@ func (s *Server) Serve(l net.Listener) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIPWhitelist sets the function to check if a given IP is allowed
|
||||||
|
func (s *Server) SetIPWhitelist(allowedIPs []net.IP) {
|
||||||
|
s.isIPAllowed = func(ip net.IP) bool {
|
||||||
|
for _, allowedIP := range allowedIPs {
|
||||||
|
if ip.Equal(allowedIP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ServeConn is used to serve a single connection.
|
// ServeConn is used to serve a single connection.
|
||||||
func (s *Server) ServeConn(conn net.Conn) error {
|
func (s *Server) ServeConn(conn net.Conn) error {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
bufConn := bufio.NewReader(conn)
|
bufConn := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
// Check client IP against whitelist
|
||||||
|
clientIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
s.config.Logger.Printf("[ERR] socks: Failed to get client IP address: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(clientIP)
|
||||||
|
if s.isIPAllowed(ip) {
|
||||||
|
s.config.Logger.Printf("[INFO] socks: Connection from allowed IP address: %s", clientIP)
|
||||||
|
} else {
|
||||||
|
s.config.Logger.Printf("[WARN] socks: Connection from not allowed IP address: %s", clientIP)
|
||||||
|
return fmt.Errorf("connection from not allowed IP address")
|
||||||
|
}
|
||||||
|
|
||||||
// Read the version byte
|
// Read the version byte
|
||||||
version := []byte{0}
|
version := []byte{0}
|
||||||
if _, err := bufConn.Read(version); err != nil {
|
if _, err := bufConn.Read(version); err != nil {
|
||||||
|
1
vendor/github.com/caarlos0/env/.gitignore
generated
vendored
1
vendor/github.com/caarlos0/env/.gitignore
generated
vendored
@ -1 +0,0 @@
|
|||||||
coverage.out
|
|
2
vendor/github.com/caarlos0/env/.hound.yml
generated
vendored
2
vendor/github.com/caarlos0/env/.hound.yml
generated
vendored
@ -1,2 +0,0 @@
|
|||||||
go:
|
|
||||||
enabled: true
|
|
16
vendor/github.com/caarlos0/env/.travis.yml
generated
vendored
16
vendor/github.com/caarlos0/env/.travis.yml
generated
vendored
@ -1,16 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.5
|
|
||||||
- 1.6
|
|
||||||
- 1.7
|
|
||||||
- 1.8
|
|
||||||
- tip
|
|
||||||
before_install:
|
|
||||||
- go get github.com/axw/gocov/gocov
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
script:
|
|
||||||
- go test -v -cover -race -coverprofile=coverage.out
|
|
||||||
after_script:
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
- goveralls -coverprofile=coverage.out -service=travis-ci -repotoken='eCcizKmTdSaJCz8Ih33WDppdqb9kioYwi'
|
|
115
vendor/github.com/caarlos0/env/README.md
generated
vendored
115
vendor/github.com/caarlos0/env/README.md
generated
vendored
@ -1,115 +0,0 @@
|
|||||||
# env [![Build Status](https://travis-ci.org/caarlos0/env.svg?branch=master)](https://travis-ci.org/caarlos0/env) [![Coverage Status](https://coveralls.io/repos/caarlos0/env/badge.svg?branch=master&service=github)](https://coveralls.io/github/caarlos0/env?branch=master) [![](https://godoc.org/github.com/caarlos0/env?status.svg)](http://godoc.org/github.com/caarlos0/env) [![](http://goreportcard.com/badge/caarlos0/env)](http://goreportcard.com/report/caarlos0/env) [![SayThanks.io](https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg?style=flat-square)](https://saythanks.io/to/caarlos0)
|
|
||||||
|
|
||||||
A KISS way to deal with environment variables in Go.
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
At first, it was boring for me to write down an entire function just to
|
|
||||||
get some `var` from the environment and default to another in case it's missing.
|
|
||||||
|
|
||||||
For that manner, I wrote a `GetOr` function in the
|
|
||||||
[go-idioms](https://github.com/caarlos0/go-idioms) project.
|
|
||||||
|
|
||||||
Then, I got pissed about writing `os.Getenv`, `os.Setenv`, `os.Unsetenv`...
|
|
||||||
it kind of make more sense to me write it as `env.Get`, `env.Set`, `env.Unset`.
|
|
||||||
So I did.
|
|
||||||
|
|
||||||
Then I got a better idea: to use `struct` tags to do all that work for me.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
A very basic example (check the `examples` folder):
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caarlos0/env"
|
|
||||||
)
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Home string `env:"HOME"`
|
|
||||||
Port int `env:"PORT" envDefault:"3000"`
|
|
||||||
IsProduction bool `env:"PRODUCTION"`
|
|
||||||
Hosts []string `env:"HOSTS" envSeparator:":"`
|
|
||||||
Duration time.Duration `env:"DURATION"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cfg := config{}
|
|
||||||
err := env.Parse(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%+v\n", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("%+v\n", cfg)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can run it like this:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run examples/first.go
|
|
||||||
{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported types and defaults
|
|
||||||
|
|
||||||
The library has built-in support for the following types:
|
|
||||||
|
|
||||||
* `string`
|
|
||||||
* `int`
|
|
||||||
* `uint`
|
|
||||||
* `int64`
|
|
||||||
* `bool`
|
|
||||||
* `float32`
|
|
||||||
* `float64`
|
|
||||||
* `time.Duration`
|
|
||||||
* `[]string`
|
|
||||||
* `[]int`
|
|
||||||
* `[]bool`
|
|
||||||
* `[]float32`
|
|
||||||
* `[]float64`
|
|
||||||
* `[]time.Duration`
|
|
||||||
* .. or use/define a [custom parser func](#custom-parser-funcs) for any other type
|
|
||||||
|
|
||||||
If you set the `envDefault` tag for something, this value will be used in the
|
|
||||||
case of absence of it in the environment. If you don't do that AND the
|
|
||||||
environment variable is also not set, the zero-value
|
|
||||||
of the type will be used: empty for `string`s, `false` for `bool`s
|
|
||||||
and `0` for `int`s.
|
|
||||||
|
|
||||||
By default, slice types will split the environment value on `,`; you can change this behavior by setting the `envSeparator` tag.
|
|
||||||
|
|
||||||
## Custom Parser Funcs
|
|
||||||
|
|
||||||
If you have a type that is not supported out of the box by the lib, you are able
|
|
||||||
to use (or define) and pass custom parsers (and their associated `reflect.Type`) to the
|
|
||||||
`env.ParseWithFuncs()` function.
|
|
||||||
|
|
||||||
In addition to accepting a struct pointer (same as `Parse()`), this function also
|
|
||||||
accepts a `env.CustomParsers` arg that under the covers is a `map[reflect.Type]env.ParserFunc`.
|
|
||||||
|
|
||||||
To see what this looks like in practice, take a look at the [commented block in the example](https://github.com/caarlos0/env/blob/master/examples/first.go#L35-L39).
|
|
||||||
|
|
||||||
`env` also ships with some pre-built custom parser funcs for common types. You
|
|
||||||
can check them out [here](parsers/).
|
|
||||||
|
|
||||||
## Required fields
|
|
||||||
|
|
||||||
The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added
|
|
||||||
to ensure that some environment variable is set. In the example above,
|
|
||||||
an error is returned if the `config` struct is changed to:
|
|
||||||
|
|
||||||
|
|
||||||
```go
|
|
||||||
type config struct {
|
|
||||||
Home string `env:"HOME"`
|
|
||||||
Port int `env:"PORT" envDefault:"3000"`
|
|
||||||
IsProduction bool `env:"PRODUCTION"`
|
|
||||||
Hosts []string `env:"HOSTS" envSeparator:":"`
|
|
||||||
SecretKey string `env:"SECRET_KEY,required"`
|
|
||||||
}
|
|
||||||
```
|
|
398
vendor/github.com/caarlos0/env/env.go
generated
vendored
398
vendor/github.com/caarlos0/env/env.go
generated
vendored
@ -1,398 +0,0 @@
|
|||||||
package env
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNotAStructPtr is returned if you pass something that is not a pointer to a
|
|
||||||
// Struct to Parse
|
|
||||||
ErrNotAStructPtr = errors.New("Expected a pointer to a Struct")
|
|
||||||
// ErrUnsupportedType if the struct field type is not supported by env
|
|
||||||
ErrUnsupportedType = errors.New("Type is not supported")
|
|
||||||
// ErrUnsupportedSliceType if the slice element type is not supported by env
|
|
||||||
ErrUnsupportedSliceType = errors.New("Unsupported slice type")
|
|
||||||
// OnEnvVarSet is an optional convenience callback, such as for logging purposes.
|
|
||||||
// If not nil, it's called after successfully setting the given field from the given value.
|
|
||||||
OnEnvVarSet func(reflect.StructField, string)
|
|
||||||
// Friendly names for reflect types
|
|
||||||
sliceOfInts = reflect.TypeOf([]int(nil))
|
|
||||||
sliceOfInt64s = reflect.TypeOf([]int64(nil))
|
|
||||||
sliceOfUint64s = reflect.TypeOf([]uint64(nil))
|
|
||||||
sliceOfStrings = reflect.TypeOf([]string(nil))
|
|
||||||
sliceOfBools = reflect.TypeOf([]bool(nil))
|
|
||||||
sliceOfFloat32s = reflect.TypeOf([]float32(nil))
|
|
||||||
sliceOfFloat64s = reflect.TypeOf([]float64(nil))
|
|
||||||
sliceOfDurations = reflect.TypeOf([]time.Duration(nil))
|
|
||||||
)
|
|
||||||
|
|
||||||
// CustomParsers is a friendly name for the type that `ParseWithFuncs()` accepts
|
|
||||||
type CustomParsers map[reflect.Type]ParserFunc
|
|
||||||
|
|
||||||
// ParserFunc defines the signature of a function that can be used within `CustomParsers`
|
|
||||||
type ParserFunc func(v string) (interface{}, error)
|
|
||||||
|
|
||||||
// Parse parses a struct containing `env` tags and loads its values from
|
|
||||||
// environment variables.
|
|
||||||
func Parse(v interface{}) error {
|
|
||||||
ptrRef := reflect.ValueOf(v)
|
|
||||||
if ptrRef.Kind() != reflect.Ptr {
|
|
||||||
return ErrNotAStructPtr
|
|
||||||
}
|
|
||||||
ref := ptrRef.Elem()
|
|
||||||
if ref.Kind() != reflect.Struct {
|
|
||||||
return ErrNotAStructPtr
|
|
||||||
}
|
|
||||||
return doParse(ref, make(map[reflect.Type]ParserFunc, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseWithFuncs is the same as `Parse` except it also allows the user to pass
|
|
||||||
// in custom parsers.
|
|
||||||
func ParseWithFuncs(v interface{}, funcMap CustomParsers) error {
|
|
||||||
ptrRef := reflect.ValueOf(v)
|
|
||||||
if ptrRef.Kind() != reflect.Ptr {
|
|
||||||
return ErrNotAStructPtr
|
|
||||||
}
|
|
||||||
ref := ptrRef.Elem()
|
|
||||||
if ref.Kind() != reflect.Struct {
|
|
||||||
return ErrNotAStructPtr
|
|
||||||
}
|
|
||||||
return doParse(ref, funcMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doParse(ref reflect.Value, funcMap CustomParsers) error {
|
|
||||||
refType := ref.Type()
|
|
||||||
var errorList []string
|
|
||||||
|
|
||||||
for i := 0; i < refType.NumField(); i++ {
|
|
||||||
refField := ref.Field(i)
|
|
||||||
if reflect.Ptr == refField.Kind() && !refField.IsNil() && refField.CanSet() {
|
|
||||||
err := Parse(refField.Interface())
|
|
||||||
if nil != err {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
refTypeField := refType.Field(i)
|
|
||||||
value, err := get(refTypeField)
|
|
||||||
if err != nil {
|
|
||||||
errorList = append(errorList, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := set(refField, refTypeField, value, funcMap); err != nil {
|
|
||||||
errorList = append(errorList, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if OnEnvVarSet != nil {
|
|
||||||
OnEnvVarSet(refTypeField, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errorList) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New(strings.Join(errorList, ". "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func get(field reflect.StructField) (string, error) {
|
|
||||||
var (
|
|
||||||
val string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
key, opts := parseKeyForOption(field.Tag.Get("env"))
|
|
||||||
|
|
||||||
defaultValue := field.Tag.Get("envDefault")
|
|
||||||
val = getOr(key, defaultValue)
|
|
||||||
|
|
||||||
if len(opts) > 0 {
|
|
||||||
for _, opt := range opts {
|
|
||||||
// The only option supported is "required".
|
|
||||||
switch opt {
|
|
||||||
case "":
|
|
||||||
break
|
|
||||||
case "required":
|
|
||||||
val, err = getRequired(key)
|
|
||||||
default:
|
|
||||||
err = errors.New("Env tag option " + opt + " not supported.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return val, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// split the env tag's key into the expected key and desired option, if any.
|
|
||||||
func parseKeyForOption(key string) (string, []string) {
|
|
||||||
opts := strings.Split(key, ",")
|
|
||||||
return opts[0], opts[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRequired(key string) (string, error) {
|
|
||||||
if value, ok := os.LookupEnv(key); ok {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
// We do not use fmt.Errorf to avoid another import.
|
|
||||||
return "", errors.New("Required environment variable " + key + " is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOr(key, defaultValue string) string {
|
|
||||||
value, ok := os.LookupEnv(key)
|
|
||||||
if ok {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(field reflect.Value, refType reflect.StructField, value string, funcMap CustomParsers) error {
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.Slice:
|
|
||||||
separator := refType.Tag.Get("envSeparator")
|
|
||||||
return handleSlice(field, value, separator)
|
|
||||||
case reflect.String:
|
|
||||||
field.SetString(value)
|
|
||||||
case reflect.Bool:
|
|
||||||
bvalue, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetBool(bvalue)
|
|
||||||
case reflect.Int:
|
|
||||||
intValue, err := strconv.ParseInt(value, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetInt(intValue)
|
|
||||||
case reflect.Uint:
|
|
||||||
uintValue, err := strconv.ParseUint(value, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetUint(uintValue)
|
|
||||||
case reflect.Float32:
|
|
||||||
v, err := strconv.ParseFloat(value, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetFloat(v)
|
|
||||||
case reflect.Float64:
|
|
||||||
v, err := strconv.ParseFloat(value, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(v))
|
|
||||||
case reflect.Int64:
|
|
||||||
if refType.Type.String() == "time.Duration" {
|
|
||||||
dValue, err := time.ParseDuration(value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(dValue))
|
|
||||||
} else {
|
|
||||||
intValue, err := strconv.ParseInt(value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetInt(intValue)
|
|
||||||
}
|
|
||||||
case reflect.Uint64:
|
|
||||||
uintValue, err := strconv.ParseUint(value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.SetUint(uintValue)
|
|
||||||
case reflect.Struct:
|
|
||||||
return handleStruct(field, refType, value, funcMap)
|
|
||||||
default:
|
|
||||||
parserFunc, ok := funcMap[refType.Type]
|
|
||||||
if !ok {
|
|
||||||
return ErrUnsupportedType
|
|
||||||
}
|
|
||||||
val, err := parserFunc(value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(val))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStruct(field reflect.Value, refType reflect.StructField, value string, funcMap CustomParsers) error {
|
|
||||||
// Does the custom parser func map contain this type?
|
|
||||||
parserFunc, ok := funcMap[field.Type()]
|
|
||||||
if !ok {
|
|
||||||
// Map does not contain a custom parser for this type
|
|
||||||
return ErrUnsupportedType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call on the custom parser func
|
|
||||||
data, err := parserFunc(value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Custom parser error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the field to the data returned by the customer parser func
|
|
||||||
rv := reflect.ValueOf(data)
|
|
||||||
field.Set(rv)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSlice(field reflect.Value, value, separator string) error {
|
|
||||||
if separator == "" {
|
|
||||||
separator = ","
|
|
||||||
}
|
|
||||||
|
|
||||||
splitData := strings.Split(value, separator)
|
|
||||||
|
|
||||||
switch field.Type() {
|
|
||||||
case sliceOfStrings:
|
|
||||||
field.Set(reflect.ValueOf(splitData))
|
|
||||||
case sliceOfInts:
|
|
||||||
intData, err := parseInts(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(intData))
|
|
||||||
case sliceOfInt64s:
|
|
||||||
int64Data, err := parseInt64s(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(int64Data))
|
|
||||||
case sliceOfUint64s:
|
|
||||||
uint64Data, err := parseUint64s(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(uint64Data))
|
|
||||||
case sliceOfFloat32s:
|
|
||||||
data, err := parseFloat32s(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(data))
|
|
||||||
case sliceOfFloat64s:
|
|
||||||
data, err := parseFloat64s(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(data))
|
|
||||||
case sliceOfBools:
|
|
||||||
boolData, err := parseBools(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(boolData))
|
|
||||||
case sliceOfDurations:
|
|
||||||
durationData, err := parseDurations(splitData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(durationData))
|
|
||||||
default:
|
|
||||||
return ErrUnsupportedSliceType
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInts(data []string) ([]int, error) {
|
|
||||||
intSlice := make([]int, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
intValue, err := strconv.ParseInt(v, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
intSlice = append(intSlice, int(intValue))
|
|
||||||
}
|
|
||||||
return intSlice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt64s(data []string) ([]int64, error) {
|
|
||||||
intSlice := make([]int64, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
intValue, err := strconv.ParseInt(v, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
intSlice = append(intSlice, int64(intValue))
|
|
||||||
}
|
|
||||||
return intSlice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseUint64s(data []string) ([]uint64, error) {
|
|
||||||
var uintSlice []uint64
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
uintValue, err := strconv.ParseUint(v, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
uintSlice = append(uintSlice, uint64(uintValue))
|
|
||||||
}
|
|
||||||
return uintSlice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFloat32s(data []string) ([]float32, error) {
|
|
||||||
float32Slice := make([]float32, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
data, err := strconv.ParseFloat(v, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
float32Slice = append(float32Slice, float32(data))
|
|
||||||
}
|
|
||||||
return float32Slice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFloat64s(data []string) ([]float64, error) {
|
|
||||||
float64Slice := make([]float64, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
data, err := strconv.ParseFloat(v, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
float64Slice = append(float64Slice, float64(data))
|
|
||||||
}
|
|
||||||
return float64Slice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBools(data []string) ([]bool, error) {
|
|
||||||
boolSlice := make([]bool, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
bvalue, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
boolSlice = append(boolSlice, bvalue)
|
|
||||||
}
|
|
||||||
return boolSlice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDurations(data []string) ([]time.Duration, error) {
|
|
||||||
durationSlice := make([]time.Duration, 0, len(data))
|
|
||||||
|
|
||||||
for _, v := range data {
|
|
||||||
dvalue, err := time.ParseDuration(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
durationSlice = append(durationSlice, dvalue)
|
|
||||||
}
|
|
||||||
return durationSlice, nil
|
|
||||||
}
|
|
4
vendor/github.com/caarlos0/env/v6/.gitignore
generated
vendored
Normal file
4
vendor/github.com/caarlos0/env/v6/.gitignore
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
coverage.txt
|
||||||
|
bin
|
||||||
|
card.png
|
||||||
|
dist
|
8
vendor/github.com/caarlos0/env/v6/.golangci.yml
generated
vendored
Normal file
8
vendor/github.com/caarlos0/env/v6/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- thelper
|
||||||
|
- gofumpt
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- wastedassign
|
3
vendor/github.com/caarlos0/env/v6/.goreleaser.yml
generated
vendored
Normal file
3
vendor/github.com/caarlos0/env/v6/.goreleaser.yml
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
includes:
|
||||||
|
- from_url:
|
||||||
|
url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml
|
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015-2016 Carlos Alexandro Becker
|
Copyright (c) 2015-2022 Carlos Alexandro Becker
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
37
vendor/github.com/caarlos0/env/v6/Makefile
generated
vendored
Normal file
37
vendor/github.com/caarlos0/env/v6/Makefile
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
SOURCE_FILES?=./...
|
||||||
|
TEST_PATTERN?=.
|
||||||
|
|
||||||
|
export GO111MODULE := on
|
||||||
|
|
||||||
|
setup:
|
||||||
|
go mod tidy
|
||||||
|
.PHONY: setup
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build
|
||||||
|
.PHONY: build
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
|
||||||
|
.PHONY: test
|
||||||
|
|
||||||
|
cover: test
|
||||||
|
go tool cover -html=coverage.txt
|
||||||
|
.PHONY: cover
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofumpt -w -l .
|
||||||
|
.PHONY: fmt
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
.PHONY: lint
|
||||||
|
|
||||||
|
ci: build test
|
||||||
|
.PHONY: ci
|
||||||
|
|
||||||
|
card:
|
||||||
|
wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png"
|
||||||
|
.PHONY: card
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := ci
|
452
vendor/github.com/caarlos0/env/v6/README.md
generated
vendored
Normal file
452
vendor/github.com/caarlos0/env/v6/README.md
generated
vendored
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
# env
|
||||||
|
|
||||||
|
[![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build)
|
||||||
|
[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env)
|
||||||
|
[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6)
|
||||||
|
|
||||||
|
A simple and zero-dependencies library to parse environment variables into structs.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Get the module with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/caarlos0/env/v6
|
||||||
|
```
|
||||||
|
|
||||||
|
The usage looks like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Home string `env:"HOME"`
|
||||||
|
Port int `env:"PORT" envDefault:"3000"`
|
||||||
|
Password string `env:"PASSWORD,unset"`
|
||||||
|
IsProduction bool `env:"PRODUCTION"`
|
||||||
|
Hosts []string `env:"HOSTS" envSeparator:":"`
|
||||||
|
Duration time.Duration `env:"DURATION"`
|
||||||
|
TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config{}
|
||||||
|
if err := env.Parse(&cfg); err != nil {
|
||||||
|
fmt.Printf("%+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can run it like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go
|
||||||
|
{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s}
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**.
|
||||||
|
|
||||||
|
## Supported types and defaults
|
||||||
|
|
||||||
|
Out of the box all built-in types are supported, plus a few others that
|
||||||
|
are commonly used.
|
||||||
|
|
||||||
|
Complete list:
|
||||||
|
|
||||||
|
- `string`
|
||||||
|
- `bool`
|
||||||
|
- `int`
|
||||||
|
- `int8`
|
||||||
|
- `int16`
|
||||||
|
- `int32`
|
||||||
|
- `int64`
|
||||||
|
- `uint`
|
||||||
|
- `uint8`
|
||||||
|
- `uint16`
|
||||||
|
- `uint32`
|
||||||
|
- `uint64`
|
||||||
|
- `float32`
|
||||||
|
- `float64`
|
||||||
|
- `time.Duration`
|
||||||
|
- `encoding.TextUnmarshaler`
|
||||||
|
- `url.URL`
|
||||||
|
|
||||||
|
Pointers, slices and slices of pointers of those types are also supported.
|
||||||
|
|
||||||
|
You can also use/define a [custom parser func](#custom-parser-funcs) for any
|
||||||
|
other type you want.
|
||||||
|
|
||||||
|
If you set the `envDefault` tag for something, this value will be used in the
|
||||||
|
case of absence of it in the environment.
|
||||||
|
|
||||||
|
By default, slice types will split the environment value on `,`; you can change
|
||||||
|
this behavior by setting the `envSeparator` tag.
|
||||||
|
|
||||||
|
If you set the `envExpand` tag, environment variables (either in `${var}` or
|
||||||
|
`$var` format) in the string will be replaced according with the actual value
|
||||||
|
of the variable.
|
||||||
|
|
||||||
|
## Custom Parser Funcs
|
||||||
|
|
||||||
|
If you have a type that is not supported out of the box by the lib, you are able
|
||||||
|
to use (or define) and pass custom parsers (and their associated `reflect.Type`)
|
||||||
|
to the `env.ParseWithFuncs()` function.
|
||||||
|
|
||||||
|
In addition to accepting a struct pointer (same as `Parse()`), this function
|
||||||
|
also accepts a `map[reflect.Type]env.ParserFunc`.
|
||||||
|
|
||||||
|
If you add a custom parser for, say `Foo`, it will also be used to parse
|
||||||
|
`*Foo` and `[]Foo` types.
|
||||||
|
|
||||||
|
Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v6)
|
||||||
|
for more info.
|
||||||
|
|
||||||
|
### A note about `TextUnmarshaler` and `time.Time`
|
||||||
|
|
||||||
|
Env supports by default anything that implements the `TextUnmarshaler` interface.
|
||||||
|
That includes things like `time.Time` for example.
|
||||||
|
The upside is that depending on the format you need, you don't need to change anything.
|
||||||
|
The downside is that if you do need time in another format, you'll need to create your own type.
|
||||||
|
|
||||||
|
Its fairly straightforward:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyTime time.Time
|
||||||
|
|
||||||
|
func (t *MyTime) UnmarshalText(text []byte) error {
|
||||||
|
tt, err := time.Parse("2006-01-02", string(text))
|
||||||
|
*t = MyTime(tt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SomeTime MyTime `env:"SOME_TIME"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then you can parse `Config` with `env.Parse`.
|
||||||
|
|
||||||
|
## Required fields
|
||||||
|
|
||||||
|
The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to ensure that some environment variable is set.
|
||||||
|
In the example above, an error is returned if the `config` struct is changed to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type config struct {
|
||||||
|
SecretKey string `env:"SECRET_KEY,required"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Not Empty fields
|
||||||
|
|
||||||
|
While `required` demands the environment variable to be check, it doesn't check its value.
|
||||||
|
If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type config struct {
|
||||||
|
SecretKey string `env:"SECRET_KEY,notEmpty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unset environment variable after reading it
|
||||||
|
|
||||||
|
The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added
|
||||||
|
to ensure that some environment variable is unset after reading it.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type config struct {
|
||||||
|
SecretKey string `env:"SECRET_KEY,unset"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## From file
|
||||||
|
|
||||||
|
The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added
|
||||||
|
to in order to indicate that the value of the variable shall be loaded from a file. The path of that file is given
|
||||||
|
by the environment variable associated with it
|
||||||
|
Example below
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Secret string `env:"SECRET,file"`
|
||||||
|
Password string `env:"PASSWORD,file" envDefault:"/tmp/password"`
|
||||||
|
Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config{}
|
||||||
|
if err := env.Parse(&cfg); err != nil {
|
||||||
|
fmt.Printf("%+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo qwerty > /tmp/secret
|
||||||
|
$ echo dvorak > /tmp/password
|
||||||
|
$ echo coleman > /tmp/certificate
|
||||||
|
|
||||||
|
$ SECRET=/tmp/secret \
|
||||||
|
CERTIFICATE_FILE=/tmp/certificate \
|
||||||
|
go run main.go
|
||||||
|
{Secret:qwerty Password:dvorak Certificate:coleman}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values`
|
||||||
|
as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`.
|
||||||
|
This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used.
|
||||||
|
|
||||||
|
This can make your testing scenarios a bit more clean and easy to handle.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Password string `env:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := &env.Options{Environment: map[string]string{
|
||||||
|
"PASSWORD": "MY_PASSWORD",
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.Parse(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg.envData)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing default tag name
|
||||||
|
|
||||||
|
You can change what tag name to use for setting the env vars by setting the `Options.TagName`
|
||||||
|
variable.
|
||||||
|
|
||||||
|
For example
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Password string `json:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := &env.Options{TagName: "json"}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.Parse(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg.envData)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prefixes
|
||||||
|
|
||||||
|
You can prefix sub-structs env tags, as well as a whole `env.Parse` call.
|
||||||
|
|
||||||
|
Here's an example flexing it a bit:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Home string `env:"HOME"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexConfig struct {
|
||||||
|
Foo Config `envPrefix:"FOO_"`
|
||||||
|
Clean Config
|
||||||
|
Bar Config `envPrefix:"BAR_"`
|
||||||
|
Blah string `env:"BLAH"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := ComplexConfig{}
|
||||||
|
if err := Parse(&cfg, Options{
|
||||||
|
Prefix: "T_",
|
||||||
|
Environment: map[string]string{
|
||||||
|
"T_FOO_HOME": "/foo",
|
||||||
|
"T_BAR_HOME": "/bar",
|
||||||
|
"T_BLAH": "blahhh",
|
||||||
|
"T_HOME": "/clean",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.Parse(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg.envData)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### On set hooks
|
||||||
|
|
||||||
|
You might want to listen to value sets and, for example, log something or do some other kind of logic.
|
||||||
|
You can do this by passing a `OnSet` option:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `env:"USERNAME" envDefault:"admin"`
|
||||||
|
Password string `env:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := &env.Options{
|
||||||
|
OnSet: func(tag string, value interface{}, isDefault bool) {
|
||||||
|
fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.Parse(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg.envData)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making all fields to required
|
||||||
|
|
||||||
|
You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `env:"USERNAME" envDefault:"admin"`
|
||||||
|
Password string `env:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := &env.Options{RequiredIfNoDef: true}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.Parse(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg.envData)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defaults from code
|
||||||
|
|
||||||
|
You may define default value also in code, by initialising the config data before it's filled by `env.Parse`.
|
||||||
|
Default values defined as struct tags will overwrite existing values during Parse.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `env:"USERNAME" envDefault:"admin"`
|
||||||
|
Password string `env:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var cfg = Config{
|
||||||
|
Username: "test",
|
||||||
|
Password: "123456",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := env.Parse(&cfg); err != nil {
|
||||||
|
fmt.Println("failed:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v", cfg) // {Username:admin Password:123456}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stargazers over time
|
||||||
|
|
||||||
|
[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env)
|
504
vendor/github.com/caarlos0/env/v6/env.go
generated
vendored
Normal file
504
vendor/github.com/caarlos0/env/v6/env.go
generated
vendored
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var (
|
||||||
|
// ErrNotAStructPtr is returned if you pass something that is not a pointer to a
|
||||||
|
// Struct to Parse.
|
||||||
|
ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct")
|
||||||
|
|
||||||
|
defaultBuiltInParsers = map[reflect.Kind]ParserFunc{
|
||||||
|
reflect.Bool: func(v string) (interface{}, error) {
|
||||||
|
return strconv.ParseBool(v)
|
||||||
|
},
|
||||||
|
reflect.String: func(v string) (interface{}, error) {
|
||||||
|
return v, nil
|
||||||
|
},
|
||||||
|
reflect.Int: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseInt(v, 10, 32)
|
||||||
|
return int(i), err
|
||||||
|
},
|
||||||
|
reflect.Int16: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseInt(v, 10, 16)
|
||||||
|
return int16(i), err
|
||||||
|
},
|
||||||
|
reflect.Int32: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseInt(v, 10, 32)
|
||||||
|
return int32(i), err
|
||||||
|
},
|
||||||
|
reflect.Int64: func(v string) (interface{}, error) {
|
||||||
|
return strconv.ParseInt(v, 10, 64)
|
||||||
|
},
|
||||||
|
reflect.Int8: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseInt(v, 10, 8)
|
||||||
|
return int8(i), err
|
||||||
|
},
|
||||||
|
reflect.Uint: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseUint(v, 10, 32)
|
||||||
|
return uint(i), err
|
||||||
|
},
|
||||||
|
reflect.Uint16: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseUint(v, 10, 16)
|
||||||
|
return uint16(i), err
|
||||||
|
},
|
||||||
|
reflect.Uint32: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseUint(v, 10, 32)
|
||||||
|
return uint32(i), err
|
||||||
|
},
|
||||||
|
reflect.Uint64: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseUint(v, 10, 64)
|
||||||
|
return i, err
|
||||||
|
},
|
||||||
|
reflect.Uint8: func(v string) (interface{}, error) {
|
||||||
|
i, err := strconv.ParseUint(v, 10, 8)
|
||||||
|
return uint8(i), err
|
||||||
|
},
|
||||||
|
reflect.Float64: func(v string) (interface{}, error) {
|
||||||
|
return strconv.ParseFloat(v, 64)
|
||||||
|
},
|
||||||
|
reflect.Float32: func(v string) (interface{}, error) {
|
||||||
|
f, err := strconv.ParseFloat(v, 32)
|
||||||
|
return float32(f), err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultTypeParsers() map[reflect.Type]ParserFunc {
|
||||||
|
return map[reflect.Type]ParserFunc{
|
||||||
|
reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) {
|
||||||
|
u, err := url.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse URL: %v", err)
|
||||||
|
}
|
||||||
|
return *u, nil
|
||||||
|
},
|
||||||
|
reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) {
|
||||||
|
s, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse duration: %v", err)
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParserFunc defines the signature of a function that can be used within `CustomParsers`.
|
||||||
|
type ParserFunc func(v string) (interface{}, error)
|
||||||
|
|
||||||
|
// OnSetFn is a hook that can be run when a value is set.
|
||||||
|
type OnSetFn func(tag string, value interface{}, isDefault bool)
|
||||||
|
|
||||||
|
// Options for the parser.
|
||||||
|
type Options struct {
|
||||||
|
// Environment keys and values that will be accessible for the service.
|
||||||
|
Environment map[string]string
|
||||||
|
|
||||||
|
// TagName specifies another tagname to use rather than the default env.
|
||||||
|
TagName string
|
||||||
|
|
||||||
|
// RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault'
|
||||||
|
RequiredIfNoDef bool
|
||||||
|
|
||||||
|
// OnSet allows to run a function when a value is set
|
||||||
|
OnSet OnSetFn
|
||||||
|
|
||||||
|
// Prefix define a prefix for each key
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
// Sets to true if we have already configured once.
|
||||||
|
configured bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure will do the basic configurations and defaults.
|
||||||
|
func configure(opts []Options) []Options {
|
||||||
|
// If we have already configured the first item
|
||||||
|
// of options will have been configured set to true.
|
||||||
|
if len(opts) > 0 && opts[0].configured {
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Created options with defaults.
|
||||||
|
opt := Options{
|
||||||
|
TagName: "env",
|
||||||
|
Environment: toMap(os.Environ()),
|
||||||
|
configured: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all opts structs and set
|
||||||
|
// to opt if value is not default/empty.
|
||||||
|
for _, item := range opts {
|
||||||
|
if item.Environment != nil {
|
||||||
|
opt.Environment = item.Environment
|
||||||
|
}
|
||||||
|
if item.TagName != "" {
|
||||||
|
opt.TagName = item.TagName
|
||||||
|
}
|
||||||
|
if item.OnSet != nil {
|
||||||
|
opt.OnSet = item.OnSet
|
||||||
|
}
|
||||||
|
if item.Prefix != "" {
|
||||||
|
opt.Prefix = item.Prefix
|
||||||
|
}
|
||||||
|
opt.RequiredIfNoDef = item.RequiredIfNoDef
|
||||||
|
}
|
||||||
|
|
||||||
|
return []Options{opt}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOnSetFn(opts []Options) OnSetFn {
|
||||||
|
return opts[0].OnSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTagName returns the tag name.
|
||||||
|
func getTagName(opts []Options) string {
|
||||||
|
return opts[0].TagName
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvironment returns the environment map.
|
||||||
|
func getEnvironment(opts []Options) map[string]string {
|
||||||
|
return opts[0].Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a struct containing `env` tags and loads its values from
|
||||||
|
// environment variables.
|
||||||
|
func Parse(v interface{}, opts ...Options) error {
|
||||||
|
return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithFuncs is the same as `Parse` except it also allows the user to pass
|
||||||
|
// in custom parsers.
|
||||||
|
func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error {
|
||||||
|
opts = configure(opts)
|
||||||
|
|
||||||
|
ptrRef := reflect.ValueOf(v)
|
||||||
|
if ptrRef.Kind() != reflect.Ptr {
|
||||||
|
return ErrNotAStructPtr
|
||||||
|
}
|
||||||
|
ref := ptrRef.Elem()
|
||||||
|
if ref.Kind() != reflect.Struct {
|
||||||
|
return ErrNotAStructPtr
|
||||||
|
}
|
||||||
|
parsers := defaultTypeParsers()
|
||||||
|
for k, v := range funcMap {
|
||||||
|
parsers[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return doParse(ref, parsers, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error {
|
||||||
|
refType := ref.Type()
|
||||||
|
|
||||||
|
var agrErr aggregateError
|
||||||
|
|
||||||
|
for i := 0; i < refType.NumField(); i++ {
|
||||||
|
refField := ref.Field(i)
|
||||||
|
refTypeField := refType.Field(i)
|
||||||
|
|
||||||
|
if err := doParseField(refField, refTypeField, funcMap, opts); err != nil {
|
||||||
|
if val, ok := err.(aggregateError); ok {
|
||||||
|
agrErr.errors = append(agrErr.errors, val.errors...)
|
||||||
|
} else {
|
||||||
|
agrErr.errors = append(agrErr.errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(agrErr.errors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return agrErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error {
|
||||||
|
if !refField.CanSet() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if reflect.Ptr == refField.Kind() && refField.Elem().Kind() == reflect.Struct {
|
||||||
|
return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
|
||||||
|
}
|
||||||
|
if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" {
|
||||||
|
return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
|
||||||
|
}
|
||||||
|
value, err := get(refTypeField, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
return set(refField, refTypeField, value, funcMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.Struct == refField.Kind() {
|
||||||
|
return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(field reflect.StructField, opts []Options) (val string, err error) {
|
||||||
|
var exists bool
|
||||||
|
var isDefault bool
|
||||||
|
var loadFile bool
|
||||||
|
var unset bool
|
||||||
|
var notEmpty bool
|
||||||
|
|
||||||
|
required := opts[0].RequiredIfNoDef
|
||||||
|
prefix := opts[0].Prefix
|
||||||
|
ownKey, tags := parseKeyForOption(field.Tag.Get(getTagName(opts)))
|
||||||
|
key := prefix + ownKey
|
||||||
|
for _, tag := range tags {
|
||||||
|
switch tag {
|
||||||
|
case "":
|
||||||
|
continue
|
||||||
|
case "file":
|
||||||
|
loadFile = true
|
||||||
|
case "required":
|
||||||
|
required = true
|
||||||
|
case "unset":
|
||||||
|
unset = true
|
||||||
|
case "notEmpty":
|
||||||
|
notEmpty = true
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("tag option %q not supported", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expand := strings.EqualFold(field.Tag.Get("envExpand"), "true")
|
||||||
|
defaultValue, defExists := field.Tag.Lookup("envDefault")
|
||||||
|
val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts))
|
||||||
|
|
||||||
|
if expand {
|
||||||
|
val = os.ExpandEnv(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unset {
|
||||||
|
defer os.Unsetenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if required && !exists && len(ownKey) > 0 {
|
||||||
|
return "", fmt.Errorf(`required environment variable %q is not set`, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if notEmpty && val == "" {
|
||||||
|
return "", fmt.Errorf("environment variable %q should not be empty", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadFile && val != "" {
|
||||||
|
filename := val
|
||||||
|
val, err = getFromFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if onSetFn := getOnSetFn(opts); onSetFn != nil {
|
||||||
|
onSetFn(key, val, isDefault)
|
||||||
|
}
|
||||||
|
return val, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the env tag's key into the expected key and desired option, if any.
|
||||||
|
func parseKeyForOption(key string) (string, []string) {
|
||||||
|
opts := strings.Split(key, ",")
|
||||||
|
return opts[0], opts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromFile(filename string) (value string, err error) {
|
||||||
|
b, err := os.ReadFile(filename)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) {
|
||||||
|
value, exists := envs[key]
|
||||||
|
switch {
|
||||||
|
case (!exists || key == "") && defExists:
|
||||||
|
return defaultValue, true, true
|
||||||
|
case !exists:
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error {
|
||||||
|
if tm := asTextUnmarshaler(field); tm != nil {
|
||||||
|
if err := tm.UnmarshalText([]byte(value)); err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
typee := sf.Type
|
||||||
|
fieldee := field
|
||||||
|
if typee.Kind() == reflect.Ptr {
|
||||||
|
typee = typee.Elem()
|
||||||
|
fieldee = field.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
parserFunc, ok := funcMap[typee]
|
||||||
|
if ok {
|
||||||
|
val, err := parserFunc(value)
|
||||||
|
if err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldee.Set(reflect.ValueOf(val))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||||
|
if ok {
|
||||||
|
val, err := parserFunc(value)
|
||||||
|
if err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldee.Set(reflect.ValueOf(val).Convert(typee))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Kind() == reflect.Slice {
|
||||||
|
return handleSlice(field, value, sf, funcMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNoParserError(sf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error {
|
||||||
|
separator := sf.Tag.Get("envSeparator")
|
||||||
|
if separator == "" {
|
||||||
|
separator = ","
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, separator)
|
||||||
|
|
||||||
|
typee := sf.Type.Elem()
|
||||||
|
if typee.Kind() == reflect.Ptr {
|
||||||
|
typee = typee.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok {
|
||||||
|
return parseTextUnmarshalers(field, parts, sf)
|
||||||
|
}
|
||||||
|
|
||||||
|
parserFunc, ok := funcMap[typee]
|
||||||
|
if !ok {
|
||||||
|
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||||
|
if !ok {
|
||||||
|
return newNoParserError(sf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := reflect.MakeSlice(sf.Type, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
r, err := parserFunc(part)
|
||||||
|
if err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
v := reflect.ValueOf(r).Convert(typee)
|
||||||
|
if sf.Type.Elem().Kind() == reflect.Ptr {
|
||||||
|
v = reflect.New(typee)
|
||||||
|
v.Elem().Set(reflect.ValueOf(r).Convert(typee))
|
||||||
|
}
|
||||||
|
result = reflect.Append(result, v)
|
||||||
|
}
|
||||||
|
field.Set(result)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler {
|
||||||
|
if reflect.Ptr == field.Kind() {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
} else if field.CanAddr() {
|
||||||
|
field = field.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
tm, ok := field.Interface().(encoding.TextUnmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error {
|
||||||
|
s := len(data)
|
||||||
|
elemType := field.Type().Elem()
|
||||||
|
slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s)
|
||||||
|
for i, v := range data {
|
||||||
|
sv := slice.Index(i)
|
||||||
|
kind := sv.Kind()
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
sv = reflect.New(elemType.Elem())
|
||||||
|
} else {
|
||||||
|
sv = sv.Addr()
|
||||||
|
}
|
||||||
|
tm := sv.Interface().(encoding.TextUnmarshaler)
|
||||||
|
if err := tm.UnmarshalText([]byte(v)); err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
slice.Index(i).Set(sv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(slice)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseError(sf reflect.StructField, err error) error {
|
||||||
|
return parseError{
|
||||||
|
sf: sf,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseError struct {
|
||||||
|
sf reflect.StructField
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e parseError) Error() string {
|
||||||
|
return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNoParserError(sf reflect.StructField) error {
|
||||||
|
return fmt.Errorf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optsWithPrefix(field reflect.StructField, opts []Options) []Options {
|
||||||
|
subOpts := make([]Options, len(opts))
|
||||||
|
copy(subOpts, opts)
|
||||||
|
if prefix := field.Tag.Get("envPrefix"); prefix != "" {
|
||||||
|
subOpts[0].Prefix += prefix
|
||||||
|
}
|
||||||
|
return subOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
type aggregateError struct {
|
||||||
|
errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e aggregateError) Error() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("env:")
|
||||||
|
|
||||||
|
for _, err := range e.errors {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %v;", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRight(sb.String(), ";")
|
||||||
|
}
|
15
vendor/github.com/caarlos0/env/v6/env_unix.go
generated
vendored
Normal file
15
vendor/github.com/caarlos0/env/v6/env_unix.go
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||||
|
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
|
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func toMap(env []string) map[string]string {
|
||||||
|
r := map[string]string{}
|
||||||
|
for _, e := range env {
|
||||||
|
p := strings.SplitN(e, "=", 2)
|
||||||
|
r[p[0]] = p[1]
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
25
vendor/github.com/caarlos0/env/v6/env_windows.go
generated
vendored
Normal file
25
vendor/github.com/caarlos0/env/v6/env_windows.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func toMap(env []string) map[string]string {
|
||||||
|
r := map[string]string{}
|
||||||
|
for _, e := range env {
|
||||||
|
p := strings.SplitN(e, "=", 2)
|
||||||
|
|
||||||
|
// On Windows, environment variables can start with '='. If so, Split at next character.
|
||||||
|
// See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58
|
||||||
|
prefixEqualSign := false
|
||||||
|
if len(e) > 0 && e[0] == '=' {
|
||||||
|
e = e[1:]
|
||||||
|
prefixEqualSign = true
|
||||||
|
}
|
||||||
|
p = strings.SplitN(e, "=", 2)
|
||||||
|
if prefixEqualSign {
|
||||||
|
p[0] = "=" + p[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
r[p[0]] = p[1]
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
3
vendor/golang.org/x/net/AUTHORS
generated
vendored
3
vendor/golang.org/x/net/AUTHORS
generated
vendored
@ -1,3 +0,0 @@
|
|||||||
# This source code refers to The Go Authors for copyright purposes.
|
|
||||||
# The master list of authors is in the main Go distribution,
|
|
||||||
# visible at http://tip.golang.org/AUTHORS.
|
|
3
vendor/golang.org/x/net/CONTRIBUTORS
generated
vendored
3
vendor/golang.org/x/net/CONTRIBUTORS
generated
vendored
@ -1,3 +0,0 @@
|
|||||||
# This source code was written by the Go contributors.
|
|
||||||
# The master list of contributors is in the main Go distribution,
|
|
||||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
5
vendor/golang.org/x/net/context/go17.go
generated
vendored
5
vendor/golang.org/x/net/context/go17.go
generated
vendored
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.7
|
||||||
// +build go1.7
|
// +build go1.7
|
||||||
|
|
||||||
package context
|
package context
|
||||||
@ -31,7 +32,7 @@ var DeadlineExceeded = context.DeadlineExceeded
|
|||||||
// call cancel as soon as the operations running in this Context complete.
|
// call cancel as soon as the operations running in this Context complete.
|
||||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||||
ctx, f := context.WithCancel(parent)
|
ctx, f := context.WithCancel(parent)
|
||||||
return ctx, CancelFunc(f)
|
return ctx, f
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||||
@ -45,7 +46,7 @@ func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
|||||||
// call cancel as soon as the operations running in this Context complete.
|
// call cancel as soon as the operations running in this Context complete.
|
||||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||||
ctx, f := context.WithDeadline(parent, deadline)
|
ctx, f := context.WithDeadline(parent, deadline)
|
||||||
return ctx, CancelFunc(f)
|
return ctx, f
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||||
|
1
vendor/golang.org/x/net/context/go19.go
generated
vendored
1
vendor/golang.org/x/net/context/go19.go
generated
vendored
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.9
|
||||||
// +build go1.9
|
// +build go1.9
|
||||||
|
|
||||||
package context
|
package context
|
||||||
|
1
vendor/golang.org/x/net/context/pre_go17.go
generated
vendored
1
vendor/golang.org/x/net/context/pre_go17.go
generated
vendored
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !go1.7
|
||||||
// +build !go1.7
|
// +build !go1.7
|
||||||
|
|
||||||
package context
|
package context
|
||||||
|
1
vendor/golang.org/x/net/context/pre_go19.go
generated
vendored
1
vendor/golang.org/x/net/context/pre_go19.go
generated
vendored
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !go1.9
|
||||||
// +build !go1.9
|
// +build !go1.9
|
||||||
|
|
||||||
package context
|
package context
|
||||||
|
10
vendor/modules.txt
vendored
10
vendor/modules.txt
vendored
@ -1,9 +1,9 @@
|
|||||||
# github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
# github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
## explicit
|
## explicit
|
||||||
github.com/armon/go-socks5
|
github.com/armon/go-socks5
|
||||||
# github.com/caarlos0/env v3.3.1-0.20180521112546-3e0f30cbf50b+incompatible
|
# github.com/caarlos0/env/v6 v6.10.1
|
||||||
## explicit
|
## explicit; go 1.17
|
||||||
github.com/caarlos0/env
|
github.com/caarlos0/env/v6
|
||||||
# golang.org/x/net v0.0.0-20180406214816-61147c48b25b
|
# golang.org/x/net v0.1.0
|
||||||
## explicit
|
## explicit; go 1.17
|
||||||
golang.org/x/net/context
|
golang.org/x/net/context
|
||||||
|
Loading…
Reference in New Issue
Block a user