Repository: compose-spec/compose-go
Pull request #605 (2024-03-20 09:07:16)

allow COMPOSE_PROFILES to be set by a multiline string
closes https://github.com/docker/compose/issues/11631


Patch:
diff --git a/cli/options.go b/cli/options.go
index 6680939..2c5ee1a 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -217,7 +217,10 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
 // profiles specified via the COMPOSE_PROFILES environment variable otherwise.
 func WithDefaultProfiles(profile ...string) ProjectOptionsFn {
 	if len(profile) == 0 {
-		profile = strings.Split(os.Getenv(consts.ComposeProfiles), ",")
+		for _, s := range strings.Split(os.Getenv(consts.ComposeProfiles), ",") {
+			profile = append(profile, strings.TrimSpace(s))
+		}
+
 	}
 	return WithProfiles(profile)
 }
diff --git a/loader/loader.go b/loader/loader.go
index 1714889..1ed3977 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -554,6 +554,10 @@ func modelToProject(dict map[string]interface{}, opts *Options, configDetails ty
 		}
 	}
 
+	if project, err = project.WithProfiles(opts.Profiles); err != nil {
+		return nil, err
+	}
+
 	if !opts.SkipConsistencyCheck {
 		err := checkConsistency(project)
 		if err != nil {
@@ -561,10 +565,6 @@ func modelToProject(dict map[string]interface{}, opts *Options, configDetails ty
 		}
 	}
 
-	if project, err = project.WithProfiles(opts.Profiles); err != nil {
-		return nil, err
-	}
-
 	if !opts.SkipResolveEnvironment {
 		project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles)
 		if err != nil {
diff --git a/template/template.go b/template/template.go
index 63ae61c..0d9c17e 100644
--- a/template/template.go
+++ b/template/template.go
@@ -44,7 +44,7 @@ var patternString = fmt.Sprintf(
 	groupInvalid,
 )
 
-var defaultPattern = regexp.MustCompile(patternString)
+var DefaultPattern = regexp.MustCompile(patternString)
 
 // InvalidTemplateError is returned when a variable template is not in a valid
 // format
@@ -121,7 +121,7 @@ func SubstituteWithOptions(template string, mapping Mapping, options ...Option)
 	var returnErr error
 
 	cfg := &Config{
-		pattern:         defaultPattern,
+		pattern:         DefaultPattern,
 		replacementFunc: DefaultReplacementFunc,
 		logging:         true,
 	}
@@ -268,14 +268,14 @@ func getFirstBraceClosingIndex(s string) int {
 
 // Substitute variables in the string with their values
 func Substitute(template string, mapping Mapping) (string, error) {
-	return SubstituteWith(template, mapping, defaultPattern)
+	return SubstituteWith(template, mapping, DefaultPattern)
 }
 
 // ExtractVariables returns a map of all the variables defined in the specified
 // composefile (dict representation) and their default value if any.
 func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
 	if pattern == nil {
-		pattern = defaultPattern
+		pattern = DefaultPattern
 	}
 	return recurseExtract(configDict, pattern)
 }
diff --git a/types/project.go b/types/project.go
index c12e966..b9d18eb 100644
--- a/types/project.go
+++ b/types/project.go
@@ -332,6 +332,9 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
 		return true
 	}
 	for _, p := range profiles {
+		if p == "*" {
+			return true
+		}
 		for _, sp := range s.Profiles {
 			if sp == p {
 				return true
@@ -345,11 +348,6 @@ func (s ServiceConfig) HasProfile(profiles []string) bool {
 // It returns a new Project instance with the changes and keep the original Project unchanged
 func (p *Project) WithProfiles(profiles []string) (*Project, error) {
 	newProject := p.deepCopy()
-	for _, p := range profiles {
-		if p == "*" {
-			return newProject, nil
-		}
-	}
 	enabled := Services{}
 	disabled := Services{}
 	for name, service := range newProject.AllServices() {


Test patch:
diff --git a/template/template_test.go b/template/template_test.go
index 66fa3c5..b1afa21 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -366,15 +366,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
 		return value, true, nil
 	}
 
-	result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
+	result, err := SubstituteWith("ok ${FOO}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal("ok first", result))
 
-	result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
+	result, err = SubstituteWith("ok ${BAR}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal("ok ", result))
 
-	_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
+	_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, DefaultPattern, errIsMissing)
 	assert.Check(t, is.ErrorContains(err, "required variable"))
 }
 
@@ -594,7 +594,7 @@ func TestExtractVariables(t *testing.T) {
 	for _, tc := range testCases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			actual := ExtractVariables(tc.dict, defaultPattern)
+			actual := ExtractVariables(tc.dict, DefaultPattern)
 			assert.Check(t, is.DeepEqual(actual, tc.expected))
 		})
 	}