diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 9a19ed05..4b6f8570 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -19,6 +19,7 @@ import ( "errors" "flag" "fmt" + "io" "io/ioutil" "log" "os" @@ -50,6 +51,7 @@ func init() { flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") + flag.StringVar(&envFile, "env", "", "Path to file with environment variables to load in KEY=VALUE format") flag.BoolVar(&plugins, "plugins", false, "List installed plugins") flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout") @@ -90,6 +92,11 @@ func Run() { }) } + //Load all additional envs as soon as possible + if err := LoadEnvFromFile(envFile); err != nil { + mustLogFatalf("%v", err) + } + // initialize telemetry client if enableTelemetry { err := initTelemetry() @@ -409,6 +416,80 @@ func initTelemetry() error { return nil } +// LoadEnvFromFile loads additional envs if file provided and exists +// Envs in file should be in KEY=VALUE format +func LoadEnvFromFile(envFile string) error { + if envFile == "" { + return nil + } + + file, err := os.Open(envFile) + if err != nil { + return err + } + defer file.Close() + + envMap, err := ParseEnvFile(file) + if err != nil { + return err + } + + for k, v := range envMap { + if err := os.Setenv(k, v); err != nil { + return err + } + } + + return nil +} + +// ParseEnvFile implements parse logic for environment files +func ParseEnvFile(envInput io.Reader) (map[string]string, error) { + envMap := make(map[string]string) + + scanner := bufio.NewScanner(envInput) + var line string + lineNumber := 0 + + for scanner.Scan() { + line = strings.TrimSpace(scanner.Text()) + lineNumber++ + + // skip lines starting with comment + if strings.HasPrefix(line, "#") { + continue + } + + // skip empty line + if len(line) == 0 { + continue + } + + fields := strings.SplitN(line, "=", 2) + if len(fields) != 2 { + return nil, fmt.Errorf("Can't parse line %d; line should be in KEY=VALUE format", lineNumber) + } + + if strings.Contains(fields[0], " ") { + return nil, fmt.Errorf("Can't parse line %d; KEY contains whitespace", lineNumber) + } + + key := fields[0] + val := fields[1] + + if key == "" { + return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber) + } + envMap[key] = val + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return envMap, nil +} + const appName = "Caddy" // Flags that control program flow or startup @@ -416,6 +497,7 @@ var ( serverType string conf string cpu string + envFile string logfile string revoke string version bool diff --git a/caddy/caddymain/run_test.go b/caddy/caddymain/run_test.go index c26a54a9..6daa3e26 100644 --- a/caddy/caddymain/run_test.go +++ b/caddy/caddymain/run_test.go @@ -15,7 +15,9 @@ package caddymain import ( + "reflect" "runtime" + "strings" "testing" ) @@ -57,3 +59,34 @@ func TestSetCPU(t *testing.T) { runtime.GOMAXPROCS(currentCPU) } } + +func TestParseEnvFile(t *testing.T) { + tests := []struct { + name string + input string + want map[string]string + wantErr bool + }{ + {"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false}, + {"empty KEY", "=4096", nil, true}, + {"one value", "test", nil, true}, + {"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false}, + {"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false}, + {"comments with space skipped", " #TEST=1", map[string]string{}, false}, + {"KEY with space", "PORT =8888", nil, true}, + {"only spaces", " ", map[string]string{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := strings.NewReader(tt.input) + got, err := ParseEnvFile(reader) + if (err != nil) != tt.wantErr { + t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want) + } + }) + } +}