diff --git a/README.md b/README.md index 6d9db5d4..a305433a 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,8 @@ Run `caddy help docker-proxy` to see all available flags. Usage of docker-proxy: --caddyfile-path string Path to a base Caddyfile that will be extended with Docker sites + --envfile + Path to an environment file with environment variables in the KEY=VALUE format to load into the Caddy process --controller-network string Network allowed to configure Caddy server in CIDR notation. Ex: 10.200.200.0/24 --ingress-networks string @@ -515,6 +517,7 @@ Those flags can also be set via environment variables: ``` CADDY_DOCKER_CADDYFILE_PATH= +CADDY_DOCKER_ENVFILE= CADDY_CONTROLLER_NETWORK= CADDY_INGRESS_NETWORKS= CADDY_DOCKER_SOCKETS= diff --git a/cmd.go b/cmd.go index 872fb9ed..ee6127dd 100644 --- a/cmd.go +++ b/cmd.go @@ -49,6 +49,9 @@ func init() { fs.String("caddyfile-path", "", "Path to a base Caddyfile that will be extended with docker sites") + fs.String("envfile", "", + "Environment file with environment variables in the KEY=VALUE format") + fs.String("label-prefix", generator.DefaultLabelPrefix, "Prefix for Docker labels") @@ -141,6 +144,7 @@ func getAdminListen(options *config.Options) string { func createOptions(flags caddycmd.Flags) *config.Options { caddyfilePath := flags.String("caddyfile-path") + envFile := flags.String("envfile") labelPrefixFlag := flags.String("label-prefix") proxyServiceTasksFlag := flags.Bool("proxy-service-tasks") processCaddyfileFlag := flags.Bool("process-caddyfile") @@ -222,6 +226,12 @@ func createOptions(flags caddycmd.Flags) *config.Options { options.CaddyfilePath = caddyfilePath } + if envFileEnv := os.Getenv("CADDY_DOCKER_ENVFILE"); envFileEnv != "" { + options.EnvFile = envFileEnv + } else { + options.EnvFile = envFile + } + if labelPrefixEnv := os.Getenv("CADDY_DOCKER_LABEL_PREFIX"); labelPrefixEnv != "" { options.LabelPrefix = labelPrefixEnv } else { diff --git a/config/options.go b/config/options.go index 92e3e12e..c06b8b54 100644 --- a/config/options.go +++ b/config/options.go @@ -8,6 +8,7 @@ import ( // Options are the options for generator type Options struct { CaddyfilePath string + EnvFile string DockerSockets []string DockerCertsPath []string DockerAPIsVersion []string diff --git a/go.mod b/go.mod index 12613890..c941e80d 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.1 // indirect diff --git a/go.sum b/go.sum index 558cfaa4..de139d9f 100644 --- a/go.sum +++ b/go.sum @@ -417,6 +417,8 @@ github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/loader.go b/loader.go index ca875e66..fda073a2 100644 --- a/loader.go +++ b/loader.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" + "github.com/joho/godotenv" "github.com/lucaslorentz/caddy-docker-proxy/v2/config" "github.com/lucaslorentz/caddy-docker-proxy/v2/docker" "github.com/lucaslorentz/caddy-docker-proxy/v2/generator" @@ -59,104 +60,115 @@ func logger() *zap.Logger { // Start docker loader func (dockerLoader *DockerLoader) Start() error { - if !dockerLoader.initialized { - dockerLoader.initialized = true - log := logger() - - dockerClients := []docker.Client{} - for i, dockerSocket := range dockerLoader.options.DockerSockets { - // cf https://github.com/docker/go-docker/blob/master/client.go - // setenv to use NewEnvClient - // or manually + if dockerLoader.initialized { + return nil + } - os.Setenv("DOCKER_HOST", dockerSocket) + dockerLoader.initialized = true + log := logger() - if len(dockerLoader.options.DockerCertsPath) >= i+1 && dockerLoader.options.DockerCertsPath[i] != "" { - os.Setenv("DOCKER_CERT_PATH", dockerLoader.options.DockerCertsPath[i]) - } else { - os.Unsetenv("DOCKER_CERT_PATH") - } + if envFile := dockerLoader.options.EnvFile; envFile != "" { + if err := godotenv.Load(dockerLoader.options.EnvFile); err != nil { + log.Error("Load variables from environment file failed", zap.Error(err), zap.String("envFile", dockerLoader.options.EnvFile)) + return err + } + log.Info("environment file loaded", zap.String("envFile", dockerLoader.options.EnvFile)) + } - if len(dockerLoader.options.DockerAPIsVersion) >= i+1 && dockerLoader.options.DockerAPIsVersion[i] != "" { - os.Setenv("DOCKER_API_VERSION", dockerLoader.options.DockerAPIsVersion[i]) - } else { - os.Unsetenv("DOCKER_API_VERSION") - } + dockerClients := []docker.Client{} + for i, dockerSocket := range dockerLoader.options.DockerSockets { + // cf https://github.com/docker/go-docker/blob/master/client.go + // setenv to use NewEnvClient + // or manually - dockerClient, err := client.NewEnvClient() - if err != nil { - log.Error("Docker connection failed to docker specify socket", zap.Error(err), zap.String("DockerSocket", dockerSocket)) - return err - } + os.Setenv("DOCKER_HOST", dockerSocket) - dockerPing, err := dockerClient.Ping(context.Background()) - if err != nil { - log.Error("Docker ping failed on specify socket", zap.Error(err), zap.String("DockerSocket", dockerSocket)) - return err - } + if len(dockerLoader.options.DockerCertsPath) >= i+1 && dockerLoader.options.DockerCertsPath[i] != "" { + os.Setenv("DOCKER_CERT_PATH", dockerLoader.options.DockerCertsPath[i]) + } else { + os.Unsetenv("DOCKER_CERT_PATH") + } - dockerClient.NegotiateAPIVersionPing(dockerPing) + if len(dockerLoader.options.DockerAPIsVersion) >= i+1 && dockerLoader.options.DockerAPIsVersion[i] != "" { + os.Setenv("DOCKER_API_VERSION", dockerLoader.options.DockerAPIsVersion[i]) + } else { + os.Unsetenv("DOCKER_API_VERSION") + } - wrappedClient := docker.WrapClient(dockerClient) + dockerClient, err := client.NewEnvClient() + if err != nil { + log.Error("Docker connection failed to docker specify socket", zap.Error(err), zap.String("DockerSocket", dockerSocket)) + return err + } - dockerClients = append(dockerClients, wrappedClient) + dockerPing, err := dockerClient.Ping(context.Background()) + if err != nil { + log.Error("Docker ping failed on specify socket", zap.Error(err), zap.String("DockerSocket", dockerSocket)) + return err } - // by default it will used the env docker - if len(dockerClients) == 0 { - dockerClient, err := client.NewEnvClient() - dockerLoader.options.DockerSockets = append(dockerLoader.options.DockerSockets, os.Getenv("DOCKER_HOST")) - if err != nil { - log.Error("Docker connection failed", zap.Error(err)) - return err - } + dockerClient.NegotiateAPIVersionPing(dockerPing) - dockerPing, err := dockerClient.Ping(context.Background()) - if err != nil { - log.Error("Docker ping failed", zap.Error(err)) - return err - } + wrappedClient := docker.WrapClient(dockerClient) - dockerClient.NegotiateAPIVersionPing(dockerPing) + dockerClients = append(dockerClients, wrappedClient) + } - wrappedClient := docker.WrapClient(dockerClient) + // by default it will used the env docker + if len(dockerClients) == 0 { + dockerClient, err := client.NewEnvClient() + dockerLoader.options.DockerSockets = append(dockerLoader.options.DockerSockets, os.Getenv("DOCKER_HOST")) + if err != nil { + log.Error("Docker connection failed", zap.Error(err)) + return err + } - dockerClients = append(dockerClients, wrappedClient) + dockerPing, err := dockerClient.Ping(context.Background()) + if err != nil { + log.Error("Docker ping failed", zap.Error(err)) + return err } - dockerLoader.dockerClients = dockerClients - dockerLoader.skipEvents = make([]bool, len(dockerLoader.dockerClients)) - - dockerLoader.generator = generator.CreateGenerator( - dockerClients, - docker.CreateUtils(), - dockerLoader.options, - ) - - log.Info( - "Start", - zap.String("CaddyfilePath", dockerLoader.options.CaddyfilePath), - zap.String("LabelPrefix", dockerLoader.options.LabelPrefix), - zap.Duration("PollingInterval", dockerLoader.options.PollingInterval), - zap.Bool("ProxyServiceTasks", dockerLoader.options.ProxyServiceTasks), - zap.Bool("ProcessCaddyfile", dockerLoader.options.ProcessCaddyfile), - zap.Bool("ScanStoppedContainers", dockerLoader.options.ScanStoppedContainers), - zap.String("IngressNetworks", fmt.Sprintf("%v", dockerLoader.options.IngressNetworks)), - zap.Strings("DockerSockets", dockerLoader.options.DockerSockets), - zap.Strings("DockerCertsPath", dockerLoader.options.DockerCertsPath), - zap.Strings("DockerAPIsVersion", dockerLoader.options.DockerAPIsVersion), - ) - - ready := make(chan struct{}) - dockerLoader.timer = time.AfterFunc(0, func() { - <-ready - dockerLoader.update() - }) - close(ready) + dockerClient.NegotiateAPIVersionPing(dockerPing) + + wrappedClient := docker.WrapClient(dockerClient) - go dockerLoader.monitorEvents() + dockerClients = append(dockerClients, wrappedClient) } + dockerLoader.dockerClients = dockerClients + dockerLoader.skipEvents = make([]bool, len(dockerLoader.dockerClients)) + + dockerLoader.generator = generator.CreateGenerator( + dockerClients, + docker.CreateUtils(), + dockerLoader.options, + ) + + log.Info( + "Start", + zap.String("CaddyfilePath", dockerLoader.options.CaddyfilePath), + zap.String("EnvFile", dockerLoader.options.EnvFile), + zap.String("LabelPrefix", dockerLoader.options.LabelPrefix), + zap.Duration("PollingInterval", dockerLoader.options.PollingInterval), + zap.Bool("ProxyServiceTasks", dockerLoader.options.ProxyServiceTasks), + zap.Bool("ProcessCaddyfile", dockerLoader.options.ProcessCaddyfile), + zap.Bool("ScanStoppedContainers", dockerLoader.options.ScanStoppedContainers), + zap.String("IngressNetworks", fmt.Sprintf("%v", dockerLoader.options.IngressNetworks)), + zap.Strings("DockerSockets", dockerLoader.options.DockerSockets), + zap.Strings("DockerCertsPath", dockerLoader.options.DockerCertsPath), + zap.Strings("DockerAPIsVersion", dockerLoader.options.DockerAPIsVersion), + ) + + ready := make(chan struct{}) + dockerLoader.timer = time.AfterFunc(0, func() { + <-ready + dockerLoader.update() + }) + close(ready) + + go dockerLoader.monitorEvents() + return nil } diff --git a/tests/envfile/Caddyfile b/tests/envfile/Caddyfile new file mode 100644 index 00000000..b614f2e5 --- /dev/null +++ b/tests/envfile/Caddyfile @@ -0,0 +1,6 @@ +service.local { + handle {$ENV_HANDLE_PATH} { + respond "Hello from TestEnv" + } + tls internal +} diff --git a/tests/envfile/Envfile b/tests/envfile/Envfile new file mode 100644 index 00000000..dde144e7 --- /dev/null +++ b/tests/envfile/Envfile @@ -0,0 +1 @@ +ENV_HANDLE_PATH=/testenv diff --git a/tests/envfile/compose.yaml b/tests/envfile/compose.yaml new file mode 100644 index 00000000..fea5a60b --- /dev/null +++ b/tests/envfile/compose.yaml @@ -0,0 +1,31 @@ +version: '3.7' + +services: + caddy: + image: caddy-docker-proxy:local + ports: + - 80:80 + - 443:443 + networks: + - caddy + environment: + - CADDY_DOCKER_CADDYFILE_PATH=/etc/caddy/Caddyfile + command: ["docker-proxy", "--envfile", "/etc/caddy/env"] + volumes: + - source: "./Caddyfile" + target: '/etc/caddy/Caddyfile' + type: bind + - source: "./Envfile" + target: "/etc/caddy/env" + type: bind + - source: "${DOCKER_SOCKET_PATH}" + target: "${DOCKER_SOCKET_PATH}" + type: ${DOCKER_SOCKET_TYPE} + +networks: + caddy: + name: caddy_test + external: true + internal: + name: internal + internal: true diff --git a/tests/envfile/run.sh b/tests/envfile/run.sh new file mode 100755 index 00000000..6fa66ba8 --- /dev/null +++ b/tests/envfile/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +. ../functions.sh + +docker stack deploy -c compose.yaml --prune caddy_test + +retry curl --show-error -s -k -f --resolve service.local:443:127.0.0.1 https://service.local/testenv | grep "Hello from TestEnv" || { + docker service logs caddy_test_caddy + exit 1 +}