diff --git a/types/config.go b/types/config.go index 790b3871..020e67dd 100644 --- a/types/config.go +++ b/types/config.go @@ -18,11 +18,17 @@ package types import ( "encoding/json" + "runtime" + "strings" - "github.com/compose-spec/compose-go/utils" "github.com/mitchellh/mapstructure" ) +var ( + // isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively. + isCaseInsensitiveEnvVars = (runtime.GOOS == "windows") +) + // ConfigDetails are the details about a group of ConfigFiles type ConfigDetails struct { Version string @@ -33,8 +39,22 @@ type ConfigDetails struct { // LookupEnv provides a lookup function for environment variables func (cd ConfigDetails) LookupEnv(key string) (string, bool) { - v, ok := utils.EnvResolver(cd.Environment)(key) - return v, ok + v, ok := cd.Environment[key] + if !isCaseInsensitiveEnvVars || ok { + return v, ok + } + // variable names must be treated case-insensitively on some platforms (that is, Windows). + // Resolves in this way: + // * Return the value if its name matches with the passed name case-sensitively. + // * Otherwise, return the value if its lower-cased name matches lower-cased passed name. + // * The value is indefinite if multiple variables match. + lowerKey := strings.ToLower(key) + for k, v := range cd.Environment { + if strings.ToLower(k) == lowerKey { + return v, true + } + } + return "", false } // ConfigFile is a filename and the contents of the file as a Dict diff --git a/types/config_test.go b/types/config_test.go index 32ebc746..7743bf4a 100644 --- a/types/config_test.go +++ b/types/config_test.go @@ -53,3 +53,102 @@ func Test_WithServices(t *testing.T) { assert.NilError(t, err) assert.DeepEqual(t, order, []string{"service_2", "service_3", "service_1"}) } + +func Test_LookupEnv(t *testing.T) { + tests := []struct { + name string + environment map[string]string + caseInsensitive bool + search string + expectedValue string + expectedOk bool + }{ + { + name: "case sensitive/case match", + environment: map[string]string{ + "Env1": "Value1", + "Env2": "Value2", + }, + caseInsensitive: false, + search: "Env1", + expectedValue: "Value1", + expectedOk: true, + }, + { + name: "case sensitive/case unmatch", + environment: map[string]string{ + "Env1": "Value1", + "Env2": "Value2", + }, + caseInsensitive: false, + search: "ENV1", + expectedValue: "", + expectedOk: false, + }, + { + name: "case sensitive/nil environment", + environment: nil, + caseInsensitive: false, + search: "Env1", + expectedValue: "", + expectedOk: false, + }, + { + name: "case insensitive/case match", + environment: map[string]string{ + "Env1": "Value1", + "Env2": "Value2", + }, + caseInsensitive: true, + search: "Env1", + expectedValue: "Value1", + expectedOk: true, + }, + { + name: "case insensitive/case unmatch", + environment: map[string]string{ + "Env1": "Value1", + "Env2": "Value2", + }, + caseInsensitive: true, + search: "ENV1", + expectedValue: "Value1", + expectedOk: true, + }, + { + name: "case insensitive/unmatch", + environment: map[string]string{ + "Env1": "Value1", + "Env2": "Value2", + }, + caseInsensitive: true, + search: "Env3", + expectedValue: "", + expectedOk: false, + }, + { + name: "case insensitive/nil environment", + environment: nil, + caseInsensitive: true, + search: "Env1", + expectedValue: "", + expectedOk: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + origIsCaseInsensitiveEnvVars := isCaseInsensitiveEnvVars + defer func() { + isCaseInsensitiveEnvVars = origIsCaseInsensitiveEnvVars + }() + isCaseInsensitiveEnvVars = test.caseInsensitive + cd := ConfigDetails{ + Environment: test.environment, + } + v, ok := cd.LookupEnv(test.search) + assert.Equal(t, v, test.expectedValue) + assert.Equal(t, ok, test.expectedOk) + }) + } +} diff --git a/utils/envresolver.go b/utils/envresolver.go deleted file mode 100644 index d41d9adb..00000000 --- a/utils/envresolver.go +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright 2022 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 utils - -import ( - "runtime" - "strings" -) - -var ( - // isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively. - isCaseInsensitiveEnvVars = (runtime.GOOS == "windows") -) - -// EnvResolver returns resolver for environment variables suitable for the current platform. -// Expected to be used with `MappingWithEquals.Resolve`. -// Updates in environments may not be reflected. -func EnvResolver(environments map[string]string) func(string) (string, bool) { - return EnvResolverWithCase(environments, isCaseInsensitiveEnvVars) -} - -// EnvResolverWithCase returns resolver for environment variables with the specified case-sensitive condition. -// Expected to be used with `MappingWithEquals.Resolve`. -// Updates in environments may not be reflected. -func EnvResolverWithCase(environment map[string]string, caseInsensitive bool) func(string) (string, bool) { - if environment == nil { - return func(s string) (string, bool) { - return "", false - } - } - if !caseInsensitive { - return func(s string) (string, bool) { - v, ok := environment[s] - return v, ok - } - } - // variable names must be treated case-insensitively. - // Resolves in this way: - // * Return the value if its name matches with the passed name case-sensitively. - // * Otherwise, return the value if its lower-cased name matches lower-cased passed name. - // * The value is indefinite if multiple variable matches. - var loweredEnvironment map[string]string - return func(s string) (string, bool) { - v, ok := environment[s] - if ok { - return v, ok - } - if loweredEnvironment == nil { - // create only when it's necessary - newLoweredEnvironment := make(map[string]string, len(environment)) - for k, v := range environment { - newLoweredEnvironment[strings.ToLower(k)] = v - } - loweredEnvironment = newLoweredEnvironment - } - v, ok = loweredEnvironment[strings.ToLower(s)] - return v, ok - } -} diff --git a/utils/envresolver_test.go b/utils/envresolver_test.go deleted file mode 100644 index af72b015..00000000 --- a/utils/envresolver_test.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - Copyright 2022 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 utils - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func Test_EnvResolverWithCase(t *testing.T) { - tests := []struct { - name string - environment map[string]string - caseInsensitive bool - search string - expectedValue string - expectedOk bool - }{ - { - name: "case sensitive/case match", - environment: map[string]string{ - "Env1": "Value1", - "Env2": "Value2", - }, - caseInsensitive: false, - search: "Env1", - expectedValue: "Value1", - expectedOk: true, - }, - { - name: "case sensitive/case unmatch", - environment: map[string]string{ - "Env1": "Value1", - "Env2": "Value2", - }, - caseInsensitive: false, - search: "ENV1", - expectedValue: "", - expectedOk: false, - }, - { - name: "case sensitive/nil environment", - environment: nil, - caseInsensitive: false, - search: "Env1", - expectedValue: "", - expectedOk: false, - }, - { - name: "case insensitive/case match", - environment: map[string]string{ - "Env1": "Value1", - "Env2": "Value2", - }, - caseInsensitive: true, - search: "Env1", - expectedValue: "Value1", - expectedOk: true, - }, - { - name: "case insensitive/case unmatch", - environment: map[string]string{ - "Env1": "Value1", - "Env2": "Value2", - }, - caseInsensitive: true, - search: "ENV1", - expectedValue: "Value1", - expectedOk: true, - }, - { - name: "case insensitive/unmatch", - environment: map[string]string{ - "Env1": "Value1", - "Env2": "Value2", - }, - caseInsensitive: true, - search: "Env3", - expectedValue: "", - expectedOk: false, - }, - { - name: "case insensitive/nil environment", - environment: nil, - caseInsensitive: true, - search: "Env1", - expectedValue: "", - expectedOk: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - f := EnvResolverWithCase(test.environment, test.caseInsensitive) - v, ok := f(test.search) - assert.Equal(t, v, test.expectedValue) - assert.Equal(t, ok, test.expectedOk) - }) - } -}