diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 41c85c587e0..995ada52f70 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -50,10 +50,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti opts := map[string]build.Options{} var imagesToBuild []string - args := flatten(options.Args.Resolve(func(s string) (string, bool) { - s, ok := project.Environment[s] - return s, ok - })) + args := flatten(options.Args.Resolve(envResolver(project.Environment))) services, err := project.GetServices(options.Services...) if err != nil { @@ -214,10 +211,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se var tags []string tags = append(tags, imageTag) - buildArgs := flatten(service.Build.Args.Resolve(func(s string) (string, bool) { - s, ok := project.Environment[s] - return s, ok - })) + buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment))) var plats []specs.Platform if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok { diff --git a/pkg/compose/envresolver.go b/pkg/compose/envresolver.go new file mode 100644 index 00000000000..0d12c3dc5ee --- /dev/null +++ b/pkg/compose/envresolver.go @@ -0,0 +1,68 @@ +/* + Copyright 2020 Docker Compose CLI 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 compose + +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 `environment` may not be reflected. +func envResolver(environment map[string]string) func(string) (string, bool) { + return envResolverWithCase(environment, isCaseInsensitiveEnvVars) +} + +// envResolverWithCase returns resolver for environment variables with the specified case-sensitive condition. +// Expected to be used with `MappingWithEquals.Resolve`. +// Updates in `environment` 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. + loweredEnvironment := make(map[string]string, len(environment)) + for k, v := range environment { + loweredEnvironment[strings.ToLower(k)] = v + } + return func(s string) (string, bool) { + v, ok := environment[s] + if ok { + return v, ok + } + v, ok = loweredEnvironment[strings.ToLower(s)] + return v, ok + } +} diff --git a/pkg/compose/envresolver_test.go b/pkg/compose/envresolver_test.go new file mode 100644 index 00000000000..791bd5fbb93 --- /dev/null +++ b/pkg/compose/envresolver_test.go @@ -0,0 +1,115 @@ +/* + Copyright 2020 Docker Compose CLI 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 compose + +import ( + "testing" + + "gotest.tools/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) + }) + } +} diff --git a/pkg/compose/run.go b/pkg/compose/run.go index 93db7bc9df9..2557e037221 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -116,7 +116,7 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts if len(opts.Environment) > 0 { cmdEnv := types.NewMappingWithEquals(opts.Environment) serviceOverrideEnv := cmdEnv.Resolve(func(s string) (string, bool) { - v, ok := project.Environment[s] + v, ok := envResolver(project.Environment)(s) return v, ok }).RemoveEmpty() service.Environment.OverrideBy(serviceOverrideEnv)