From f63fde434b7ad621efca4ed13b55aa9140ce13dd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 4 Nov 2025 09:36:00 +0100 Subject: [PATCH 1/9] fix apply !override while processing extends on services declared in same file Signed-off-by: Nicolas De Loof --- loader/extends.go | 25 ++++---- loader/loader.go | 12 ++-- loader/loader_test.go | 62 -------------------- loader/override_test.go | 124 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 82 deletions(-) diff --git a/loader/extends.go b/loader/extends.go index c4fd0be2..d85e84ba 100644 --- a/loader/extends.go +++ b/loader/extends.go @@ -27,7 +27,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" ) -func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, tracker *cycleTracker, post ...PostProcessor) error { +func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, tracker *cycleTracker, post PostProcessor) error { a, ok := dict["services"] if !ok { return nil @@ -37,7 +37,7 @@ func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, track return fmt.Errorf("services must be a mapping") } for name := range services { - merged, err := applyServiceExtends(ctx, name, services, opts, tracker, post...) + merged, err := applyServiceExtends(ctx, name, services, opts, tracker, post) if err != nil { return err } @@ -47,7 +47,7 @@ func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, track return nil } -func applyServiceExtends(ctx context.Context, name string, services map[string]any, opts *Options, tracker *cycleTracker, post ...PostProcessor) (any, error) { +func applyServiceExtends(ctx context.Context, name string, services map[string]any, opts *Options, tracker *cycleTracker, post PostProcessor) (any, error) { s := services[name] if s == nil { return nil, nil @@ -81,7 +81,7 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a var ( base any - processor PostProcessor = NoopPostProcessor{} + processor = post ) if file != nil { @@ -114,16 +114,15 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a } source := deepClone(base).(map[string]any) - for _, processor := range post { - err = processor.Apply(map[string]any{ - "services": map[string]any{ - name: source, - }, - }) - if err != nil { - return nil, err - } + err = post.Apply(map[string]any{ + "services": map[string]any{ + name: source, + }, + }) + if err != nil { + return nil, err } + merged, err := override.ExtendService(source, service) if err != nil { return nil, err diff --git a/loader/loader.go b/loader/loader.go index 99b15364..91f687f9 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -427,7 +427,7 @@ func loadYamlFile(ctx context.Context, file.Content = content } - processRawYaml := func(raw interface{}, processors ...PostProcessor) error { + processRawYaml := func(raw interface{}, processor PostProcessor) error { converted, err := convertToStringKeysRecursive(raw, "") if err != nil { return err @@ -447,16 +447,14 @@ func loadYamlFile(ctx context.Context, fixEmptyNotNull(cfg) if !opts.SkipExtends { - err = ApplyExtends(ctx, cfg, opts, ct, processors...) + err = ApplyExtends(ctx, cfg, opts, ct, processor) if err != nil { return err } } - for _, processor := range processors { - if err := processor.Apply(dict); err != nil { - return err - } + if err := processor.Apply(dict); err != nil { + return err } if !opts.SkipInclude { @@ -519,7 +517,7 @@ func loadYamlFile(ctx context.Context, } } } else { - if err := processRawYaml(file.Config); err != nil { + if err := processRawYaml(file.Config, NoopPostProcessor{}); err != nil { return nil, nil, err } } diff --git a/loader/loader_test.go b/loader/loader_test.go index 101afa3d..66449711 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3917,65 +3917,3 @@ services: assert.Equal(t, build.Provenance, "mode=max") assert.Equal(t, build.SBOM, "true") } - -func TestOverrideMiddle(t *testing.T) { - pwd := t.TempDir() - base := filepath.Join(pwd, "base.yaml") - err := os.WriteFile(base, []byte(` -services: - base: - volumes: - - /foo:/foo -`), 0o700) - assert.NilError(t, err) - - override := filepath.Join(pwd, "override.yaml") - err = os.WriteFile(override, []byte(` -services: - override: - extends: - file: ./base.yaml - service: base - volumes: !override - - /bar:/bar -`), 0o700) - assert.NilError(t, err) - - compose := filepath.Join(pwd, "compose.yaml") - err = os.WriteFile(compose, []byte(` -name: test -services: - test: - image: test - extends: - file: ./override.yaml - service: override - volumes: - - /zot:/zot -`), 0o700) - assert.NilError(t, err) - - project, err := LoadWithContext(context.TODO(), types.ConfigDetails{ - WorkingDir: pwd, - ConfigFiles: []types.ConfigFile{ - {Filename: compose}, - }, - }) - assert.NilError(t, err) - test := project.Services["test"] - assert.Equal(t, len(test.Volumes), 2) - assert.DeepEqual(t, test.Volumes, []types.ServiceVolumeConfig{ - { - Type: "bind", - Source: "/bar", - Target: "/bar", - Bind: &types.ServiceVolumeBind{CreateHostPath: true}, - }, - { - Type: "bind", - Source: "/zot", - Target: "/zot", - Bind: &types.ServiceVolumeBind{CreateHostPath: true}, - }, - }) -} diff --git a/loader/override_test.go b/loader/override_test.go index 035fb296..7a4b62e3 100644 --- a/loader/override_test.go +++ b/loader/override_test.go @@ -18,6 +18,8 @@ package loader import ( "context" + "os" + "path/filepath" "testing" "github.com/compose-spec/compose-go/v2/types" @@ -197,3 +199,125 @@ services: assert.NilError(t, err) assert.Equal(t, len(p.Services["test"].Volumes), 1) } + +// see https://github.com/docker/compose/issues/13298 +func TestOverrideMiddle(t *testing.T) { + pwd := t.TempDir() + base := filepath.Join(pwd, "base.yaml") + err := os.WriteFile(base, []byte(` +services: + base: + volumes: + - /foo:/foo + networks: + - foo +`), 0o700) + assert.NilError(t, err) + + override := filepath.Join(pwd, "override.yaml") + err = os.WriteFile(override, []byte(` +services: + override: + extends: + file: ./base.yaml + service: base + volumes: !override + - /bar:/bar + networks: !override + - bar +`), 0o700) + assert.NilError(t, err) + + compose := filepath.Join(pwd, "compose.yaml") + err = os.WriteFile(compose, []byte(` +name: test +services: + test: + image: test + extends: + file: ./override.yaml + service: override + volumes: + - /zot:/zot + networks: !override + - zot + +networks: + zot: {} +`), 0o700) + assert.NilError(t, err) + + project, err := LoadWithContext(context.TODO(), types.ConfigDetails{ + WorkingDir: pwd, + ConfigFiles: []types.ConfigFile{ + {Filename: compose}, + }, + }) + assert.NilError(t, err) + test := project.Services["test"] + assert.Equal(t, len(test.Volumes), 2) + assert.DeepEqual(t, test.Volumes, []types.ServiceVolumeConfig{ + { + Type: "bind", + Source: "/bar", + Target: "/bar", + Bind: &types.ServiceVolumeBind{CreateHostPath: true}, + }, + { + Type: "bind", + Source: "/zot", + Target: "/zot", + Bind: &types.ServiceVolumeBind{CreateHostPath: true}, + }, + }) + assert.DeepEqual(t, test.Networks, map[string]*types.ServiceNetworkConfig{ + "zot": nil, + }) +} + +// https://github.com/docker/compose/issues/13346 +func TestOverrideSelfExtends(t *testing.T) { + yaml := ` +name: test-override-extends +services: + depend_base: + image: nginx + ports: + - "8092:80" + depend_one: + image: nginx + ports: + - "8091:80" + depend_two: + extends: + service: depend_one + main_one: + image: nginx + depends_on: + - depend_one + ports: + - "8090:80" + main_two: + extends: main_one + depends_on: !override + - depend_two + main: + extends: + service: main_two + depends_on: + - depend_base +` + p, err := LoadWithContext(context.Background(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + { + Filename: "-", + Content: []byte(yaml), + }, + }, + }) + assert.NilError(t, err) + assert.DeepEqual(t, p.Services["main"].DependsOn, types.DependsOnConfig{ + "depend_base": {Condition: "service_started", Required: true}, + "depend_two": {Condition: "service_started", Required: true}, + }) +} From ea0b69cf3d1d7cc5c888551996b45fc1f66b4479 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 4 Nov 2025 15:33:56 +0100 Subject: [PATCH 2/9] introduce no-cache-filter Signed-off-by: Nicolas De Loof --- loader/loader_test.go | 18 +++++++++++++++ schema/compose-spec.json | 1 + types/build.go | 48 ++++++++++++++++++++++++++++++++++++++++ types/derived.gen.go | 18 +++++++++++++++ types/types.go | 30 ------------------------- 5 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 types/build.go diff --git a/loader/loader_test.go b/loader/loader_test.go index 66449711..144a77f7 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3917,3 +3917,21 @@ services: assert.Equal(t, build.Provenance, "mode=max") assert.Equal(t, build.SBOM, "true") } + +func TestNoCacheFilter(t *testing.T) { + p, err := loadYAML(` +name: no-cache-filter +services: + string: + build: + context: . + no_cache_filter: foo + list: + build: + context: . + no_cache_filter: [foo, bar] +`) + assert.NilError(t, err) + assert.DeepEqual(t, p.Services["string"].Build.NoCacheFilter, types.StringList{"foo"}) + assert.DeepEqual(t, p.Services["list"].Build.NoCacheFilter, types.StringList{"foo", "bar"}) +} diff --git a/schema/compose-spec.json b/schema/compose-spec.json index 15e818dc..462de285 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -121,6 +121,7 @@ "cache_from": {"type": "array", "items": {"type": "string"}, "description": "List of sources the image builder should use for cache resolution"}, "cache_to": {"type": "array", "items": {"type": "string"}, "description": "Cache destinations for the build cache."}, "no_cache": {"type": ["boolean", "string"], "description": "Do not use cache when building the image."}, + "no_cache_filter": {"$ref": "#/definitions/string_or_list", "description": "Do not use build cache for the specified stages."}, "additional_contexts": {"$ref": "#/definitions/list_or_dict", "description": "Additional build contexts to use, specified as a map of name to context path or URL."}, "network": {"type": "string", "description": "Network mode to use for the build. Options include 'default', 'none', 'host', or a network name."}, "provenance": {"type": ["string","boolean"], "description": "Add a provenance attestation"}, diff --git a/types/build.go b/types/build.go new file mode 100644 index 00000000..98931400 --- /dev/null +++ b/types/build.go @@ -0,0 +1,48 @@ +/* + 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 types + +// BuildConfig is a type for build +type BuildConfig struct { + Context string `yaml:"context,omitempty" json:"context,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"` + Entitlements []string `yaml:"entitlements,omitempty" json:"entitlements,omitempty"` + Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"` + Provenance string `yaml:"provenance,omitempty" json:"provenance,omitempty"` + SBOM string `yaml:"sbom,omitempty" json:"sbom,omitempty"` + SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"` + Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + CacheFrom StringList `yaml:"cache_from,omitempty" json:"cache_from,omitempty"` + CacheTo StringList `yaml:"cache_to,omitempty" json:"cache_to,omitempty"` + NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"` + NoCacheFilter StringList `yaml:"no_cache_filter,omitempty" json:"no_cache_filter,omitempty"` + AdditionalContexts Mapping `yaml:"additional_contexts,omitempty" json:"additional_contexts,omitempty"` + Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"` + ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` + Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` + Network string `yaml:"network,omitempty" json:"network,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` + ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` + Tags StringList `yaml:"tags,omitempty" json:"tags,omitempty"` + Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"` + Platforms StringList `yaml:"platforms,omitempty" json:"platforms,omitempty"` + Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` + + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} diff --git a/types/derived.gen.go b/types/derived.gen.go index 3857b4bb..e284fa9f 100644 --- a/types/derived.gen.go +++ b/types/derived.gen.go @@ -938,6 +938,24 @@ func deriveDeepCopy_6(dst, src *BuildConfig) { copy(dst.CacheTo, src.CacheTo) } dst.NoCache = src.NoCache + if src.NoCacheFilter == nil { + dst.NoCacheFilter = nil + } else { + if dst.NoCacheFilter != nil { + if len(src.NoCacheFilter) > len(dst.NoCacheFilter) { + if cap(dst.NoCacheFilter) >= len(src.NoCacheFilter) { + dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] + } else { + dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) + } + } else if len(src.NoCacheFilter) < len(dst.NoCacheFilter) { + dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] + } + } else { + dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) + } + copy(dst.NoCacheFilter, src.NoCacheFilter) + } if src.AdditionalContexts != nil { dst.AdditionalContexts = make(map[string]string, len(src.AdditionalContexts)) deriveDeepCopy_5(dst.AdditionalContexts, src.AdditionalContexts) diff --git a/types/types.go b/types/types.go index b73d9340..b8630f48 100644 --- a/types/types.go +++ b/types/types.go @@ -302,36 +302,6 @@ func (s ServiceConfig) GetPullPolicy() (string, time.Duration, error) { } } -// BuildConfig is a type for build -type BuildConfig struct { - Context string `yaml:"context,omitempty" json:"context,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` - DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"` - Entitlements []string `yaml:"entitlements,omitempty" json:"entitlements,omitempty"` - Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"` - Provenance string `yaml:"provenance,omitempty" json:"provenance,omitempty"` - SBOM string `yaml:"sbom,omitempty" json:"sbom,omitempty"` - SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"` - Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` - CacheFrom StringList `yaml:"cache_from,omitempty" json:"cache_from,omitempty"` - CacheTo StringList `yaml:"cache_to,omitempty" json:"cache_to,omitempty"` - NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"` - AdditionalContexts Mapping `yaml:"additional_contexts,omitempty" json:"additional_contexts,omitempty"` - Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"` - ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` - Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` - Network string `yaml:"network,omitempty" json:"network,omitempty"` - Target string `yaml:"target,omitempty" json:"target,omitempty"` - Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` - ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` - Tags StringList `yaml:"tags,omitempty" json:"tags,omitempty"` - Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"` - Platforms StringList `yaml:"platforms,omitempty" json:"platforms,omitempty"` - Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` - - Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` -} - // BlkioConfig define blkio config type BlkioConfig struct { Weight uint16 `yaml:"weight,omitempty" json:"weight,omitempty"` From 1b2139eefd8a7f8bb652fedc37295f1b5bc64447 Mon Sep 17 00:00:00 2001 From: Suleiman Dibirov Date: Fri, 31 Oct 2025 20:44:27 +0200 Subject: [PATCH 3/9] feat(interpolate): Add cast to bool for 'depends_on' required and restart options Signed-off-by: Suleiman Dibirov --- interpolation/interpolation_test.go | 19 ++++++++++++++----- loader/interpolate.go | 2 ++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/interpolation/interpolation_test.go b/interpolation/interpolation_test.go index f9860ca7..8a4b8b17 100644 --- a/interpolation/interpolation_test.go +++ b/interpolation/interpolation_test.go @@ -27,9 +27,10 @@ import ( ) var defaults = map[string]string{ - "USER": "jenny", - "FOO": "bar", - "count": "5", + "USER": "jenny", + "FOO": "bar", + "count": "5", + "REQUIRED": "true", } func defaultMapping(name string) (string, bool) { @@ -196,19 +197,27 @@ func TestInterpolateWithCast(t *testing.T) { config := map[string]interface{}{ "foo": map[string]interface{}{ "replicas": "$count", + "required": "$REQUIRED", }, } toInt := func(value string) (interface{}, error) { return strconv.Atoi(value) } + toBoolean := func(value string) (interface{}, error) { + return strconv.ParseBool(value) + } result, err := Interpolate(config, Options{ - LookupValue: defaultMapping, - TypeCastMapping: map[tree.Path]Cast{tree.NewPath(tree.PathMatchAll, "replicas"): toInt}, + LookupValue: defaultMapping, + TypeCastMapping: map[tree.Path]Cast{ + tree.NewPath(tree.PathMatchAll, "replicas"): toInt, + tree.NewPath(tree.PathMatchAll, "required"): toBoolean, + }, }) assert.NilError(t, err) expected := map[string]interface{}{ "foo": map[string]interface{}{ "replicas": 5, + "required": true, }, } assert.Check(t, is.DeepEqual(expected, result)) diff --git a/loader/interpolate.go b/loader/interpolate.go index 491de5bd..dc8dc735 100644 --- a/loader/interpolate.go +++ b/loader/interpolate.go @@ -36,6 +36,8 @@ var interpolateTypeCastMapping = map[tree.Path]interp.Cast{ servicePath("cpus"): toFloat32, servicePath("cpu_shares"): toInt64, servicePath("init"): toBoolean, + servicePath("depends_on", tree.PathMatchAll, "required"): toBoolean, + servicePath("depends_on", tree.PathMatchAll, "restart"): toBoolean, servicePath("deploy", "replicas"): toInt, servicePath("deploy", "update_config", "parallelism"): toInt, servicePath("deploy", "update_config", "max_failure_ratio"): toFloat, From 4405e6802a44c3faab8b18afb771a62a1b1775f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 05:10:05 +0000 Subject: [PATCH 4/9] Bump golangci/golangci-lint-action from 8 to 9 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 8 to 9. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf70f50d..d71ddc3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true cache: true - - uses: golangci/golangci-lint-action@v8 + - uses: golangci/golangci-lint-action@v9 with: version: v2.1.6 args: --verbose From dabd353441f599227cb3ec240113af6c1fa648ca Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 6 Nov 2025 12:31:01 +0100 Subject: [PATCH 5/9] create_host_path must not be omitted when set to false Signed-off-by: Nicolas De Loof --- .github/workflows/ci.yml | 2 +- go.mod | 2 +- loader/full-struct_test.go | 34 ++++++++++++---------------------- schema/schema.go | 21 ++++++++++++++++++--- types/envfile.go | 26 +------------------------- types/types.go | 10 +++++++++- types/types_test.go | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d71ddc3a..ab0f2eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: test: strategy: matrix: - go-version: ['1.23', '1.24'] + go-version: ['1.24', '1.25'] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} timeout-minutes: 10 diff --git a/go.mod b/go.mod index f2e1f95b..99836cde 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/compose-spec/compose-go/v2 -go 1.23 +go 1.24 require ( github.com/distribution/reference v0.5.0 diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go index 07e258d6..77e05f08 100644 --- a/loader/full-struct_test.go +++ b/loader/full-struct_test.go @@ -755,7 +755,7 @@ services: FOO: foo_from_env_file QUX: qux_from_environment env_file: - - %s + - path: %s - path: %s required: false expose: @@ -949,24 +949,20 @@ services: - type: bind source: /opt/data target: /var/lib/data - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /code - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /var/www/html - bind: - create_host_path: true + bind: {} - type: bind source: %s target: /etc/configs read_only: true - bind: - create_host_path: true + bind: {} - type: volume source: datavolume target: /var/lib/volume @@ -1370,7 +1366,9 @@ func fullExampleJSON(workingDir, homeDir string) string { "QUX": "qux_from_environment" }, "env_file": [ - "%s", + { + "path": "%s" + }, { "path": "%s", "required": false @@ -1640,34 +1638,26 @@ func fullExampleJSON(workingDir, homeDir string) string { "type": "bind", "source": "/opt/data", "target": "/var/lib/data", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/code", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/var/www/html", - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "bind", "source": "%s", "target": "/etc/configs", "read_only": true, - "bind": { - "create_host_path": true - } + "bind": {} }, { "type": "volume", diff --git a/schema/schema.go b/schema/schema.go index 8aa7ccf6..a73eda24 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -19,6 +19,7 @@ package schema import ( // Enable support for embedded static resources _ "embed" + "encoding/json" "errors" "fmt" "slices" @@ -48,11 +49,11 @@ var Schema string // Validate uses the jsonschema to validate the configuration func Validate(config map[string]interface{}) error { compiler := jsonschema.NewCompiler() - json, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) + shema, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) if err != nil { return err } - err = compiler.AddResource("compose-spec.json", json) + err = compiler.AddResource("compose-spec.json", shema) if err != nil { return err } @@ -61,7 +62,21 @@ func Validate(config map[string]interface{}) error { Validate: durationFormatChecker, }) schema := compiler.MustCompile("compose-spec.json") - err = schema.Validate(config) + + // santhosh-tekuri doesn't allow derived types + // see https://github.com/santhosh-tekuri/jsonschema/pull/240 + marshaled, err := json.Marshal(config) + if err != nil { + return err + } + + var raw map[string]interface{} + err = json.Unmarshal(marshaled, &raw) + if err != nil { + return err + } + + err = schema.Validate(raw) var verr *jsonschema.ValidationError if ok := errors.As(err, &verr); ok { return validationError{getMostSpecificError(verr)} diff --git a/types/envfile.go b/types/envfile.go index 1348f132..a7d239ee 100644 --- a/types/envfile.go +++ b/types/envfile.go @@ -16,32 +16,8 @@ package types -import ( - "encoding/json" -) - type EnvFile struct { Path string `yaml:"path,omitempty" json:"path,omitempty"` - Required bool `yaml:"required" json:"required"` + Required OptOut `yaml:"required,omitempty" json:"required,omitzero"` Format string `yaml:"format,omitempty" json:"format,omitempty"` } - -// MarshalYAML makes EnvFile implement yaml.Marshaler -func (e EnvFile) MarshalYAML() (interface{}, error) { - if e.Required { - return e.Path, nil - } - return map[string]any{ - "path": e.Path, - "required": e.Required, - }, nil -} - -// MarshalJSON makes EnvFile implement json.Marshaler -func (e *EnvFile) MarshalJSON() ([]byte, error) { - if e.Required { - return json.Marshal(e.Path) - } - // Pass as a value to avoid re-entering this method and use the default implementation - return json.Marshal(*e) -} diff --git a/types/types.go b/types/types.go index b8630f48..fd4f3513 100644 --- a/types/types.go +++ b/types/types.go @@ -564,12 +564,20 @@ const ( type ServiceVolumeBind struct { SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"` Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"` - CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"` + CreateHostPath OptOut `yaml:"create_host_path,omitempty" json:"create_host_path,omitzero"` Recursive string `yaml:"recursive,omitempty" json:"recursive,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } +// OptOut is a boolean which default value is 'true' +type OptOut bool + +func (o OptOut) IsZero() bool { + // Attribute can be omitted if value is true + return bool(o) +} + // SELinux represents the SELinux re-labeling options. const ( // SELinuxShared option indicates that the bind mount content is shared among multiple containers diff --git a/types/types_test.go b/types/types_test.go index f79ceac0..874079df 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -380,3 +380,37 @@ func TestMappingValues(t *testing.T) { }) assert.DeepEqual(t, mapping.Values(), values) } + +func TestMarhsall(t *testing.T) { + p := Project{ + Services: Services{ + "test": ServiceConfig{ + Volumes: []ServiceVolumeConfig{ + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: true, // default + }, + }, + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: false, + }, + }, + }, + }, + }, + } + marshalYAML, err := p.MarshalYAML() + assert.NilError(t, err) + assert.Equal(t, string(marshalYAML), `services: + test: + volumes: + - type: bind + bind: {} + - type: bind + bind: + create_host_path: false +`) +} From 61f9ceae27da038bc40c37e44ce0dfef3086d04c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 12 Nov 2025 14:30:24 +0100 Subject: [PATCH 6/9] use go.yaml.in/yaml/v4 Signed-off-by: Nicolas De Loof --- cli/options.go | 2 +- cmd/main.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- loader/loader.go | 2 +- loader/merge_reset_test.go | 2 +- loader/normalize_test.go | 2 +- loader/reset.go | 2 +- loader/reset_test.go | 4 ++-- override/merge_test.go | 2 +- schema/schema_test.go | 2 +- transform/defaults_test.go | 2 +- transform/envfile_test.go | 2 +- types/command.go | 4 ++-- types/project.go | 2 +- types/types_test.go | 2 +- validation/validation_test.go | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cli/options.go b/cli/options.go index 13a38242..69ea5654 100644 --- a/cli/options.go +++ b/cli/options.go @@ -25,7 +25,7 @@ import ( "strings" "github.com/sirupsen/logrus" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/dotenv" diff --git a/cmd/main.go b/cmd/main.go index e2069096..bbc2971c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,7 +24,7 @@ import ( "os" "github.com/compose-spec/compose-go/v2/cli" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" ) func main() { diff --git a/go.mod b/go.mod index 99836cde..8a2e93e2 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.4 github.com/xhit/go-str2duration/v2 v2.1.0 - go.yaml.in/yaml/v3 v3.0.4 + go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/sync v0.3.0 golang.org/x/text v0.14.0 gotest.tools/v3 v3.4.0 diff --git a/go.sum b/go.sum index e2bc70d3..ba13dee5 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/loader/loader.go b/loader/loader.go index 91f687f9..b2439c38 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -43,7 +43,7 @@ import ( "github.com/compose-spec/compose-go/v2/validation" "github.com/go-viper/mapstructure/v2" "github.com/sirupsen/logrus" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" ) // Options supported by Load diff --git a/loader/merge_reset_test.go b/loader/merge_reset_test.go index fcb2aaaf..f721b5f8 100644 --- a/loader/merge_reset_test.go +++ b/loader/merge_reset_test.go @@ -79,5 +79,5 @@ func Test_DuplicateReset(t *testing.T) { }, func(options *Options) { options.SkipNormalization = true }) - assert.Error(t, err, "failed to parse duplicate.yml: line 6: mapping key \"command\" already defined at line 5") + assert.ErrorContains(t, err, "line 6: mapping key \"command\" already defined at line 5") } diff --git a/loader/normalize_test.go b/loader/normalize_test.go index eb2c9e46..80e13d25 100644 --- a/loader/normalize_test.go +++ b/loader/normalize_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/types" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) diff --git a/loader/reset.go b/loader/reset.go index ea98dd9b..ed1fc0c3 100644 --- a/loader/reset.go +++ b/loader/reset.go @@ -22,7 +22,7 @@ import ( "strings" "github.com/compose-spec/compose-go/v2/tree" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" ) type ResetProcessor struct { diff --git a/loader/reset_test.go b/loader/reset_test.go index 2c51dee3..879b1288 100644 --- a/loader/reset_test.go +++ b/loader/reset_test.go @@ -163,7 +163,7 @@ x-healthcheck: &healthcheck <<: *healthcheck `, expectError: true, - errorMsg: "failed to parse (inline): cycle detected: node at path x-healthcheck.egress-service.egress-service references node at path x-healthcheck.egress-service", + errorMsg: "cycle detected: node at path x-healthcheck.egress-service.egress-service references node at path x-healthcheck.egress-service", }, } @@ -185,7 +185,7 @@ x-healthcheck: &healthcheck ) if tt.expectError { - assert.Error(t, err, tt.errorMsg) + assert.ErrorContains(t, err, tt.errorMsg) } else { assert.NilError(t, err) } diff --git a/override/merge_test.go b/override/merge_test.go index 53bf7611..ac1d47fd 100644 --- a/override/merge_test.go +++ b/override/merge_test.go @@ -19,7 +19,7 @@ package override import ( "testing" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) diff --git a/schema/schema_test.go b/schema/schema_test.go index 0e462342..fb35f41d 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -22,7 +22,7 @@ import ( "testing" "github.com/santhosh-tekuri/jsonschema/v6" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) diff --git a/transform/defaults_test.go b/transform/defaults_test.go index 89beeb67..1e6bee22 100644 --- a/transform/defaults_test.go +++ b/transform/defaults_test.go @@ -22,7 +22,7 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/tree" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) diff --git a/transform/envfile_test.go b/transform/envfile_test.go index b6a50513..97b2012d 100644 --- a/transform/envfile_test.go +++ b/transform/envfile_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/tree" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) diff --git a/types/command.go b/types/command.go index 7e56ce23..559dc305 100644 --- a/types/command.go +++ b/types/command.go @@ -34,7 +34,7 @@ import "github.com/mattn/go-shellwords" // preserved so that it can override any base value (e.g. container entrypoint). // // The different semantics between YAML and JSON are due to limitations with -// JSON marshaling + `omitempty` in the Go stdlib, while go.yaml.in/yaml/v3 gives +// JSON marshaling + `omitempty` in the Go stdlib, while go.yaml.in/yaml/v4 gives // us more flexibility via the yaml.IsZeroer interface. // // In the future, it might make sense to make fields of this type be @@ -58,7 +58,7 @@ func (s ShellCommand) IsZero() bool { // accurately if the `omitempty` struct tag is omitted/forgotten. // // A similar MarshalJSON() implementation is not needed because the Go stdlib -// already serializes nil slices to `null`, whereas go.yaml.in/yaml/v3 by default +// already serializes nil slices to `null`, whereas go.yaml.in/yaml/v4 by default // serializes nil slices to `[]`. func (s ShellCommand) MarshalYAML() (interface{}, error) { if s == nil { diff --git a/types/project.go b/types/project.go index a0d363cf..58330e8d 100644 --- a/types/project.go +++ b/types/project.go @@ -32,7 +32,7 @@ import ( "github.com/compose-spec/compose-go/v2/utils" "github.com/distribution/reference" godigest "github.com/opencontainers/go-digest" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "golang.org/x/sync/errgroup" ) diff --git a/types/types_test.go b/types/types_test.go index 874079df..2d03daa7 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -23,7 +23,7 @@ import ( "strings" "testing" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" diff --git a/validation/validation_test.go b/validation/validation_test.go index bc8f8dbf..14b71c59 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/tree" - "go.yaml.in/yaml/v3" + "go.yaml.in/yaml/v4" "gotest.tools/v3/assert" ) From 2837330ff2764cbb854b124a7d897f359b2d645f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 05:11:54 +0000 Subject: [PATCH 7/9] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab0f2eae..4e3e1d56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check license run: DOCKER_BUILDKIT=1 make check-license - name: Check deepcopy @@ -30,7 +30,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9308bc8..0a29a79f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download artifacts uses: actions/download-artifact@v6 From dbfe61974b3f20155aaf2907d95251972b097845 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 13 Nov 2025 11:05:38 +0100 Subject: [PATCH 8/9] Introduced ability to skip env_file parsing with include Signed-off-by: Nicolas De Loof --- loader/include.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/loader/include.go b/loader/include.go index 3e49b8d8..bd0e45ee 100644 --- a/loader/include.go +++ b/loader/include.go @@ -117,6 +117,9 @@ func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapp } else { envFile := []string{} for _, f := range r.EnvFile { + if f == "/dev/null" { + continue + } if !filepath.IsAbs(f) { f = filepath.Join(workingDir, f) s, err := os.Stat(f) From 3d3411d9d856d55396d121b4e20bbe7b954932e9 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 6 Nov 2025 08:34:22 +0100 Subject: [PATCH 9/9] Allow importing compose file to override resource definition Signed-off-by: Nicolas De Loof --- loader/include.go | 47 ++++++++++++++++++---------- loader/include_test.go | 13 ++++++-- loader/loader.go | 2 +- loader/testdata/compose-include.yaml | 4 ++- override/extends.go | 2 +- override/merge.go | 8 ++--- 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/loader/include.go b/loader/include.go index bd0e45ee..ff310447 100644 --- a/loader/include.go +++ b/loader/include.go @@ -21,11 +21,12 @@ import ( "fmt" "os" "path/filepath" - "reflect" "strings" "github.com/compose-spec/compose-go/v2/dotenv" interp "github.com/compose-spec/compose-go/v2/interpolation" + "github.com/compose-spec/compose-go/v2/override" + "github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/types" ) @@ -50,7 +51,7 @@ func loadIncludeConfig(source any) ([]types.IncludeConfig, error) { return requires, err } -func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string) error { +func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string, processor PostProcessor) error { includeConfig, err := loadIncludeConfig(model["include"]) if err != nil { return err @@ -154,7 +155,7 @@ func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapp if err != nil { return err } - err = importResources(imported, model) + err = importResources(imported, model, processor) if err != nil { return err } @@ -164,29 +165,29 @@ func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapp } // importResources import into model all resources defined by imported, and report error on conflict -func importResources(source map[string]any, target map[string]any) error { - if err := importResource(source, target, "services"); err != nil { +func importResources(source map[string]any, target map[string]any, processor PostProcessor) error { + if err := importResource(source, target, "services", processor); err != nil { return err } - if err := importResource(source, target, "volumes"); err != nil { + if err := importResource(source, target, "volumes", processor); err != nil { return err } - if err := importResource(source, target, "networks"); err != nil { + if err := importResource(source, target, "networks", processor); err != nil { return err } - if err := importResource(source, target, "secrets"); err != nil { + if err := importResource(source, target, "secrets", processor); err != nil { return err } - if err := importResource(source, target, "configs"); err != nil { + if err := importResource(source, target, "configs", processor); err != nil { return err } - if err := importResource(source, target, "models"); err != nil { + if err := importResource(source, target, "models", processor); err != nil { return err } return nil } -func importResource(source map[string]any, target map[string]any, key string) error { +func importResource(source map[string]any, target map[string]any, key string, processor PostProcessor) error { from := source[key] if from != nil { var to map[string]any @@ -196,13 +197,25 @@ func importResource(source map[string]any, target map[string]any, key string) er to = map[string]any{} } for name, a := range from.(map[string]any) { - if conflict, ok := to[name]; ok { - if reflect.DeepEqual(a, conflict) { - continue - } - return fmt.Errorf("%s.%s conflicts with imported resource", key, name) + conflict, ok := to[name] + if !ok { + to[name] = a + continue + } + err := processor.Apply(map[string]any{ + key: map[string]any{ + name: a, + }, + }) + if err != nil { + return err + } + + merged, err := override.MergeYaml(a, conflict, tree.NewPath(key, name)) + if err != nil { + return err } - to[name] = a + to[name] = merged } target[key] = to } diff --git a/loader/include_test.go b/loader/include_test.go index dbce5f04..775fa88c 100644 --- a/loader/include_test.go +++ b/loader/include_test.go @@ -81,12 +81,21 @@ include: services: bar: image: busybox + environment: !override + - ZOT=QIX `, map[string]string{"SOURCE": "override"}) - _, err := LoadWithContext(context.TODO(), details, func(options *Options) { + p, err := LoadWithContext(context.TODO(), details, func(options *Options) { options.SkipNormalization = true options.ResolvePaths = true }) - assert.ErrorContains(t, err, "services.bar conflicts with imported resource", err) + assert.NilError(t, err) + assert.DeepEqual(t, p.Services["bar"], types.ServiceConfig{ + Name: "bar", + Image: "busybox", + Environment: types.MappingWithEquals{ + "ZOT": strPtr("QIX"), + }, + }) } func TestIncludeRelative(t *testing.T) { diff --git a/loader/loader.go b/loader/loader.go index b2439c38..f73ad92e 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -459,7 +459,7 @@ func loadYamlFile(ctx context.Context, if !opts.SkipInclude { included = append(included, file.Filename) - err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included) + err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor) if err != nil { return err } diff --git a/loader/testdata/compose-include.yaml b/loader/testdata/compose-include.yaml index e201ab52..67a7ebfa 100644 --- a/loader/testdata/compose-include.yaml +++ b/loader/testdata/compose-include.yaml @@ -3,4 +3,6 @@ include: services: bar: - image: bar \ No newline at end of file + image: bar + environment: + - FOO=BAR \ No newline at end of file diff --git a/override/extends.go b/override/extends.go index f47912dd..de92fd29 100644 --- a/override/extends.go +++ b/override/extends.go @@ -19,7 +19,7 @@ package override import "github.com/compose-spec/compose-go/v2/tree" func ExtendService(base, override map[string]any) (map[string]any, error) { - yaml, err := mergeYaml(base, override, tree.NewPath("services.x")) + yaml, err := MergeYaml(base, override, tree.NewPath("services.x")) if err != nil { return nil, err } diff --git a/override/merge.go b/override/merge.go index 6fae6e5f..525299cd 100644 --- a/override/merge.go +++ b/override/merge.go @@ -26,7 +26,7 @@ import ( // Merge applies overrides to a config model func Merge(right, left map[string]any) (map[string]any, error) { - merged, err := mergeYaml(right, left, tree.NewPath()) + merged, err := MergeYaml(right, left, tree.NewPath()) if err != nil { return nil, err } @@ -70,8 +70,8 @@ func init() { mergeSpecials["services.*.ulimits.*"] = mergeUlimit } -// mergeYaml merges map[string]any yaml trees handling special rules -func mergeYaml(e any, o any, p tree.Path) (any, error) { +// MergeYaml merges map[string]any yaml trees handling special rules +func MergeYaml(e any, o any, p tree.Path) (any, error) { for pattern, merger := range mergeSpecials { if p.Matches(pattern) { merged, err := merger(e, o, p) @@ -110,7 +110,7 @@ func mergeMappings(mapping map[string]any, other map[string]any, p tree.Path) (m continue } next := p.Next(k) - merged, err := mergeYaml(e, v, next) + merged, err := MergeYaml(e, v, next) if err != nil { return nil, err }