Skip to content

Commit

Permalink
set default values to required attributes
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Feb 6, 2024
1 parent 58f2fbb commit 2539b8e
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 8 deletions.
1 change: 1 addition & 0 deletions loader/extends.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts
extendsOpts.SkipInclude = true
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
extendsOpts.SkipValidation = true // we validate the merge result
extendsOpts.SkipDefaultValues = true
source, err := loadYamlModel(ctx, types.ConfigDetails{
WorkingDir: relworkingdir,
ConfigFiles: []types.ConfigFile{
Expand Down
57 changes: 57 additions & 0 deletions loader/extends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package loader

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)

func TestExtends(t *testing.T) {
Expand Down Expand Up @@ -202,3 +204,58 @@ services:
assert.Equal(t, len(p.Services["test"].Ports), 1)

}

func TestLoadExtendsSameFile(t *testing.T) {
tmpdir := t.TempDir()

aDir := filepath.Join(tmpdir, "sub")
assert.NilError(t, os.Mkdir(aDir, 0o700))
aYAML := `
services:
base:
build:
context: ..
service:
extends: base
build:
target: target
`

assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "sub", "compose.yaml"), []byte(aYAML), 0o600))

rootYAML := `
services:
out-base:
extends:
file: sub/compose.yaml
service: base
out-service:
extends:
file: sub/compose.yaml
service: service
`

assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "compose.yaml"), []byte(rootYAML), 0o600))

actual, err := Load(types.ConfigDetails{
WorkingDir: tmpdir,
ConfigFiles: []types.ConfigFile{{
Filename: filepath.Join(tmpdir, "compose.yaml"),
}},
Environment: nil,
}, func(options *Options) {
options.SkipNormalization = true
options.SkipConsistencyCheck = true
options.SetProjectName("project", true)
})
assert.NilError(t, err)
assert.Assert(t, is.Len(actual.Services, 2))

svcA, err := actual.GetService("out-base")
assert.NilError(t, err)
assert.Equal(t, svcA.Build.Context, tmpdir)

svcB, err := actual.GetService("out-service")
assert.NilError(t, err)
assert.Equal(t, svcB.Build.Context, tmpdir)
}
9 changes: 9 additions & 0 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type Options struct {
SkipInclude bool
// SkipResolveEnvironment will ignore computing `environment` for services
SkipResolveEnvironment bool
// SkipDefaultValues will ignore missing required attributes
SkipDefaultValues bool
// Interpolation options
Interpolate *interp.Options
// Discard 'env_file' entries after resolving to 'environment' section
Expand Down Expand Up @@ -417,6 +419,13 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
return nil, err
}

if !opts.SkipDefaultValues {
dict, err = transform.SetDefaultValues(dict)
if err != nil {
return nil, err
}
}

if !opts.SkipValidation {
if err := validation.Validate(dict); err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ services:

svcB, err := actual.GetService("b")
assert.NilError(t, err)
assert.Equal(t, svcB.Build.Context, bDir)
assert.Equal(t, svcB.Build.Context, tmpdir)
}

func TestLoadExtendsWihReset(t *testing.T) {
Expand Down
12 changes: 9 additions & 3 deletions parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,19 @@ During loading, all those attributes are transformed into canonical
representation, so that we get a single format that will match to go structs
for binding.

# Phase 12: extensions
# Phase 12: set-defaults

Some attributes are required by the model but optional in the compose file, as an implicit
default value is defined by the specification, like [`build.context`](https://github.com/compose-spec/compose-spec/blob/master/build.md#context)
During this phase, such unset attributes get default value assigned.

# Phase 13: extensions

Extension (`x-*` attributes) can be used in any place in the yaml document.
To make unmarshalling easier, parsing move them all into a custom `#extension`
attribute. This hack is very specific to the go binding.

# Phase 13: relative paths
# Phase 14: relative paths

Compose allows paths to be set relative to the project directory. Those get resolved
into absolute paths during this phase. This involves a few corner cases, as
Expand All @@ -152,7 +158,7 @@ volumes:
device: './data' # such a relative path must be resolved
```

# Phase 14: go binding
# Phase 15: go binding

Eventually, the yaml tree can be unmarshalled into go structs. We rely on
[mapstructure](https://github.com/mitchellh/mapstructure) library for this purpose.
Expand Down
15 changes: 12 additions & 3 deletions transform/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ import (
func transformBuild(data any, p tree.Path) (any, error) {
switch v := data.(type) {
case map[string]any:
if _, ok := v["context"]; !ok {
v["context"] = "." // TODO(ndeloof) maybe we miss an explicit "set-defaults" loading phase
}
return transformMapping(v, p)
case string:
return map[string]any{
Expand All @@ -37,3 +34,15 @@ func transformBuild(data any, p tree.Path) (any, error) {
return data, fmt.Errorf("%s: invalid type %T for build", p, v)
}
}

func defaultBuildContext(data any, _ tree.Path) (any, error) {
switch v := data.(type) {
case map[string]any:
if _, ok := v["context"]; !ok {
v["context"] = "."
}
return v, nil
default:
return data, nil
}
}
1 change: 0 additions & 1 deletion transform/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func Test_transformBuild(t *testing.T) {
"dockerfile": "foo.Dockerfile",
},
want: map[string]any{
"context": ".",
"dockerfile": "foo.Dockerfile",
},
},
Expand Down
86 changes: 86 additions & 0 deletions transform/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
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 transform

import (
"github.com/compose-spec/compose-go/v2/tree"
)

var defaultValues = map[tree.Path]transformFunc{}

func init() {
defaultValues["services.*.build"] = defaultBuildContext
}

// SetDefaultValues transforms a compose model to set default values to missing attributes
func SetDefaultValues(yaml map[string]any) (map[string]any, error) {
result, err := setDefaults(yaml, tree.NewPath())
if err != nil {
return nil, err
}
return result.(map[string]any), nil
}

func setDefaults(data any, p tree.Path) (any, error) {
for pattern, transformer := range defaultValues {
if p.Matches(pattern) {
t, err := transformer(data, p)
if err != nil {
return nil, err
}
return t, nil
}
}
switch v := data.(type) {
case map[string]any:
a, err := setDefaultsMapping(v, p)
if err != nil {
return a, err
}
return v, nil
case []any:
a, err := setDefaultsSequence(v, p)
if err != nil {
return a, err
}
return v, nil
default:
return data, nil
}
}

func setDefaultsSequence(v []any, p tree.Path) ([]any, error) {
for i, e := range v {
t, err := setDefaults(e, p.Next("[]"))
if err != nil {
return nil, err
}
v[i] = t
}
return v, nil
}

func setDefaultsMapping(v map[string]any, p tree.Path) (map[string]any, error) {
for k, e := range v {
t, err := setDefaults(e, p.Next(k))
if err != nil {
return nil, err
}
v[k] = t
}
return v, nil
}

0 comments on commit 2539b8e

Please sign in to comment.