Skip to content

Commit 2539b8e

Browse files
committed
set default values to required attributes
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 58f2fbb commit 2539b8e

File tree

8 files changed

+175
-8
lines changed

8 files changed

+175
-8
lines changed

loader/extends.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts
140140
extendsOpts.SkipInclude = true
141141
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
142142
extendsOpts.SkipValidation = true // we validate the merge result
143+
extendsOpts.SkipDefaultValues = true
143144
source, err := loadYamlModel(ctx, types.ConfigDetails{
144145
WorkingDir: relworkingdir,
145146
ConfigFiles: []types.ConfigFile{

loader/extends_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package loader
1818

1919
import (
2020
"context"
21+
"os"
2122
"path/filepath"
2223
"testing"
2324

2425
"github.com/compose-spec/compose-go/v2/types"
2526
"gotest.tools/v3/assert"
27+
is "gotest.tools/v3/assert/cmp"
2628
)
2729

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

204206
}
207+
208+
func TestLoadExtendsSameFile(t *testing.T) {
209+
tmpdir := t.TempDir()
210+
211+
aDir := filepath.Join(tmpdir, "sub")
212+
assert.NilError(t, os.Mkdir(aDir, 0o700))
213+
aYAML := `
214+
services:
215+
base:
216+
build:
217+
context: ..
218+
service:
219+
extends: base
220+
build:
221+
target: target
222+
`
223+
224+
assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "sub", "compose.yaml"), []byte(aYAML), 0o600))
225+
226+
rootYAML := `
227+
services:
228+
out-base:
229+
extends:
230+
file: sub/compose.yaml
231+
service: base
232+
out-service:
233+
extends:
234+
file: sub/compose.yaml
235+
service: service
236+
`
237+
238+
assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "compose.yaml"), []byte(rootYAML), 0o600))
239+
240+
actual, err := Load(types.ConfigDetails{
241+
WorkingDir: tmpdir,
242+
ConfigFiles: []types.ConfigFile{{
243+
Filename: filepath.Join(tmpdir, "compose.yaml"),
244+
}},
245+
Environment: nil,
246+
}, func(options *Options) {
247+
options.SkipNormalization = true
248+
options.SkipConsistencyCheck = true
249+
options.SetProjectName("project", true)
250+
})
251+
assert.NilError(t, err)
252+
assert.Assert(t, is.Len(actual.Services, 2))
253+
254+
svcA, err := actual.GetService("out-base")
255+
assert.NilError(t, err)
256+
assert.Equal(t, svcA.Build.Context, tmpdir)
257+
258+
svcB, err := actual.GetService("out-service")
259+
assert.NilError(t, err)
260+
assert.Equal(t, svcB.Build.Context, tmpdir)
261+
}

loader/loader.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type Options struct {
6464
SkipInclude bool
6565
// SkipResolveEnvironment will ignore computing `environment` for services
6666
SkipResolveEnvironment bool
67+
// SkipDefaultValues will ignore missing required attributes
68+
SkipDefaultValues bool
6769
// Interpolation options
6870
Interpolate *interp.Options
6971
// Discard 'env_file' entries after resolving to 'environment' section
@@ -417,6 +419,13 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
417419
return nil, err
418420
}
419421

