Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set default values to required attributes #558

Merged
merged 2 commits into from
Feb 6, 2024
Merged
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
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)
}
6 changes: 4 additions & 2 deletions loader/full-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ services:
additional_contexts:
foo: ./bar
secrets:
- secret1
- source: secret1
target: /run/secrets/secret1
- source: secret2
target: my_secret
uid: '103'
Expand Down Expand Up @@ -257,7 +258,8 @@ services:
restart: always

secrets:
- secret1
- source: secret1
target: /run/secrets/secret1
- source: secret2
target: my_secret
uid: '103'
Expand Down
10 changes: 8 additions & 2 deletions loader/full-struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func services(workingDir, homeDir string) types.Services {
Secrets: []types.ServiceSecretConfig{
{
Source: "secret1",
Target: "/run/secrets/secret1",
},
{
Source: "secret2",
Expand Down Expand Up @@ -396,6 +397,7 @@ func services(workingDir, homeDir string) types.Services {
Secrets: []types.ServiceSecretConfig{
{
Source: "secret1",
Target: "/run/secrets/secret1",
},
{
Source: "secret2",
Expand Down Expand Up @@ -627,6 +629,7 @@ services:
target: foo
secrets:
- source: secret1
target: /run/secrets/secret1
- source: secret2
target: my_secret
uid: "103"
Expand Down Expand Up @@ -885,6 +888,7 @@ services:
restart: always
secrets:
- source: secret1
target: /run/secrets/secret1
- source: secret2
target: my_secret
uid: "103"
Expand Down Expand Up @@ -1180,7 +1184,8 @@ func fullExampleJSON(workingDir, homeDir string) string {
"target": "foo",
"secrets": [
{
"source": "secret1"
"source": "secret1",
"target": "/run/secrets/secret1"
},
{
"source": "secret2",
Expand Down Expand Up @@ -1544,7 +1549,8 @@ func fullExampleJSON(workingDir, homeDir string) string {
"restart": "always",
"secrets": [
{
"source": "secret1"
"source": "secret1",
"target": "/run/secrets/secret1"
},
{
"source": "secret2",
Expand Down
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
5 changes: 4 additions & 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 Expand Up @@ -828,6 +828,7 @@ networks:
Secrets: []types.ServiceSecretConfig{
{
Source: "super",
Target: "/run/secrets/super",
Mode: uint32Ptr(555),
},
},
Expand Down Expand Up @@ -1842,6 +1843,7 @@ secrets:
Secrets: []types.ServiceSecretConfig{
{
Source: "secret",
Target: "/run/secrets/secret",
},
},
},
Expand Down Expand Up @@ -1911,6 +1913,7 @@ secrets:
Secrets: []types.ServiceSecretConfig{
{
Source: "secret",
Target: "/run/secrets/secret",
},
},
},
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
87 changes: 87 additions & 0 deletions transform/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
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
defaultValues["services.*.secrets.*"] = defaultSecretMount
}

// 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
}
13 changes: 13 additions & 0 deletions transform/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,16 @@ func transformFileMount(data any, p tree.Path) (any, error) {
return nil, fmt.Errorf("%s: unsupported type %T", p, data)
}
}

func defaultSecretMount(data any, p tree.Path) (any, error) {
switch v := data.(type) {
case map[string]any:
source := v["source"]
if _, ok := v["target"]; !ok {
v["target"] = fmt.Sprintf("/run/secrets/%s", source)
}
return v, nil
default:
return nil, fmt.Errorf("%s: unsupported type %T", p, data)
}
}
Loading