diff --git a/loader/loader.go b/loader/loader.go index 26e9d4d0..62030f23 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -516,6 +516,8 @@ func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, wor return err } + dict = OmitEmpty(dict) + // Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override dict, err = override.EnforceUnicity(dict) return err diff --git a/loader/loader_test.go b/loader/loader_test.go index ec686860..53186366 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -3581,3 +3581,14 @@ services: }, }) } + +func TestOmitEmptyDNS(t *testing.T) { + p, err := loadYAML(` +name: load-empty-dsn +services: + test: + dns: ${UNSET_VAR} +`) + assert.NilError(t, err) + assert.Equal(t, len(p.Services["test"].DNS), 0) +} diff --git a/loader/normalize.go b/loader/normalize.go index 6e7fb71f..bae70be8 100644 --- a/loader/normalize.go +++ b/loader/normalize.go @@ -135,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { } services[name] = service } + dict["services"] = services } - setNameFromKey(dict) return dict, nil diff --git a/loader/omitEmpty.go b/loader/omitEmpty.go new file mode 100644 index 00000000..bc1cb1a5 --- /dev/null +++ b/loader/omitEmpty.go @@ -0,0 +1,74 @@ +/* + 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 loader + +import "github.com/compose-spec/compose-go/v2/tree" + +var omitempty = []tree.Path{ + "services.*.dns"} + +// OmitEmpty removes empty attributes which are irrelevant when unset +func OmitEmpty(yaml map[string]any) map[string]any { + cleaned := omitEmpty(yaml, tree.NewPath()) + return cleaned.(map[string]any) +} + +func omitEmpty(data any, p tree.Path) any { + switch v := data.(type) { + case map[string]any: + for k, e := range v { + if isEmpty(e) && mustOmit(p) { + delete(v, k) + continue + } + + v[k] = omitEmpty(e, p.Next(k)) + } + return v + case []any: + var c []any + for _, e := range v { + if isEmpty(e) && mustOmit(p) { + continue + } + + c = append(c, omitEmpty(e, p.Next("[]"))) + } + return c + default: + return data + } +} + +func mustOmit(p tree.Path) bool { + for _, pattern := range omitempty { + if p.Matches(pattern) { + return true + } + } + return false +} + +func isEmpty(e any) bool { + if e == nil { + return true + } + if v, ok := e.(string); ok && v == "" { + return true + } + return false +} diff --git a/transform/canonical.go b/transform/canonical.go index a248b4be..ff5bb37d 100644 --- a/transform/canonical.go +++ b/transform/canonical.go @@ -33,6 +33,7 @@ func init() { transformers["services.*.extends"] = transformExtends transformers["services.*.networks"] = transformServiceNetworks transformers["services.*.volumes.*"] = transformVolumeMount + transformers["services.*.dns"] = transformStringOrList transformers["services.*.devices.*"] = transformDeviceMapping transformers["services.*.secrets.*"] = transformFileMount transformers["services.*.configs.*"] = transformFileMount @@ -48,6 +49,15 @@ func init() { transformers["include.*"] = transformInclude } +func transformStringOrList(data any, _ tree.Path, _ bool) (any, error) { + switch t := data.(type) { + case string: + return []any{t}, nil + default: + return data, nil + } +} + // Canonical transforms a compose model into canonical syntax func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) { canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)