diff --git a/cli/options.go b/cli/options.go index ebe61d2ec..5497f14f1 100644 --- a/cli/options.go +++ b/cli/options.go @@ -17,6 +17,7 @@ package cli import ( + "bytes" "fmt" "io" "os" @@ -39,7 +40,7 @@ type ProjectOptions struct { WorkingDir string ConfigPaths []string Environment map[string]string - EnvFile string + EnvFiles []string loadOptions []func(*loader.Options) } @@ -186,10 +187,10 @@ func WithOsEnv(o *ProjectOptions) error { return nil } -// WithEnvFile set an alternate env file -func WithEnvFile(file string) ProjectOptionsFn { +// WithEnvFiles set alternate env files +func WithEnvFiles(file ...string) ProjectOptionsFn { return func(options *ProjectOptions) error { - options.EnvFile = file + options.EnvFiles = file return nil } } @@ -200,7 +201,7 @@ func WithDotEnv(o *ProjectOptions) error { if err != nil { return err } - envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFile) + envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFiles) if err != nil { return err } @@ -213,55 +214,48 @@ func WithDotEnv(o *ProjectOptions) error { return nil } -func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename string) (map[string]string, error) { +func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames []string) (map[string]string, error) { envMap := make(map[string]string) - dotEnvFile := filename - if dotEnvFile == "" { - dotEnvFile = filepath.Join(workingDir, ".env") + dotEnvFiles := filenames + if len(dotEnvFiles) == 0 { + dotEnvFiles = append(dotEnvFiles, filepath.Join(workingDir, ".env")) } - abs, err := filepath.Abs(dotEnvFile) - if err != nil { - return envMap, err - } - dotEnvFile = abs - - s, err := os.Stat(dotEnvFile) - if os.IsNotExist(err) { - if filename != "" { - return nil, errors.Errorf("Couldn't find env file: %s", filename) + for _, dotEnvFile := range dotEnvFiles { + abs, err := filepath.Abs(dotEnvFile) + if err != nil { + return envMap, err } - return envMap, nil - } - if err != nil { - return envMap, err - } + dotEnvFile = abs - if s.IsDir() { - if filename == "" { + b, err := os.ReadFile(dotEnvFile) + if os.IsNotExist(err) { + if len(filenames) > 0 { + return nil, errors.Errorf("Couldn't read env file: %s", dotEnvFile) + } return envMap, nil } - return envMap, errors.Errorf("%s is a directory", dotEnvFile) - } - - file, err := os.Open(dotEnvFile) - if err != nil { - return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) - } - defer file.Close() + if err != nil { + return envMap, err + } - env, err := dotenv.ParseWithLookup(file, func(k string) (string, bool) { - v, ok := currentEnv[k] - if !ok { - return "", false + env, err := dotenv.ParseWithLookup(bytes.NewReader(b), func(k string) (string, bool) { + v, ok := envMap[k] + if ok { + return v, true + } + v, ok = currentEnv[k] + if !ok { + return "", false + } + return v, true + }) + if err != nil { + return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) + } + for k, v := range env { + envMap[k] = v } - return v, true - }) - if err != nil { - return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) - } - for k, v := range env { - envMap[k] = v } return envMap, nil diff --git a/cli/options_test.go b/cli/options_test.go index ffbb5e52c..7e41c77b5 100644 --- a/cli/options_test.go +++ b/cli/options_test.go @@ -226,6 +226,24 @@ func TestProjectWithDiscardEnvFile(t *testing.T) { assert.NilError(t, err) assert.Equal(t, *service.Environment["DEFAULT_PORT"], "8080") assert.Assert(t, service.EnvFile == nil) + assert.Equal(t, service.Ports[0].Published, "8000") +} + +func TestProjectWithMultipleEnvFile(t *testing.T) { + opts, err := NewProjectOptions([]string{ + "testdata/env-file/compose-with-env-files.yaml", + }, WithDiscardEnvFile, + WithEnvFiles("testdata/env-file/.env", "testdata/env-file/override.env"), + WithDotEnv) + + assert.NilError(t, err) + p, err := ProjectFromOptions(opts) + assert.NilError(t, err) + service, err := p.GetService("simple") + assert.NilError(t, err) + assert.Equal(t, *service.Environment["DEFAULT_PORT"], "9090") + assert.Assert(t, service.EnvFile == nil) + assert.Equal(t, service.Ports[0].Published, "9000") } func TestProjectNameFromWorkingDir(t *testing.T) { diff --git a/cli/testdata/env-file/.env b/cli/testdata/env-file/.env index 7171c6846..876feea7d 100644 --- a/cli/testdata/env-file/.env +++ b/cli/testdata/env-file/.env @@ -1,2 +1,3 @@ COMPOSE_FILE=compose-with-env-file.yaml -COMPOSE_PROJECT_NAME=my_project_from_dot_env \ No newline at end of file +COMPOSE_PROJECT_NAME=my_project_from_dot_env +PORT=8000 diff --git a/cli/testdata/env-file/compose-with-env-files.yaml b/cli/testdata/env-file/compose-with-env-files.yaml new file mode 100644 index 000000000..5b313973c --- /dev/null +++ b/cli/testdata/env-file/compose-with-env-files.yaml @@ -0,0 +1,9 @@ +version: "3" +services: + simple: + image: nginx + env_file: + - ./simple-env + - ./second-env + ports: + - ${PORT}:80 diff --git a/cli/testdata/env-file/override.env b/cli/testdata/env-file/override.env new file mode 100644 index 000000000..00ee76b17 --- /dev/null +++ b/cli/testdata/env-file/override.env @@ -0,0 +1 @@ +PORT=9000 diff --git a/cli/testdata/env-file/second-env b/cli/testdata/env-file/second-env new file mode 100644 index 000000000..3f5001b0d --- /dev/null +++ b/cli/testdata/env-file/second-env @@ -0,0 +1 @@ +DEFAULT_PORT=9090 diff --git a/cli/testdata/env-file/simple-env b/cli/testdata/env-file/simple-env index e2ba36873..d2a816549 100644 --- a/cli/testdata/env-file/simple-env +++ b/cli/testdata/env-file/simple-env @@ -1 +1 @@ -DEFAULT_PORT=8080 \ No newline at end of file +DEFAULT_PORT=8080