Repository: compose-spec/compose-go
Pull request #654 (2024-07-04 07:43:17)

keep empty environment variables as those must be UNSET in container
environment `FOO` with no value nor equal sign means "propagate value from user's environment, or unset variable"
So, as we resolve environment, empty value with no matching entry in user's environment MUST not result into removing entry from service definition.
Build args do not follow this rule. `--build-arg FOO` will propagate user's environment value, but if not set build arg will keep using value set by Dockerfile

fixes https://github.com/docker/compose/issues/11962
supersede https://github.com/compose-spec/compose-go/pull/634
see https://github.com/docker/compose/pull/11965


Patch:
diff --git a/loader/normalize.go b/loader/normalize.go
index 5f7c8e8..e99b61b 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -52,14 +52,14 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
 				}
 
 				if a, ok := build["args"]; ok {
-					build["args"], _ = resolve(a, fn)
+					build["args"], _ = resolve(a, fn, false)
 				}
 
 				service["build"] = build
 			}
 
 			if e, ok := service["environment"]; ok {
-				service["environment"], _ = resolve(e, fn)
+				service["environment"], _ = resolve(e, fn, true)
 			}
 
 			var dependsOn map[string]any
@@ -178,12 +178,12 @@ func normalizeNetworks(dict map[string]any) {
 	}
 }
 
-func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
+func resolve(a any, fn func(s string) (string, bool), keepEmpty bool) (any, bool) {
 	switch v := a.(type) {
 	case []any:
 		var resolved []any
 		for _, val := range v {
-			if r, ok := resolve(val, fn); ok {
+			if r, ok := resolve(val, fn, keepEmpty); ok {
 				resolved = append(resolved, r)
 			}
 		}
@@ -197,6 +197,8 @@ func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
 			}
 			if s, ok := fn(key); ok {
 				resolved[key] = s
+			} else if keepEmpty {
+				resolved[key] = nil
 			}
 		}
 		return resolved, true
@@ -205,6 +207,9 @@ func resolve(a any, fn func(s string) (string, bool)) (any, bool) {
 			if val, ok := fn(v); ok {
 				return fmt.Sprintf("%s=%s", v, val), true
 			}
+			if keepEmpty {
+				return v, true
+			}
 			return "", false
 		}
 		return v, true


Test patch:
diff --git a/loader/normalize_test.go b/loader/normalize_test.go
index 5e41d50..d85d0b1 100644
--- a/loader/normalize_test.go
+++ b/loader/normalize_test.go
@@ -336,3 +336,43 @@ services:
 	assert.NilError(t, err)
 	assert.DeepEqual(t, expect, model)
 }
+
+func TestNormalizeEnvironment(t *testing.T) {
+	project := `
+name: myProject
+services:  
+  test: 
+    environment:
+      - FOO
+      - BAR
+      - ZOT=QIX
+`
+
+	expected := `
+name: myProject
+networks:
+  default:
+    name: myProject_default
+services:  
+  test: 
+    environment:
+      - FOO
+      - BAR=bar
+      - ZOT=QIX
+    networks:
+      default: null
+`
+	var model map[string]any
+	err := yaml.Unmarshal([]byte(project), &model)
+	assert.NilError(t, err)
+	model, err = Normalize(model, map[string]string{
+		"BAR": "bar",
+		"ZOT": "zot",
+	})
+	assert.NilError(t, err)
+
+	var expect map[string]any
+	err = yaml.Unmarshal([]byte(expected), &expect)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, expect, model)
+}