422+
if !opts.SkipDefaultValues {
423+
dict, err = transform.SetDefaultValues(dict)
424+
if err != nil {
425+
return nil, err
426+
}
427+
}
428+
420429
if !opts.SkipValidation {
421430
if err := validation.Validate(dict); err != nil {
422431
return nil, err

loader/loader_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ services:
438438

439439
svcB, err := actual.GetService("b")
440440
assert.NilError(t, err)
441-
assert.Equal(t, svcB.Build.Context, bDir)
441+
assert.Equal(t, svcB.Build.Context, tmpdir)
442442
}
443443

444444
func TestLoadExtendsWihReset(t *testing.T) {

parsing.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,19 @@ During loading, all those attributes are transformed into canonical
127127
representation, so that we get a single format that will match to go structs
128128
for binding.
129129

130-
# Phase 12: extensions
130+
# Phase 12: set-defaults
131+
132+
Some attributes are required by the model but optional in the compose file, as an implicit
133+
default value is defined by the specification, like [`build.context`](https://github.com/compose-spec/compose-spec/blob/master/build.md#context)
134+
During this phase, such unset attributes get default value assigned.
135+
136+
# Phase 13: extensions
131137

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

136-
# Phase 13: relative paths
142+
# Phase 14: relative paths
137143

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

155-
# Phase 14: go binding
161+
# Phase 15: go binding
156162

157163
Eventually, the yaml tree can be unmarshalled into go structs. We rely on
158164
[mapstructure](https://github.com/mitchellh/mapstructure) library for this purpose.

transform/build.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ import (
2525
func transformBuild(data any, p tree.Path) (any, error) {
2626
switch v := data.(type) {
2727
case map[string]any:
28-
if _, ok := v["context"]; !ok {
29-
v["context"] = "." // TODO(ndeloof) maybe we miss an explicit "set-defaults" loading phase
30-
}
3128
return transformMapping(v, p)
3229
case string:
3330
return map[string]any{
@@ -37,3 +34,15 @@ func transformBuild(data any, p tree.Path) (any, error) {
3734
return data, fmt.Errorf("%s: invalid type %T for build", p, v)
3835
}
3936
}
37+
38+
func defaultBuildContext(data any, _ tree.Path) (any, error) {
39+
switch v := data.(type) {
40+
case map[string]any:
41+
if _, ok := v["context"]; !ok {
42+
v["context"] = "."
43+
}
44+
return v, nil
45+
default:
46+
return data, nil
47+
}
48+
}

transform/build_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func Test_transformBuild(t *testing.T) {
4343
"dockerfile": "foo.Dockerfile",
4444
},
4545
want: map[string]any{
46-
"context": ".",
4746
"dockerfile": "foo.Dockerfile",
4847
},
4948
},

transform/defaults.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package transform
18+
19+
import (
20+
"github.com/compose-spec/compose-go/v2/tree"
21+
)
22+
23+
var defaultValues = map[tree.Path]transformFunc{}
24+
25+
func init() {
26+
defaultValues["services.*.build"] = defaultBuildContext
27+
}
28+
29+
// SetDefaultValues transforms a compose model to set default values to missing attributes
30+
func SetDefaultValues(yaml map[string]any) (map[string]any, error) {
31+
result, err := setDefaults(yaml, tree.NewPath())
32+
if err != nil {
33+
return nil, err
34+
}
35+
return result.(map[string]any), nil
36+
}
37+
38+
func setDefaults(data any, p tree.Path) (any, error) {
39+
for pattern, transformer := range defaultValues {
40+
if p.Matches(pattern) {
41+
t, err := transformer(data, p)
42+
if err != nil {
43+
return nil, err
44+
}
45+
return t, nil
46+
}
47+
}
48+
switch v := data.(type) {
49+
case map[string]any:
50+
a, err := setDefaultsMapping(v, p)
51+
if err != nil {
52+
return a, err
53+
}
54+
return v, nil
55+
case []any:
56+
a, err := setDefaultsSequence(v, p)
57+
if err != nil {
58+
return a, err
59+
}
60+
return v, nil
61+
default:
62+
return data, nil
63+
}
64+
}
65+
66+
func setDefaultsSequence(v []any, p tree.Path) ([]any, error) {
67+
for i, e := range v {
68+
t, err := setDefaults(e, p.Next("[]"))
69+
if err != nil {
70+
return nil, err
71+
}
72+
v[i] = t
73+
}
74+
return v, nil
75+
}
76+
77+
func setDefaultsMapping(v map[string]any, p tree.Path) (map[string]any, error) {
78+
for k, e := range v {
79+
t, err := setDefaults(e, p.Next(k))
80+
if err != nil {
81+
return nil, err
82+
}
83+
v[k] = t
84+
}
85+
return v, nil
86+
}

0 commit comments

Comments
 (0)