Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cli/compose/loader/full-struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,6 @@ func configs(workingDir string) map[string]types.ConfigObjConfig {
},
"config4": {
Name: "foo",
File: workingDir,
Extras: map[string]any{
"x-bar": "baz",
"x-foo": "bar",
Expand All @@ -550,7 +549,6 @@ func secrets(workingDir string) map[string]types.SecretConfig {
},
"secret4": {
Name: "bar",
File: workingDir,
Extras: map[string]any{
"x-bar": "baz",
"x-foo": "bar",
Expand Down
4 changes: 3 additions & 1 deletion cli/compose/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,9 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
return obj, fmt.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
}
default:
obj.File = absPath(details.WorkingDir, obj.File)
if obj.File != "" {
obj.File = absPath(details.WorkingDir, obj.File)
}
}

return obj, nil
Expand Down
24 changes: 22 additions & 2 deletions cli/compose/loader/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ func merge(configs []*types.Config) (*types.Config, error) {
if err := mergo.Map(&base.Networks, &override.Networks, mergo.WithOverride); err != nil {
errs = append(errs, fmt.Errorf("cannot merge networks: %w", err))
}
if err := mergo.Map(&base.Secrets, &override.Secrets, mergo.WithOverride); err != nil {
if err := mergeResourceMap(&base.Secrets, override.Secrets); err != nil {
errs = append(errs, fmt.Errorf("cannot merge secrets: %w", err))
}
if err := mergo.Map(&base.Configs, &override.Configs, mergo.WithOverride); err != nil {
if err := mergeResourceMap(&base.Configs, override.Configs); err != nil {
errs = append(errs, fmt.Errorf("cannot merge configs: %w", err))
}
if err := errors.Join(errs...); err != nil {
Expand All @@ -54,6 +54,26 @@ func merge(configs []*types.Config) (*types.Config, error) {
return base, nil
}

// mergeResourceMap merges secrets/configs by name so a later `name:` overlay
// keeps `external: true` from an earlier file.
func mergeResourceMap[T any](dst *map[string]T, src map[string]T) error {
if *dst == nil {
*dst = make(map[string]T, len(src))
}
for k, sv := range src {
dv, ok := (*dst)[k]
if !ok {
(*dst)[k] = sv
continue
}
if err := mergo.Merge(&dv, &sv, mergo.WithOverride); err != nil {
return err
}
(*dst)[k] = dv
}
return nil
}

func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
mergeOpts := []func(*mergo.Config){
mergo.WithAppendSlice,
Expand Down
86 changes: 86 additions & 0 deletions cli/compose/loader/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,92 @@ func TestLoadMultipleNetworks(t *testing.T) {
}, config)
}

// Issue#7264: a later compose file that only sets the secret/config name
// must keep `external: true` from the earlier file (same as `docker compose config`).
func TestLoadMergedExternalSecretName(t *testing.T) {
configDetails := types.ConfigDetails{
WorkingDir: "/tmp/example",
ConfigFiles: []types.ConfigFile{
{
Filename: "docker-stack.yml",
Config: map[string]any{
"version": "3.8",
"services": map[string]any{
"app": map[string]any{
"image": "foo",
"secrets": []any{"FOO_BAR"},
},
},
"secrets": map[string]any{
"FOO_BAR": map[string]any{
"external": true,
},
},
},
},
{
Filename: "docker-stack.prod.yml",
Config: map[string]any{
"version": "3.8",
"secrets": map[string]any{
"FOO_BAR": map[string]any{
"name": "app-prod-foo-bar",
},
},
},
},
},
}
config, err := Load(configDetails)
assert.NilError(t, err)
assert.DeepEqual(t, types.SecretConfig{
Name: "app-prod-foo-bar",
External: types.External{External: true},
}, config.Secrets["FOO_BAR"])
}

func TestLoadMergedExternalConfigName(t *testing.T) {
configDetails := types.ConfigDetails{
WorkingDir: "/tmp/example",
ConfigFiles: []types.ConfigFile{
{
Filename: "base.yml",
Config: map[string]any{
"version": "3.8",
"services": map[string]any{
"app": map[string]any{
"image": "foo",
"configs": []any{"APP_CFG"},
},
},
"configs": map[string]any{
"APP_CFG": map[string]any{
"external": true,
},
},
},
},
{
Filename: "override.yml",
Config: map[string]any{
"version": "3.8",
"configs": map[string]any{
"APP_CFG": map[string]any{
"name": "app-prod-cfg",
},
},
},
},
},
}
config, err := Load(configDetails)
assert.NilError(t, err)
assert.DeepEqual(t, types.ConfigObjConfig{
Name: "app-prod-cfg",
External: types.External{External: true},
}, config.Configs["APP_CFG"])
}

func TestLoadMultipleServiceCommands(t *testing.T) {
base := map[string]any{
"version": "3.7",
Expand Down
2 changes: 0 additions & 2 deletions cli/compose/loader/testdata/full-example.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
},
"config4": {
"name": "foo",
"file": "/foo",
"external": false
}
},
Expand Down Expand Up @@ -77,7 +76,6 @@
},
"secret4": {
"name": "bar",
"file": "/foo",
"external": false
}
},
Expand Down
2 changes: 0 additions & 2 deletions cli/compose/loader/testdata/full-example.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,6 @@ secrets:
external: true
secret4:
name: bar
file: /foo
x-bar: baz
x-foo: bar
configs:
Expand All @@ -407,7 +406,6 @@ configs:
external: true
config4:
name: foo
file: /foo
x-bar: baz
x-foo: bar
x-bar: baz
Expand Down