From 5a2af60d90e777611c37e06b6b0a068fbe9cee45 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 1 Oct 2024 17:06:33 +0200 Subject: [PATCH] introduce ability to use custom env_file format Signed-off-by: Nicolas De Loof --- dotenv/fixtures/custom.format | 2 ++ dotenv/format.go | 38 +++++++++++++++++++++++++++++++ dotenv/godotenv.go | 6 ++--- dotenv/godotenv_test.go | 34 ++++++++++++++++++++++++++++ schema/compose-spec.json | 3 +++ types/envfile.go | 1 + types/project.go | 42 +++++++++++++++++++++++------------ 7 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 dotenv/fixtures/custom.format create mode 100644 dotenv/format.go diff --git a/dotenv/fixtures/custom.format b/dotenv/fixtures/custom.format new file mode 100644 index 00000000..6b5acf85 --- /dev/null +++ b/dotenv/fixtures/custom.format @@ -0,0 +1,2 @@ +FOO:BAR +ZOT:QIX \ No newline at end of file diff --git a/dotenv/format.go b/dotenv/format.go new file mode 100644 index 00000000..c583d212 --- /dev/null +++ b/dotenv/format.go @@ -0,0 +1,38 @@ +/* + Copyright 2020 The Compose Specification Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dotenv + +import ( + "fmt" + "io" +) + +var formats = map[string]Parser{} + +type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error) + +func RegisterFormat(format string, p Parser) { + formats[format] = p +} + +func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) { + parser, ok := formats[format] + if !ok { + return nil, fmt.Errorf("unsupported env_file format %q", format) + } + return parser(r, filename, resolve) +} diff --git a/dotenv/godotenv.go b/dotenv/godotenv.go index e6635ce3..76907249 100644 --- a/dotenv/godotenv.go +++ b/dotenv/godotenv.go @@ -86,7 +86,7 @@ func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string, envMap := make(map[string]string) for _, filename := range filenames { - individualEnvMap, individualErr := readFile(filename, lookupFn) + individualEnvMap, individualErr := ReadFile(filename, lookupFn) if individualErr != nil { return envMap, individualErr @@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string { } func loadFile(filename string, overload bool) error { - envMap, err := readFile(filename, nil) + envMap, err := ReadFile(filename, nil) if err != nil { return err } @@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error { return nil } -func readFile(filename string, lookupFn LookupFn) (map[string]string, error) { +func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) { file, err := os.Open(filename) if err != nil { return nil, err diff --git a/dotenv/godotenv_test.go b/dotenv/godotenv_test.go index 0d77906c..9e71b7a2 100644 --- a/dotenv/godotenv_test.go +++ b/dotenv/godotenv_test.go @@ -1,8 +1,10 @@ package dotenv import ( + "bufio" "bytes" "errors" + "io" "os" "path/filepath" "strings" @@ -708,3 +710,35 @@ func TestGetEnvFromFile(t *testing.T) { _, err = GetEnvFromFile(nil, []string{f}) assert.Check(t, strings.HasSuffix(err.Error(), ".env is a directory")) } + +func TestLoadWithFormat(t *testing.T) { + envFileName := "fixtures/custom.format" + expectedValues := map[string]string{ + "FOO": "BAR", + "ZOT": "QIX", + } + + custom := func(r io.Reader, f string, lookup func(key string) (string, bool)) (map[string]string, error) { + vars := map[string]string{} + scanner := bufio.NewScanner(r) + for scanner.Scan() { + key, value, found := strings.Cut(scanner.Text(), ":") + if !found { + value, found = lookup(key) + if !found { + continue + } + } + vars[key] = value + } + return vars, nil + } + + RegisterFormat("custom", custom) + + f, err := os.Open(envFileName) + assert.NilError(t, err) + env, err := ParseWithFormat(f, envFileName, nil, "custom") + assert.NilError(t, err) + assert.DeepEqual(t, expectedValues, env) +} diff --git a/schema/compose-spec.json b/schema/compose-spec.json index abd564f8..79d0a3f1 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -831,6 +831,9 @@ "path": { "type": "string" }, + "format": { + "type": "string" + }, "required": { "type": ["boolean", "string"], "default": true diff --git a/types/envfile.go b/types/envfile.go index f0fa7221..1348f132 100644 --- a/types/envfile.go +++ b/types/envfile.go @@ -23,6 +23,7 @@ import ( type EnvFile struct { Path string `yaml:"path,omitempty" json:"path,omitempty"` Required bool `yaml:"required" json:"required"` + Format string `yaml:"format,omitempty" json:"format,omitempty"` } // MarshalYAML makes EnvFile implement yaml.Marshaler diff --git a/types/project.go b/types/project.go index 7e3ef5f0..19d6e32b 100644 --- a/types/project.go +++ b/types/project.go @@ -616,22 +616,11 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project } for _, envFile := range service.EnvFiles { - if _, err := os.Stat(envFile.Path); os.IsNotExist(err) { - if envFile.Required { - return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err) - } - continue - } - b, err := os.ReadFile(envFile.Path) + vars, err := loadEnvFile(envFile, resolve) if err != nil { - return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err) + return nil, err } - - fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve) - if err != nil { - return nil, fmt.Errorf("failed to read %s: %w", envFile.Path, err) - } - environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals()) + environment.OverrideBy(vars.ToMappingWithEquals()) } service.Environment = environment.OverrideBy(service.Environment) @@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project return newProject, nil } +func loadEnvFile(envFile EnvFile, resolve dotenv.LookupFn) (Mapping, error) { + if _, err := os.Stat(envFile.Path); os.IsNotExist(err) { + if envFile.Required { + return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err) + } + return nil, nil + } + file, err := os.Open(envFile.Path) + if err != nil { + return nil, err + } + defer file.Close() //nolint:errcheck + + var fileVars map[string]string + if envFile.Format != "" { + fileVars, err = dotenv.ParseWithFormat(file, envFile.Path, resolve, envFile.Format) + } else { + fileVars, err = dotenv.ParseWithLookup(file, resolve) + } + if err != nil { + return nil, err + } + return fileVars, nil +} + func (p *Project) deepCopy() *Project { if p == nil { return nil