From 80aabe69b0ee797489ead01560df592f73f1ec2d Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:18:34 +0100 Subject: [PATCH] vendor: update compose-go to v2.0.0-rc.2 Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/bake.go | 2 +- bake/compose.go | 24 +- bake/compose_test.go | 50 +- go.mod | 4 +- go.sum | 8 +- .../compose-spec/compose-go/loader/include.go | 167 --- .../compose-spec/compose-go/loader/loader.go | 1277 ----------------- .../compose-spec/compose-go/loader/merge.go | 378 ----- .../compose-spec/compose-go/{ => v2}/LICENSE | 0 .../compose-spec/compose-go/{ => v2}/NOTICE | 0 .../compose-go/{ => v2}/cli/options.go | 58 +- .../compose-go/v2/consts/consts.go | 29 + .../compose-go/{ => v2}/dotenv/LICENSE | 0 .../compose-go/{ => v2}/dotenv/env.go | 22 +- .../compose-go/{ => v2}/dotenv/godotenv.go | 2 +- .../compose-go/{ => v2}/dotenv/parser.go | 15 +- .../compose-go/{ => v2}/errdefs/errors.go | 0 .../{loader => v2/format}/volume.go | 9 +- .../{ => v2}/interpolation/interpolation.go | 25 +- .../compose-go/{ => v2}/loader/example1.env | 0 .../compose-go/{ => v2}/loader/example2.env | 0 .../compose-go/v2/loader/extends.go | 147 ++ .../compose-spec/compose-go/v2/loader/fix.go | 36 + .../{ => v2}/loader/full-example.yml | 14 +- .../compose-go/v2/loader/include.go | 155 ++ .../compose-go/{ => v2}/loader/interpolate.go | 8 +- .../compose-go/v2/loader/loader.go | 722 ++++++++++ .../{ => v2}/loader/mapstructure.go | 28 +- .../compose-go/{ => v2}/loader/normalize.go | 120 +- .../compose-go/{ => v2}/loader/null.go | 97 +- .../compose-go/{ => v2}/loader/paths.go | 81 +- .../compose-go/{ => v2}/loader/validate.go | 47 +- .../consts.go => v2/override/extends.go} | 17 +- .../compose-go/v2/override/merge.go | 199 +++ .../compose-go/v2/override/uncity.go | 171 +++ .../compose-go/v2/paths/context.go | 44 + .../compose-spec/compose-go/v2/paths/home.go | 37 + .../compose-go/v2/paths/resolve.go | 146 ++ .../compose-spec/compose-go/v2/paths/unix.go | 40 + .../{loader => v2/paths}/windows_path.go | 4 +- .../{ => v2}/schema/compose-spec.json | 94 +- .../compose-go/{ => v2}/schema/schema.go | 0 .../compose-go/{ => v2}/template/template.go | 0 .../compose-go/v2/transform/build.go | 39 + .../compose-go/v2/transform/canonical.go | 107 ++ .../compose-go/v2/transform/dependson.go | 53 + .../compose-go/v2/transform/envfile.go | 55 + .../compose-go/v2/transform/extends.go | 36 + .../compose-go/v2/transform/external.go | 54 + .../compose-go/v2/transform/include.go | 36 + .../compose-go/v2/transform/mapping.go | 43 + .../compose-go/v2/transform/ports.go | 86 ++ .../compose-go/v2/transform/secrets.go | 36 + .../compose-go/v2/transform/services.go | 37 + .../compose-go/v2/transform/ssh.go | 51 + .../compose-go/v2/transform/ulimits.go | 34 + .../compose-go/v2/transform/volume.go | 49 + .../compose-go/{ => v2}/tree/path.go | 22 +- .../compose-go/{ => v2}/types/bytes.go | 12 +- .../compose-go/{ => v2}/types/command.go | 0 .../compose-go/{ => v2}/types/config.go | 0 .../compose-go/{ => v2}/types/develop.go | 0 .../compose-go/{ => v2}/types/device.go | 7 +- .../compose-go/{ => v2}/types/duration.go | 0 .../compose-go/v2/types/envfile.go | 46 + .../compose-go/{ => v2}/types/healthcheck.go | 2 +- .../compose-go/v2/types/hostList.go | 83 ++ .../compose-go/{ => v2}/types/labels.go | 5 +- .../compose-go/v2/types/mapping.go | 217 +++ .../compose-go/{ => v2}/types/options.go | 8 +- .../compose-go/{ => v2}/types/project.go | 366 +++-- .../compose-go/v2/types/services.go | 35 + .../compose-spec/compose-go/v2/types/ssh.go | 73 + .../compose-go/{ => v2}/types/stringOrList.go | 10 +- .../compose-go/{ => v2}/types/types.go | 404 ++---- .../{ => v2}/utils/collectionutils.go | 16 +- .../compose-spec/compose-go/v2/utils/set.go | 95 ++ .../compose-go/{ => v2}/utils/stringutils.go | 10 - .../compose-go/v2/validation/external.go | 49 + .../compose-go/v2/validation/validation.go | 96 ++ .../compose-go/v2/validation/volume.go | 39 + .../mitchellh/copystructure/LICENSE | 21 + .../mitchellh/copystructure/README.md | 21 + .../mitchellh/copystructure/copier_time.go | 15 + .../mitchellh/copystructure/copystructure.go | 631 ++++++++ .../mitchellh/reflectwalk/.travis.yml | 1 + .../github.com/mitchellh/reflectwalk/LICENSE | 21 + .../mitchellh/reflectwalk/README.md | 6 + .../mitchellh/reflectwalk/location.go | 19 + .../mitchellh/reflectwalk/location_string.go | 16 + .../mitchellh/reflectwalk/reflectwalk.go | 420 ++++++ vendor/golang.org/x/exp/maps/maps.go | 94 ++ vendor/modules.txt | 38 +- 93 files changed, 5172 insertions(+), 2719 deletions(-) delete mode 100644 vendor/github.com/compose-spec/compose-go/loader/include.go delete mode 100644 vendor/github.com/compose-spec/compose-go/loader/loader.go delete mode 100644 vendor/github.com/compose-spec/compose-go/loader/merge.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/LICENSE (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/NOTICE (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/cli/options.go (91%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/consts/consts.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/dotenv/LICENSE (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/dotenv/env.go (68%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/dotenv/godotenv.go (98%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/dotenv/parser.go (96%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/errdefs/errors.go (100%) rename vendor/github.com/compose-spec/compose-go/{loader => v2/format}/volume.go (96%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/interpolation/interpolation.go (88%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/example1.env (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/example2.env (100%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/loader/extends.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/loader/fix.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/full-example.yml (97%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/loader/include.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/interpolate.go (96%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/loader/loader.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/mapstructure.go (75%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/normalize.go (68%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/null.go (53%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/paths.go (53%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/loader/validate.go (57%) rename vendor/github.com/compose-spec/compose-go/{consts/consts.go => v2/override/extends.go} (67%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/override/merge.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/override/uncity.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/paths/context.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/paths/home.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/paths/unix.go rename vendor/github.com/compose-spec/compose-go/{loader => v2/paths}/windows_path.go (97%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/schema/compose-spec.json (93%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/schema/schema.go (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/template/template.go (100%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/build.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/extends.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/external.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/include.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/ports.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/services.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/transform/volume.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/tree/path.go (81%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/bytes.go (86%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/command.go (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/config.go (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/develop.go (100%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/device.go (88%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/duration.go (100%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/types/envfile.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/healthcheck.go (96%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/types/hostList.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/labels.go (93%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/types/mapping.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/options.go (91%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/project.go (55%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/types/services.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/types/ssh.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/stringOrList.go (88%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/types/types.go (75%) rename vendor/github.com/compose-spec/compose-go/{ => v2}/utils/collectionutils.go (83%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/utils/set.go rename vendor/github.com/compose-spec/compose-go/{ => v2}/utils/stringutils.go (86%) create mode 100644 vendor/github.com/compose-spec/compose-go/v2/validation/external.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/validation/validation.go create mode 100644 vendor/github.com/compose-spec/compose-go/v2/validation/volume.go create mode 100644 vendor/github.com/mitchellh/copystructure/LICENSE create mode 100644 vendor/github.com/mitchellh/copystructure/README.md create mode 100644 vendor/github.com/mitchellh/copystructure/copier_time.go create mode 100644 vendor/github.com/mitchellh/copystructure/copystructure.go create mode 100644 vendor/github.com/mitchellh/reflectwalk/.travis.yml create mode 100644 vendor/github.com/mitchellh/reflectwalk/LICENSE create mode 100644 vendor/github.com/mitchellh/reflectwalk/README.md create mode 100644 vendor/github.com/mitchellh/reflectwalk/location.go create mode 100644 vendor/github.com/mitchellh/reflectwalk/location_string.go create mode 100644 vendor/github.com/mitchellh/reflectwalk/reflectwalk.go create mode 100644 vendor/golang.org/x/exp/maps/maps.go diff --git a/bake/bake.go b/bake/bake.go index 331b2251c7eb..786ea9ed6bbb 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -13,7 +13,7 @@ import ( "strings" "time" - composecli "github.com/compose-spec/compose-go/cli" + composecli "github.com/compose-spec/compose-go/v2/cli" "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" controllerapi "github.com/docker/buildx/controller/pb" diff --git a/bake/compose.go b/bake/compose.go index b07247b0fc33..c9fd5ff122e3 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -6,9 +6,9 @@ import ( "path/filepath" "strings" - "github.com/compose-spec/compose-go/dotenv" - "github.com/compose-spec/compose-go/loader" - compose "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/loader" + composetypes "github.com/compose-spec/compose-go/v2/types" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) @@ -18,9 +18,9 @@ func ParseComposeFiles(fs []File) (*Config, error) { if err != nil { return nil, err } - var cfgs []compose.ConfigFile + var cfgs []composetypes.ConfigFile for _, f := range fs { - cfgs = append(cfgs, compose.ConfigFile{ + cfgs = append(cfgs, composetypes.ConfigFile{ Filename: f.Name, Content: f.Data, }) @@ -28,11 +28,11 @@ func ParseComposeFiles(fs []File) (*Config, error) { return ParseCompose(cfgs, envs) } -func ParseCompose(cfgs []compose.ConfigFile, envs map[string]string) (*Config, error) { +func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) { if envs == nil { envs = make(map[string]string) } - cfg, err := loader.LoadWithContext(context.Background(), compose.ConfigDetails{ + cfg, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{ ConfigFiles: cfgs, Environment: envs, }, func(options *loader.Options) { @@ -159,8 +159,8 @@ func validateComposeFile(dt []byte, fn string) (bool, error) { } func validateCompose(dt []byte, envs map[string]string) error { - _, err := loader.Load(compose.ConfigDetails{ - ConfigFiles: []compose.ConfigFile{ + _, err := loader.Load(composetypes.ConfigDetails{ + ConfigFiles: []composetypes.ConfigFile{ { Content: dt, }, @@ -223,7 +223,7 @@ func loadDotEnv(curenv map[string]string, workingDir string) (map[string]string, return curenv, nil } -func flatten(in compose.MappingWithEquals) map[string]*string { +func flatten(in composetypes.MappingWithEquals) map[string]*string { if len(in) == 0 { return nil } @@ -327,8 +327,8 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { // composeToBuildkitSecret converts secret from compose format to buildkit's // csv format. -func composeToBuildkitSecret(inp compose.ServiceSecretConfig, psecret compose.SecretConfig) (string, error) { - if psecret.External.External { +func composeToBuildkitSecret(inp composetypes.ServiceSecretConfig, psecret composetypes.SecretConfig) (string, error) { + if psecret.External { return "", errors.Errorf("unsupported external secret %s", psecret.Name) } diff --git a/bake/compose_test.go b/bake/compose_test.go index 89ccb142f018..da1ce9cb4a57 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -6,7 +6,7 @@ import ( "sort" "testing" - compose "github.com/compose-spec/compose-go/types" + composetypes "github.com/compose-spec/compose-go/v2/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -52,7 +52,7 @@ secrets: cwd, err := os.Getwd() require.NoError(t, err) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) @@ -95,7 +95,7 @@ services: webapp: build: ./db `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Groups)) require.Equal(t, 1, len(c.Targets)) @@ -114,7 +114,7 @@ services: target: webapp `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) @@ -139,7 +139,7 @@ services: target: webapp `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { @@ -170,7 +170,7 @@ services: t.Setenv("BAR", "foo") t.Setenv("ZZZ_BAR", "zzz_foo") - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, sliceToMap(os.Environ())) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, sliceToMap(os.Environ())) require.NoError(t, err) require.Equal(t, ptrstr("bar"), c.Targets[0].Args["FOO"]) require.Equal(t, ptrstr("zzz_foo"), c.Targets[0].Args["BAR"]) @@ -184,7 +184,7 @@ services: entrypoint: echo 1 `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.Error(t, err) } @@ -209,7 +209,7 @@ networks: gateway: 10.5.0.254 `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } @@ -226,7 +226,7 @@ services: - bar `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags) } @@ -263,7 +263,7 @@ networks: name: test-net `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } @@ -316,7 +316,7 @@ services: no-cache: true `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 2, len(c.Targets)) sort.Slice(c.Targets, func(i, j int) bool { @@ -360,7 +360,7 @@ services: - type=local,dest=path/to/cache `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags) @@ -393,7 +393,7 @@ services: - ` + envf.Name() + ` `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "FOO": ptrstr("bsdf -csdf"), "NODE_ENV": ptrstr("test")}, c.Targets[0].Args) } @@ -415,7 +415,7 @@ services: chdir(t, tmpdir) c, err := ParseComposeFiles([]File{{ - Name: "docker-compose.yml", + Name: "docker-composetypes.yml", Data: dt, }}) require.NoError(t, err) @@ -439,7 +439,7 @@ services: published: "3306" protocol: tcp `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } @@ -485,7 +485,7 @@ func TestServiceName(t *testing.T) { for _, tt := range cases { tt := tt t.Run(tt.svc, func(t *testing.T) { - _, err := ParseCompose([]compose.ConfigFile{{Content: []byte(` + _, err := ParseCompose([]composetypes.ConfigFile{{Content: []byte(` services: ` + tt.svc + `: build: @@ -556,7 +556,7 @@ services: for _, tt := range cases { tt := tt t.Run(tt.name, func(t *testing.T) { - _, err := ParseCompose([]compose.ConfigFile{{Content: tt.dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: tt.dt}}, nil) if tt.wantErr { require.Error(t, err) } else { @@ -576,7 +576,7 @@ func TestValidateComposeFile(t *testing.T) { }{ { name: "empty service", - fn: "docker-compose.yml", + fn: "docker-composetypes.yml", dt: []byte(` services: foo: @@ -586,7 +586,7 @@ services: }, { name: "build", - fn: "docker-compose.yml", + fn: "docker-composetypes.yml", dt: []byte(` services: foo: @@ -597,7 +597,7 @@ services: }, { name: "image", - fn: "docker-compose.yml", + fn: "docker-composetypes.yml", dt: []byte(` services: simple: @@ -608,7 +608,7 @@ services: }, { name: "unknown ext", - fn: "docker-compose.foo", + fn: "docker-composetypes.foo", dt: []byte(` services: simple: @@ -654,7 +654,7 @@ services: bar: "baz" `) - c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, c.Targets[0].Args) } @@ -673,7 +673,7 @@ services: build: context: . `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } @@ -704,7 +704,7 @@ services: chdir(t, tmpdir) c, err := ParseComposeFiles([]File{{ - Name: "compose.yml", + Name: "composetypes.yml", Data: dt, }}) require.NoError(t, err) @@ -734,7 +734,7 @@ services: - node_modules/ `) - _, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil) + _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) require.NoError(t, err) } diff --git a/go.mod b/go.mod index 65f2f1ec777b..4bb614091825 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/aws/aws-sdk-go-v2/config v1.18.16 - github.com/compose-spec/compose-go v1.20.0 + github.com/compose-spec/compose-go/v2 v2.0.0-rc.2 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.7.11 github.com/containerd/continuity v0.4.2 @@ -112,8 +112,10 @@ require ( github.com/mattn/go-shellwords v1.0.12 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/spdystream v0.2.0 // indirect diff --git a/go.sum b/go.sum index 9d232707b805..079d66a60bbc 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go v1.20.0 h1:h4ZKOst1EF/DwZp7dWkb+wbTVE4nEyT9Lc89to84Ol4= -github.com/compose-spec/compose-go v1.20.0/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.2 h1:eJ01FpliL/02KvsaPyH1bSLbM1S70yWQUojHVRbyvy4= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.2/go.mod h1:IVsvFyGVhw4FASzUtlWNVaAOhYmakXAFY9IlZ7LAuD8= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -311,12 +311,16 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/buildkit v0.13.0-beta1.0.20240105171016-6e200afad512 h1:jbAJul+OeYr9/mGeNDQc9Q4hd58Z/FXCehQ27ooD8JI= github.com/moby/buildkit v0.13.0-beta1.0.20240105171016-6e200afad512/go.mod h1:9jtsOuy5EtZ13ArTt+RqH61qZ3Ik7kly25Qy4m0zSCk= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= diff --git a/vendor/github.com/compose-spec/compose-go/loader/include.go b/vendor/github.com/compose-spec/compose-go/loader/include.go deleted file mode 100644 index aaebfd3019a6..000000000000 --- a/vendor/github.com/compose-spec/compose-go/loader/include.go +++ /dev/null @@ -1,167 +0,0 @@ -/* - 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 ( - "context" - "fmt" - "path/filepath" - "reflect" - - "github.com/compose-spec/compose-go/dotenv" - interp "github.com/compose-spec/compose-go/interpolation" - "github.com/compose-spec/compose-go/types" - "github.com/pkg/errors" -) - -// LoadIncludeConfig parse the require config from raw yaml -func LoadIncludeConfig(source []interface{}) ([]types.IncludeConfig, error) { - var requires []types.IncludeConfig - err := Transform(source, &requires) - return requires, err -} - -var transformIncludeConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case string: - return map[string]interface{}{"path": value}, nil - case map[string]interface{}: - return value, nil - default: - return data, errors.Errorf("invalid type %T for `include` configuration", value) - } -} - -func loadInclude(ctx context.Context, filename string, configDetails types.ConfigDetails, model *types.Config, options *Options, loaded []string) (*types.Config, map[string][]types.IncludeConfig, error) { - included := make(map[string][]types.IncludeConfig) - for _, r := range model.Include { - included[filename] = append(included[filename], r) - - for i, p := range r.Path { - for _, loader := range options.ResourceLoaders { - if loader.Accept(p) { - path, err := loader.Load(ctx, p) - if err != nil { - return nil, nil, err - } - p = path - break - } - } - r.Path[i] = absPath(configDetails.WorkingDir, p) - } - if r.ProjectDirectory == "" { - r.ProjectDirectory = filepath.Dir(r.Path[0]) - } - - loadOptions := options.clone() - loadOptions.SetProjectName(model.Name, true) - loadOptions.ResolvePaths = true - loadOptions.SkipNormalization = true - loadOptions.SkipConsistencyCheck = true - - envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.ProjectDirectory, r.EnvFile) - if err != nil { - return nil, nil, err - } - - config := types.ConfigDetails{ - WorkingDir: r.ProjectDirectory, - ConfigFiles: types.ToConfigFiles(r.Path), - Environment: configDetails.Environment.Clone().Merge(envFromFile), - } - loadOptions.Interpolate = &interp.Options{ - Substitute: options.Interpolate.Substitute, - LookupValue: config.LookupEnv, - TypeCastMapping: options.Interpolate.TypeCastMapping, - } - imported, err := load(ctx, config, loadOptions, loaded) - if err != nil { - return nil, nil, err - } - for k, v := range imported.IncludeReferences { - included[k] = append(included[k], v...) - } - - err = importResources(model, imported, r.Path) - if err != nil { - return nil, nil, err - } - } - model.Include = nil - return model, included, nil -} - -// importResources import into model all resources defined by imported, and report error on conflict -func importResources(model *types.Config, imported *types.Project, path []string) error { - services := mapByName(model.Services) - for _, service := range imported.Services { - if present, ok := services[service.Name]; ok { - if reflect.DeepEqual(present, service) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting service %s", path, service.Name) - } - model.Services = append(model.Services, service) - } - for _, service := range imported.DisabledServices { - if disabled, ok := services[service.Name]; ok { - if reflect.DeepEqual(disabled, service) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting service %s", path, service.Name) - } - model.Services = append(model.Services, service) - } - for n, network := range imported.Networks { - if present, ok := model.Networks[n]; ok { - if reflect.DeepEqual(present, network) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting network %s", path, n) - } - model.Networks[n] = network - } - for n, volume := range imported.Volumes { - if present, ok := model.Volumes[n]; ok { - if reflect.DeepEqual(present, volume) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting volume %s", path, n) - } - model.Volumes[n] = volume - } - for n, secret := range imported.Secrets { - if present, ok := model.Secrets[n]; ok { - if reflect.DeepEqual(present, secret) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting secret %s", path, n) - } - model.Secrets[n] = secret - } - for n, config := range imported.Configs { - if present, ok := model.Configs[n]; ok { - if reflect.DeepEqual(present, config) { - continue - } - return fmt.Errorf("imported compose file %s defines conflicting config %s", path, n) - } - model.Configs[n] = config - } - return nil -} diff --git a/vendor/github.com/compose-spec/compose-go/loader/loader.go b/vendor/github.com/compose-spec/compose-go/loader/loader.go deleted file mode 100644 index a70004671d73..000000000000 --- a/vendor/github.com/compose-spec/compose-go/loader/loader.go +++ /dev/null @@ -1,1277 +0,0 @@ -/* - 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 ( - "bytes" - "context" - "fmt" - "io" - "os" - paths "path" - "path/filepath" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/compose-spec/compose-go/consts" - interp "github.com/compose-spec/compose-go/interpolation" - "github.com/compose-spec/compose-go/schema" - "github.com/compose-spec/compose-go/template" - "github.com/compose-spec/compose-go/types" - "github.com/mitchellh/mapstructure" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" -) - -// Options supported by Load -type Options struct { - // Skip schema validation - SkipValidation bool - // Skip interpolation - SkipInterpolation bool - // Skip normalization - SkipNormalization bool - // Resolve paths - ResolvePaths bool - // Convert Windows paths - ConvertWindowsPaths bool - // Skip consistency check - SkipConsistencyCheck bool - // Skip extends - SkipExtends bool - // SkipInclude will ignore `include` and only load model from file(s) set by ConfigDetails - SkipInclude bool - // SkipResolveEnvironment will ignore computing `environment` for services - SkipResolveEnvironment bool - // Interpolation options - Interpolate *interp.Options - // Discard 'env_file' entries after resolving to 'environment' section - discardEnvFiles bool - // Set project projectName - projectName string - // Indicates when the projectName was imperatively set or guessed from path - projectNameImperativelySet bool - // Profiles set profiles to enable - Profiles []string - // ResourceLoaders manages support for remote resources - ResourceLoaders []ResourceLoader -} - -// ResourceLoader is a plugable remote resource resolver -type ResourceLoader interface { - // Accept returns `true` is the resource reference matches ResourceLoader supported protocol(s) - Accept(path string) bool - // Load returns the path to a local copy of remote resource identified by `path`. - Load(ctx context.Context, path string) (string, error) -} - -func (o *Options) clone() *Options { - return &Options{ - SkipValidation: o.SkipValidation, - SkipInterpolation: o.SkipInterpolation, - SkipNormalization: o.SkipNormalization, - ResolvePaths: o.ResolvePaths, - ConvertWindowsPaths: o.ConvertWindowsPaths, - SkipConsistencyCheck: o.SkipConsistencyCheck, - SkipExtends: o.SkipExtends, - SkipInclude: o.SkipInclude, - Interpolate: o.Interpolate, - discardEnvFiles: o.discardEnvFiles, - projectName: o.projectName, - projectNameImperativelySet: o.projectNameImperativelySet, - Profiles: o.Profiles, - ResourceLoaders: o.ResourceLoaders, - } -} - -func (o *Options) SetProjectName(name string, imperativelySet bool) { - o.projectName = name - o.projectNameImperativelySet = imperativelySet -} - -func (o Options) GetProjectName() (string, bool) { - return o.projectName, o.projectNameImperativelySet -} - -// serviceRef identifies a reference to a service. It's used to detect cyclic -// references in "extends". -type serviceRef struct { - filename string - service string -} - -type cycleTracker struct { - loaded []serviceRef -} - -func (ct *cycleTracker) Add(filename, service string) error { - toAdd := serviceRef{filename: filename, service: service} - for _, loaded := range ct.loaded { - if toAdd == loaded { - // Create an error message of the form: - // Circular reference: - // service-a in docker-compose.yml - // extends service-b in docker-compose.yml - // extends service-a in docker-compose.yml - errLines := []string{ - "Circular reference:", - fmt.Sprintf(" %s in %s", ct.loaded[0].service, ct.loaded[0].filename), - } - for _, service := range append(ct.loaded[1:], toAdd) { - errLines = append(errLines, fmt.Sprintf(" extends %s in %s", service.service, service.filename)) - } - - return errors.New(strings.Join(errLines, "\n")) - } - } - - ct.loaded = append(ct.loaded, toAdd) - return nil -} - -// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to -// the `environment` section -func WithDiscardEnvFiles(opts *Options) { - opts.discardEnvFiles = true -} - -// WithSkipValidation sets the Options to skip validation when loading sections -func WithSkipValidation(opts *Options) { - opts.SkipValidation = true -} - -// WithProfiles sets profiles to be activated -func WithProfiles(profiles []string) func(*Options) { - return func(opts *Options) { - opts.Profiles = profiles - } -} - -// ParseYAML reads the bytes from a file, parses the bytes into a mapping -// structure, and returns it. -func ParseYAML(source []byte) (map[string]interface{}, error) { - r := bytes.NewReader(source) - decoder := yaml.NewDecoder(r) - m, _, err := parseYAML(decoder) - return m, err -} - -// PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase -// that hardly can be implemented using go-yaml and mapstructure -type PostProcessor interface { - yaml.Unmarshaler - - // Apply changes to compose model based on recorder metadata - Apply(config *types.Config) error -} - -func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, error) { - var cfg interface{} - processor := ResetProcessor{target: &cfg} - - if err := decoder.Decode(&processor); err != nil { - return nil, nil, err - } - stringMap, ok := cfg.(map[string]interface{}) - if ok { - converted, err := convertToStringKeysRecursive(stringMap, "") - if err != nil { - return nil, nil, err - } - return converted.(map[string]interface{}), &processor, nil - } - cfgMap, ok := cfg.(map[interface{}]interface{}) - if !ok { - return nil, nil, errors.Errorf("Top-level object must be a mapping") - } - converted, err := convertToStringKeysRecursive(cfgMap, "") - if err != nil { - return nil, nil, err - } - return converted.(map[string]interface{}), &processor, nil -} - -// Load reads a ConfigDetails and returns a fully loaded configuration. -// Deprecated: use LoadWithContext. -func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { - return LoadWithContext(context.Background(), configDetails, options...) -} - -// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration -func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { - if len(configDetails.ConfigFiles) < 1 { - return nil, errors.Errorf("No files specified") - } - - opts := &Options{ - Interpolate: &interp.Options{ - Substitute: template.Substitute, - LookupValue: configDetails.LookupEnv, - TypeCastMapping: interpolateTypeCastMapping, - }, - ResolvePaths: true, - } - - for _, op := range options { - op(opts) - } - - projectName, err := projectName(configDetails, opts) - if err != nil { - return nil, err - } - opts.projectName = projectName - - // TODO(milas): this should probably ALWAYS set (overriding any existing) - if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && projectName != "" { - if configDetails.Environment == nil { - configDetails.Environment = map[string]string{} - } - configDetails.Environment[consts.ComposeProjectName] = projectName - } - - return load(ctx, configDetails, opts, nil) -} - -func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) { - var model *types.Config - - mainFile := configDetails.ConfigFiles[0].Filename - for _, f := range loaded { - if f == mainFile { - loaded = append(loaded, mainFile) - return nil, errors.Errorf("include cycle detected:\n%s\n include %s", loaded[0], strings.Join(loaded[1:], "\n include ")) - } - } - loaded = append(loaded, mainFile) - - includeRefs := make(map[string][]types.IncludeConfig) - for _, file := range configDetails.ConfigFiles { - var postProcessor PostProcessor - configDict := file.Config - - processYaml := func() error { - if !opts.SkipValidation { - if err := schema.Validate(configDict); err != nil { - return fmt.Errorf("validating %s: %w", file.Filename, err) - } - } - - configDict = groupXFieldsIntoExtensions(configDict) - - cfg, err := loadSections(ctx, file.Filename, configDict, configDetails, opts) - if err != nil { - return err - } - - if !opts.SkipInclude { - var included map[string][]types.IncludeConfig - cfg, included, err = loadInclude(ctx, file.Filename, configDetails, cfg, opts, loaded) - if err != nil { - return err - } - for k, v := range included { - includeRefs[k] = append(includeRefs[k], v...) - } - } - - if model == nil { - model = cfg - } else { - merged, err := merge([]*types.Config{model, cfg}) - if err != nil { - return err - } - model = merged - } - if postProcessor != nil { - err = postProcessor.Apply(model) - if err != nil { - return err - } - } - return nil - } - - if configDict == nil { - if len(file.Content) == 0 { - content, err := os.ReadFile(file.Filename) - if err != nil { - return nil, err - } - file.Content = content - } - - r := bytes.NewReader(file.Content) - decoder := yaml.NewDecoder(r) - for { - dict, p, err := parseConfig(decoder, opts) - if err != nil { - if err != io.EOF { - return nil, fmt.Errorf("parsing %s: %w", file.Filename, err) - } - break - } - configDict = dict - postProcessor = p - - if err := processYaml(); err != nil { - return nil, err - } - } - } else { - if err := processYaml(); err != nil { - return nil, err - } - } - } - - if model == nil { - return nil, errors.New("empty compose file") - } - - project := &types.Project{ - Name: opts.projectName, - WorkingDir: configDetails.WorkingDir, - Services: model.Services, - Networks: model.Networks, - Volumes: model.Volumes, - Secrets: model.Secrets, - Configs: model.Configs, - Environment: configDetails.Environment, - Extensions: model.Extensions, - } - - if len(includeRefs) != 0 { - project.IncludeReferences = includeRefs - } - - if !opts.SkipNormalization { - err := Normalize(project) - if err != nil { - return nil, err - } - } - - if opts.ResolvePaths { - err := ResolveRelativePaths(project) - if err != nil { - return nil, err - } - } - - if opts.ConvertWindowsPaths { - for i, service := range project.Services { - for j, volume := range service.Volumes { - service.Volumes[j] = convertVolumePath(volume) - } - project.Services[i] = service - } - } - - if !opts.SkipConsistencyCheck { - err := checkConsistency(project) - if err != nil { - return nil, err - } - } - - project.ApplyProfiles(opts.Profiles) - - if !opts.SkipResolveEnvironment { - err := project.ResolveServicesEnvironment(opts.discardEnvFiles) - if err != nil { - return nil, err - } - } - - return project, nil -} - -func InvalidProjectNameErr(v string) error { - return fmt.Errorf( - "invalid project name %q: must consist only of lowercase alphanumeric characters, hyphens, and underscores as well as start with a letter or number", - v, - ) -} - -// projectName determines the canonical name to use for the project considering -// the loader Options as well as `name` fields in Compose YAML fields (which -// also support interpolation). -// -// TODO(milas): restructure loading so that we don't need to re-parse the YAML -// here, as it's both wasteful and makes this code error-prone. -func projectName(details types.ConfigDetails, opts *Options) (string, error) { - projectName, projectNameImperativelySet := opts.GetProjectName() - - // if user did NOT provide a name explicitly, then see if one is defined - // in any of the config files - if !projectNameImperativelySet { - var pjNameFromConfigFile string - for _, configFile := range details.ConfigFiles { - yml, err := ParseYAML(configFile.Content) - if err != nil { - // HACK: the way that loading is currently structured, this is - // a duplicative parse just for the `name`. if it fails, we - // give up but don't return the error, knowing that it'll get - // caught downstream for us - return "", nil - } - if val, ok := yml["name"]; ok && val != "" { - sVal, ok := val.(string) - if !ok { - // HACK: see above - this is a temporary parsed version - // that hasn't been schema-validated, but we don't want - // to be the ones to actually report that, so give up, - // knowing that it'll get caught downstream for us - return "", nil - } - pjNameFromConfigFile = sVal - } - } - if !opts.SkipInterpolation { - interpolated, err := interp.Interpolate( - map[string]interface{}{"name": pjNameFromConfigFile}, - *opts.Interpolate, - ) - if err != nil { - return "", err - } - pjNameFromConfigFile = interpolated["name"].(string) - } - pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile) - if pjNameFromConfigFile != "" { - projectName = pjNameFromConfigFile - } - } - - if projectName == "" { - return "", errors.New("project name must not be empty") - } - - if NormalizeProjectName(projectName) != projectName { - return "", InvalidProjectNameErr(projectName) - } - - return projectName, nil -} - -func NormalizeProjectName(s string) string { - r := regexp.MustCompile("[a-z0-9_-]") - s = strings.ToLower(s) - s = strings.Join(r.FindAllString(s, -1), "") - return strings.TrimLeft(s, "_-") -} - -func parseConfig(decoder *yaml.Decoder, opts *Options) (map[string]interface{}, PostProcessor, error) { - yml, postProcessor, err := parseYAML(decoder) - if err != nil { - return nil, nil, err - } - if !opts.SkipInterpolation { - interpolated, err := interp.Interpolate(yml, *opts.Interpolate) - return interpolated, postProcessor, err - } - return yml, postProcessor, err -} - -const extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key - -func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} { - extras := map[string]interface{}{} - for key, value := range dict { - if strings.HasPrefix(key, "x-") { - extras[key] = value - delete(dict, key) - } - if d, ok := value.(map[string]interface{}); ok { - dict[key] = groupXFieldsIntoExtensions(d) - } - } - if len(extras) > 0 { - dict[extensions] = extras - } - return dict -} - -func loadSections(ctx context.Context, filename string, config map[string]interface{}, configDetails types.ConfigDetails, opts *Options) (*types.Config, error) { - var err error - cfg := types.Config{ - Filename: filename, - } - name := "" - if n, ok := config["name"]; ok { - name, ok = n.(string) - if !ok { - return nil, errors.New("project name must be a string") - } - } - cfg.Name = name - cfg.Services, err = LoadServices(ctx, filename, getSection(config, "services"), configDetails.WorkingDir, configDetails.LookupEnv, opts) - if err != nil { - return nil, err - } - cfg.Networks, err = LoadNetworks(getSection(config, "networks")) - if err != nil { - return nil, err - } - cfg.Volumes, err = LoadVolumes(getSection(config, "volumes")) - if err != nil { - return nil, err - } - cfg.Secrets, err = LoadSecrets(getSection(config, "secrets")) - if err != nil { - return nil, err - } - cfg.Configs, err = LoadConfigObjs(getSection(config, "configs")) - if err != nil { - return nil, err - } - cfg.Include, err = LoadIncludeConfig(getSequence(config, "include")) - if err != nil { - return nil, err - } - extensions := getSection(config, extensions) - if len(extensions) > 0 { - cfg.Extensions = extensions - } - return &cfg, nil -} - -func getSection(config map[string]interface{}, key string) map[string]interface{} { - section, ok := config[key] - if !ok { - return make(map[string]interface{}) - } - return section.(map[string]interface{}) -} - -func getSequence(config map[string]interface{}, key string) []interface{} { - section, ok := config[key] - if !ok { - return make([]interface{}, 0) - } - return section.([]interface{}) -} - -// ForbiddenPropertiesError is returned when there are properties in the Compose -// file that are forbidden. -type ForbiddenPropertiesError struct { - Properties map[string]string -} - -func (e *ForbiddenPropertiesError) Error() string { - return "Configuration contains forbidden properties" -} - -// Transform converts the source into the target struct with compose types transformer -// and the specified transformers if any. -func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error { - data := mapstructure.Metadata{} - config := &mapstructure.DecoderConfig{ - DecodeHook: mapstructure.ComposeDecodeHookFunc( - createTransformHook(additionalTransformers...), - decoderHook), - Result: target, - TagName: "yaml", - Metadata: &data, - } - decoder, err := mapstructure.NewDecoder(config) - if err != nil { - return err - } - return decoder.Decode(source) -} - -// TransformerFunc defines a function to perform the actual transformation -type TransformerFunc func(interface{}) (interface{}, error) - -// Transformer defines a map to type transformer -type Transformer struct { - TypeOf reflect.Type - Func TransformerFunc -} - -func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType { - transforms := map[reflect.Type]func(interface{}) (interface{}, error){ - reflect.TypeOf(types.External{}): transformExternal, - reflect.TypeOf(types.Options{}): transformOptions, - reflect.TypeOf(types.UlimitsConfig{}): transformUlimits, - reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort, - reflect.TypeOf(types.ServiceSecretConfig{}): transformFileReferenceConfig, - reflect.TypeOf(types.ServiceConfigObjConfig{}): transformFileReferenceConfig, - reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap, - reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false), - reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), - reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), - reflect.TypeOf(types.HostsList{}): transformMappingOrListFunc(":", false), - reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, - reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, - reflect.TypeOf(types.DependsOnConfig{}): transformDependsOnConfig, - reflect.TypeOf(types.ExtendsConfig{}): transformExtendsConfig, - reflect.TypeOf(types.SSHConfig{}): transformSSHConfig, - reflect.TypeOf(types.IncludeConfig{}): transformIncludeConfig, - } - - for _, transformer := range additionalTransformers { - transforms[transformer.TypeOf] = transformer.Func - } - - return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) { - transform, ok := transforms[target] - if !ok { - return data, nil - } - return transform(data) - } -} - -// keys need to be converted to strings for jsonschema -func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { - if mapping, ok := value.(map[string]interface{}); ok { - for key, entry := range mapping { - var newKeyPrefix string - if keyPrefix == "" { - newKeyPrefix = key - } else { - newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, key) - } - convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) - if err != nil { - return nil, err - } - mapping[key] = convertedEntry - } - return mapping, nil - } - if mapping, ok := value.(map[interface{}]interface{}); ok { - dict := make(map[string]interface{}) - for key, entry := range mapping { - str, ok := key.(string) - if !ok { - return nil, formatInvalidKeyError(keyPrefix, key) - } - var newKeyPrefix string - if keyPrefix == "" { - newKeyPrefix = str - } else { - newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) - } - convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) - if err != nil { - return nil, err - } - dict[str] = convertedEntry - } - return dict, nil - } - if list, ok := value.([]interface{}); ok { - var convertedList []interface{} - for index, entry := range list { - newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) - convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) - if err != nil { - return nil, err - } - convertedList = append(convertedList, convertedEntry) - } - return convertedList, nil - } - return value, nil -} - -func formatInvalidKeyError(keyPrefix string, key interface{}) error { - var location string - if keyPrefix == "" { - location = "at top level" - } else { - location = fmt.Sprintf("in %s", keyPrefix) - } - return errors.Errorf("Non-string key %s: %#v", location, key) -} - -// LoadServices produces a ServiceConfig map from a compose file Dict -// the servicesDict is not validated if directly used. Use Load() to enable validation -func LoadServices(ctx context.Context, filename string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options) ([]types.ServiceConfig, error) { - var services []types.ServiceConfig - - x, ok := servicesDict[extensions] - if ok { - // as a top-level attribute, "services" doesn't support extensions, and a service can be named `x-foo` - for k, v := range x.(map[string]interface{}) { - servicesDict[k] = v - } - delete(servicesDict, extensions) - } - - for name := range servicesDict { - serviceConfig, err := loadServiceWithExtends(ctx, filename, name, servicesDict, workingDir, lookupEnv, opts, &cycleTracker{}) - if err != nil { - return nil, err - } - - services = append(services, *serviceConfig) - } - - return services, nil -} - -func loadServiceWithExtends(ctx context.Context, filename, name string, servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, opts *Options, ct *cycleTracker) (*types.ServiceConfig, error) { - if err := ct.Add(filename, name); err != nil { - return nil, err - } - - target, ok := servicesDict[name] - if !ok { - return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, filename) - } - - if target == nil { - target = map[string]interface{}{} - } - - serviceConfig, err := LoadService(name, target.(map[string]interface{})) - if err != nil { - return nil, err - } - - if serviceConfig.Extends != nil && !opts.SkipExtends { - baseServiceName := serviceConfig.Extends.Service - var baseService *types.ServiceConfig - file := serviceConfig.Extends.File - if file == "" { - baseService, err = loadServiceWithExtends(ctx, filename, baseServiceName, servicesDict, workingDir, lookupEnv, opts, ct) - if err != nil { - return nil, err - } - } else { - for _, loader := range opts.ResourceLoaders { - if loader.Accept(file) { - path, err := loader.Load(ctx, file) - if err != nil { - return nil, err - } - file = path - break - } - } - // Resolve the path to the imported file, and load it. - baseFilePath := absPath(workingDir, file) - - b, err := os.ReadFile(baseFilePath) - if err != nil { - return nil, err - } - - r := bytes.NewReader(b) - decoder := yaml.NewDecoder(r) - - baseFile, _, err := parseConfig(decoder, opts) - if err != nil { - return nil, err - } - - baseFileServices := getSection(baseFile, "services") - baseService, err = loadServiceWithExtends(ctx, baseFilePath, baseServiceName, baseFileServices, filepath.Dir(baseFilePath), lookupEnv, opts, ct) - if err != nil { - return nil, err - } - - // Make paths relative to the importing Compose file. Note that we - // make the paths relative to `file` rather than `baseFilePath` so - // that the resulting paths won't be absolute if `file` isn't an - // absolute path. - - baseFileParent := filepath.Dir(file) - ResolveServiceRelativePaths(baseFileParent, baseService) - } - - serviceConfig, err = _merge(baseService, serviceConfig) - if err != nil { - return nil, err - } - serviceConfig.Extends = nil - } - - return serviceConfig, nil -} - -// LoadService produces a single ServiceConfig from a compose file Dict -// the serviceDict is not validated if directly used. Use Load() to enable validation -func LoadService(name string, serviceDict map[string]interface{}) (*types.ServiceConfig, error) { - serviceConfig := &types.ServiceConfig{ - Scale: 1, - } - if err := Transform(serviceDict, serviceConfig); err != nil { - return nil, err - } - serviceConfig.Name = name - - for i, volume := range serviceConfig.Volumes { - if volume.Type != types.VolumeTypeBind { - continue - } - if volume.Source == "" { - return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`) - } - - serviceConfig.Volumes[i] = volume - } - - return serviceConfig, nil -} - -// Windows paths, c:\\my\\path\\shiny, need to be changed to be compatible with -// the Engine. Volume paths are expected to be linux style /c/my/path/shiny/ -func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig { - volumeName := strings.ToLower(filepath.VolumeName(volume.Source)) - if len(volumeName) != 2 { - return volume - } - - convertedSource := fmt.Sprintf("/%c%s", volumeName[0], volume.Source[len(volumeName):]) - convertedSource = strings.ReplaceAll(convertedSource, "\\", "/") - - volume.Source = convertedSource - return volume -} - -func resolveMaybeUnixPath(workingDir string, path string) string { - filePath := expandUser(path) - // Check if source is an absolute path (either Unix or Windows), to - // handle a Windows client with a Unix daemon or vice-versa. - // - // Note that this is not required for Docker for Windows when specifying - // a local Windows path, because Docker for Windows translates the Windows - // path into a valid path within the VM. - if !paths.IsAbs(filePath) && !isAbs(filePath) { - filePath = absPath(workingDir, filePath) - } - return filePath -} - -// TODO: make this more robust -func expandUser(path string) string { - if strings.HasPrefix(path, "~") { - home, err := os.UserHomeDir() - if err != nil { - logrus.Warn("cannot expand '~', because the environment lacks HOME") - return path - } - return filepath.Join(home, path[1:]) - } - return path -} - -func transformUlimits(data interface{}) (interface{}, error) { - switch value := data.(type) { - case int: - return types.UlimitsConfig{Single: value}, nil - case map[string]interface{}: - ulimit := types.UlimitsConfig{} - if v, ok := value["soft"]; ok { - ulimit.Soft = v.(int) - } - if v, ok := value["hard"]; ok { - ulimit.Hard = v.(int) - } - return ulimit, nil - default: - return data, errors.Errorf("invalid type %T for ulimits", value) - } -} - -// LoadNetworks produces a NetworkConfig map from a compose file Dict -// the source Dict is not validated if directly used. Use Load() to enable validation -func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig, error) { - networks := make(map[string]types.NetworkConfig) - err := Transform(source, &networks) - if err != nil { - return networks, err - } - for name, network := range networks { - if !network.External.External { - continue - } - switch { - case network.External.Name != "": - if network.Name != "" { - return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) - } - logrus.Warnf("network %s: network.external.name is deprecated. Please set network.name with external: true", name) - network.Name = network.External.Name - network.External.Name = "" - case network.Name == "": - network.Name = name - } - networks[name] = network - } - return networks, nil -} - -func externalVolumeError(volume, key string) error { - return errors.Errorf( - "conflicting parameters \"external\" and %q specified for volume %q", - key, volume) -} - -// LoadVolumes produces a VolumeConfig map from a compose file Dict -// the source Dict is not validated if directly used. Use Load() to enable validation -func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, error) { - volumes := make(map[string]types.VolumeConfig) - if err := Transform(source, &volumes); err != nil { - return volumes, err - } - - for name, volume := range volumes { - if !volume.External.External { - continue - } - switch { - case volume.Driver != "": - return nil, externalVolumeError(name, "driver") - case len(volume.DriverOpts) > 0: - return nil, externalVolumeError(name, "driver_opts") - case len(volume.Labels) > 0: - return nil, externalVolumeError(name, "labels") - case volume.External.Name != "": - if volume.Name != "" { - return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) - } - logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name) - volume.Name = volume.External.Name - volume.External.Name = "" - case volume.Name == "": - volume.Name = name - } - volumes[name] = volume - } - return volumes, nil -} - -// LoadSecrets produces a SecretConfig map from a compose file Dict -// the source Dict is not validated if directly used. Use Load() to enable validation -func LoadSecrets(source map[string]interface{}) (map[string]types.SecretConfig, error) { - secrets := make(map[string]types.SecretConfig) - if err := Transform(source, &secrets); err != nil { - return secrets, err - } - for name, secret := range secrets { - obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret)) - if err != nil { - return nil, err - } - secrets[name] = types.SecretConfig(obj) - } - return secrets, nil -} - -// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict -// the source Dict is not validated if directly used. Use Load() to enable validation -func LoadConfigObjs(source map[string]interface{}) (map[string]types.ConfigObjConfig, error) { - configs := make(map[string]types.ConfigObjConfig) - if err := Transform(source, &configs); err != nil { - return configs, err - } - for name, config := range configs { - obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config)) - if err != nil { - return nil, err - } - configs[name] = types.ConfigObjConfig(obj) - } - return configs, nil -} - -func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig) (types.FileObjectConfig, error) { - // if "external: true" - switch { - case obj.External.External: - // handle deprecated external.name - if obj.External.Name != "" { - if obj.Name != "" { - return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) - } - logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) - obj.Name = obj.External.Name - obj.External.Name = "" - } else if obj.Name == "" { - obj.Name = name - } - // if not "external: true" - case obj.Driver != "": - if obj.File != "" { - return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) - } - } - - return obj, nil -} - -var transformOptions TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case map[string]interface{}: - return toMapStringString(value, false), nil - case map[string]string: - return value, nil - default: - return data, errors.Errorf("invalid type %T for map[string]string", value) - } -} - -var transformExternal TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case bool: - return map[string]interface{}{"external": value}, nil - case map[string]interface{}: - return map[string]interface{}{"external": true, "name": value["name"]}, nil - default: - return data, errors.Errorf("invalid type %T for external", value) - } -} - -var transformServicePort TransformerFunc = func(data interface{}) (interface{}, error) { - switch entries := data.(type) { - case []interface{}: - // We process the list instead of individual items here. - // The reason is that one entry might be mapped to multiple ServicePortConfig. - // Therefore we take an input of a list and return an output of a list. - var ports []interface{} - for _, entry := range entries { - switch value := entry.(type) { - case int: - parsed, err := types.ParsePortConfig(fmt.Sprint(value)) - if err != nil { - return data, err - } - for _, v := range parsed { - ports = append(ports, v) - } - case string: - parsed, err := types.ParsePortConfig(value) - if err != nil { - return data, err - } - for _, v := range parsed { - ports = append(ports, v) - } - case map[string]interface{}: - published := value["published"] - if v, ok := published.(int); ok { - value["published"] = strconv.Itoa(v) - } - ports = append(ports, groupXFieldsIntoExtensions(value)) - default: - return data, errors.Errorf("invalid type %T for port", value) - } - } - return ports, nil - default: - return data, errors.Errorf("invalid type %T for port", entries) - } -} - -var transformFileReferenceConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case string: - return map[string]interface{}{"source": value}, nil - case map[string]interface{}: - if target, ok := value["target"]; ok { - value["target"] = cleanTarget(target.(string)) - } - return groupXFieldsIntoExtensions(value), nil - default: - return data, errors.Errorf("invalid type %T for secret", value) - } -} - -func cleanTarget(target string) string { - if target == "" { - return "" - } - return paths.Clean(target) -} - -var transformBuildConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case string: - return map[string]interface{}{"context": value}, nil - case map[string]interface{}: - return groupXFieldsIntoExtensions(data.(map[string]interface{})), nil - default: - return data, errors.Errorf("invalid type %T for service build", value) - } -} - -var transformDependsOnConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case []interface{}: - transformed := map[string]interface{}{} - for _, serviceIntf := range value { - service, ok := serviceIntf.(string) - if !ok { - return data, errors.Errorf("invalid type %T for service depends_on element, expected string", value) - } - transformed[service] = map[string]interface{}{"condition": types.ServiceConditionStarted, "required": true} - } - return transformed, nil - case map[string]interface{}: - transformed := map[string]interface{}{} - for service, val := range value { - dependsConfigIntf, ok := val.(map[string]interface{}) - if !ok { - return data, errors.Errorf("invalid type %T for service depends_on element", value) - } - if _, ok := dependsConfigIntf["required"]; !ok { - dependsConfigIntf["required"] = true - } - transformed[service] = dependsConfigIntf - } - return groupXFieldsIntoExtensions(transformed), nil - default: - return data, errors.Errorf("invalid type %T for service depends_on", value) - } -} - -var transformExtendsConfig TransformerFunc = func(value interface{}) (interface{}, error) { - switch value.(type) { - case string: - return map[string]interface{}{"service": value}, nil - case map[string]interface{}: - return value, nil - default: - return value, errors.Errorf("invalid type %T for extends", value) - } -} - -var transformServiceVolumeConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case string: - volume, err := ParseVolume(value) - volume.Target = cleanTarget(volume.Target) - return volume, err - case map[string]interface{}: - data := groupXFieldsIntoExtensions(data.(map[string]interface{})) - if target, ok := data["target"]; ok { - data["target"] = cleanTarget(target.(string)) - } - return data, nil - default: - return data, errors.Errorf("invalid type %T for service volume", value) - } -} - -var transformServiceNetworkMap TransformerFunc = func(value interface{}) (interface{}, error) { - if list, ok := value.([]interface{}); ok { - mapValue := map[interface{}]interface{}{} - for _, name := range list { - mapValue[name] = nil - } - return mapValue, nil - } - return value, nil -} - -var transformSSHConfig TransformerFunc = func(data interface{}) (interface{}, error) { - switch value := data.(type) { - case map[string]interface{}: - var result []types.SSHKey - for key, val := range value { - if val == nil { - val = "" - } - result = append(result, types.SSHKey{ID: key, Path: val.(string)}) - } - return result, nil - case []interface{}: - var result []types.SSHKey - for _, v := range value { - key, val := transformValueToMapEntry(v.(string), "=", false) - result = append(result, types.SSHKey{ID: key, Path: val.(string)}) - } - return result, nil - case string: - return ParseShortSSHSyntax(value) - } - return nil, errors.Errorf("expected a sting, map or a list, got %T: %#v", data, data) -} - -// ParseShortSSHSyntax parse short syntax for SSH authentications -func ParseShortSSHSyntax(value string) ([]types.SSHKey, error) { - if value == "" { - value = "default" - } - key, val := transformValueToMapEntry(value, "=", false) - result := []types.SSHKey{{ID: key, Path: val.(string)}} - return result, nil -} - -func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc { - return func(data interface{}) (interface{}, error) { - return transformMappingOrList(data, sep, allowNil) - } -} - -func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) (interface{}, error) { - switch value := mappingOrList.(type) { - case map[string]interface{}: - return toMapStringString(value, allowNil), nil - case []interface{}: - result := make(map[string]interface{}) - for _, value := range value { - key, val := transformValueToMapEntry(value.(string), sep, allowNil) - result[key] = val - } - return result, nil - } - return nil, errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList) -} - -func transformValueToMapEntry(value string, separator string, allowNil bool) (string, interface{}) { - parts := strings.SplitN(value, separator, 2) - key := parts[0] - switch { - case len(parts) == 1 && allowNil: - return key, nil - case len(parts) == 1 && !allowNil: - return key, "" - default: - return key, parts[1] - } -} - -func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} { - output := make(map[string]interface{}) - for key, value := range value { - output[key] = toString(value, allowNil) - } - return output -} - -func toString(value interface{}, allowNil bool) interface{} { - switch { - case value != nil: - return fmt.Sprint(value) - case allowNil: - return nil - default: - return "" - } -} diff --git a/vendor/github.com/compose-spec/compose-go/loader/merge.go b/vendor/github.com/compose-spec/compose-go/loader/merge.go deleted file mode 100644 index 654d711deea7..000000000000 --- a/vendor/github.com/compose-spec/compose-go/loader/merge.go +++ /dev/null @@ -1,378 +0,0 @@ -/* - 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 ( - "reflect" - "sort" - - "github.com/compose-spec/compose-go/types" - "github.com/imdario/mergo" - "github.com/pkg/errors" -) - -type specials struct { - m map[reflect.Type]func(dst, src reflect.Value) error -} - -var serviceSpecials = &specials{ - m: map[reflect.Type]func(dst, src reflect.Value) error{ - reflect.TypeOf(&types.LoggingConfig{}): safelyMerge(mergeLoggingConfig), - reflect.TypeOf(&types.UlimitsConfig{}): safelyMerge(mergeUlimitsConfig), - reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice), - reflect.TypeOf([]types.ServicePortConfig{}): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice), - reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice), - reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice), - reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig, - }, -} - -func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) error { - // TODO this is a workaround waiting for imdario/mergo#131 - if t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Bool { - return func(dst, src reflect.Value) error { - if dst.CanSet() && !src.IsNil() { - dst.Set(src) - } - return nil - } - } - if fn, ok := s.m[t]; ok { - return fn - } - return nil -} - -func merge(configs []*types.Config) (*types.Config, error) { - base := configs[0] - for _, override := range configs[1:] { - var err error - base.Name = mergeNames(base.Name, override.Name) - base.Services, err = mergeServices(base.Services, override.Services) - if err != nil { - return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename) - } - base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes) - if err != nil { - return base, errors.Wrapf(err, "cannot merge volumes from %s", override.Filename) - } - base.Networks, err = mergeNetworks(base.Networks, override.Networks) - if err != nil { - return base, errors.Wrapf(err, "cannot merge networks from %s", override.Filename) - } - base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets) - if err != nil { - return base, errors.Wrapf(err, "cannot merge secrets from %s", override.Filename) - } - base.Configs, err = mergeConfigs(base.Configs, override.Configs) - if err != nil { - return base, errors.Wrapf(err, "cannot merge configs from %s", override.Filename) - } - base.Extensions, err = mergeExtensions(base.Extensions, override.Extensions) - if err != nil { - return base, errors.Wrapf(err, "cannot merge extensions from %s", override.Filename) - } - } - return base, nil -} - -func mergeNames(base, override string) string { - if override != "" { - return override - } - return base -} - -func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) { - baseServices := mapByName(base) - overrideServices := mapByName(override) - for name, overrideService := range overrideServices { - overrideService := overrideService - if baseService, ok := baseServices[name]; ok { - merged, err := _merge(&baseService, &overrideService) - if err != nil { - return nil, errors.Wrapf(err, "cannot merge service %s", name) - } - baseServices[name] = *merged - continue - } - baseServices[name] = overrideService - } - services := []types.ServiceConfig{} - for _, baseService := range baseServices { - services = append(services, baseService) - } - sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) - return services, nil -} - -func _merge(baseService *types.ServiceConfig, overrideService *types.ServiceConfig) (*types.ServiceConfig, error) { - if err := mergo.Merge(baseService, overrideService, - mergo.WithAppendSlice, - mergo.WithOverride, - mergo.WithTransformers(serviceSpecials)); err != nil { - return nil, err - } - if overrideService.Command != nil { - baseService.Command = overrideService.Command - } - if overrideService.HealthCheck != nil && overrideService.HealthCheck.Test != nil { - baseService.HealthCheck.Test = overrideService.HealthCheck.Test - } - if overrideService.Entrypoint != nil { - baseService.Entrypoint = overrideService.Entrypoint - } - if baseService.Environment != nil { - baseService.Environment.OverrideBy(overrideService.Environment) - } else { - baseService.Environment = overrideService.Environment - } - baseService.Expose = unique(baseService.Expose) - return baseService, nil -} - -func unique(slice []string) []string { - if slice == nil { - return nil - } - uniqMap := make(map[string]struct{}) - var uniqSlice []string - for _, v := range slice { - if _, ok := uniqMap[v]; !ok { - uniqSlice = append(uniqSlice, v) - uniqMap[v] = struct{}{} - } - } - return uniqSlice -} - -func toServiceSecretConfigsMap(s interface{}) (map[interface{}]interface{}, error) { - secrets, ok := s.([]types.ServiceSecretConfig) - if !ok { - return nil, errors.Errorf("not a serviceSecretConfig: %v", s) - } - m := map[interface{}]interface{}{} - for _, secret := range secrets { - m[secret.Source] = secret - } - return m, nil -} - -func toServiceConfigObjConfigsMap(s interface{}) (map[interface{}]interface{}, error) { - secrets, ok := s.([]types.ServiceConfigObjConfig) - if !ok { - return nil, errors.Errorf("not a serviceSecretConfig: %v", s) - } - m := map[interface{}]interface{}{} - for _, secret := range secrets { - m[secret.Source] = secret - } - return m, nil -} - -func toServicePortConfigsMap(s interface{}) (map[interface{}]interface{}, error) { - ports, ok := s.([]types.ServicePortConfig) - if !ok { - return nil, errors.Errorf("not a servicePortConfig slice: %v", s) - } - m := map[interface{}]interface{}{} - type port struct { - target uint32 - published string - ip string - protocol string - } - - for _, p := range ports { - mergeKey := port{ - target: p.Target, - published: p.Published, - ip: p.HostIP, - protocol: p.Protocol, - } - m[mergeKey] = p - } - return m, nil -} - -func toServiceVolumeConfigsMap(s interface{}) (map[interface{}]interface{}, error) { - volumes, ok := s.([]types.ServiceVolumeConfig) - if !ok { - return nil, errors.Errorf("not a ServiceVolumeConfig slice: %v", s) - } - m := map[interface{}]interface{}{} - for _, v := range volumes { - m[v.Target] = v - } - return m, nil -} - -func toServiceSecretConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { - var s []types.ServiceSecretConfig - for _, v := range m { - s = append(s, v.(types.ServiceSecretConfig)) - } - sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source }) - dst.Set(reflect.ValueOf(s)) - return nil -} - -func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { - var s []types.ServiceConfigObjConfig - for _, v := range m { - s = append(s, v.(types.ServiceConfigObjConfig)) - } - sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source }) - dst.Set(reflect.ValueOf(s)) - return nil -} - -func toServicePortConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { - var s []types.ServicePortConfig - for _, v := range m { - s = append(s, v.(types.ServicePortConfig)) - } - sort.Slice(s, func(i, j int) bool { - if s[i].Target != s[j].Target { - return s[i].Target < s[j].Target - } - if s[i].Published != s[j].Published { - return s[i].Published < s[j].Published - } - if s[i].HostIP != s[j].HostIP { - return s[i].HostIP < s[j].HostIP - } - return s[i].Protocol < s[j].Protocol - }) - dst.Set(reflect.ValueOf(s)) - return nil -} - -func toServiceVolumeConfigsSlice(dst reflect.Value, m map[interface{}]interface{}) error { - var s []types.ServiceVolumeConfig - for _, v := range m { - s = append(s, v.(types.ServiceVolumeConfig)) - } - sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target }) - dst.Set(reflect.ValueOf(s)) - return nil -} - -type toMapFn func(s interface{}) (map[interface{}]interface{}, error) -type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error - -func safelyMerge(mergeFn func(dst, src reflect.Value) error) func(dst, src reflect.Value) error { - return func(dst, src reflect.Value) error { - if src.IsNil() { - return nil - } - if dst.IsNil() { - dst.Set(src) - return nil - } - return mergeFn(dst, src) - } -} - -func mergeSlice(toMap toMapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error { - return func(dst, src reflect.Value) error { - dstMap, err := sliceToMap(toMap, dst) - if err != nil { - return err - } - srcMap, err := sliceToMap(toMap, src) - if err != nil { - return err - } - if err := mergo.Map(&dstMap, srcMap, mergo.WithOverride); err != nil { - return err - } - return writeValue(dst, dstMap) - } -} - -func sliceToMap(toMap toMapFn, v reflect.Value) (map[interface{}]interface{}, error) { - // check if valid - if !v.IsValid() { - return nil, errors.Errorf("invalid value : %+v", v) - } - return toMap(v.Interface()) -} - -func mergeLoggingConfig(dst, src reflect.Value) error { - // Same driver, merging options - if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) || - getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" { - if getLoggingDriver(dst.Elem()) == "" { - dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem())) - } - dstOptions := dst.Elem().FieldByName("Options").Interface().(types.Options) - srcOptions := src.Elem().FieldByName("Options").Interface().(types.Options) - return mergo.Merge(&dstOptions, srcOptions, mergo.WithOverride) - } - // Different driver, override with src - dst.Set(src) - return nil -} - -// nolint: unparam -func mergeUlimitsConfig(dst, src reflect.Value) error { - if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { - dst.Elem().Set(src.Elem()) - } - return nil -} - -func getLoggingDriver(v reflect.Value) string { - return v.FieldByName("Driver").String() -} - -func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig { - m := map[string]types.ServiceConfig{} - for _, service := range services { - m[service.Name] = service - } - return m -} - -func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) { - err := mergo.Map(&base, &override, mergo.WithOverride) - return base, err -} - -func mergeNetworks(base, override map[string]types.NetworkConfig) (map[string]types.NetworkConfig, error) { - err := mergo.Map(&base, &override, mergo.WithOverride) - return base, err -} - -func mergeSecrets(base, override map[string]types.SecretConfig) (map[string]types.SecretConfig, error) { - err := mergo.Map(&base, &override, mergo.WithOverride) - return base, err -} - -func mergeConfigs(base, override map[string]types.ConfigObjConfig) (map[string]types.ConfigObjConfig, error) { - err := mergo.Map(&base, &override, mergo.WithOverride) - return base, err -} - -func mergeExtensions(base, override map[string]interface{}) (map[string]interface{}, error) { - if base == nil { - base = map[string]interface{}{} - } - err := mergo.Map(&base, &override, mergo.WithOverride) - return base, err -} diff --git a/vendor/github.com/compose-spec/compose-go/LICENSE b/vendor/github.com/compose-spec/compose-go/v2/LICENSE similarity index 100% rename from vendor/github.com/compose-spec/compose-go/LICENSE rename to vendor/github.com/compose-spec/compose-go/v2/LICENSE diff --git a/vendor/github.com/compose-spec/compose-go/NOTICE b/vendor/github.com/compose-spec/compose-go/v2/NOTICE similarity index 100% rename from vendor/github.com/compose-spec/compose-go/NOTICE rename to vendor/github.com/compose-spec/compose-go/v2/NOTICE diff --git a/vendor/github.com/compose-spec/compose-go/cli/options.go b/vendor/github.com/compose-spec/compose-go/v2/cli/options.go similarity index 91% rename from vendor/github.com/compose-spec/compose-go/cli/options.go rename to vendor/github.com/compose-spec/compose-go/v2/cli/options.go index 6981ebc10bf7..013180494563 100644 --- a/vendor/github.com/compose-spec/compose-go/cli/options.go +++ b/vendor/github.com/compose-spec/compose-go/v2/cli/options.go @@ -18,20 +18,21 @@ package cli import ( "context" + "fmt" "io" "os" "path/filepath" + "strconv" "strings" - "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/compose-spec/compose-go/consts" - "github.com/compose-spec/compose-go/dotenv" - "github.com/compose-spec/compose-go/errdefs" - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/compose-spec/compose-go/utils" + "github.com/compose-spec/compose-go/v2/consts" + "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/errdefs" + "github.com/compose-spec/compose-go/v2/loader" + "github.com/compose-spec/compose-go/v2/types" + "github.com/compose-spec/compose-go/v2/utils" ) // ProjectOptions provides common configuration for loading a project. @@ -52,7 +53,7 @@ type ProjectOptions struct { // ConfigPaths are file paths to one or more Compose files. // - // These are applied in order by the loader following the merge logic + // These are applied in order by the loader following the override logic // as described in the spec. // // The first entry is required and is the primary Compose file. @@ -248,21 +249,44 @@ func WithEnvFile(file string) ProjectOptionsFn { return WithEnvFiles(files...) } -// WithEnvFiles set alternate env files +// WithEnvFiles set env file(s) to be loaded to set project environment. +// defaults to local .env file if no explicit file is selected, until COMPOSE_DISABLE_ENV_FILE is set func WithEnvFiles(file ...string) ProjectOptionsFn { - return func(options *ProjectOptions) error { - options.EnvFiles = file + return func(o *ProjectOptions) error { + if len(file) > 0 { + o.EnvFiles = file + return nil + } + if v, ok := os.LookupEnv(consts.ComposeDisableDefaultEnvFile); ok { + b, err := strconv.ParseBool(v) + if err != nil { + return err + } + if b { + return nil + } + } + + wd, err := o.GetWorkingDir() + if err != nil { + return err + } + defaultDotEnv := filepath.Join(wd, ".env") + + s, err := os.Stat(defaultDotEnv) + if os.IsNotExist(err) { + return nil + } + if !s.IsDir() { + o.EnvFiles = []string{defaultDotEnv} + } return nil } } // WithDotEnv imports environment variables from .env file func WithDotEnv(o *ProjectOptions) error { - wd, err := o.GetWorkingDir() - if err != nil { - return err - } - envMap, err := dotenv.GetEnvFromFile(o.Environment, wd, o.EnvFiles) + envMap, err := dotenv.GetEnvFromFile(o.Environment, o.EnvFiles) if err != nil { return err } @@ -448,7 +472,7 @@ func getConfigPathsFromOptions(options *ProjectOptions) ([]string, error) { if len(options.ConfigPaths) != 0 { return absolutePaths(options.ConfigPaths) } - return nil, errors.Wrap(errdefs.ErrNotFound, "no configuration file provided") + return nil, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound) } func findFiles(names []string, pwd string) []string { diff --git a/vendor/github.com/compose-spec/compose-go/v2/consts/consts.go b/vendor/github.com/compose-spec/compose-go/v2/consts/consts.go new file mode 100644 index 000000000000..592e6f067dbf --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/consts/consts.go @@ -0,0 +1,29 @@ +/* + 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 consts + +const ( + ComposeProjectName = "COMPOSE_PROJECT_NAME" + ComposePathSeparator = "COMPOSE_PATH_SEPARATOR" + ComposeFilePath = "COMPOSE_FILE" + ComposeDisableDefaultEnvFile = "COMPOSE_DISABLE_ENV_FILE" + ComposeProfiles = "COMPOSE_PROFILES" +) + +const Extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key + +type ComposeFileKey struct{} diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/LICENSE b/vendor/github.com/compose-spec/compose-go/v2/dotenv/LICENSE similarity index 100% rename from vendor/github.com/compose-spec/compose-go/dotenv/LICENSE rename to vendor/github.com/compose-spec/compose-go/v2/dotenv/LICENSE diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/env.go b/vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go similarity index 68% rename from vendor/github.com/compose-spec/compose-go/dotenv/env.go rename to vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go index c8a538bcb5af..cc472a60a3ad 100644 --- a/vendor/github.com/compose-spec/compose-go/dotenv/env.go +++ b/vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go @@ -18,20 +18,15 @@ package dotenv import ( "bytes" + "fmt" "os" "path/filepath" - - "github.com/pkg/errors" ) -func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames []string) (map[string]string, error) { +func GetEnvFromFile(currentEnv map[string]string, filenames []string) (map[string]string, error) { envMap := make(map[string]string) - dotEnvFiles := filenames - if len(dotEnvFiles) == 0 { - dotEnvFiles = append(dotEnvFiles, filepath.Join(workingDir, ".env")) - } - for _, dotEnvFile := range dotEnvFiles { + for _, dotEnvFile := range filenames { abs, err := filepath.Abs(dotEnvFile) if err != nil { return envMap, err @@ -40,10 +35,7 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames [ s, err := os.Stat(dotEnvFile) if os.IsNotExist(err) { - if len(filenames) == 0 { - return envMap, nil - } - return envMap, errors.Errorf("Couldn't find env file: %s", dotEnvFile) + return envMap, fmt.Errorf("Couldn't find env file: %s", dotEnvFile) } if err != nil { return envMap, err @@ -53,12 +45,12 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames [ if len(filenames) == 0 { return envMap, nil } - return envMap, errors.Errorf("%s is a directory", dotEnvFile) + return envMap, fmt.Errorf("%s is a directory", dotEnvFile) } b, err := os.ReadFile(dotEnvFile) if os.IsNotExist(err) { - return nil, errors.Errorf("Couldn't read env file: %s", dotEnvFile) + return nil, fmt.Errorf("Couldn't read env file: %s", dotEnvFile) } if err != nil { return envMap, err @@ -73,7 +65,7 @@ func GetEnvFromFile(currentEnv map[string]string, workingDir string, filenames [ return v, ok }) if err != nil { - return envMap, errors.Wrapf(err, "failed to read %s", dotEnvFile) + return envMap, fmt.Errorf("failed to read %s: %w", dotEnvFile, err) } for k, v := range env { envMap[k] = v diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go b/vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go similarity index 98% rename from vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go rename to vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go index 9b95c990e955..e6635ce33b07 100644 --- a/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go +++ b/vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go @@ -20,7 +20,7 @@ import ( "regexp" "strings" - "github.com/compose-spec/compose-go/template" + "github.com/compose-spec/compose-go/v2/template" ) var utf8BOM = []byte("\uFEFF") diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/parser.go b/vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go similarity index 96% rename from vendor/github.com/compose-spec/compose-go/dotenv/parser.go rename to vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go index 11b6d027c9ff..861cd953d6ba 100644 --- a/vendor/github.com/compose-spec/compose-go/dotenv/parser.go +++ b/vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go @@ -159,27 +159,34 @@ func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn previousCharIsEscape := false // lookup quoted string terminator + var chars []byte for i := 1; i < len(src); i++ { - if src[i] == '\n' { + char := src[i] + if char == '\n' { p.line++ } - if char := src[i]; char != quote { + if char != quote { if !previousCharIsEscape && char == '\\' { previousCharIsEscape = true - } else { + continue + } + if previousCharIsEscape { previousCharIsEscape = false + chars = append(chars, '\\') } + chars = append(chars, char) continue } // skip escaped quote symbol (\" or \', depends on quote) if previousCharIsEscape { previousCharIsEscape = false + chars = append(chars, char) continue } // trim quotes - value := string(src[1:i]) + value := string(chars) if quote == prefixDoubleQuote { // expand standard shell escape sequences & then interpolate // variables on the result diff --git a/vendor/github.com/compose-spec/compose-go/errdefs/errors.go b/vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/errdefs/errors.go rename to vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go diff --git a/vendor/github.com/compose-spec/compose-go/loader/volume.go b/vendor/github.com/compose-spec/compose-go/v2/format/volume.go similarity index 96% rename from vendor/github.com/compose-spec/compose-go/loader/volume.go rename to vendor/github.com/compose-spec/compose-go/v2/format/volume.go index dd83414ac934..0083a62cb2be 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/volume.go +++ b/vendor/github.com/compose-spec/compose-go/v2/format/volume.go @@ -14,15 +14,16 @@ limitations under the License. */ -package loader +package format import ( + "errors" + "fmt" "strings" "unicode" "unicode/utf8" - "github.com/compose-spec/compose-go/types" - "github.com/pkg/errors" + "github.com/compose-spec/compose-go/v2/types" ) const endOfSpec = rune(0) @@ -48,7 +49,7 @@ func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { case char == ':' || char == endOfSpec: if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { populateType(&volume) - return volume, errors.Wrapf(err, "invalid spec: %s", spec) + return volume, fmt.Errorf("invalid spec: %s: %w", spec, err) } buffer = nil default: diff --git a/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go b/vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go similarity index 88% rename from vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go rename to vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go index 305730838c49..b56e0afeb987 100644 --- a/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go +++ b/vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go @@ -17,11 +17,12 @@ package interpolation import ( + "errors" + "fmt" "os" - "github.com/compose-spec/compose-go/template" - "github.com/compose-spec/compose-go/tree" - "github.com/pkg/errors" + "github.com/compose-spec/compose-go/v2/template" + "github.com/compose-spec/compose-go/v2/tree" ) // Options supported by Interpolate @@ -80,7 +81,10 @@ func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (inte return newValue, nil } casted, err := caster(newValue) - return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type")) + if err != nil { + return casted, newPathError(path, fmt.Errorf("failed to cast to expected type: %w", err)) + } + return casted, nil case map[string]interface{}: out := map[string]interface{}{} @@ -110,15 +114,16 @@ func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (inte } func newPathError(path tree.Path, err error) error { - switch err := err.(type) { - case nil: + var ite *template.InvalidTemplateError + switch { + case err == nil: return nil - case *template.InvalidTemplateError: - return errors.Errorf( + case errors.As(err, &ite): + return fmt.Errorf( "invalid interpolation format for %s.\nYou may need to escape any $ with another $.\n%s", - path, err.Template) + path, ite.Template) default: - return errors.Wrapf(err, "error while interpolating %s", path) + return fmt.Errorf("error while interpolating %s: %w", path, err) } } diff --git a/vendor/github.com/compose-spec/compose-go/loader/example1.env b/vendor/github.com/compose-spec/compose-go/v2/loader/example1.env similarity index 100% rename from vendor/github.com/compose-spec/compose-go/loader/example1.env rename to vendor/github.com/compose-spec/compose-go/v2/loader/example1.env diff --git a/vendor/github.com/compose-spec/compose-go/loader/example2.env b/vendor/github.com/compose-spec/compose-go/v2/loader/example2.env similarity index 100% rename from vendor/github.com/compose-spec/compose-go/loader/example2.env rename to vendor/github.com/compose-spec/compose-go/v2/loader/example2.env diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go b/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go new file mode 100644 index 000000000000..97c6ca366f35 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go @@ -0,0 +1,147 @@ +/* + 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 ( + "context" + "fmt" + "path/filepath" + + "github.com/compose-spec/compose-go/v2/consts" + "github.com/compose-spec/compose-go/v2/override" + "github.com/compose-spec/compose-go/v2/types" +) + +func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, tracker *cycleTracker, post ...PostProcessor) error { + a, ok := dict["services"] + if !ok { + return nil + } + services, ok := a.(map[string]any) + if !ok { + return fmt.Errorf("services must be a mapping") + } + for name, s := range services { + service, ok := s.(map[string]any) + if !ok { + return fmt.Errorf("services.%s must be a mapping", name) + } + x, ok := service["extends"] + if !ok { + continue + } + ct, err := tracker.Add(ctx.Value(consts.ComposeFileKey{}).(string), name) + if err != nil { + return err + } + var ( + ref string + file any + ) + switch v := x.(type) { + case map[string]any: + ref = v["service"].(string) + file = v["file"] + case string: + ref = v + } + + var base any + if file != nil { + path := file.(string) + for _, loader := range opts.ResourceLoaders { + if !loader.Accept(path) { + continue + } + local, err := loader.Load(ctx, path) + if err != nil { + return err + } + localdir := filepath.Dir(local) + relworkingdir := loader.Dir(path) + + extendsOpts := opts.clone() + extendsOpts.ResourceLoaders = append([]ResourceLoader{}, opts.ResourceLoaders...) + // replace localResourceLoader with a new flavour, using extended file base path + extendsOpts.ResourceLoaders[len(opts.ResourceLoaders)-1] = localResourceLoader{ + WorkingDir: localdir, + } + extendsOpts.ResolvePaths = true + extendsOpts.SkipNormalization = true + extendsOpts.SkipConsistencyCheck = true + extendsOpts.SkipInclude = true + source, err := loadYamlModel(ctx, types.ConfigDetails{ + WorkingDir: relworkingdir, + ConfigFiles: []types.ConfigFile{ + {Filename: local}, + }, + }, extendsOpts, ct, nil) + if err != nil { + return err + } + services := source["services"].(map[string]any) + base, ok = services[ref] + if !ok { + return fmt.Errorf("cannot extend service %q in %s: service not found", name, path) + } + } + if base == nil { + return fmt.Errorf("cannot read %s", path) + } + } else { + base, ok = services[ref] + if !ok { + return fmt.Errorf("cannot extend service %q in %s: service not found", name, "filename") // TODO track filename + } + } + source := deepClone(base).(map[string]any) + for _, processor := range post { + processor.Apply(map[string]any{ + "services": map[string]any{ + name: source, + }, + }) + } + merged, err := override.ExtendService(source, service) + if err != nil { + return err + } + delete(merged, "extends") + services[name] = merged + } + dict["services"] = services + return nil +} + +func deepClone(value any) any { + switch v := value.(type) { + case []any: + cp := make([]any, len(v)) + for i, e := range v { + cp[i] = deepClone(e) + } + return cp + case map[string]any: + cp := make(map[string]any, len(v)) + for k, e := range v { + cp[k] = deepClone(e) + } + return cp + default: + return value + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/fix.go b/vendor/github.com/compose-spec/compose-go/v2/loader/fix.go new file mode 100644 index 000000000000..7a6e88d817ee --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/fix.go @@ -0,0 +1,36 @@ +/* + 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 + +// fixEmptyNotNull is a workaround for https://github.com/xeipuuv/gojsonschema/issues/141 +// as go-yaml `[]` will load as a `[]any(nil)`, which is not the same as an empty array +func fixEmptyNotNull(value any) interface{} { + switch v := value.(type) { + case []any: + if v == nil { + return []any{} + } + for i, e := range v { + v[i] = fixEmptyNotNull(e) + } + case map[string]any: + for k, e := range v { + v[k] = fixEmptyNotNull(e) + } + } + return value +} diff --git a/vendor/github.com/compose-spec/compose-go/loader/full-example.yml b/vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml similarity index 97% rename from vendor/github.com/compose-spec/compose-go/loader/full-example.yml rename to vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml index 24d954578dd5..494e16d6ec17 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/full-example.yml +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml @@ -141,7 +141,8 @@ services: # env_file: .env env_file: - ./example1.env - - ./example2.env + - path: ./example2.env + required: false # Mapping or list # Mapping values can be strings, numbers or null @@ -235,6 +236,7 @@ services: other-network: ipv4_address: 172.16.238.10 ipv6_address: 2001:3984:3989::10 + mac_address: 02:42:72:98:65:08 other-other-network: pid: "host" @@ -295,22 +297,22 @@ services: volumes: # Just specify a path and let the Engine create a volume - - /var/lib/mysql + - /var/lib/anonymous # Specify an absolute path mapping - - /opt/data:/var/lib/mysql + - /opt/data:/var/lib/data # Path on the host, relative to the Compose file - .:/code - ./static:/var/www/html # User-relative path - ~/configs:/etc/configs:ro # Named volume - - datavolume:/var/lib/mysql + - datavolume:/var/lib/volume - type: bind source: ./opt - target: /opt + target: /opt/cached consistency: cached - type: tmpfs - target: /opt + target: /opt/tmpfs tmpfs: size: 10000 diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/include.go b/vendor/github.com/compose-spec/compose-go/v2/loader/include.go new file mode 100644 index 000000000000..e6e3300829a6 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/include.go @@ -0,0 +1,155 @@ +/* + 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 ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/compose-spec/compose-go/v2/dotenv" + interp "github.com/compose-spec/compose-go/v2/interpolation" + "github.com/compose-spec/compose-go/v2/types" +) + +// loadIncludeConfig parse the require config from raw yaml +func loadIncludeConfig(source any) ([]types.IncludeConfig, error) { + if source == nil { + return nil, nil + } + var requires []types.IncludeConfig + err := Transform(source, &requires) + return requires, err +} + +func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model map[string]any, options *Options, included []string) error { + includeConfig, err := loadIncludeConfig(model["include"]) + if err != nil { + return err + } + for _, r := range includeConfig { + for i, p := range r.Path { + for _, loader := range options.ResourceLoaders { + if loader.Accept(p) { + path, err := loader.Load(ctx, p) + if err != nil { + return err + } + p = path + break + } + } + r.Path[i] = absPath(configDetails.WorkingDir, p) + } + + mainFile := r.Path[0] + for _, f := range included { + if f == mainFile { + included = append(included, mainFile) + return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include ")) + } + } + + if r.ProjectDirectory == "" { + r.ProjectDirectory = filepath.Dir(mainFile) + } + + loadOptions := options.clone() + loadOptions.ResolvePaths = true + loadOptions.SkipNormalization = true + loadOptions.SkipConsistencyCheck = true + + if len(r.EnvFile) == 0 { + f := filepath.Join(r.ProjectDirectory, ".env") + if s, err := os.Stat(f); err == nil && !s.IsDir() { + r.EnvFile = types.StringList{f} + } + } + + envFromFile, err := dotenv.GetEnvFromFile(configDetails.Environment, r.EnvFile) + if err != nil { + return err + } + + config := types.ConfigDetails{ + WorkingDir: r.ProjectDirectory, + ConfigFiles: types.ToConfigFiles(r.Path), + Environment: configDetails.Environment.Clone().Merge(envFromFile), + } + loadOptions.Interpolate = &interp.Options{ + Substitute: options.Interpolate.Substitute, + LookupValue: config.LookupEnv, + TypeCastMapping: options.Interpolate.TypeCastMapping, + } + imported, err := loadYamlModel(ctx, config, loadOptions, &cycleTracker{}, included) + if err != nil { + return err + } + err = importResources(imported, model) + if err != nil { + return err + } + } + delete(model, "include") + return nil +} + +// importResources import into model all resources defined by imported, and report error on conflict +func importResources(source map[string]any, target map[string]any) error { + if err := importResource(source, target, "services"); err != nil { + return err + } + if err := importResource(source, target, "volumes"); err != nil { + return err + } + if err := importResource(source, target, "networks"); err != nil { + return err + } + if err := importResource(source, target, "secrets"); err != nil { + return err + } + if err := importResource(source, target, "configs"); err != nil { + return err + } + return nil +} + +func importResource(source map[string]any, target map[string]any, key string) error { + from := source[key] + if from != nil { + var to map[string]any + if v, ok := target[key]; ok { + to = v.(map[string]any) + } else { + to = map[string]any{} + } + for name, a := range from.(map[string]any) { + if conflict, ok := to[name]; ok { + if reflect.DeepEqual(a, conflict) { + continue + } + return fmt.Errorf("%s.%s conflicts with imported resource", key, name) + } + to[name] = a + } + target[key] = to + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/loader/interpolate.go b/vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go similarity index 96% rename from vendor/github.com/compose-spec/compose-go/loader/interpolate.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go index 655e58e11f36..a1cef1ecc1db 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/interpolate.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go @@ -17,12 +17,12 @@ package loader import ( + "fmt" "strconv" "strings" - interp "github.com/compose-spec/compose-go/interpolation" - "github.com/compose-spec/compose-go/tree" - "github.com/pkg/errors" + interp "github.com/compose-spec/compose-go/v2/interpolation" + "github.com/compose-spec/compose-go/v2/tree" "github.com/sirupsen/logrus" ) @@ -112,6 +112,6 @@ func toBoolean(value string) (interface{}, error) { logrus.Warnf("%q for boolean is not supported by YAML 1.2, please use `false`", value) return false, nil default: - return nil, errors.Errorf("invalid boolean: %s", value) + return nil, fmt.Errorf("invalid boolean: %s", value) } } diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go b/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go new file mode 100644 index 000000000000..5100d9f5177e --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go @@ -0,0 +1,722 @@ +/* + 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 ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/compose-spec/compose-go/v2/consts" + interp "github.com/compose-spec/compose-go/v2/interpolation" + "github.com/compose-spec/compose-go/v2/override" + "github.com/compose-spec/compose-go/v2/paths" + "github.com/compose-spec/compose-go/v2/schema" + "github.com/compose-spec/compose-go/v2/template" + "github.com/compose-spec/compose-go/v2/transform" + "github.com/compose-spec/compose-go/v2/tree" + "github.com/compose-spec/compose-go/v2/types" + "github.com/compose-spec/compose-go/v2/validation" + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" +) + +// Options supported by Load +type Options struct { + // Skip schema validation + SkipValidation bool + // Skip interpolation + SkipInterpolation bool + // Skip normalization + SkipNormalization bool + // Resolve path + ResolvePaths bool + // Convert Windows path + ConvertWindowsPaths bool + // Skip consistency check + SkipConsistencyCheck bool + // Skip extends + SkipExtends bool + // SkipInclude will ignore `include` and only load model from file(s) set by ConfigDetails + SkipInclude bool + // SkipResolveEnvironment will ignore computing `environment` for services + SkipResolveEnvironment bool + // Interpolation options + Interpolate *interp.Options + // Discard 'env_file' entries after resolving to 'environment' section + discardEnvFiles bool + // Set project projectName + projectName string + // Indicates when the projectName was imperatively set or guessed from path + projectNameImperativelySet bool + // Profiles set profiles to enable + Profiles []string + // ResourceLoaders manages support for remote resources + ResourceLoaders []ResourceLoader +} + +// ResourceLoader is a plugable remote resource resolver +type ResourceLoader interface { + // Accept returns `true` is the resource reference matches ResourceLoader supported protocol(s) + Accept(path string) bool + // Load returns the path to a local copy of remote resource identified by `path`. + Load(ctx context.Context, path string) (string, error) + // Dir computes path to resource"s parent folder, made relative if possible + Dir(path string) string +} + +type localResourceLoader struct { + WorkingDir string +} + +func (l localResourceLoader) abs(p string) string { + if filepath.IsAbs(p) { + return p + } + return filepath.Join(l.WorkingDir, p) +} + +func (l localResourceLoader) Accept(p string) bool { + _, err := os.Stat(l.abs(p)) + return err == nil +} + +func (l localResourceLoader) Load(_ context.Context, p string) (string, error) { + return l.abs(p), nil +} + +func (l localResourceLoader) Dir(path string) string { + path = l.abs(filepath.Dir(path)) + rel, err := filepath.Rel(l.WorkingDir, path) + if err != nil { + return path + } + return rel +} + +func (o *Options) clone() *Options { + return &Options{ + SkipValidation: o.SkipValidation, + SkipInterpolation: o.SkipInterpolation, + SkipNormalization: o.SkipNormalization, + ResolvePaths: o.ResolvePaths, + ConvertWindowsPaths: o.ConvertWindowsPaths, + SkipConsistencyCheck: o.SkipConsistencyCheck, + SkipExtends: o.SkipExtends, + SkipInclude: o.SkipInclude, + Interpolate: o.Interpolate, + discardEnvFiles: o.discardEnvFiles, + projectName: o.projectName, + projectNameImperativelySet: o.projectNameImperativelySet, + Profiles: o.Profiles, + ResourceLoaders: o.ResourceLoaders, + } +} + +func (o *Options) SetProjectName(name string, imperativelySet bool) { + o.projectName = name + o.projectNameImperativelySet = imperativelySet +} + +func (o Options) GetProjectName() (string, bool) { + return o.projectName, o.projectNameImperativelySet +} + +// serviceRef identifies a reference to a service. It's used to detect cyclic +// references in "extends". +type serviceRef struct { + filename string + service string +} + +type cycleTracker struct { + loaded []serviceRef +} + +func (ct *cycleTracker) Add(filename, service string) (*cycleTracker, error) { + toAdd := serviceRef{filename: filename, service: service} + for _, loaded := range ct.loaded { + if toAdd == loaded { + // Create an error message of the form: + // Circular reference: + // service-a in docker-compose.yml + // extends service-b in docker-compose.yml + // extends service-a in docker-compose.yml + errLines := []string{ + "Circular reference:", + fmt.Sprintf(" %s in %s", ct.loaded[0].service, ct.loaded[0].filename), + } + for _, service := range append(ct.loaded[1:], toAdd) { + errLines = append(errLines, fmt.Sprintf(" extends %s in %s", service.service, service.filename)) + } + + return nil, errors.New(strings.Join(errLines, "\n")) + } + } + + var branch []serviceRef + branch = append(branch, ct.loaded...) + branch = append(branch, toAdd) + return &cycleTracker{ + loaded: branch, + }, nil +} + +// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to +// the `environment` section +func WithDiscardEnvFiles(opts *Options) { + opts.discardEnvFiles = true +} + +// WithSkipValidation sets the Options to skip validation when loading sections +func WithSkipValidation(opts *Options) { + opts.SkipValidation = true +} + +// WithProfiles sets profiles to be activated +func WithProfiles(profiles []string) func(*Options) { + return func(opts *Options) { + opts.Profiles = profiles + } +} + +// ParseYAML reads the bytes from a file, parses the bytes into a mapping +// structure, and returns it. +func ParseYAML(source []byte) (map[string]interface{}, error) { + r := bytes.NewReader(source) + decoder := yaml.NewDecoder(r) + m, _, err := parseYAML(decoder) + return m, err +} + +// PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase +// that hardly can be implemented using go-yaml and mapstructure +type PostProcessor interface { + yaml.Unmarshaler + + // Apply changes to compose model based on recorder metadata + Apply(interface{}) error +} + +func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, error) { + var cfg interface{} + processor := ResetProcessor{target: &cfg} + + if err := decoder.Decode(&processor); err != nil { + return nil, nil, err + } + stringMap, ok := cfg.(map[string]interface{}) + if ok { + converted, err := convertToStringKeysRecursive(stringMap, "") + if err != nil { + return nil, nil, err + } + return converted.(map[string]interface{}), &processor, nil + } + cfgMap, ok := cfg.(map[interface{}]interface{}) + if !ok { + return nil, nil, errors.New("Top-level object must be a mapping") + } + converted, err := convertToStringKeysRecursive(cfgMap, "") + if err != nil { + return nil, nil, err + } + return converted.(map[string]interface{}), &processor, nil +} + +// Load reads a ConfigDetails and returns a fully loaded configuration. +// Deprecated: use LoadWithContext. +func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { + return LoadWithContext(context.Background(), configDetails, options...) +} + +// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration +func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { + if len(configDetails.ConfigFiles) < 1 { + return nil, errors.New("No files specified") + } + + opts := &Options{ + Interpolate: &interp.Options{ + Substitute: template.Substitute, + LookupValue: configDetails.LookupEnv, + TypeCastMapping: interpolateTypeCastMapping, + }, + ResolvePaths: true, + } + + for _, op := range options { + op(opts) + } + opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir}) + + projectName, err := projectName(configDetails, opts) + if err != nil { + return nil, err + } + opts.projectName = projectName + + // TODO(milas): this should probably ALWAYS set (overriding any existing) + if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && projectName != "" { + if configDetails.Environment == nil { + configDetails.Environment = map[string]string{} + } + configDetails.Environment[consts.ComposeProjectName] = projectName + } + + return load(ctx, configDetails, opts, nil) +} + +func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options, ct *cycleTracker, included []string) (map[string]interface{}, error) { + var ( + dict = map[string]interface{}{} + err error + ) + for _, file := range config.ConfigFiles { + fctx := context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename) + if len(file.Content) == 0 && file.Config == nil { + content, err := os.ReadFile(file.Filename) + if err != nil { + return nil, err + } + file.Content = content + } + + processRawYaml := func(raw interface{}, processors ...PostProcessor) error { + converted, err := convertToStringKeysRecursive(raw, "") + if err != nil { + return err + } + cfg, ok := converted.(map[string]interface{}) + if !ok { + return errors.New("Top-level object must be a mapping") + } + + if opts.Interpolate != nil && !opts.SkipInterpolation { + cfg, err = interp.Interpolate(cfg, *opts.Interpolate) + if err != nil { + return err + } + } + + fixEmptyNotNull(cfg) + + if !opts.SkipExtends { + err = ApplyExtends(fctx, cfg, opts, ct, processors...) + if err != nil { + return err + } + } + + for _, processor := range processors { + if err := processor.Apply(dict); err != nil { + return err + } + } + + dict, err = override.Merge(dict, cfg) + if err != nil { + return err + } + + dict, err = override.EnforceUnicity(dict) + if err != nil { + return err + } + + if !opts.SkipValidation { + if err := schema.Validate(dict); err != nil { + return fmt.Errorf("validating %s: %w", file.Filename, err) + } + } + + return err + } + + if file.Config == nil { + r := bytes.NewReader(file.Content) + decoder := yaml.NewDecoder(r) + for { + var raw interface{} + processor := &ResetProcessor{target: &raw} + err := decoder.Decode(processor) + if err != nil && errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + if err := processRawYaml(raw, processor); err != nil { + return nil, err + } + } + } else { + if err := processRawYaml(file.Config); err != nil { + return nil, err + } + } + } + + dict, err = transform.Canonical(dict) + if err != nil { + return nil, err + } + + if !opts.SkipInclude { + included = append(included, config.ConfigFiles[0].Filename) + err = ApplyInclude(ctx, config, dict, opts, included) + if err != nil { + return nil, err + } + } + + if !opts.SkipValidation { + if err := validation.Validate(dict); err != nil { + return nil, err + } + } + + if opts.ResolvePaths { + err = paths.ResolveRelativePaths(dict, config.WorkingDir) + if err != nil { + return nil, err + } + } + + return dict, nil +} + +func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) { + mainFile := configDetails.ConfigFiles[0].Filename + for _, f := range loaded { + if f == mainFile { + loaded = append(loaded, mainFile) + return nil, fmt.Errorf("include cycle detected:\n%s\n include %s", loaded[0], strings.Join(loaded[1:], "\n include ")) + } + } + loaded = append(loaded, mainFile) + + includeRefs := make(map[string][]types.IncludeConfig) + + dict, err := loadYamlModel(ctx, configDetails, opts, &cycleTracker{}, nil) + if err != nil { + return nil, err + } + + if len(dict) == 0 { + return nil, errors.New("empty compose file") + } + + project := &types.Project{ + Name: opts.projectName, + WorkingDir: configDetails.WorkingDir, + Environment: configDetails.Environment, + } + delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName + + dict = groupXFieldsIntoExtensions(dict, tree.NewPath()) + err = Transform(dict, project) + if err != nil { + return nil, err + } + + if len(includeRefs) != 0 { + project.IncludeReferences = includeRefs + } + + if !opts.SkipNormalization { + err := Normalize(project) + if err != nil { + return nil, err + } + } + + if opts.ConvertWindowsPaths { + for i, service := range project.Services { + for j, volume := range service.Volumes { + service.Volumes[j] = convertVolumePath(volume) + } + project.Services[i] = service + } + } + + if !opts.SkipConsistencyCheck { + err := checkConsistency(project) + if err != nil { + return nil, err + } + } + + if project, err = project.WithProfiles(opts.Profiles); err != nil { + return nil, err + } + + if !opts.SkipResolveEnvironment { + project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles) + if err != nil { + return nil, err + } + } + + return project, nil +} + +func InvalidProjectNameErr(v string) error { + return fmt.Errorf( + "invalid project name %q: must consist only of lowercase alphanumeric characters, hyphens, and underscores as well as start with a letter or number", + v, + ) +} + +// projectName determines the canonical name to use for the project considering +// the loader Options as well as `name` fields in Compose YAML fields (which +// also support interpolation). +// +// TODO(milas): restructure loading so that we don't need to re-parse the YAML +// here, as it's both wasteful and makes this code error-prone. +func projectName(details types.ConfigDetails, opts *Options) (string, error) { + projectName, projectNameImperativelySet := opts.GetProjectName() + + // if user did NOT provide a name explicitly, then see if one is defined + // in any of the config files + if !projectNameImperativelySet { + var pjNameFromConfigFile string + for _, configFile := range details.ConfigFiles { + content := configFile.Content + if content == nil { + // This can be hit when Filename is set but Content is not. One + // example is when using ToConfigFiles(). + d, err := os.ReadFile(configFile.Filename) + if err != nil { + return "", fmt.Errorf("failed to read file %q: %w", configFile.Filename, err) + } + content = d + } + yml, err := ParseYAML(content) + if err != nil { + // HACK: the way that loading is currently structured, this is + // a duplicative parse just for the `name`. if it fails, we + // give up but don't return the error, knowing that it'll get + // caught downstream for us + return "", nil + } + if val, ok := yml["name"]; ok && val != "" { + sVal, ok := val.(string) + if !ok { + // HACK: see above - this is a temporary parsed version + // that hasn't been schema-validated, but we don't want + // to be the ones to actually report that, so give up, + // knowing that it'll get caught downstream for us + return "", nil + } + pjNameFromConfigFile = sVal + } + } + if !opts.SkipInterpolation { + interpolated, err := interp.Interpolate( + map[string]interface{}{"name": pjNameFromConfigFile}, + *opts.Interpolate, + ) + if err != nil { + return "", err + } + pjNameFromConfigFile = interpolated["name"].(string) + } + pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile) + if pjNameFromConfigFile != "" { + projectName = pjNameFromConfigFile + } + } + + if projectName == "" { + return "", errors.New("project name must not be empty") + } + + if NormalizeProjectName(projectName) != projectName { + return "", InvalidProjectNameErr(projectName) + } + + return projectName, nil +} + +func NormalizeProjectName(s string) string { + r := regexp.MustCompile("[a-z0-9_-]") + s = strings.ToLower(s) + s = strings.Join(r.FindAllString(s, -1), "") + return strings.TrimLeft(s, "_-") +} + +var userDefinedKeys = []tree.Path{ + "services", + "volumes", + "networks", + "secrets", + "configs", +} + +func groupXFieldsIntoExtensions(dict map[string]interface{}, p tree.Path) map[string]interface{} { + extras := map[string]interface{}{} + for key, value := range dict { + skip := false + for _, uk := range userDefinedKeys { + if uk.Matches(p) { + skip = true + break + } + } + if !skip && strings.HasPrefix(key, "x-") { + extras[key] = value + delete(dict, key) + continue + } + switch v := value.(type) { + case map[string]interface{}: + dict[key] = groupXFieldsIntoExtensions(v, p.Next(key)) + case []interface{}: + for i, e := range v { + if m, ok := e.(map[string]interface{}); ok { + v[i] = groupXFieldsIntoExtensions(m, p.Next(strconv.Itoa(i))) + } + } + } + } + if len(extras) > 0 { + dict[consts.Extensions] = extras + } + return dict +} + +// Transform converts the source into the target struct with compose types transformer +// and the specified transformers if any. +func Transform(source interface{}, target interface{}) error { + data := mapstructure.Metadata{} + config := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + nameServices, + decoderHook, + cast), + Result: target, + TagName: "yaml", + Metadata: &data, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + return decoder.Decode(source) +} + +// nameServices create implicit `name` key for convenience accessing service +func nameServices(from reflect.Value, to reflect.Value) (interface{}, error) { + if to.Type() == reflect.TypeOf(types.Services{}) { + nameK := reflect.ValueOf("name") + iter := from.MapRange() + for iter.Next() { + name := iter.Key() + elem := iter.Value() + elem.Elem().SetMapIndex(nameK, name) + } + } + return from.Interface(), nil +} + +// keys need to be converted to strings for jsonschema +func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { + if mapping, ok := value.(map[string]interface{}); ok { + for key, entry := range mapping { + var newKeyPrefix string + if keyPrefix == "" { + newKeyPrefix = key + } else { + newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, key) + } + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + mapping[key] = convertedEntry + } + return mapping, nil + } + if mapping, ok := value.(map[interface{}]interface{}); ok { + dict := make(map[string]interface{}) + for key, entry := range mapping { + str, ok := key.(string) + if !ok { + return nil, formatInvalidKeyError(keyPrefix, key) + } + var newKeyPrefix string + if keyPrefix == "" { + newKeyPrefix = str + } else { + newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) + } + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + dict[str] = convertedEntry + } + return dict, nil + } + if list, ok := value.([]interface{}); ok { + var convertedList []interface{} + for index, entry := range list { + newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + convertedList = append(convertedList, convertedEntry) + } + return convertedList, nil + } + return value, nil +} + +func formatInvalidKeyError(keyPrefix string, key interface{}) error { + var location string + if keyPrefix == "" { + location = "at top level" + } else { + location = fmt.Sprintf("in %s", keyPrefix) + } + return fmt.Errorf("Non-string key %s: %#v", location, key) +} + +// Windows path, c:\\my\\path\\shiny, need to be changed to be compatible with +// the Engine. Volume path are expected to be linux style /c/my/path/shiny/ +func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig { + volumeName := strings.ToLower(filepath.VolumeName(volume.Source)) + if len(volumeName) != 2 { + return volume + } + + convertedSource := fmt.Sprintf("/%c%s", volumeName[0], volume.Source[len(volumeName):]) + convertedSource = strings.ReplaceAll(convertedSource, "\\", "/") + + volume.Source = convertedSource + return volume +} diff --git a/vendor/github.com/compose-spec/compose-go/loader/mapstructure.go b/vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go similarity index 75% rename from vendor/github.com/compose-spec/compose-go/loader/mapstructure.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go index 97a2e39c124a..e5b902ab2918 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/mapstructure.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go @@ -16,7 +16,10 @@ package loader -import "reflect" +import ( + "reflect" + "strconv" +) // comparable to yaml.Unmarshaler, decoder allow a type to define it's own custom logic to convert value // see https://github.com/mitchellh/mapstructure/pull/294 @@ -51,3 +54,26 @@ func decoderHook(from reflect.Value, to reflect.Value) (interface{}, error) { } return to.Interface(), nil } + +func cast(from reflect.Value, to reflect.Value) (interface{}, error) { + switch from.Type().Kind() { + case reflect.String: + switch to.Kind() { + case reflect.Bool: + return toBoolean(from.String()) + case reflect.Int: + return toInt(from.String()) + case reflect.Int64: + return toInt64(from.String()) + case reflect.Float32: + return toFloat32(from.String()) + case reflect.Float64: + return toFloat(from.String()) + } + case reflect.Int: + if to.Kind() == reflect.String { + return strconv.FormatInt(from.Int(), 10), nil + } + } + return from.Interface(), nil +} diff --git a/vendor/github.com/compose-spec/compose-go/loader/normalize.go b/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go similarity index 68% rename from vendor/github.com/compose-spec/compose-go/loader/normalize.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go index 58863b5fab07..e1f0beebfa03 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/normalize.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go @@ -20,9 +20,8 @@ import ( "fmt" "strings" - "github.com/compose-spec/compose-go/errdefs" - "github.com/compose-spec/compose-go/types" - "github.com/pkg/errors" + "github.com/compose-spec/compose-go/v2/errdefs" + "github.com/compose-spec/compose-go/v2/types" "github.com/sirupsen/logrus" ) @@ -37,11 +36,7 @@ func Normalize(project *types.Project) error { project.Networks["default"] = types.NetworkConfig{} } - if err := relocateExternalName(project); err != nil { - return err - } - - for i, s := range project.Services { + for name, s := range project.Services { if len(s.Networks) == 0 && s.NetworkMode == "" { // Service without explicit network attachment are implicitly exposed on default network s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} @@ -116,14 +111,9 @@ func Normalize(project *types.Project) error { return err } - err = relocateScale(&s) - if err != nil { - return err - } - inferImplicitDependencies(&s) - project.Services[i] = s + project.Services[name] = s } setNameFromKey(project) @@ -198,93 +188,51 @@ func setIfMissing(d types.DependsOnConfig, service string, dep types.ServiceDepe return d } -func relocateScale(s *types.ServiceConfig) error { - scale := uint64(s.Scale) - if scale > 1 { - logrus.Warn("`scale` is deprecated. Use the `deploy.replicas` element") - if s.Deploy == nil { - s.Deploy = &types.DeployConfig{} - } - if s.Deploy.Replicas != nil && *s.Deploy.Replicas != scale { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'scale' (deprecated) and 'deploy.replicas'") - } - s.Deploy.Replicas = &scale - } - return nil -} - // Resources with no explicit name are actually named by their key in map func setNameFromKey(project *types.Project) { - for i, n := range project.Networks { + for key, n := range project.Networks { if n.Name == "" { - n.Name = fmt.Sprintf("%s_%s", project.Name, i) - project.Networks[i] = n - } - } - - for i, v := range project.Volumes { - if v.Name == "" { - v.Name = fmt.Sprintf("%s_%s", project.Name, i) - project.Volumes[i] = v - } - } - - for i, c := range project.Configs { - if c.Name == "" { - c.Name = fmt.Sprintf("%s_%s", project.Name, i) - project.Configs[i] = c - } - } - - for i, s := range project.Secrets { - if s.Name == "" { - s.Name = fmt.Sprintf("%s_%s", project.Name, i) - project.Secrets[i] = s - } - } -} - -func relocateExternalName(project *types.Project) error { - for i, n := range project.Networks { - if n.External.Name != "" { - if n.Name != "" { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'networks.external.name' (deprecated) and 'networks.name'") + if n.External { + n.Name = key + } else { + n.Name = fmt.Sprintf("%s_%s", project.Name, key) } - n.Name = n.External.Name + project.Networks[key] = n } - project.Networks[i] = n } - for i, v := range project.Volumes { - if v.External.Name != "" { - if v.Name != "" { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'volumes.external.name' (deprecated) and 'volumes.name'") + for key, v := range project.Volumes { + if v.Name == "" { + if v.External { + v.Name = key + } else { + v.Name = fmt.Sprintf("%s_%s", project.Name, key) } - v.Name = v.External.Name + project.Volumes[key] = v } - project.Volumes[i] = v } - for i, s := range project.Secrets { - if s.External.Name != "" { - if s.Name != "" { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'secrets.external.name' (deprecated) and 'secrets.name'") + for key, c := range project.Configs { + if c.Name == "" { + if c.External { + c.Name = key + } else { + c.Name = fmt.Sprintf("%s_%s", project.Name, key) } - s.Name = s.External.Name + project.Configs[key] = c } - project.Secrets[i] = s } - for i, c := range project.Configs { - if c.External.Name != "" { - if c.Name != "" { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'configs.external.name' (deprecated) and 'configs.name'") + for key, s := range project.Secrets { + if s.Name == "" { + if s.External { + s.Name = key + } else { + s.Name = fmt.Sprintf("%s_%s", project.Name, key) } - c.Name = c.External.Name + project.Secrets[key] = s } - project.Configs[i] = c } - return nil } func relocateLogOpt(s *types.ServiceConfig) error { @@ -297,7 +245,7 @@ func relocateLogOpt(s *types.ServiceConfig) error { if _, ok := s.Logging.Options[k]; !ok { s.Logging.Options[k] = v } else { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_opt' (deprecated) and 'logging.options'") + return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options': %w", errdefs.ErrInvalid) } } } @@ -313,7 +261,7 @@ func relocateLogDriver(s *types.ServiceConfig) error { if s.Logging.Driver == "" { s.Logging.Driver = s.LogDriver } else { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_driver' (deprecated) and 'logging.driver'") + return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver': %w", errdefs.ErrInvalid) } } return nil @@ -328,7 +276,7 @@ func relocateDockerfile(s *types.ServiceConfig) error { if s.Dockerfile == "" { s.Build.Dockerfile = s.Dockerfile } else { - return errors.Wrap(errdefs.ErrInvalid, "can't use both 'dockerfile' (deprecated) and 'build.dockerfile'") + return fmt.Errorf("can't use both 'dockerfile' (deprecated) and 'build.dockerfile': %w", errdefs.ErrInvalid) } } return nil diff --git a/vendor/github.com/compose-spec/compose-go/loader/null.go b/vendor/github.com/compose-spec/compose-go/v2/loader/null.go similarity index 53% rename from vendor/github.com/compose-spec/compose-go/loader/null.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/null.go index 648aacde4dbc..d3e99f4bd3a5 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/null.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/null.go @@ -18,12 +18,9 @@ package loader import ( "fmt" - "reflect" "strconv" - "strings" - "github.com/compose-spec/compose-go/tree" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/tree" "gopkg.in/yaml.v3" ) @@ -45,111 +42,77 @@ func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error { func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) { if node.Tag == "!reset" { p.paths = append(p.paths, path) + return nil, nil } switch node.Kind { case yaml.SequenceNode: - var err error + var nodes []*yaml.Node for idx, v := range node.Content { next := path.Next(strconv.Itoa(idx)) - node.Content[idx], err = p.resolveReset(v, next) + resolved, err := p.resolveReset(v, next) if err != nil { return nil, err } + if resolved != nil { + nodes = append(nodes, resolved) + } } + node.Content = nodes case yaml.MappingNode: - var err error var key string + var nodes []*yaml.Node for idx, v := range node.Content { if idx%2 == 0 { key = v.Value } else { - node.Content[idx], err = p.resolveReset(v, path.Next(key)) + resolved, err := p.resolveReset(v, path.Next(key)) if err != nil { return nil, err } + if resolved != nil { + nodes = append(nodes, node.Content[idx-1], resolved) + } } } + node.Content = nodes } return node, nil } // Apply finds the go attributes matching recorded paths and reset them to zero value -func (p *ResetProcessor) Apply(target *types.Config) error { - return p.applyNullOverrides(reflect.ValueOf(target), tree.NewPath()) +func (p *ResetProcessor) Apply(target any) error { + return p.applyNullOverrides(target, tree.NewPath()) } // applyNullOverrides set val to Zero if it matches any of the recorded paths -func (p *ResetProcessor) applyNullOverrides(val reflect.Value, path tree.Path) error { - val = reflect.Indirect(val) - if !val.IsValid() { - return nil - } - typ := val.Type() - switch { - case path == "services": - // Project.Services is a slice in compose-go, but a mapping in yaml - for i := 0; i < val.Len(); i++ { - service := val.Index(i) - name := service.FieldByName("Name") - next := path.Next(name.String()) - err := p.applyNullOverrides(service, next) - if err != nil { - return err - } - } - case typ.Kind() == reflect.Map: - iter := val.MapRange() +func (p *ResetProcessor) applyNullOverrides(target any, path tree.Path) error { + switch v := target.(type) { + case map[string]any: KEYS: - for iter.Next() { - k := iter.Key() - next := path.Next(k.String()) + for k, e := range v { + next := path.Next(k) for _, pattern := range p.paths { if next.Matches(pattern) { - val.SetMapIndex(k, reflect.Value{}) + delete(v, k) continue KEYS } } - return p.applyNullOverrides(iter.Value(), next) + err := p.applyNullOverrides(e, next) + if err != nil { + return err + } } - case typ.Kind() == reflect.Slice: + case []any: ITER: - for i := 0; i < val.Len(); i++ { + for i, e := range v { next := path.Next(fmt.Sprintf("[%d]", i)) for _, pattern := range p.paths { if next.Matches(pattern) { - continue ITER + // TODO(ndeloof) support removal from sequence } } - // TODO(ndeloof) support removal from sequence - return p.applyNullOverrides(val.Index(i), next) - } - - case typ.Kind() == reflect.Struct: - FIELDS: - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - name := field.Name - attr := strings.ToLower(name) - tag := field.Tag.Get("yaml") - tag = strings.Split(tag, ",")[0] - if tag != "" && tag != "-" { - attr = tag - } - next := path.Next(attr) - f := val.Field(i) - for _, pattern := range p.paths { - if next.Matches(pattern) { - f := f - if !f.CanSet() { - return fmt.Errorf("can't override attribute %s", name) - } - // f.SetZero() requires go 1.20 - f.Set(reflect.Zero(f.Type())) - continue FIELDS - } - } - err := p.applyNullOverrides(f, next) + err := p.applyNullOverrides(e, next) if err != nil { return err } diff --git a/vendor/github.com/compose-spec/compose-go/loader/paths.go b/vendor/github.com/compose-spec/compose-go/v2/loader/paths.go similarity index 53% rename from vendor/github.com/compose-spec/compose-go/loader/paths.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/paths.go index 519a6a6900af..17b11d326afa 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/paths.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/paths.go @@ -21,7 +21,7 @@ import ( "path/filepath" "strings" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" ) // ResolveRelativePaths resolves relative paths based on project WorkingDirectory @@ -38,33 +38,6 @@ func ResolveRelativePaths(project *types.Project) error { } project.ComposeFiles = absComposeFiles - for i, s := range project.Services { - ResolveServiceRelativePaths(project.WorkingDir, &s) - project.Services[i] = s - } - - for i, obj := range project.Configs { - if obj.File != "" { - obj.File = absPath(project.WorkingDir, obj.File) - project.Configs[i] = obj - } - } - - for i, obj := range project.Secrets { - if obj.File != "" { - obj.File = resolveMaybeUnixPath(project.WorkingDir, obj.File) - project.Secrets[i] = obj - } - } - - for name, config := range project.Volumes { - if config.Driver == "local" && config.DriverOpts["o"] == "bind" { - // This is actually a bind mount - config.DriverOpts["device"] = resolveMaybeUnixPath(project.WorkingDir, config.DriverOpts["device"]) - project.Volumes[name] = config - } - } - // don't coerce a nil map to an empty map if project.IncludeReferences != nil { absIncludes := make(map[string][]types.IncludeConfig, len(project.IncludeReferences)) @@ -86,44 +59,6 @@ func ResolveRelativePaths(project *types.Project) error { return nil } -func ResolveServiceRelativePaths(workingDir string, s *types.ServiceConfig) { - if s.Build != nil { - if !isRemoteContext(s.Build.Context) { - s.Build.Context = absPath(workingDir, s.Build.Context) - } - for name, path := range s.Build.AdditionalContexts { - if strings.Contains(path, "://") { // `docker-image://` or any builder specific context type - continue - } - if isRemoteContext(path) { - continue - } - s.Build.AdditionalContexts[name] = absPath(workingDir, path) - } - } - for j, f := range s.EnvFile { - s.EnvFile[j] = absPath(workingDir, f) - } - - if s.Extends != nil && s.Extends.File != "" { - s.Extends.File = absPath(workingDir, s.Extends.File) - } - - for i, vol := range s.Volumes { - if vol.Type != types.VolumeTypeBind { - continue - } - s.Volumes[i].Source = resolveMaybeUnixPath(workingDir, vol.Source) - } - - if s.Develop != nil { - for i, w := range s.Develop.Watch { - w.Path = absPath(workingDir, w.Path) - s.Develop.Watch[i] = w - } - } -} - func absPath(workingDir string, filePath string) string { if strings.HasPrefix(filePath, "~") { home, _ := os.UserHomeDir() @@ -146,20 +81,6 @@ func absComposeFiles(composeFiles []string) ([]string, error) { return composeFiles, nil } -// isRemoteContext returns true if the value is a Git reference or HTTP(S) URL. -// -// Any other value is assumed to be a local filesystem path and returns false. -// -// See: https://github.com/moby/buildkit/blob/18fc875d9bfd6e065cd8211abc639434ba65aa56/frontend/dockerui/context.go#L76-L79 -func isRemoteContext(maybeURL string) bool { - for _, prefix := range []string{"https://", "http://", "git://", "ssh://", "github.com/", "git@"} { - if strings.HasPrefix(maybeURL, prefix) { - return true - } - } - return false -} - func resolvePaths(basePath string, in types.StringList) types.StringList { if in == nil { return nil diff --git a/vendor/github.com/compose-spec/compose-go/loader/validate.go b/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go similarity index 57% rename from vendor/github.com/compose-spec/compose-go/loader/validate.go rename to vendor/github.com/compose-spec/compose-go/v2/loader/validate.go index b4c42c7f1fdb..47264ab8e1eb 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/validate.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go @@ -17,24 +17,24 @@ package loader import ( + "errors" "fmt" "strings" - "github.com/compose-spec/compose-go/errdefs" - "github.com/compose-spec/compose-go/types" - "github.com/pkg/errors" + "github.com/compose-spec/compose-go/v2/errdefs" + "github.com/compose-spec/compose-go/v2/types" ) // checkConsistency validate a compose model is consistent func checkConsistency(project *types.Project) error { for _, s := range project.Services { if s.Build == nil && s.Image == "" { - return errors.Wrapf(errdefs.ErrInvalid, "service %q has neither an image nor a build context specified", s.Name) + return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid) } if s.Build != nil { if s.Build.DockerfileInline != "" && s.Build.Dockerfile != "" { - return errors.Wrapf(errdefs.ErrInvalid, "service %q declares mutualy exclusive dockerfile and dockerfile_inline", s.Name) + return fmt.Errorf("service %q declares mutualy exclusive dockerfile and dockerfile_inline: %w", s.Name, errdefs.ErrInvalid) } if len(s.Build.Platforms) > 0 && s.Platform != "" { @@ -46,17 +46,17 @@ func checkConsistency(project *types.Project) error { } } if !found { - return errors.Wrapf(errdefs.ErrInvalid, "service.build.platforms MUST include service.platform %q ", s.Platform) + return fmt.Errorf("service.build.platforms MUST include service.platform %q: %w", s.Platform, errdefs.ErrInvalid) } } } if s.NetworkMode != "" && len(s.Networks) > 0 { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %s declares mutually exclusive `network_mode` and `networks`", s.Name)) + return fmt.Errorf("service %s declares mutually exclusive `network_mode` and `networks`: %w", s.Name, errdefs.ErrInvalid) } for network := range s.Networks { if _, ok := project.Networks[network]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined network %s", s.Name, network)) + return fmt.Errorf("service %q refers to undefined network %s: %w", s.Name, network, errdefs.ErrInvalid) } } @@ -70,7 +70,7 @@ func checkConsistency(project *types.Project) error { for dependedService := range s.DependsOn { if _, err := project.GetService(dependedService); err != nil { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q depends on undefined service %s", s.Name, dependedService)) + return fmt.Errorf("service %q depends on undefined service %s: %w", s.Name, dependedService, errdefs.ErrInvalid) } } @@ -84,36 +84,53 @@ func checkConsistency(project *types.Project) error { for _, volume := range s.Volumes { if volume.Type == types.VolumeTypeVolume && volume.Source != "" { // non anonymous volumes if _, ok := project.Volumes[volume.Source]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source)) + return fmt.Errorf("service %q refers to undefined volume %s: %w", s.Name, volume.Source, errdefs.ErrInvalid) } } } if s.Build != nil { for _, secret := range s.Build.Secrets { if _, ok := project.Secrets[secret.Source]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined build secret %s", s.Name, secret.Source)) + return fmt.Errorf("service %q refers to undefined build secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid) } } } for _, config := range s.Configs { if _, ok := project.Configs[config.Source]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined config %s", s.Name, config.Source)) + return fmt.Errorf("service %q refers to undefined config %s: %w", s.Name, config.Source, errdefs.ErrInvalid) } } for _, secret := range s.Secrets { if _, ok := project.Secrets[secret.Source]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined secret %s", s.Name, secret.Source)) + return fmt.Errorf("service %q refers to undefined secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid) } } + + if s.Scale != nil && s.Deploy != nil { + if s.Deploy.Replicas != nil && *s.Scale != *s.Deploy.Replicas { + return fmt.Errorf("services.%s: can't set distinct values on 'scale' and 'deploy.replicas': %w", + s.Name, errdefs.ErrInvalid) + } + s.Deploy.Replicas = s.Scale + } + + if s.GetScale() > 1 && s.ContainerName != "" { + attr := "scale" + if s.Scale == nil { + attr = "deploy.replicas" + } + return fmt.Errorf("services.%s: can't set container_name and %s as container name must be unique: %w", attr, + s.Name, errdefs.ErrInvalid) + } } for name, secret := range project.Secrets { - if secret.External.External { + if secret.External { continue } if secret.File == "" && secret.Environment == "" { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("secret %q must declare either `file` or `environment`", name)) + return fmt.Errorf("secret %q must declare either `file` or `environment`: %w", name, errdefs.ErrInvalid) } } diff --git a/vendor/github.com/compose-spec/compose-go/consts/consts.go b/vendor/github.com/compose-spec/compose-go/v2/override/extends.go similarity index 67% rename from vendor/github.com/compose-spec/compose-go/consts/consts.go rename to vendor/github.com/compose-spec/compose-go/v2/override/extends.go index 76bdb82e1c75..f47912ddffc0 100644 --- a/vendor/github.com/compose-spec/compose-go/consts/consts.go +++ b/vendor/github.com/compose-spec/compose-go/v2/override/extends.go @@ -14,11 +14,14 @@ limitations under the License. */ -package consts +package override -const ( - ComposeProjectName = "COMPOSE_PROJECT_NAME" - ComposePathSeparator = "COMPOSE_PATH_SEPARATOR" - ComposeFilePath = "COMPOSE_FILE" - ComposeProfiles = "COMPOSE_PROFILES" -) +import "github.com/compose-spec/compose-go/v2/tree" + +func ExtendService(base, override map[string]any) (map[string]any, error) { + yaml, err := mergeYaml(base, override, tree.NewPath("services.x")) + if err != nil { + return nil, err + } + return yaml.(map[string]any), nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/override/merge.go b/vendor/github.com/compose-spec/compose-go/v2/override/merge.go new file mode 100644 index 000000000000..6cbfdc5464a7 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/override/merge.go @@ -0,0 +1,199 @@ +/* + 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 override + +import ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/tree" + "golang.org/x/exp/slices" +) + +// Merge applies overrides to a config model +func Merge(right, left map[string]any) (map[string]any, error) { + merged, err := mergeYaml(right, left, tree.NewPath()) + if err != nil { + return nil, err + } + return merged.(map[string]any), nil +} + +type merger func(any, any, tree.Path) (any, error) + +// mergeSpecials defines the custom rules applied by compose when merging yaml trees +var mergeSpecials = map[tree.Path]merger{} + +func init() { + mergeSpecials["services.*.build.args"] = mergeToSequence + mergeSpecials["services.*.build"] = mergeBuild + mergeSpecials["services.*.depends_on"] = mergeDependsOn + mergeSpecials["services.*.logging"] = mergeLogging + mergeSpecials["services.*.networks"] = mergeNetworks + mergeSpecials["services.*.command"] = override + mergeSpecials["services.*.entrypoint"] = override + mergeSpecials["services.*.healthcheck.test"] = override + mergeSpecials["services.*.environment"] = mergeToSequence + mergeSpecials["services.*.ulimits.*"] = mergeUlimit +} + +// mergeYaml merges map[string]any yaml trees handling special rules +func mergeYaml(e any, o any, p tree.Path) (any, error) { + for pattern, merger := range mergeSpecials { + if p.Matches(pattern) { + merged, err := merger(e, o, p) + if err != nil { + return nil, err + } + return merged, nil + } + } + switch value := e.(type) { + case map[string]any: + other, ok := o.(map[string]any) + if !ok { + return nil, fmt.Errorf("cannot override %s", p) + } + return mergeMappings(value, other, p) + case []any: + other, ok := o.([]any) + if !ok { + return nil, fmt.Errorf("cannont override %s", p) + } + return append(value, other...), nil + default: + return o, nil + } +} + +func mergeMappings(mapping map[string]any, other map[string]any, p tree.Path) (map[string]any, error) { + for k, v := range other { + e, ok := mapping[k] + if !ok || strings.HasPrefix(k, "x-") { + mapping[k] = v + continue + } + next := p.Next(k) + merged, err := mergeYaml(e, v, next) + if err != nil { + return nil, err + } + mapping[k] = merged + } + return mapping, nil +} + +// logging driver options are merged only when both compose file define the same driver +func mergeLogging(c any, o any, p tree.Path) (any, error) { + config := c.(map[string]any) + other := o.(map[string]any) + // we override logging config if source and override have the same driver set, or none + d, ok1 := other["driver"] + o, ok2 := config["driver"] + if d == o || !ok1 || !ok2 { + return mergeMappings(config, other, p) + } + return other, nil +} + +func mergeBuild(c any, o any, path tree.Path) (any, error) { + toBuild := func(c any) map[string]any { + switch v := c.(type) { + case string: + return map[string]any{ + "context": v, + } + case map[string]any: + return v + } + return nil + } + return mergeMappings(toBuild(c), toBuild(o), path) +} + +func mergeDependsOn(c any, o any, path tree.Path) (any, error) { + right := convertIntoMapping(c, map[string]any{ + "condition": "service_started", + "required": true, + }) + left := convertIntoMapping(o, map[string]any{ + "condition": "service_started", + "required": true, + }) + return mergeMappings(right, left, path) +} + +func mergeNetworks(c any, o any, path tree.Path) (any, error) { + right := convertIntoMapping(c, nil) + left := convertIntoMapping(o, nil) + return mergeMappings(right, left, path) +} + +func mergeToSequence(c any, o any, _ tree.Path) (any, error) { + right := convertIntoSequence(c) + left := convertIntoSequence(o) + return append(right, left...), nil +} + +func convertIntoSequence(value any) []any { + switch v := value.(type) { + case map[string]any: + seq := make([]any, len(v)) + i := 0 + for k, v := range v { + if v == nil { + seq[i] = k + } else { + seq[i] = fmt.Sprintf("%s=%v", k, v) + } + i++ + } + slices.SortFunc(seq, func(a, b any) bool { + return a.(string) < b.(string) + }) + return seq + case []any: + return v + } + return nil +} + +func mergeUlimit(_ any, o any, p tree.Path) (any, error) { + over, ismapping := o.(map[string]any) + if base, ok := o.(map[string]any); ok && ismapping { + return mergeMappings(base, over, p) + } + return o, nil +} + +func convertIntoMapping(a any, defaultValue any) map[string]any { + switch v := a.(type) { + case map[string]any: + return v + case []any: + converted := map[string]any{} + for _, s := range v { + converted[s.(string)] = defaultValue + } + return converted + } + return nil +} + +func override(_ any, other any, _ tree.Path) (any, error) { + return other, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/override/uncity.go b/vendor/github.com/compose-spec/compose-go/v2/override/uncity.go new file mode 100644 index 000000000000..432bb9ce344b --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/override/uncity.go @@ -0,0 +1,171 @@ +/* + 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 override + +import ( + "fmt" + "strconv" + "strings" + + "github.com/compose-spec/compose-go/v2/format" + "github.com/compose-spec/compose-go/v2/tree" +) + +type indexer func(any, tree.Path) (string, error) + +// mergeSpecials defines the custom rules applied by compose when merging yaml trees +var unique = map[tree.Path]indexer{} + +func init() { + unique["services.*.environment"] = keyValueIndexer + unique["services.*.build.args"] = keyValueIndexer + unique["services.*.volumes"] = volumeIndexer + unique["services.*.expose"] = exposeIndexer + unique["services.*.secrets"] = mountIndexer("/run/secrets") + unique["services.*.configs"] = mountIndexer("") + unique["services.*.ports"] = portIndexer +} + +// EnforceUnicity removes redefinition of elements declared in a sequence +func EnforceUnicity(value map[string]any) (map[string]any, error) { + uniq, err := enforceUnicity(value, tree.NewPath()) + if err != nil { + return nil, err + } + return uniq.(map[string]any), nil +} + +func enforceUnicity(value any, p tree.Path) (any, error) { + switch v := value.(type) { + case map[string]any: + for k, e := range v { + u, err := enforceUnicity(e, p.Next(k)) + if err != nil { + return nil, err + } + v[k] = u + } + return v, nil + case []any: + for pattern, indexer := range unique { + if p.Matches(pattern) { + seq := []any{} + keys := map[string]int{} + for i, entry := range v { + key, err := indexer(entry, p.Next(fmt.Sprintf("[%d]", i))) + if err != nil { + return nil, err + } + if j, ok := keys[key]; ok { + seq[j] = entry + } else { + seq = append(seq, entry) + keys[key] = len(seq) - 1 + } + } + return seq, nil + } + } + } + return value, nil +} + +func keyValueIndexer(y any, _ tree.Path) (string, error) { + value := y.(string) + key, _, found := strings.Cut(value, "=") + if !found { + return value, nil + } + return key, nil +} + +func volumeIndexer(y any, p tree.Path) (string, error) { + switch value := y.(type) { + case map[string]any: + target, ok := value["target"].(string) + if !ok { + return "", fmt.Errorf("service volume %s is missing a mount target", p) + } + return target, nil + case string: + volume, err := format.ParseVolume(value) + if err != nil { + return "", err + } + return volume.Target, nil + } + return "", nil +} + +func exposeIndexer(a any, path tree.Path) (string, error) { + switch v := a.(type) { + case string: + return v, nil + case int: + return strconv.Itoa(v), nil + default: + return "", fmt.Errorf("%s: unsupported expose value %s", path, a) + } +} + +func mountIndexer(defaultPath string) indexer { + return func(a any, path tree.Path) (string, error) { + switch v := a.(type) { + case string: + return fmt.Sprintf("%s/%s", defaultPath, v), nil + case map[string]any: + t, ok := v["target"] + if ok { + return t.(string), nil + } + return fmt.Sprintf("%s/%s", defaultPath, v["source"]), nil + default: + return "", fmt.Errorf("%s: unsupported expose value %s", path, a) + } + } +} + +func portIndexer(y any, p tree.Path) (string, error) { + switch value := y.(type) { + case int: + return strconv.Itoa(value), nil + case map[string]any: + target, ok := value["target"] + if !ok { + return "", fmt.Errorf("service ports %s is missing a target port", p) + } + published, ok := value["published"] + if !ok { + // try to parse it as an int + if pub, ok := value["published"]; ok { + published = fmt.Sprintf("%d", pub) + } + } + host, ok := value["host_ip"] + if !ok { + host = "0.0.0.0" + } + protocol, ok := value["protocol"] + if !ok { + protocol = "tcp" + } + return fmt.Sprintf("%s:%s:%d/%s", host, published, target, protocol), nil + case string: + return value, nil + } + return "", nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/paths/context.go b/vendor/github.com/compose-spec/compose-go/v2/paths/context.go new file mode 100644 index 000000000000..b4585dc4e0ea --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/paths/context.go @@ -0,0 +1,44 @@ +/* + 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 paths + +import "strings" + +func (r *relativePathsResolver) absContextPath(value any) (any, error) { + v := value.(string) + if strings.Contains(v, "://") { // `docker-image://` or any builder specific context type + return v, nil + } + if isRemoteContext(v) { + return v, nil + } + return r.absPath(v) +} + +// isRemoteContext returns true if the value is a Git reference or HTTP(S) URL. +// +// Any other value is assumed to be a local filesystem path and returns false. +// +// See: https://github.com/moby/buildkit/blob/18fc875d9bfd6e065cd8211abc639434ba65aa56/frontend/dockerui/context.go#L76-L79 +func isRemoteContext(maybeURL string) bool { + for _, prefix := range []string{"https://", "http://", "git://", "ssh://", "github.com/", "git@"} { + if strings.HasPrefix(maybeURL, prefix) { + return true + } + } + return false +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/paths/home.go b/vendor/github.com/compose-spec/compose-go/v2/paths/home.go new file mode 100644 index 000000000000..a5579262be09 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/paths/home.go @@ -0,0 +1,37 @@ +/* + 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 paths + +import ( + "os" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" +) + +func ExpandUser(p string) string { + if strings.HasPrefix(p, "~") { + home, err := os.UserHomeDir() + if err != nil { + logrus.Warn("cannot expand '~', because the environment lacks HOME") + return p + } + return filepath.Join(home, p[1:]) + } + return p +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go b/vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go new file mode 100644 index 000000000000..d7e100f739c1 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go @@ -0,0 +1,146 @@ +/* + 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 paths + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/compose-spec/compose-go/v2/tree" + "github.com/compose-spec/compose-go/v2/types" +) + +type resolver func(any) (any, error) + +// ResolveRelativePaths make relative paths absolute +func ResolveRelativePaths(project map[string]any, base string) error { + r := relativePathsResolver{workingDir: base} + r.resolvers = map[tree.Path]resolver{ + "services.*.build.context": r.absContextPath, + "services.*.build.additional_contexts.*": r.absContextPath, + "services.*.env_file.*.path": r.absPath, + "services.*.extends.file": r.absPath, + "services.*.develop.watch.*.path": r.absPath, + "services.*.volumes.*": r.absVolumeMount, + "configs.*.file": r.maybeUnixPath, + "secrets.*.file": r.maybeUnixPath, + "include.path": r.absPath, + "include.project_directory": r.absPath, + "include.env_file": r.absPath, + "volumes.*": r.volumeDriverOpts, + } + _, err := r.resolveRelativePaths(project, tree.NewPath()) + return err +} + +type relativePathsResolver struct { + workingDir string + resolvers map[tree.Path]resolver +} + +func (r *relativePathsResolver) resolveRelativePaths(value any, p tree.Path) (any, error) { + for pattern, resolver := range r.resolvers { + if p.Matches(pattern) { + return resolver(value) + } + } + switch v := value.(type) { + case map[string]any: + for k, e := range v { + resolved, err := r.resolveRelativePaths(e, p.Next(k)) + if err != nil { + return nil, err + } + v[k] = resolved + } + case []any: + for i, e := range v { + resolved, err := r.resolveRelativePaths(e, p.Next("[]")) + if err != nil { + return nil, err + } + v[i] = resolved + } + } + return value, nil +} + +func (r *relativePathsResolver) absPath(value any) (any, error) { + switch v := value.(type) { + case []any: + for i, s := range v { + abs, err := r.absPath(s) + if err != nil { + return nil, err + } + v[i] = abs + } + return v, nil + case string: + v = ExpandUser(v) + if filepath.IsAbs(v) { + return v, nil + } + if v != "" { + return filepath.Join(r.workingDir, v), nil + } + return v, nil + } + return nil, fmt.Errorf("unexpected type %T", value) +} + +func (r *relativePathsResolver) absVolumeMount(a any) (any, error) { + vol := a.(map[string]any) + if vol["type"] != types.VolumeTypeBind { + return vol, nil + } + src, ok := vol["source"] + if !ok { + return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`) + } + abs, err := r.maybeUnixPath(src.(string)) + if err != nil { + return nil, err + } + vol["source"] = abs + return vol, nil +} + +func (r *relativePathsResolver) volumeDriverOpts(a any) (any, error) { + if a == nil { + return nil, nil + } + vol := a.(map[string]any) + if vol["driver"] != "local" { + return vol, nil + } + do, ok := vol["driver_opts"] + if !ok { + return vol, nil + } + opts := do.(map[string]any) + if dev, ok := opts["device"]; opts["o"] == "bind" && ok { + // This is actually a bind mount + path, err := r.maybeUnixPath(dev) + if err != nil { + return nil, err + } + opts["device"] = path + } + return vol, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/paths/unix.go b/vendor/github.com/compose-spec/compose-go/v2/paths/unix.go new file mode 100644 index 000000000000..dc1c68b62a24 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/paths/unix.go @@ -0,0 +1,40 @@ +/* + 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 paths + +import ( + "path" + "path/filepath" +) + +func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) { + p := a.(string) + p = ExpandUser(p) + // Check if source is an absolute path (either Unix or Windows), to + // handle a Windows client with a Unix daemon or vice-versa. + // + // Note that this is not required for Docker for Windows when specifying + // a local Windows path, because Docker for Windows translates the Windows + // path into a valid path within the VM. + if !path.IsAbs(p) && !isWindowsAbs(p) { + if filepath.IsAbs(p) { + return p, nil + } + return filepath.Join(r.workingDir, p), nil + } + return p, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/loader/windows_path.go b/vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go similarity index 97% rename from vendor/github.com/compose-spec/compose-go/loader/windows_path.go rename to vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go index 5094f5b57670..746aefd15e35 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/windows_path.go +++ b/vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go @@ -14,7 +14,7 @@ limitations under the License. */ -package loader +package paths // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -30,7 +30,7 @@ func isSlash(c uint8) bool { } // isAbs reports whether the path is a Windows absolute path. -func isAbs(path string) (b bool) { +func isWindowsAbs(path string) (b bool) { l := volumeNameLen(path) if l == 0 { return false diff --git a/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json b/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json similarity index 93% rename from vendor/github.com/compose-spec/compose-go/schema/compose-spec.json rename to vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json index d4bff3845830..1b12822d21c9 100644 --- a/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json +++ b/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json @@ -120,6 +120,7 @@ "privileged": {"type": "boolean"}, "secrets": {"$ref": "#/definitions/service_config_or_secret"}, "tags": {"type": "array", "items": {"type": "string"}}, + "ulimits": {"$ref": "#/definitions/ulimits"}, "platforms": {"type": "array", "items": {"type": "string"}} }, "additionalProperties": false, @@ -214,7 +215,7 @@ "dns_search": {"$ref": "#/definitions/string_or_list"}, "domainname": {"type": "string"}, "entrypoint": {"$ref": "#/definitions/command"}, - "env_file": {"$ref": "#/definitions/string_or_list"}, + "env_file": {"$ref": "#/definitions/env_file"}, "environment": {"$ref": "#/definitions/list_or_dict"}, "expose": { @@ -293,6 +294,7 @@ "ipv4_address": {"type": "string"}, "ipv6_address": {"type": "string"}, "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, + "mac_address": {"type": "string"}, "priority": {"type": "number"} }, "additionalProperties": false, @@ -356,26 +358,7 @@ "storage_opt": {"type": "object"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type": "object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false, - "patternProperties": {"^x-": {}} - } - ] - } - } - }, + "ulimits": {"$ref": "#/definitions/ulimits"}, "user": {"type": "string"}, "uts": {"type": "string"}, "userns_mode": {"type": "string"}, @@ -479,6 +462,7 @@ "target": {"type": "string"} } }, + "required": ["path", "action"], "additionalProperties": false, "patternProperties": {"^x-": {}} } @@ -613,12 +597,12 @@ "items": { "type": "object", "properties": { - "capabilities": {"$ref": "#/definitions/list_of_strings"}, - "count": {"type": ["string", "integer"]}, - "device_ids": {"$ref": "#/definitions/list_of_strings"}, - "driver":{"type": "string"}, - "options":{"$ref": "#/definitions/list_or_dict"} - }, + "capabilities": {"$ref": "#/definitions/list_of_strings"}, + "count": {"type": ["string", "integer"]}, + "device_ids": {"$ref": "#/definitions/list_of_strings"}, + "driver":{"type": "string"}, + "options":{"$ref": "#/definitions/list_or_dict"} + }, "additionalProperties": false, "patternProperties": {"^x-": {}} } @@ -764,6 +748,8 @@ "type": "object", "properties": { "name": {"type": "string"}, + "content": {"type": "string"}, + "environment": {"type": "string"}, "file": {"type": "string"}, "external": { "type": ["boolean", "object"], @@ -789,6 +775,36 @@ ] }, + "env_file": { + "oneOf": [ + {"type": "string"}, + { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": true + } + }, + "required": [ + "path" + ] + } + ] + } + } + ] + }, + "string_or_list": { "oneOf": [ {"type": "string"}, @@ -833,7 +849,6 @@ }, "additionalProperties": false }, - "service_config_or_secret": { "type": "array", "items": { @@ -854,7 +869,26 @@ ] } }, - + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type": "object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + } + } + }, "constraints": { "service": { "id": "#/definitions/constraints/service", @@ -870,4 +904,4 @@ } } } -} +} \ No newline at end of file diff --git a/vendor/github.com/compose-spec/compose-go/schema/schema.go b/vendor/github.com/compose-spec/compose-go/v2/schema/schema.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/schema/schema.go rename to vendor/github.com/compose-spec/compose-go/v2/schema/schema.go diff --git a/vendor/github.com/compose-spec/compose-go/template/template.go b/vendor/github.com/compose-spec/compose-go/v2/template/template.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/template/template.go rename to vendor/github.com/compose-spec/compose-go/v2/template/template.go diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/build.go b/vendor/github.com/compose-spec/compose-go/v2/transform/build.go new file mode 100644 index 000000000000..f56e1a3c393b --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/build.go @@ -0,0 +1,39 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +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{ + "context": v, + }, nil + default: + return data, fmt.Errorf("%s: invalid type %T for build", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go b/vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go new file mode 100644 index 000000000000..4b6603fcb088 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go @@ -0,0 +1,107 @@ +/* + 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" +) + +type transformFunc func(data any, p tree.Path) (any, error) + +var transformers = map[tree.Path]transformFunc{} + +func init() { + transformers["services.*"] = transformService + transformers["services.*.build.secrets.*"] = transformFileMount + transformers["services.*.build.additional_contexts"] = transformKeyValue + transformers["services.*.depends_on"] = transformDependsOn + transformers["services.*.env_file"] = transformEnvFile + transformers["services.*.extends"] = transformExtends + transformers["services.*.networks"] = transformServiceNetworks + transformers["services.*.volumes.*"] = transformVolumeMount + transformers["services.*.secrets.*"] = transformFileMount + transformers["services.*.configs.*"] = transformFileMount + transformers["services.*.ports"] = transformPorts + transformers["services.*.build"] = transformBuild + transformers["services.*.build.ssh"] = transformSSH + transformers["services.*.ulimits.*"] = transformUlimits + transformers["services.*.build.ulimits.*"] = transformUlimits + transformers["volumes.*"] = transformMaybeExternal + transformers["networks.*"] = transformMaybeExternal + transformers["secrets.*"] = transformMaybeExternal + transformers["configs.*"] = transformMaybeExternal + transformers["include.*"] = transformInclude +} + +// Canonical transforms a compose model into canonical syntax +func Canonical(yaml map[string]any) (map[string]any, error) { + canonical, err := transform(yaml, tree.NewPath()) + if err != nil { + return nil, err + } + return canonical.(map[string]any), nil +} + +func transform(data any, p tree.Path) (any, error) { + for pattern, transformer := range transformers { + 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 := transformMapping(v, p) + if err != nil { + return a, err + } + return v, nil + case []any: + a, err := transformSequence(v, p) + if err != nil { + return a, err + } + return v, nil + default: + return data, nil + } +} + +func transformSequence(v []any, p tree.Path) ([]any, error) { + for i, e := range v { + t, err := transform(e, p.Next("[]")) + if err != nil { + return nil, err + } + v[i] = t + } + return v, nil +} + +func transformMapping(v map[string]any, p tree.Path) (map[string]any, error) { + for k, e := range v { + t, err := transform(e, p.Next(k)) + if err != nil { + return nil, err + } + v[k] = t + } + return v, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go b/vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go new file mode 100644 index 000000000000..8c6e1ed37dbb --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go @@ -0,0 +1,53 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformDependsOn(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + for i, e := range v { + d, ok := e.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s.%s: unsupported value %s", p, i, v) + } + if _, ok := d["condition"]; !ok { + d["condition"] = "service_started" + } + if _, ok := d["required"]; !ok { + d["required"] = true + } + } + return v, nil + case []any: + d := map[string]any{} + for _, k := range v { + d[k.(string)] = map[string]any{ + "condition": "service_started", + "required": true, + } + } + return d, nil + default: + return data, fmt.Errorf("%s: invalid type %T for depend_on", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go b/vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go new file mode 100644 index 000000000000..842303b1a551 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go @@ -0,0 +1,55 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformEnvFile(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case string: + return []any{ + transformEnvFileValue(v), + }, nil + case []any: + for i, e := range v { + v[i] = transformEnvFileValue(e) + } + return v, nil + default: + return nil, fmt.Errorf("%s: invalid type %T for env_file", p, v) + } +} + +func transformEnvFileValue(data any) any { + switch v := data.(type) { + case string: + return map[string]any{ + "path": v, + "required": true, + } + case map[string]any: + if _, ok := v["required"]; !ok { + v["required"] = true + } + return v + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/extends.go b/vendor/github.com/compose-spec/compose-go/v2/transform/extends.go new file mode 100644 index 000000000000..9f77176ecdf7 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/extends.go @@ -0,0 +1,36 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformExtends(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return transformMapping(v, p) + case string: + return map[string]any{ + "service": v, + }, nil + default: + return data, fmt.Errorf("%s: invalid type %T for extends", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/external.go b/vendor/github.com/compose-spec/compose-go/v2/transform/external.go new file mode 100644 index 000000000000..9e24eb83eb97 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/external.go @@ -0,0 +1,54 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" + "github.com/sirupsen/logrus" +) + +func transformMaybeExternal(data any, p tree.Path) (any, error) { + if data == nil { + return nil, nil + } + resource, err := transformMapping(data.(map[string]any), p) + if err != nil { + return nil, err + } + + if ext, ok := resource["external"]; ok { + name, named := resource["name"] + if external, ok := ext.(map[string]any); ok { + resource["external"] = true + if extname, extNamed := external["name"]; extNamed { + logrus.Warnf("%s: external.name is deprecated. Please set name and external: true", p) + if named && extname != name { + return nil, fmt.Errorf("%s: name and external.name conflict; only use name", p) + } + if !named { + // adopt (deprecated) external.name if set + resource["name"] = extname + return resource, nil + } + } + } + } + + return resource, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/include.go b/vendor/github.com/compose-spec/compose-go/v2/transform/include.go new file mode 100644 index 000000000000..2488483a6646 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/include.go @@ -0,0 +1,36 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformInclude(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return v, nil + case string: + return map[string]any{ + "path": v, + }, nil + default: + return data, fmt.Errorf("%s: invalid type %T for external", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go b/vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go new file mode 100644 index 000000000000..b247f84f655d --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go @@ -0,0 +1,43 @@ +/* + 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 ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformKeyValue(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return v, nil + case []any: + mapping := map[string]any{} + for _, e := range v { + before, after, found := strings.Cut(e.(string), "=") + if !found { + return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e) + } + mapping[before] = after + } + return mapping, nil + default: + return nil, fmt.Errorf("%s: invalid type %T", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/ports.go b/vendor/github.com/compose-spec/compose-go/v2/transform/ports.go new file mode 100644 index 000000000000..9dc72e95a9cb --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/ports.go @@ -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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" + "github.com/compose-spec/compose-go/v2/types" + "github.com/mitchellh/mapstructure" +) + +func transformPorts(data any, p tree.Path) (any, error) { + switch entries := data.(type) { + case []any: + // We process the list instead of individual items here. + // The reason is that one entry might be mapped to multiple ServicePortConfig. + // Therefore we take an input of a list and return an output of a list. + var ports []any + for _, entry := range entries { + switch value := entry.(type) { + case int: + parsed, err := types.ParsePortConfig(fmt.Sprint(value)) + if err != nil { + return data, err + } + for _, v := range parsed { + m, err := encode(v) + if err != nil { + return nil, err + } + ports = append(ports, m) + } + case string: + parsed, err := types.ParsePortConfig(value) + if err != nil { + return data, err + } + if err != nil { + return nil, err + } + for _, v := range parsed { + m, err := encode(v) + if err != nil { + return nil, err + } + ports = append(ports, m) + } + case map[string]any: + ports = append(ports, value) + default: + return data, fmt.Errorf("%s: invalid type %T for port", p, value) + } + } + return ports, nil + default: + return data, fmt.Errorf("%s: invalid type %T for port", p, entries) + } +} + +func encode(v any) (map[string]any, error) { + m := map[string]any{} + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &m, + TagName: "yaml", + }) + if err != nil { + return nil, err + } + err = decoder.Decode(v) + return m, err +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go b/vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go new file mode 100644 index 000000000000..11f8855ab6cc --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go @@ -0,0 +1,36 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformFileMount(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return data, nil + case string: + return map[string]any{ + "source": v, + }, nil + default: + return nil, fmt.Errorf("%s: unsupported type %T", p, data) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/services.go b/vendor/github.com/compose-spec/compose-go/v2/transform/services.go new file mode 100644 index 000000000000..5a290efa1ace --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/services.go @@ -0,0 +1,37 @@ +/* + 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" +) + +func transformService(data any, p tree.Path) (any, error) { + value := data.(map[string]any) + return transformMapping(value, p) +} + +func transformServiceNetworks(data any, _ tree.Path) (any, error) { + if slice, ok := data.([]any); ok { + networks := make(map[string]any, len(slice)) + for _, net := range slice { + networks[net.(string)] = nil + } + return networks, nil + } + return data, nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go b/vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go new file mode 100644 index 000000000000..48e2a6cfbab1 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go @@ -0,0 +1,51 @@ +/* + 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 ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformSSH(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return v, nil + case []any: + result := make(map[string]any, len(v)) + for _, e := range v { + s, ok := e.(string) + if !ok { + return nil, fmt.Errorf("invalid ssh key type %T", e) + } + id, path, ok := strings.Cut(s, "=") + if !ok { + if id != "default" { + return nil, fmt.Errorf("invalid ssh key %q", s) + } + result[id] = nil + continue + } + result[id] = path + } + return result, nil + default: + return data, fmt.Errorf("%s: invalid type %T for ssh", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go b/vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go new file mode 100644 index 000000000000..de7784c323f9 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go @@ -0,0 +1,34 @@ +/* + 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 ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformUlimits(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return v, nil + case int: + return v, nil + default: + return data, fmt.Errorf("%s: invalid type %T for external", p, v) + } +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/transform/volume.go b/vendor/github.com/compose-spec/compose-go/v2/transform/volume.go new file mode 100644 index 000000000000..e6dd8795721e --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/transform/volume.go @@ -0,0 +1,49 @@ +/* + 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 ( + "fmt" + "path" + + "github.com/compose-spec/compose-go/v2/format" + "github.com/compose-spec/compose-go/v2/tree" +) + +func transformVolumeMount(data any, p tree.Path) (any, error) { + switch v := data.(type) { + case map[string]any: + return v, nil + case string: + volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string] + if err != nil { + return nil, err + } + volume.Target = cleanTarget(volume.Target) + + return encode(volume) + default: + return data, fmt.Errorf("%s: invalid type %T for service volume mount", p, v) + } +} + +func cleanTarget(target string) string { + if target == "" { + return "" + } + return path.Clean(target) +} diff --git a/vendor/github.com/compose-spec/compose-go/tree/path.go b/vendor/github.com/compose-spec/compose-go/v2/tree/path.go similarity index 81% rename from vendor/github.com/compose-spec/compose-go/tree/path.go rename to vendor/github.com/compose-spec/compose-go/v2/tree/path.go index 59c250307500..f8a8d9a64a77 100644 --- a/vendor/github.com/compose-spec/compose-go/tree/path.go +++ b/vendor/github.com/compose-spec/compose-go/v2/tree/path.go @@ -16,7 +16,9 @@ package tree -import "strings" +import ( + "strings" +) const pathSeparator = "." @@ -41,6 +43,7 @@ func (p Path) Next(part string) Path { if p == "" { return Path(part) } + part = strings.ReplaceAll(part, pathSeparator, "👻") return Path(string(p) + pathSeparator + part) } @@ -65,3 +68,20 @@ func (p Path) Matches(pattern Path) bool { } return true } + +func (p Path) Last() string { + parts := p.Parts() + return parts[len(parts)-1] +} + +func (p Path) Parent() Path { + index := strings.LastIndex(string(p), pathSeparator) + if index > 0 { + return p[0:index] + } + return "" +} + +func (p Path) String() string { + return strings.ReplaceAll(string(p), "👻", pathSeparator) +} diff --git a/vendor/github.com/compose-spec/compose-go/types/bytes.go b/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go similarity index 86% rename from vendor/github.com/compose-spec/compose-go/types/bytes.go rename to vendor/github.com/compose-spec/compose-go/v2/types/bytes.go index 4c873cded04c..1b2cd4196bcf 100644 --- a/vendor/github.com/compose-spec/compose-go/types/bytes.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go @@ -36,7 +36,13 @@ func (u UnitBytes) MarshalJSON() ([]byte, error) { } func (u *UnitBytes) DecodeMapstructure(value interface{}) error { - v, err := units.RAMInBytes(fmt.Sprint(value)) - *u = UnitBytes(v) - return err + switch v := value.(type) { + case int: + *u = UnitBytes(v) + case string: + b, err := units.RAMInBytes(fmt.Sprint(value)) + *u = UnitBytes(b) + return err + } + return nil } diff --git a/vendor/github.com/compose-spec/compose-go/types/command.go b/vendor/github.com/compose-spec/compose-go/v2/types/command.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/types/command.go rename to vendor/github.com/compose-spec/compose-go/v2/types/command.go diff --git a/vendor/github.com/compose-spec/compose-go/types/config.go b/vendor/github.com/compose-spec/compose-go/v2/types/config.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/types/config.go rename to vendor/github.com/compose-spec/compose-go/v2/types/config.go diff --git a/vendor/github.com/compose-spec/compose-go/types/develop.go b/vendor/github.com/compose-spec/compose-go/v2/types/develop.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/types/develop.go rename to vendor/github.com/compose-spec/compose-go/v2/types/develop.go diff --git a/vendor/github.com/compose-spec/compose-go/types/device.go b/vendor/github.com/compose-spec/compose-go/v2/types/device.go similarity index 88% rename from vendor/github.com/compose-spec/compose-go/types/device.go rename to vendor/github.com/compose-spec/compose-go/v2/types/device.go index 81b4bea4aab7..240e87786b1b 100644 --- a/vendor/github.com/compose-spec/compose-go/types/device.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/device.go @@ -17,10 +17,9 @@ package types import ( + "fmt" "strconv" "strings" - - "github.com/pkg/errors" ) type DeviceRequest struct { @@ -43,11 +42,11 @@ func (c *DeviceCount) DecodeMapstructure(value interface{}) error { } i, err := strconv.ParseInt(v, 10, 64) if err != nil { - return errors.Errorf("invalid value %q, the only value allowed is 'all' or a number", v) + return fmt.Errorf("invalid value %q, the only value allowed is 'all' or a number", v) } *c = DeviceCount(i) default: - return errors.Errorf("invalid type %T for device count", v) + return fmt.Errorf("invalid type %T for device count", v) } return nil } diff --git a/vendor/github.com/compose-spec/compose-go/types/duration.go b/vendor/github.com/compose-spec/compose-go/v2/types/duration.go similarity index 100% rename from vendor/github.com/compose-spec/compose-go/types/duration.go rename to vendor/github.com/compose-spec/compose-go/v2/types/duration.go diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/envfile.go b/vendor/github.com/compose-spec/compose-go/v2/types/envfile.go new file mode 100644 index 000000000000..f0fa72213867 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/types/envfile.go @@ -0,0 +1,46 @@ +/* + 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 types + +import ( + "encoding/json" +) + +type EnvFile struct { + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Required bool `yaml:"required" json:"required"` +} + +// MarshalYAML makes EnvFile implement yaml.Marshaler +func (e EnvFile) MarshalYAML() (interface{}, error) { + if e.Required { + return e.Path, nil + } + return map[string]any{ + "path": e.Path, + "required": e.Required, + }, nil +} + +// MarshalJSON makes EnvFile implement json.Marshaler +func (e *EnvFile) MarshalJSON() ([]byte, error) { + if e.Required { + return json.Marshal(e.Path) + } + // Pass as a value to avoid re-entering this method and use the default implementation + return json.Marshal(*e) +} diff --git a/vendor/github.com/compose-spec/compose-go/types/healthcheck.go b/vendor/github.com/compose-spec/compose-go/v2/types/healthcheck.go similarity index 96% rename from vendor/github.com/compose-spec/compose-go/types/healthcheck.go rename to vendor/github.com/compose-spec/compose-go/v2/types/healthcheck.go index 1bbf5e9e21bb..c6c3b37e0d5a 100644 --- a/vendor/github.com/compose-spec/compose-go/types/healthcheck.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/healthcheck.go @@ -30,7 +30,7 @@ type HealthCheckConfig struct { StartInterval *Duration `yaml:"start_interval,omitempty" json:"start_interval,omitempty"` Disable bool `yaml:"disable,omitempty" json:"disable,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // HealthCheckTest is the command run to test the health of a service diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/hostList.go b/vendor/github.com/compose-spec/compose-go/v2/types/hostList.go new file mode 100644 index 000000000000..68692b7cefab --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/types/hostList.go @@ -0,0 +1,83 @@ +/* + 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 types + +import ( + "encoding/json" + "fmt" + "sort" + "strings" +) + +// HostsList is a list of colon-separated host-ip mappings +type HostsList map[string]string + +// AsList returns host-ip mappings as a list of strings, using the given +// separator. The Docker Engine API expects ':' separators, the original format +// for '--add-hosts'. But an '=' separator is used in YAML/JSON renderings to +// make IPv6 addresses more readable (for example "my-host=::1" instead of +// "my-host:::1"). +func (h HostsList) AsList(sep string) []string { + l := make([]string, 0, len(h)) + for k, v := range h { + l = append(l, fmt.Sprintf("%s%s%s", k, sep, v)) + } + return l +} + +func (h HostsList) MarshalYAML() (interface{}, error) { + list := h.AsList("=") + sort.Strings(list) + return list, nil +} + +func (h HostsList) MarshalJSON() ([]byte, error) { + list := h.AsList("=") + sort.Strings(list) + return json.Marshal(list) +} + +func (h *HostsList) DecodeMapstructure(value interface{}) error { + switch v := value.(type) { + case map[string]interface{}: + list := make(HostsList, len(v)) + for i, e := range v { + if e == nil { + e = "" + } + list[i] = fmt.Sprint(e) + } + *h = list + case []interface{}: + *h = decodeMapping(v, "=", ":") + default: + return fmt.Errorf("unexpected value type %T for mapping", value) + } + for host, ip := range *h { + // Check that there is a hostname and that it doesn't contain either + // of the allowed separators, to generate a clearer error than the + // engine would do if it splits the string differently. + if host == "" || strings.ContainsAny(host, ":=") { + return fmt.Errorf("bad host name '%s'", host) + } + // Remove brackets from IP addresses (for example "[::1]" -> "::1"). + if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' { + (*h)[host] = ip[1 : len(ip)-1] + } + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/types/labels.go b/vendor/github.com/compose-spec/compose-go/v2/types/labels.go similarity index 93% rename from vendor/github.com/compose-spec/compose-go/types/labels.go rename to vendor/github.com/compose-spec/compose-go/v2/types/labels.go index f30699b575c4..000476bf6959 100644 --- a/vendor/github.com/compose-spec/compose-go/types/labels.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/labels.go @@ -66,10 +66,7 @@ func (l *Labels) DecodeMapstructure(value interface{}) error { case []interface{}: labels := make(map[string]string, len(v)) for _, s := range v { - k, e, ok := strings.Cut(fmt.Sprint(s), "=") - if !ok { - return fmt.Errorf("invalid label %q", v) - } + k, e, _ := strings.Cut(fmt.Sprint(s), "=") labels[k] = labelValue(e) } *l = labels diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/mapping.go b/vendor/github.com/compose-spec/compose-go/v2/types/mapping.go new file mode 100644 index 000000000000..de6fb1233df4 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/types/mapping.go @@ -0,0 +1,217 @@ +/* + 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 types + +import ( + "fmt" + "sort" + "strings" +) + +// MappingWithEquals is a mapping type that can be converted from a list of +// key[=value] strings. +// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`. +// For the key without value (`key`), the mapped value is set to nil. +type MappingWithEquals map[string]*string + +// NewMappingWithEquals build a new Mapping from a set of KEY=VALUE strings +func NewMappingWithEquals(values []string) MappingWithEquals { + mapping := MappingWithEquals{} + for _, env := range values { + tokens := strings.SplitN(env, "=", 2) + if len(tokens) > 1 { + mapping[tokens[0]] = &tokens[1] + } else { + mapping[env] = nil + } + } + return mapping +} + +// OverrideBy update MappingWithEquals with values from another MappingWithEquals +func (m MappingWithEquals) OverrideBy(other MappingWithEquals) MappingWithEquals { + for k, v := range other { + m[k] = v + } + return m +} + +// Resolve update a MappingWithEquals for keys without value (`key`, but not `key=`) +func (m MappingWithEquals) Resolve(lookupFn func(string) (string, bool)) MappingWithEquals { + for k, v := range m { + if v == nil { + if value, ok := lookupFn(k); ok { + m[k] = &value + } + } + } + return m +} + +// RemoveEmpty excludes keys that are not associated with a value +func (m MappingWithEquals) RemoveEmpty() MappingWithEquals { + for k, v := range m { + if v == nil { + delete(m, k) + } + } + return m +} + +func (m *MappingWithEquals) DecodeMapstructure(value interface{}) error { + switch v := value.(type) { + case map[string]interface{}: + mapping := make(MappingWithEquals, len(v)) + for k, e := range v { + mapping[k] = mappingValue(e) + } + *m = mapping + case []interface{}: + mapping := make(MappingWithEquals, len(v)) + for _, s := range v { + k, e, ok := strings.Cut(fmt.Sprint(s), "=") + if !ok { + mapping[k] = nil + } else { + mapping[k] = mappingValue(e) + } + } + *m = mapping + default: + return fmt.Errorf("unexpected value type %T for mapping", value) + } + return nil +} + +// label value can be a string | number | boolean | null +func mappingValue(e interface{}) *string { + if e == nil { + return nil + } + switch v := e.(type) { + case string: + return &v + default: + s := fmt.Sprint(v) + return &s + } +} + +// Mapping is a mapping type that can be converted from a list of +// key[=value] strings. +// For the key with an empty value (`key=`), or key without value (`key`), the +// mapped value is set to an empty string `""`. +type Mapping map[string]string + +// NewMapping build a new Mapping from a set of KEY=VALUE strings +func NewMapping(values []string) Mapping { + mapping := Mapping{} + for _, value := range values { + parts := strings.SplitN(value, "=", 2) + key := parts[0] + switch { + case len(parts) == 1: + mapping[key] = "" + default: + mapping[key] = parts[1] + } + } + return mapping +} + +// convert values into a set of KEY=VALUE strings +func (m Mapping) Values() []string { + values := make([]string, 0, len(m)) + for k, v := range m { + values = append(values, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(values) + return values +} + +// ToMappingWithEquals converts Mapping into a MappingWithEquals with pointer references +func (m Mapping) ToMappingWithEquals() MappingWithEquals { + mapping := MappingWithEquals{} + for k, v := range m { + v := v + mapping[k] = &v + } + return mapping +} + +func (m Mapping) Resolve(s string) (string, bool) { + v, ok := m[s] + return v, ok +} + +func (m Mapping) Clone() Mapping { + clone := Mapping{} + for k, v := range m { + clone[k] = v + } + return clone +} + +// Merge adds all values from second mapping which are not already defined +func (m Mapping) Merge(o Mapping) Mapping { + for k, v := range o { + if _, set := m[k]; !set { + m[k] = v + } + } + return m +} + +func (m *Mapping) DecodeMapstructure(value interface{}) error { + switch v := value.(type) { + case map[string]interface{}: + mapping := make(Mapping, len(v)) + for k, e := range v { + if e == nil { + e = "" + } + mapping[k] = fmt.Sprint(e) + } + *m = mapping + case []interface{}: + *m = decodeMapping(v, "=") + default: + return fmt.Errorf("unexpected value type %T for mapping", value) + } + return nil +} + +// Generate a mapping by splitting strings at any of seps, which will be tried +// in-order for each input string. (For example, to allow the preferred 'host=ip' +// in 'extra_hosts', as well as 'host:ip' for backwards compatibility.) +func decodeMapping(v []interface{}, seps ...string) map[string]string { + mapping := make(Mapping, len(v)) + for _, s := range v { + for i, sep := range seps { + k, e, ok := strings.Cut(fmt.Sprint(s), sep) + if ok { + // Mapping found with this separator, stop here. + mapping[k] = e + break + } else if i == len(seps)-1 { + // No more separators to try, map to empty string. + mapping[k] = "" + } + } + } + return mapping +} diff --git a/vendor/github.com/compose-spec/compose-go/types/options.go b/vendor/github.com/compose-spec/compose-go/v2/types/options.go similarity index 91% rename from vendor/github.com/compose-spec/compose-go/types/options.go rename to vendor/github.com/compose-spec/compose-go/v2/types/options.go index 7ae85793d1e5..c603d051307b 100644 --- a/vendor/github.com/compose-spec/compose-go/types/options.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/options.go @@ -16,11 +16,7 @@ package types -import ( - "fmt" - - "github.com/pkg/errors" -) +import "fmt" // Options is a mapping type for options we pass as-is to container runtime type Options map[string]string @@ -40,7 +36,7 @@ func (d *Options) DecodeMapstructure(value interface{}) error { case map[string]string: *d = v default: - return errors.Errorf("invalid type %T for options", value) + return fmt.Errorf("invalid type %T for options", value) } return nil } diff --git a/vendor/github.com/compose-spec/compose-go/types/project.go b/vendor/github.com/compose-spec/compose-go/v2/types/project.go similarity index 55% rename from vendor/github.com/compose-spec/compose-go/types/project.go rename to vendor/github.com/compose-spec/compose-go/v2/types/project.go index 713b207470f4..89532f2c8d0c 100644 --- a/vendor/github.com/compose-spec/compose-go/types/project.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/project.go @@ -24,16 +24,18 @@ import ( "path/filepath" "sort" - "github.com/compose-spec/compose-go/dotenv" - "github.com/compose-spec/compose-go/utils" + "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/utils" "github.com/distribution/reference" + "github.com/mitchellh/copystructure" godigest "github.com/opencontainers/go-digest" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v3" ) // Project is the result of loading a set of compose files +// Since v2, Project are managed as immutable objects. +// Each public functions which mutate Project state now return a copy of the original Project with the expected changes. type Project struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` WorkingDir string `yaml:"-" json:"-"` @@ -42,7 +44,7 @@ type Project struct { Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"` Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"` Configs Configs `yaml:"configs,omitempty" json:"configs,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` // https://github.com/golang/go/issues/6213 + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` // https://github.com/golang/go/issues/6213 // IncludeReferences is keyed by Compose YAML filename and contains config for // other Compose YAML files it directly triggered a load of via `include`. @@ -60,8 +62,18 @@ type Project struct { // ServiceNames return names for all services in this Compose config func (p *Project) ServiceNames() []string { var names []string - for _, s := range p.Services { - names = append(names, s.Name) + for k := range p.Services { + names = append(names, k) + } + sort.Strings(names) + return names +} + +// DisabledServiceNames return names for all disabled services in this Compose config +func (p *Project) DisabledServiceNames() []string { + var names []string + for k := range p.DisabledServices { + names = append(names, k) } sort.Strings(names) return names @@ -109,9 +121,16 @@ func (p *Project) ConfigNames() []string { // GetServices retrieve services by names, or return all services if no name specified func (p *Project) GetServices(names ...string) (Services, error) { - services, servicesNotFound := p.getServicesByNames(names...) - if len(servicesNotFound) > 0 { - return services, fmt.Errorf("no such service: %s", servicesNotFound[0]) + if len(names) == 0 { + return p.Services, nil + } + services := Services{} + for _, name := range names { + service, err := p.GetService(name) + if err != nil { + return nil, err + } + services[name] = service } return services, nil } @@ -123,55 +142,53 @@ func (p *Project) getServicesByNames(names ...string) (Services, []string) { services := Services{} var servicesNotFound []string for _, name := range names { - var serviceConfig *ServiceConfig - for _, s := range p.Services { - if s.Name == name { - serviceConfig = &s - break - } - } - if serviceConfig == nil { + service, ok := p.Services[name] + if !ok { servicesNotFound = append(servicesNotFound, name) continue } - services = append(services, *serviceConfig) + services[name] = service } return services, servicesNotFound } // GetDisabledService retrieve disabled service by name func (p Project) GetDisabledService(name string) (ServiceConfig, error) { - for _, config := range p.DisabledServices { - if config.Name == name { - return config, nil - } + service, ok := p.DisabledServices[name] + if !ok { + return ServiceConfig{}, fmt.Errorf("no such service: %s", name) } - return ServiceConfig{}, fmt.Errorf("no such service: %s", name) + return service, nil } // GetService retrieve a specific service by name func (p *Project) GetService(name string) (ServiceConfig, error) { - services, err := p.GetServices(name) - if err != nil { - return ServiceConfig{}, err - } - if len(services) == 0 { + service, ok := p.Services[name] + if !ok { + _, ok := p.DisabledServices[name] + if ok { + return ServiceConfig{}, fmt.Errorf("service %s is disabled", name) + } return ServiceConfig{}, fmt.Errorf("no such service: %s", name) } - return services[0], nil + return service, nil } func (p *Project) AllServices() Services { - var all Services - all = append(all, p.Services...) - all = append(all, p.DisabledServices...) + all := Services{} + for name, service := range p.Services { + all[name] = service + } + for name, service := range p.DisabledServices { + all[name] = service + } return all } -type ServiceFunc func(service ServiceConfig) error +type ServiceFunc func(name string, service *ServiceConfig) error -// WithServices run ServiceFunc on each service and dependencies according to DependencyPolicy -func (p *Project) WithServices(names []string, fn ServiceFunc, options ...DependencyOption) error { +// ForEachService runs ServiceFunc on each service and dependencies according to DependencyPolicy +func (p *Project) ForEachService(names []string, fn ServiceFunc, options ...DependencyOption) error { if len(options) == 0 { // backward compatibility options = []DependencyOption{IncludeDependencies} @@ -179,6 +196,16 @@ func (p *Project) WithServices(names []string, fn ServiceFunc, options ...Depend return p.withServices(names, fn, map[string]bool{}, options, map[string]ServiceDependency{}) } +type withServicesOptions struct { + dependencyPolicy int +} + +const ( + includeDependencies = iota + includeDependents + ignoreDependencies +) + func (p *Project) withServices(names []string, fn ServiceFunc, seen map[string]bool, options []DependencyOption, dependencies map[string]ServiceDependency) error { services, servicesNotFound := p.getServicesByNames(names...) if len(servicesNotFound) > 0 { @@ -188,23 +215,26 @@ func (p *Project) withServices(names []string, fn ServiceFunc, seen map[string]b } } } - for _, service := range services { - if seen[service.Name] { + opts := withServicesOptions{ + dependencyPolicy: includeDependencies, + } + for _, option := range options { + option(&opts) + } + + for name, service := range services { + if seen[name] { continue } - seen[service.Name] = true + seen[name] = true var dependencies map[string]ServiceDependency - for _, policy := range options { - switch policy { - case IncludeDependents: - dependencies = utils.MapsAppend(dependencies, p.dependentsForService(service)) - case IncludeDependencies: - dependencies = utils.MapsAppend(dependencies, service.DependsOn) - case IgnoreDependencies: - // Noop - default: - return fmt.Errorf("unsupported dependency policy %d", policy) - } + switch opts.dependencyPolicy { + case includeDependents: + dependencies = utils.MapsAppend(dependencies, p.dependentsForService(service)) + case includeDependencies: + dependencies = utils.MapsAppend(dependencies, service.DependsOn) + case ignoreDependencies: + // Noop } if len(dependencies) > 0 { err := p.withServices(utils.MapKeys(dependencies), fn, seen, options, dependencies) @@ -212,7 +242,7 @@ func (p *Project) withServices(names []string, fn ServiceFunc, seen map[string]b return err } } - if err := fn(service); err != nil { + if err := fn(name, service.deepCopy()); err != nil { return err } } @@ -262,82 +292,64 @@ func (s ServiceConfig) HasProfile(profiles []string) bool { return false } -// GetProfiles retrieve the profiles implicitly enabled by explicitly targeting selected services -func (s Services) GetProfiles() []string { - set := map[string]struct{}{} - for _, service := range s { - for _, p := range service.Profiles { - set[p] = struct{}{} - } - } - var profiles []string - for k := range set { - profiles = append(profiles, k) - } - return profiles -} - -// ApplyProfiles disables service which don't match selected profiles -func (p *Project) ApplyProfiles(profiles []string) { +// WithProfiles disables services which don't match selected profiles +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithProfiles(profiles []string) (*Project, error) { + newProject := p.deepCopy() for _, p := range profiles { if p == "*" { - return + return newProject, nil } } - var enabled, disabled Services - for _, service := range p.AllServices() { + enabled := Services{} + disabled := Services{} + for name, service := range newProject.AllServices() { if service.HasProfile(profiles) { - enabled = append(enabled, service) + enabled[name] = service } else { - disabled = append(disabled, service) + disabled[name] = service } } - p.Services = enabled - p.DisabledServices = disabled - p.Profiles = profiles + newProject.Services = enabled + newProject.DisabledServices = disabled + newProject.Profiles = profiles + return newProject, nil } -// EnableServices ensure services are enabled and activate profiles accordingly -func (p *Project) EnableServices(names ...string) error { +// WithServicesEnabled ensures services are enabled and activate profiles accordingly +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithServicesEnabled(names ...string) (*Project, error) { + newProject := p.deepCopy() if len(names) == 0 { - return nil + return newProject, nil } - var enabled []string + + profiles := append([]string{}, p.Profiles...) for _, name := range names { - _, err := p.GetService(name) - if err == nil { + if _, ok := newProject.Services[name]; ok { // already enabled continue } - def, err := p.GetDisabledService(name) - if err != nil { - return err - } - enabled = append(enabled, def.Profiles...) + service := p.DisabledServices[name] + profiles = append(profiles, service.Profiles...) } - - profiles := p.Profiles -PROFILES: - for _, profile := range enabled { - for _, p := range profiles { - if p == profile { - continue PROFILES - } - } - profiles = append(profiles, profile) + newProject, err := newProject.WithProfiles(profiles) + if err != nil { + return newProject, err } - p.ApplyProfiles(profiles) - return p.ResolveServicesEnvironment(true) + return newProject.WithServicesEnvironmentResolved(true) } // WithoutUnnecessaryResources drops networks/volumes/secrets/configs that are not referenced by active services -func (p *Project) WithoutUnnecessaryResources() { +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithoutUnnecessaryResources() *Project { + newProject := p.deepCopy() requiredNetworks := map[string]struct{}{} requiredVolumes := map[string]struct{}{} requiredSecrets := map[string]struct{}{} requiredConfigs := map[string]struct{}{} - for _, s := range p.Services { + for _, s := range newProject.Services { for k := range s.Networks { requiredNetworks[k] = struct{}{} } @@ -366,7 +378,7 @@ func (p *Project) WithoutUnnecessaryResources() { networks[k] = value } } - p.Networks = networks + newProject.Networks = networks volumes := Volumes{} for k := range requiredVolumes { @@ -374,7 +386,7 @@ func (p *Project) WithoutUnnecessaryResources() { volumes[k] = value } } - p.Volumes = volumes + newProject.Volumes = volumes secrets := Secrets{} for k := range requiredSecrets { @@ -382,7 +394,7 @@ func (p *Project) WithoutUnnecessaryResources() { secrets[k] = value } } - p.Secrets = secrets + newProject.Secrets = secrets configs := Configs{} for k := range requiredConfigs { @@ -390,73 +402,95 @@ func (p *Project) WithoutUnnecessaryResources() { configs[k] = value } } - p.Configs = configs + newProject.Configs = configs + return newProject } -type DependencyOption int +type DependencyOption func(options *withServicesOptions) -const ( - IncludeDependencies = iota - IncludeDependents - IgnoreDependencies -) +func IncludeDependencies(options *withServicesOptions) { + options.dependencyPolicy = includeDependencies +} + +func IncludeDependents(options *withServicesOptions) { + options.dependencyPolicy = includeDependents +} + +func IgnoreDependencies(options *withServicesOptions) { + options.dependencyPolicy = ignoreDependencies +} -// ForServices restrict the project model to selected services and dependencies -func (p *Project) ForServices(names []string, options ...DependencyOption) error { +// WithSelectedServices restricts the project model to selected services and dependencies +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithSelectedServices(names []string, options ...DependencyOption) (*Project, error) { + newProject := p.deepCopy() if len(names) == 0 { // All services - return nil + return newProject, nil } - set := map[string]struct{}{} - err := p.WithServices(names, func(service ServiceConfig) error { - set[service.Name] = struct{}{} + set := utils.NewSet[string]() + err := p.ForEachService(names, func(name string, service *ServiceConfig) error { + set.Add(name) return nil }, options...) if err != nil { - return err + return nil, err } // Disable all services which are not explicit target or dependencies - var enabled Services - for _, s := range p.Services { - if _, ok := set[s.Name]; ok { - for _, option := range options { - if option == IgnoreDependencies { - // remove all dependencies but those implied by explicitly selected services - dependencies := s.DependsOn - for d := range dependencies { - if _, ok := set[d]; !ok { - delete(dependencies, d) - } - } - s.DependsOn = dependencies + enabled := Services{} + for name, s := range newProject.Services { + if _, ok := set[name]; ok { + // remove all dependencies but those implied by explicitly selected services + dependencies := s.DependsOn + for d := range dependencies { + if _, ok := set[d]; !ok { + delete(dependencies, d) } } - enabled = append(enabled, s) + s.DependsOn = dependencies + enabled[name] = s } else { - p.DisableService(s) + newProject = newProject.WithServicesDisabled(name) } } - p.Services = enabled - return nil + newProject.Services = enabled + return newProject, nil } -func (p *Project) DisableService(service ServiceConfig) { - // We should remove all dependencies which reference the disabled service - for i, s := range p.Services { - if _, ok := s.DependsOn[service.Name]; ok { - delete(s.DependsOn, service.Name) - p.Services[i] = s +// WithServicesDisabled removes from the project model the given services and their references in all dependencies +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithServicesDisabled(names ...string) *Project { + newProject := p.deepCopy() + if len(names) == 0 { + return newProject + } + if newProject.DisabledServices == nil { + newProject.DisabledServices = Services{} + } + for _, name := range names { + // We should remove all dependencies which reference the disabled service + for i, s := range newProject.Services { + if _, ok := s.DependsOn[name]; ok { + delete(s.DependsOn, name) + newProject.Services[i] = s + } + } + if service, ok := newProject.Services[name]; ok { + newProject.DisabledServices[name] = service + delete(newProject.Services, name) } } - p.DisabledServices = append(p.DisabledServices, service) + return newProject } -// ResolveImages updates services images to include digest computed by a resolver function -func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) error { +// WithImagesResolved updates services images to include digest computed by a resolver function +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godigest.Digest, error)) (*Project, error) { + newProject := p.deepCopy() eg := errgroup.Group{} - for i, s := range p.Services { + for i, s := range newProject.Services { idx := i service := s @@ -482,11 +516,11 @@ func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.D } service.Image = named.String() - p.Services[idx] = service + newProject.Services[idx] = service return nil }) } - return eg.Wait() + return newProject, eg.Wait() } // MarshalYAML marshal Project into a yaml tree @@ -527,10 +561,12 @@ func (p *Project) MarshalJSON() ([]byte, error) { return json.Marshal(m) } -// ResolveServicesEnvironment parse env_files set for services to resolve the actual environment map for services -func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error { - for i, service := range p.Services { - service.Environment = service.Environment.Resolve(p.Environment.Resolve) +// WithServicesEnvironmentResolved parses env_files set for services to resolve the actual environment map for services +// It returns a new Project instance with the changes and keep the original Project unchanged +func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project, error) { + newProject := p.deepCopy() + for i, service := range newProject.Services { + service.Environment = service.Environment.Resolve(newProject.Environment.Resolve) environment := MappingWithEquals{} // resolve variables based on other files we already parsed, + project's environment @@ -539,18 +575,24 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error { if ok && v != nil { return *v, ok } - return p.Environment.Resolve(s) + return newProject.Environment.Resolve(s) } - for _, envFile := range service.EnvFile { - b, err := os.ReadFile(envFile) + for _, envFile := range service.EnvFiles { + if _, err := os.Stat(envFile.Path); os.IsNotExist(err) { + if envFile.Required { + return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err) + } + continue + } + b, err := os.ReadFile(envFile.Path) if err != nil { - return errors.Wrapf(err, "Failed to load %s", envFile) + return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err) } fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve) if err != nil { - return errors.Wrapf(err, "failed to read %s", envFile) + return nil, fmt.Errorf("failed to read %s: %w", envFile.Path, err) } environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals()) } @@ -558,9 +600,17 @@ func (p Project) ResolveServicesEnvironment(discardEnvFiles bool) error { service.Environment = environment.OverrideBy(service.Environment) if discardEnvFiles { - service.EnvFile = nil + service.EnvFiles = nil } - p.Services[i] = service + newProject.Services[i] = service } - return nil + return newProject, nil +} + +func (p *Project) deepCopy() *Project { + instance, err := copystructure.Copy(p) + if err != nil { + panic(err) + } + return instance.(*Project) } diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/services.go b/vendor/github.com/compose-spec/compose-go/v2/types/services.go new file mode 100644 index 000000000000..0af8444c04f7 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/types/services.go @@ -0,0 +1,35 @@ +/* + 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 types + +// Services is a map of ServiceConfig +type Services map[string]ServiceConfig + +// GetProfiles retrieve the profiles implicitly enabled by explicitly targeting selected services +func (s Services) GetProfiles() []string { + set := map[string]struct{}{} + for _, service := range s { + for _, p := range service.Profiles { + set[p] = struct{}{} + } + } + var profiles []string + for k := range set { + profiles = append(profiles, k) + } + return profiles +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/ssh.go b/vendor/github.com/compose-spec/compose-go/v2/types/ssh.go new file mode 100644 index 000000000000..6d0edb6956b8 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/types/ssh.go @@ -0,0 +1,73 @@ +/* + 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 types + +import ( + "fmt" +) + +type SSHKey struct { + ID string `yaml:"id,omitempty" json:"id,omitempty"` + Path string `path:"path,omitempty" json:"path,omitempty"` +} + +// SSHConfig is a mapping type for SSH build config +type SSHConfig []SSHKey + +func (s SSHConfig) Get(id string) (string, error) { + for _, sshKey := range s { + if sshKey.ID == id { + return sshKey.Path, nil + } + } + return "", fmt.Errorf("ID %s not found in SSH keys", id) +} + +// MarshalYAML makes SSHKey implement yaml.Marshaller +func (s SSHKey) MarshalYAML() (interface{}, error) { + if s.Path == "" { + return s.ID, nil + } + return fmt.Sprintf("%s: %s", s.ID, s.Path), nil +} + +// MarshalJSON makes SSHKey implement json.Marshaller +func (s SSHKey) MarshalJSON() ([]byte, error) { + if s.Path == "" { + return []byte(fmt.Sprintf(`%q`, s.ID)), nil + } + return []byte(fmt.Sprintf(`%q: %s`, s.ID, s.Path)), nil +} + +func (s *SSHConfig) DecodeMapstructure(value interface{}) error { + v, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("invalid ssh config type %T", value) + } + result := make(SSHConfig, len(v)) + i := 0 + for id, path := range v { + key := SSHKey{ID: id} + if path != nil { + key.Path = fmt.Sprint(path) + } + result[i] = key + i++ + } + *s = result + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/types/stringOrList.go b/vendor/github.com/compose-spec/compose-go/v2/types/stringOrList.go similarity index 88% rename from vendor/github.com/compose-spec/compose-go/types/stringOrList.go rename to vendor/github.com/compose-spec/compose-go/v2/types/stringOrList.go index 3d91ad2a5c77..059043fb8840 100644 --- a/vendor/github.com/compose-spec/compose-go/types/stringOrList.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/stringOrList.go @@ -16,11 +16,7 @@ package types -import ( - "fmt" - - "github.com/pkg/errors" -) +import "fmt" // StringList is a type for fields that can be a string or list of strings type StringList []string @@ -36,7 +32,7 @@ func (l *StringList) DecodeMapstructure(value interface{}) error { } *l = list default: - return errors.Errorf("invalid type %T for string list", value) + return fmt.Errorf("invalid type %T for string list", value) } return nil } @@ -55,7 +51,7 @@ func (l *StringOrNumberList) DecodeMapstructure(value interface{}) error { } *l = list default: - return errors.Errorf("invalid type %T for string list", value) + return fmt.Errorf("invalid type %T for string list", value) } return nil } diff --git a/vendor/github.com/compose-spec/compose-go/types/types.go b/vendor/github.com/compose-spec/compose-go/v2/types/types.go similarity index 75% rename from vendor/github.com/compose-spec/compose-go/types/types.go rename to vendor/github.com/compose-spec/compose-go/v2/types/types.go index 0407462a3f87..1f6120668042 100644 --- a/vendor/github.com/compose-spec/compose-go/types/types.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/types.go @@ -23,32 +23,12 @@ import ( "strings" "github.com/docker/go-connections/nat" + "github.com/mitchellh/copystructure" ) -// Services is a list of ServiceConfig -type Services []ServiceConfig - -// MarshalYAML makes Services implement yaml.Marshaller -func (s Services) MarshalYAML() (interface{}, error) { - services := map[string]ServiceConfig{} - for _, service := range s { - services[service.Name] = service - } - return services, nil -} - -// MarshalJSON makes Services implement json.Marshaler -func (s Services) MarshalJSON() ([]byte, error) { - data, err := s.MarshalYAML() - if err != nil { - return nil, err - } - return json.MarshalIndent(data, "", " ") -} - // ServiceConfig is the configuration of one service type ServiceConfig struct { - Name string `yaml:"-" json:"-"` + Name string `yaml:"name,omitempty" json:"-"` Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"` Annotations Mapping `yaml:"annotations,omitempty" json:"annotations,omitempty"` @@ -96,7 +76,7 @@ type ServiceConfig struct { Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` - EnvFile StringList `yaml:"env_file,omitempty" json:"env_file,omitempty"` + EnvFiles []EnvFile `yaml:"env_file,omitempty" json:"env_file,omitempty"` Expose StringOrNumberList `yaml:"expose,omitempty" json:"expose,omitempty"` Extends *ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"` @@ -133,7 +113,7 @@ type ServiceConfig struct { ReadOnly bool `yaml:"read_only,omitempty" json:"read_only,omitempty"` Restart string `yaml:"restart,omitempty" json:"restart,omitempty"` Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` - Scale int `yaml:"scale,omitempty" json:"scale,omitempty"` + Scale *int `yaml:"scale,omitempty" json:"scale,omitempty"` Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` SecurityOpt []string `yaml:"security_opt,omitempty" json:"security_opt,omitempty"` ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` @@ -152,25 +132,17 @@ type ServiceConfig struct { VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // MarshalYAML makes ServiceConfig implement yaml.Marshaller func (s ServiceConfig) MarshalYAML() (interface{}, error) { type t ServiceConfig value := t(s) - value.Scale = 0 // deprecated, but default value "1" doesn't match omitempty + value.Name = "" // set during map to slice conversion, not part of the yaml representation return value, nil } -// MarshalJSON makes SSHKey implement json.Marshaller -func (s ServiceConfig) MarshalJSON() ([]byte, error) { - type t ServiceConfig - value := t(s) - value.Scale = 0 // deprecated, but default value "1" doesn't match omitempty - return json.Marshal(value) -} - // NetworksByPriority return the service networks IDs sorted according to Priority func (s *ServiceConfig) NetworksByPriority() []string { type key struct { @@ -198,6 +170,32 @@ func (s *ServiceConfig) NetworksByPriority() []string { return sorted } +func (s *ServiceConfig) GetScale() int { + if s.Scale != nil { + return *s.Scale + } + if s.Deploy != nil && s.Deploy.Replicas != nil { + // this should not be required as compose-go enforce consistency between scale anr replicas + return *s.Deploy.Replicas + } + return 1 +} + +func (s *ServiceConfig) SetScale(scale int) { + s.Scale = &scale + if s.Deploy != nil { + s.Deploy.Replicas = &scale + } +} + +func (s *ServiceConfig) deepCopy() *ServiceConfig { + instance, err := copystructure.Copy(s) + if err != nil { + panic(err) + } + return instance.(*ServiceConfig) +} + const ( // PullPolicyAlways always pull images PullPolicyAlways = "always" @@ -258,45 +256,31 @@ func (s ServiceConfig) GetDependents(p *Project) []string { return dependent } -type set map[string]struct{} - -func (s set) append(strs ...string) { - for _, str := range strs { - s[str] = struct{}{} - } -} - -func (s set) toSlice() []string { - slice := make([]string, 0, len(s)) - for v := range s { - slice = append(slice, v) - } - return slice -} - // BuildConfig is a type for build type BuildConfig struct { - Context string `yaml:"context,omitempty" json:"context,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` - DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"` - Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"` - SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"` - Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` - CacheFrom StringList `yaml:"cache_from,omitempty" json:"cache_from,omitempty"` - CacheTo StringList `yaml:"cache_to,omitempty" json:"cache_to,omitempty"` - NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"` - AdditionalContexts Mapping `yaml:"additional_contexts,omitempty" json:"additional_contexts,omitempty"` - Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"` - ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` - Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` - Network string `yaml:"network,omitempty" json:"network,omitempty"` - Target string `yaml:"target,omitempty" json:"target,omitempty"` - Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` - Tags StringList `yaml:"tags,omitempty" json:"tags,omitempty"` - Platforms StringList `yaml:"platforms,omitempty" json:"platforms,omitempty"` - Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` - - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Context string `yaml:"context,omitempty" json:"context,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"` + Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"` + SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"` + Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + CacheFrom StringList `yaml:"cache_from,omitempty" json:"cache_from,omitempty"` + CacheTo StringList `yaml:"cache_to,omitempty" json:"cache_to,omitempty"` + NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"` + AdditionalContexts Mapping `yaml:"additional_contexts,omitempty" json:"additional_contexts,omitempty"` + Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"` + ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` + Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` + Network string `yaml:"network,omitempty" json:"network,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` + ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` + Tags StringList `yaml:"tags,omitempty" json:"tags,omitempty"` + Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"` + Platforms StringList `yaml:"platforms,omitempty" json:"platforms,omitempty"` + Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` + + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // BlkioConfig define blkio config @@ -308,7 +292,7 @@ type BlkioConfig struct { DeviceWriteBps []ThrottleDevice `yaml:"device_write_bps,omitempty" json:"device_write_bps,omitempty"` DeviceWriteIOps []ThrottleDevice `yaml:"device_write_iops,omitempty" json:"device_write_iops,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // WeightDevice is a structure that holds device:weight pair @@ -316,7 +300,7 @@ type WeightDevice struct { Path string Weight uint16 - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ThrottleDevice is a structure that holds device:rate_per_second pair @@ -324,197 +308,25 @@ type ThrottleDevice struct { Path string Rate UnitBytes - Extensions Extensions `yaml:"#extensions,inline" json:"-"` -} - -// MappingWithEquals is a mapping type that can be converted from a list of -// key[=value] strings. -// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`. -// For the key without value (`key`), the mapped value is set to nil. -type MappingWithEquals map[string]*string - -// NewMappingWithEquals build a new Mapping from a set of KEY=VALUE strings -func NewMappingWithEquals(values []string) MappingWithEquals { - mapping := MappingWithEquals{} - for _, env := range values { - tokens := strings.SplitN(env, "=", 2) - if len(tokens) > 1 { - mapping[tokens[0]] = &tokens[1] - } else { - mapping[env] = nil - } - } - return mapping -} - -// OverrideBy update MappingWithEquals with values from another MappingWithEquals -func (e MappingWithEquals) OverrideBy(other MappingWithEquals) MappingWithEquals { - for k, v := range other { - e[k] = v - } - return e -} - -// Resolve update a MappingWithEquals for keys without value (`key`, but not `key=`) -func (e MappingWithEquals) Resolve(lookupFn func(string) (string, bool)) MappingWithEquals { - for k, v := range e { - if v == nil { - if value, ok := lookupFn(k); ok { - e[k] = &value - } - } - } - return e -} - -// RemoveEmpty excludes keys that are not associated with a value -func (e MappingWithEquals) RemoveEmpty() MappingWithEquals { - for k, v := range e { - if v == nil { - delete(e, k) - } - } - return e -} - -// Mapping is a mapping type that can be converted from a list of -// key[=value] strings. -// For the key with an empty value (`key=`), or key without value (`key`), the -// mapped value is set to an empty string `""`. -type Mapping map[string]string - -// NewMapping build a new Mapping from a set of KEY=VALUE strings -func NewMapping(values []string) Mapping { - mapping := Mapping{} - for _, value := range values { - parts := strings.SplitN(value, "=", 2) - key := parts[0] - switch { - case len(parts) == 1: - mapping[key] = "" - default: - mapping[key] = parts[1] - } - } - return mapping -} - -// convert values into a set of KEY=VALUE strings -func (m Mapping) Values() []string { - values := make([]string, 0, len(m)) - for k, v := range m { - values = append(values, fmt.Sprintf("%s=%s", k, v)) - } - sort.Strings(values) - return values -} - -// ToMappingWithEquals converts Mapping into a MappingWithEquals with pointer references -func (m Mapping) ToMappingWithEquals() MappingWithEquals { - mapping := MappingWithEquals{} - for k, v := range m { - v := v - mapping[k] = &v - } - return mapping -} - -func (m Mapping) Resolve(s string) (string, bool) { - v, ok := m[s] - return v, ok -} - -func (m Mapping) Clone() Mapping { - clone := Mapping{} - for k, v := range m { - clone[k] = v - } - return clone -} - -// Merge adds all values from second mapping which are not already defined -func (m Mapping) Merge(o Mapping) Mapping { - for k, v := range o { - if _, set := m[k]; !set { - m[k] = v - } - } - return m -} - -type SSHKey struct { - ID string - Path string -} - -// SSHConfig is a mapping type for SSH build config -type SSHConfig []SSHKey - -func (s SSHConfig) Get(id string) (string, error) { - for _, sshKey := range s { - if sshKey.ID == id { - return sshKey.Path, nil - } - } - return "", fmt.Errorf("ID %s not found in SSH keys", id) -} - -// MarshalYAML makes SSHKey implement yaml.Marshaller -func (s SSHKey) MarshalYAML() (interface{}, error) { - if s.Path == "" { - return s.ID, nil - } - return fmt.Sprintf("%s: %s", s.ID, s.Path), nil -} - -// MarshalJSON makes SSHKey implement json.Marshaller -func (s SSHKey) MarshalJSON() ([]byte, error) { - if s.Path == "" { - return []byte(fmt.Sprintf(`%q`, s.ID)), nil - } - return []byte(fmt.Sprintf(`%q: %s`, s.ID, s.Path)), nil + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // MappingWithColon is a mapping type that can be converted from a list of // 'key: value' strings type MappingWithColon map[string]string -// HostsList is a list of colon-separated host-ip mappings -type HostsList map[string]string - -// AsList return host-ip mappings as a list of colon-separated strings -func (h HostsList) AsList() []string { - l := make([]string, 0, len(h)) - for k, v := range h { - l = append(l, fmt.Sprintf("%s:%s", k, v)) - } - return l -} - -func (h HostsList) MarshalYAML() (interface{}, error) { - list := h.AsList() - sort.Strings(list) - return list, nil -} - -func (h HostsList) MarshalJSON() ([]byte, error) { - list := h.AsList() - sort.Strings(list) - return json.Marshal(list) -} - // LoggingConfig the logging configuration for a service type LoggingConfig struct { Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` Options Options `yaml:"options,omitempty" json:"options,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // DeployConfig the deployment configuration for a service type DeployConfig struct { Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` - Replicas *uint64 `yaml:"replicas,omitempty" json:"replicas,omitempty"` + Replicas *int `yaml:"replicas,omitempty" json:"replicas,omitempty"` Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` UpdateConfig *UpdateConfig `yaml:"update_config,omitempty" json:"update_config,omitempty"` RollbackConfig *UpdateConfig `yaml:"rollback_config,omitempty" json:"rollback_config,omitempty"` @@ -523,7 +335,7 @@ type DeployConfig struct { Placement Placement `yaml:"placement,omitempty" json:"placement,omitempty"` EndpointMode string `yaml:"endpoint_mode,omitempty" json:"endpoint_mode,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // UpdateConfig the service update configuration @@ -535,7 +347,7 @@ type UpdateConfig struct { MaxFailureRatio float32 `yaml:"max_failure_ratio,omitempty" json:"max_failure_ratio,omitempty"` Order string `yaml:"order,omitempty" json:"order,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // Resources the resource limits and reservations @@ -543,7 +355,7 @@ type Resources struct { Limits *Resource `yaml:"limits,omitempty" json:"limits,omitempty"` Reservations *Resource `yaml:"reservations,omitempty" json:"reservations,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // Resource is a resource to be limited or reserved @@ -555,7 +367,7 @@ type Resource struct { Devices []DeviceRequest `yaml:"devices,omitempty" json:"devices,omitempty"` GenericResources []GenericResource `yaml:"generic_resources,omitempty" json:"generic_resources,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // GenericResource represents a "user defined" resource which can @@ -563,7 +375,7 @@ type Resource struct { type GenericResource struct { DiscreteResourceSpec *DiscreteGenericResource `yaml:"discrete_resource_spec,omitempty" json:"discrete_resource_spec,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // DiscreteGenericResource represents a "user defined" resource which is defined @@ -574,7 +386,7 @@ type DiscreteGenericResource struct { Kind string `json:"kind"` Value int64 `json:"value"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // RestartPolicy the service restart policy @@ -584,7 +396,7 @@ type RestartPolicy struct { MaxAttempts *uint64 `yaml:"max_attempts,omitempty" json:"max_attempts,omitempty"` Window *Duration `yaml:"window,omitempty" json:"window,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // Placement constraints for the service @@ -593,14 +405,14 @@ type Placement struct { Preferences []PlacementPreferences `yaml:"preferences,omitempty" json:"preferences,omitempty"` MaxReplicas uint64 `yaml:"max_replicas_per_node,omitempty" json:"max_replicas_per_node,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // PlacementPreferences is the preferences for a service placement type PlacementPreferences struct { Spread string `yaml:"spread,omitempty" json:"spread,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ServiceNetworkConfig is the network configuration for a service @@ -610,8 +422,9 @@ type ServiceNetworkConfig struct { Ipv4Address string `yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"` Ipv6Address string `yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` LinkLocalIPs []string `yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"` + MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ServicePortConfig is the port configuration for a service @@ -622,7 +435,7 @@ type ServicePortConfig struct { Published string `yaml:"published,omitempty" json:"published,omitempty"` Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ParsePortConfig parse short syntax for service port configuration @@ -675,7 +488,7 @@ type ServiceVolumeConfig struct { Volume *ServiceVolumeVolume `yaml:"volume,omitempty" json:"volume,omitempty"` Tmpfs *ServiceVolumeTmpfs `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // String render ServiceVolumeConfig as a volume string, one can parse back using loader.ParseVolume @@ -721,7 +534,7 @@ type ServiceVolumeBind struct { Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"` CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // SELinux represents the SELinux re-labeling options. @@ -752,7 +565,7 @@ const ( type ServiceVolumeVolume struct { NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ServiceVolumeTmpfs are options for a service volume of type tmpfs @@ -761,7 +574,7 @@ type ServiceVolumeTmpfs struct { Mode uint32 `yaml:"mode,omitempty" json:"mode,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // FileReferenceConfig for a reference to a swarm file object @@ -772,7 +585,7 @@ type FileReferenceConfig struct { GID string `yaml:"gid,omitempty" json:"gid,omitempty"` Mode *uint32 `yaml:"mode,omitempty" json:"mode,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // ServiceConfigObjConfig is the config obj configuration for a service @@ -787,7 +600,32 @@ type UlimitsConfig struct { Soft int `yaml:"soft,omitempty" json:"soft,omitempty"` Hard int `yaml:"hard,omitempty" json:"hard,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} + +func (u *UlimitsConfig) DecodeMapstructure(value interface{}) error { + switch v := value.(type) { + case *UlimitsConfig: + // this call to DecodeMapstructure is triggered after initial value conversion as we use a map[string]*UlimitsConfig + return nil + case int: + u.Single = v + u.Soft = 0 + u.Hard = 0 + case map[string]any: + u.Single = 0 + soft, ok := v["soft"] + if ok { + u.Soft = soft.(int) + } + hard, ok := v["hard"] + if ok { + u.Hard = hard.(int) + } + default: + return fmt.Errorf("unexpected value type %T for ulimit", value) + } + return nil } // MarshalYAML makes UlimitsConfig implement yaml.Marshaller @@ -824,14 +662,14 @@ type NetworkConfig struct { Attachable bool `yaml:"attachable,omitempty" json:"attachable,omitempty"` Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` EnableIPv6 bool `yaml:"enable_ipv6,omitempty" json:"enable_ipv6,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // IPAMConfig for a network type IPAMConfig struct { Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` Config []*IPAMPool `yaml:"config,omitempty" json:"config,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // IPAMPool for a network @@ -850,40 +688,19 @@ type VolumeConfig struct { DriverOpts Options `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` External External `yaml:"external,omitempty" json:"external,omitempty"` Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // External identifies a Volume or Network as a reference to a resource that is // not managed, and should already exist. -// External.name is deprecated and replaced by Volume.name -type External struct { - Name string `yaml:"name,omitempty" json:"name,omitempty"` - External bool `yaml:"external,omitempty" json:"external,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` -} - -// MarshalYAML makes External implement yaml.Marshaller -func (e External) MarshalYAML() (interface{}, error) { - if e.Name == "" { - return e.External, nil - } - return External{Name: e.Name}, nil -} - -// MarshalJSON makes External implement json.Marshaller -func (e External) MarshalJSON() ([]byte, error) { - if e.Name == "" { - return []byte(fmt.Sprintf("%v", e.External)), nil - } - return []byte(fmt.Sprintf(`{"name": %q}`, e.Name)), nil -} +type External bool // CredentialSpecConfig for credential spec on Windows type CredentialSpecConfig struct { Config string `yaml:"config,omitempty" json:"config,omitempty"` // Config was added in API v1.40 File string `yaml:"file,omitempty" json:"file,omitempty"` Registry string `yaml:"registry,omitempty" json:"registry,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } // FileObjectConfig is a config type for a file used by a service @@ -891,12 +708,13 @@ type FileObjectConfig struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` File string `yaml:"file,omitempty" json:"file,omitempty"` Environment string `yaml:"environment,omitempty" json:"environment,omitempty"` + Content string `yaml:"content,omitempty" json:"content,omitempty"` External External `yaml:"external,omitempty" json:"external,omitempty"` Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` DriverOpts map[string]string `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` TemplateDriver string `yaml:"template_driver,omitempty" json:"template_driver,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } const ( @@ -915,7 +733,7 @@ type DependsOnConfig map[string]ServiceDependency type ServiceDependency struct { Condition string `yaml:"condition,omitempty" json:"condition,omitempty"` Restart bool `yaml:"restart,omitempty" json:"restart,omitempty"` - Extensions Extensions `yaml:"#extensions,inline" json:"-"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Required bool `yaml:"required" json:"required"` } diff --git a/vendor/github.com/compose-spec/compose-go/utils/collectionutils.go b/vendor/github.com/compose-spec/compose-go/v2/utils/collectionutils.go similarity index 83% rename from vendor/github.com/compose-spec/compose-go/utils/collectionutils.go rename to vendor/github.com/compose-spec/compose-go/v2/utils/collectionutils.go index 343692250d52..21abc57ba02f 100644 --- a/vendor/github.com/compose-spec/compose-go/utils/collectionutils.go +++ b/vendor/github.com/compose-spec/compose-go/v2/utils/collectionutils.go @@ -16,13 +16,15 @@ package utils -import "golang.org/x/exp/slices" - -func MapKeys[T comparable, U any](theMap map[T]U) []T { - var result []T - for key := range theMap { - result = append(result, key) - } +import ( + "golang.org/x/exp/constraints" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +func MapKeys[T constraints.Ordered, U any](theMap map[T]U) []T { + result := maps.Keys(theMap) + slices.Sort(result) return result } diff --git a/vendor/github.com/compose-spec/compose-go/v2/utils/set.go b/vendor/github.com/compose-spec/compose-go/v2/utils/set.go new file mode 100644 index 000000000000..bbbeaa966def --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/utils/set.go @@ -0,0 +1,95 @@ +/* + 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 utils + +type Set[T comparable] map[T]struct{} + +func NewSet[T comparable](v ...T) Set[T] { + if len(v) == 0 { + return make(Set[T]) + } + + out := make(Set[T], len(v)) + for i := range v { + out.Add(v[i]) + } + return out +} + +func (s Set[T]) Has(v T) bool { + _, ok := s[v] + return ok +} + +func (s Set[T]) Add(v T) { + s[v] = struct{}{} +} + +func (s Set[T]) AddAll(v ...T) { + for _, e := range v { + s[e] = struct{}{} + } +} + +func (s Set[T]) Remove(v T) bool { + _, ok := s[v] + if ok { + delete(s, v) + } + return ok +} + +func (s Set[T]) Clear() { + for v := range s { + delete(s, v) + } +} + +func (s Set[T]) Elements() []T { + elements := make([]T, 0, len(s)) + for v := range s { + elements = append(elements, v) + } + return elements +} + +func (s Set[T]) RemoveAll(elements ...T) { + for _, e := range elements { + s.Remove(e) + } +} + +func (s Set[T]) Diff(other Set[T]) Set[T] { + out := make(Set[T]) + for k := range s { + if _, ok := other[k]; !ok { + out[k] = struct{}{} + } + } + return out +} + +func (s Set[T]) Union(other Set[T]) Set[T] { + out := make(Set[T]) + for k := range s { + out[k] = struct{}{} + } + for k := range other { + out[k] = struct{}{} + } + return out +} diff --git a/vendor/github.com/compose-spec/compose-go/utils/stringutils.go b/vendor/github.com/compose-spec/compose-go/v2/utils/stringutils.go similarity index 86% rename from vendor/github.com/compose-spec/compose-go/utils/stringutils.go rename to vendor/github.com/compose-spec/compose-go/v2/utils/stringutils.go index 182ddf830285..dfabf6c97158 100644 --- a/vendor/github.com/compose-spec/compose-go/utils/stringutils.go +++ b/vendor/github.com/compose-spec/compose-go/v2/utils/stringutils.go @@ -22,16 +22,6 @@ import ( "strings" ) -// StringContains check if an array contains a specific value -func StringContains(array []string, needle string) bool { - for _, val := range array { - if val == needle { - return true - } - } - return false -} - // StringToBool converts a string to a boolean ignoring errors func StringToBool(s string) bool { b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s))) diff --git a/vendor/github.com/compose-spec/compose-go/v2/validation/external.go b/vendor/github.com/compose-spec/compose-go/v2/validation/external.go new file mode 100644 index 000000000000..b74d551a02b1 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/validation/external.go @@ -0,0 +1,49 @@ +/* + 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 validation + +import ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/consts" + "github.com/compose-spec/compose-go/v2/tree" +) + +func checkExternal(v map[string]any, p tree.Path) error { + b, ok := v["external"] + if !ok { + return nil + } + if !b.(bool) { + return nil + } + + for k := range v { + switch k { + case "name", "external", consts.Extensions: + continue + default: + if strings.HasPrefix(k, "x-") { + // custom extension, ignored + continue + } + return fmt.Errorf("%s: conflicting parameters \"external\" and %q specified", p, k) + } + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/validation/validation.go b/vendor/github.com/compose-spec/compose-go/v2/validation/validation.go new file mode 100644 index 000000000000..e7cd67545662 --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/validation/validation.go @@ -0,0 +1,96 @@ +/* + 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 validation + +import ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/tree" +) + +type checkerFunc func(value any, p tree.Path) error + +var checks = map[tree.Path]checkerFunc{ + "volumes.*": checkVolume, + "configs.*": checkFileObject("file", "environment", "content"), + "secrets.*": checkFileObject("file", "environment"), + "services.*.develop.watch.*.path": checkPath, +} + +func Validate(dict map[string]any) error { + return check(dict, tree.NewPath()) +} + +func check(value any, p tree.Path) error { + for pattern, fn := range checks { + if p.Matches(pattern) { + return fn(value, p) + } + } + switch v := value.(type) { + case map[string]any: + for k, v := range v { + err := check(v, p.Next(k)) + if err != nil { + return err + } + } + case []any: + for _, e := range v { + err := check(e, p.Next("[]")) + if err != nil { + return err + } + } + } + return nil +} + +func checkFileObject(keys ...string) checkerFunc { + return func(value any, p tree.Path) error { + + v := value.(map[string]any) + count := 0 + for _, s := range keys { + if _, ok := v[s]; ok { + count++ + } + } + if count > 1 { + return fmt.Errorf("%s: %s attributes are mutually exclusive", p, strings.Join(keys, "|")) + } + if count == 0 { + if _, ok := v["driver"]; ok { + // User specified a custom driver, which might have it's own way to set content + return nil + } + if _, ok := v["external"]; !ok { + return fmt.Errorf("%s: one of %s must be set", p, strings.Join(keys, "|")) + } + } + return nil + } +} + +func checkPath(value any, p tree.Path) error { + v := value.(string) + if v == "" { + return fmt.Errorf("%s: value can't be blank", p) + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/validation/volume.go b/vendor/github.com/compose-spec/compose-go/v2/validation/volume.go new file mode 100644 index 000000000000..5b400681163d --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/validation/volume.go @@ -0,0 +1,39 @@ +/* + 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 validation + +import ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +func checkVolume(value any, p tree.Path) error { + if value == nil { + return nil + } + v, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("expected volume, got %s", value) + } + + err := checkExternal(v, p) + if err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/mitchellh/copystructure/LICENSE b/vendor/github.com/mitchellh/copystructure/LICENSE new file mode 100644 index 000000000000..229851590442 --- /dev/null +++ b/vendor/github.com/mitchellh/copystructure/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/copystructure/README.md b/vendor/github.com/mitchellh/copystructure/README.md new file mode 100644 index 000000000000..f0fbd2e5c98c --- /dev/null +++ b/vendor/github.com/mitchellh/copystructure/README.md @@ -0,0 +1,21 @@ +# copystructure + +copystructure is a Go library for deep copying values in Go. + +This allows you to copy Go values that may contain reference values +such as maps, slices, or pointers, and copy their data as well instead +of just their references. + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/copystructure +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/copystructure). + +The `Copy` function has examples associated with it there. diff --git a/vendor/github.com/mitchellh/copystructure/copier_time.go b/vendor/github.com/mitchellh/copystructure/copier_time.go new file mode 100644 index 000000000000..db6a6aa1a1f4 --- /dev/null +++ b/vendor/github.com/mitchellh/copystructure/copier_time.go @@ -0,0 +1,15 @@ +package copystructure + +import ( + "reflect" + "time" +) + +func init() { + Copiers[reflect.TypeOf(time.Time{})] = timeCopier +} + +func timeCopier(v interface{}) (interface{}, error) { + // Just... copy it. + return v.(time.Time), nil +} diff --git a/vendor/github.com/mitchellh/copystructure/copystructure.go b/vendor/github.com/mitchellh/copystructure/copystructure.go new file mode 100644 index 000000000000..8089e6670a34 --- /dev/null +++ b/vendor/github.com/mitchellh/copystructure/copystructure.go @@ -0,0 +1,631 @@ +package copystructure + +import ( + "errors" + "reflect" + "sync" + + "github.com/mitchellh/reflectwalk" +) + +const tagKey = "copy" + +// Copy returns a deep copy of v. +// +// Copy is unable to copy unexported fields in a struct (lowercase field names). +// Unexported fields can't be reflected by the Go runtime and therefore +// copystructure can't perform any data copies. +// +// For structs, copy behavior can be controlled with struct tags. For example: +// +// struct { +// Name string +// Data *bytes.Buffer `copy:"shallow"` +// } +// +// The available tag values are: +// +// * "ignore" - The field will be ignored, effectively resulting in it being +// assigned the zero value in the copy. +// +// * "shallow" - The field will be be shallow copied. This means that references +// values such as pointers, maps, slices, etc. will be directly assigned +// versus deep copied. +// +func Copy(v interface{}) (interface{}, error) { + return Config{}.Copy(v) +} + +// CopierFunc is a function that knows how to deep copy a specific type. +// Register these globally with the Copiers variable. +type CopierFunc func(interface{}) (interface{}, error) + +// Copiers is a map of types that behave specially when they are copied. +// If a type is found in this map while deep copying, this function +// will be called to copy it instead of attempting to copy all fields. +// +// The key should be the type, obtained using: reflect.TypeOf(value with type). +// +// It is unsafe to write to this map after Copies have started. If you +// are writing to this map while also copying, wrap all modifications to +// this map as well as to Copy in a mutex. +var Copiers map[reflect.Type]CopierFunc = make(map[reflect.Type]CopierFunc) + +// ShallowCopiers is a map of pointer types that behave specially +// when they are copied. If a type is found in this map while deep +// copying, the pointer value will be shallow copied and not walked +// into. +// +// The key should be the type, obtained using: reflect.TypeOf(value +// with type). +// +// It is unsafe to write to this map after Copies have started. If you +// are writing to this map while also copying, wrap all modifications to +// this map as well as to Copy in a mutex. +var ShallowCopiers map[reflect.Type]struct{} = make(map[reflect.Type]struct{}) + +// Must is a helper that wraps a call to a function returning +// (interface{}, error) and panics if the error is non-nil. It is intended +// for use in variable initializations and should only be used when a copy +// error should be a crashing case. +func Must(v interface{}, err error) interface{} { + if err != nil { + panic("copy error: " + err.Error()) + } + + return v +} + +var errPointerRequired = errors.New("Copy argument must be a pointer when Lock is true") + +type Config struct { + // Lock any types that are a sync.Locker and are not a mutex while copying. + // If there is an RLocker method, use that to get the sync.Locker. + Lock bool + + // Copiers is a map of types associated with a CopierFunc. Use the global + // Copiers map if this is nil. + Copiers map[reflect.Type]CopierFunc + + // ShallowCopiers is a map of pointer types that when they are + // shallow copied no matter where they are encountered. Use the + // global ShallowCopiers if this is nil. + ShallowCopiers map[reflect.Type]struct{} +} + +func (c Config) Copy(v interface{}) (interface{}, error) { + if c.Lock && reflect.ValueOf(v).Kind() != reflect.Ptr { + return nil, errPointerRequired + } + + w := new(walker) + if c.Lock { + w.useLocks = true + } + + if c.Copiers == nil { + c.Copiers = Copiers + } + w.copiers = c.Copiers + + if c.ShallowCopiers == nil { + c.ShallowCopiers = ShallowCopiers + } + w.shallowCopiers = c.ShallowCopiers + + err := reflectwalk.Walk(v, w) + if err != nil { + return nil, err + } + + // Get the result. If the result is nil, then we want to turn it + // into a typed nil if we can. + result := w.Result + if result == nil { + val := reflect.ValueOf(v) + result = reflect.Indirect(reflect.New(val.Type())).Interface() + } + + return result, nil +} + +// Return the key used to index interfaces types we've seen. Store the number +// of pointers in the upper 32bits, and the depth in the lower 32bits. This is +// easy to calculate, easy to match a key with our current depth, and we don't +// need to deal with initializing and cleaning up nested maps or slices. +func ifaceKey(pointers, depth int) uint64 { + return uint64(pointers)<<32 | uint64(depth) +} + +type walker struct { + Result interface{} + + copiers map[reflect.Type]CopierFunc + shallowCopiers map[reflect.Type]struct{} + depth int + ignoreDepth int + vals []reflect.Value + cs []reflect.Value + + // This stores the number of pointers we've walked over, indexed by depth. + ps []int + + // If an interface is indirected by a pointer, we need to know the type of + // interface to create when creating the new value. Store the interface + // types here, indexed by both the walk depth and the number of pointers + // already seen at that depth. Use ifaceKey to calculate the proper uint64 + // value. + ifaceTypes map[uint64]reflect.Type + + // any locks we've taken, indexed by depth + locks []sync.Locker + // take locks while walking the structure + useLocks bool +} + +func (w *walker) Enter(l reflectwalk.Location) error { + w.depth++ + + // ensure we have enough elements to index via w.depth + for w.depth >= len(w.locks) { + w.locks = append(w.locks, nil) + } + + for len(w.ps) < w.depth+1 { + w.ps = append(w.ps, 0) + } + + return nil +} + +func (w *walker) Exit(l reflectwalk.Location) error { + locker := w.locks[w.depth] + w.locks[w.depth] = nil + if locker != nil { + defer locker.Unlock() + } + + // clear out pointers and interfaces as we exit the stack + w.ps[w.depth] = 0 + + for k := range w.ifaceTypes { + mask := uint64(^uint32(0)) + if k&mask == uint64(w.depth) { + delete(w.ifaceTypes, k) + } + } + + w.depth-- + if w.ignoreDepth > w.depth { + w.ignoreDepth = 0 + } + + if w.ignoring() { + return nil + } + + switch l { + case reflectwalk.Array: + fallthrough + case reflectwalk.Map: + fallthrough + case reflectwalk.Slice: + w.replacePointerMaybe() + + // Pop map off our container + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.MapValue: + // Pop off the key and value + mv := w.valPop() + mk := w.valPop() + m := w.cs[len(w.cs)-1] + + // If mv is the zero value, SetMapIndex deletes the key form the map, + // or in this case never adds it. We need to create a properly typed + // zero value so that this key can be set. + if !mv.IsValid() { + mv = reflect.Zero(m.Elem().Type().Elem()) + } + m.Elem().SetMapIndex(mk, mv) + case reflectwalk.ArrayElem: + // Pop off the value and the index and set it on the array + v := w.valPop() + i := w.valPop().Interface().(int) + if v.IsValid() { + a := w.cs[len(w.cs)-1] + ae := a.Elem().Index(i) // storing array as pointer on stack - so need Elem() call + if ae.CanSet() { + ae.Set(v) + } + } + case reflectwalk.SliceElem: + // Pop off the value and the index and set it on the slice + v := w.valPop() + i := w.valPop().Interface().(int) + if v.IsValid() { + s := w.cs[len(w.cs)-1] + se := s.Elem().Index(i) + if se.CanSet() { + se.Set(v) + } + } + case reflectwalk.Struct: + w.replacePointerMaybe() + + // Remove the struct from the container stack + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.StructField: + // Pop off the value and the field + v := w.valPop() + f := w.valPop().Interface().(reflect.StructField) + if v.IsValid() { + s := w.cs[len(w.cs)-1] + sf := reflect.Indirect(s).FieldByName(f.Name) + + if sf.CanSet() { + sf.Set(v) + } + } + case reflectwalk.WalkLoc: + // Clear out the slices for GC + w.cs = nil + w.vals = nil + } + + return nil +} + +func (w *walker) Map(m reflect.Value) error { + if w.ignoring() { + return nil + } + w.lock(m) + + // Create the map. If the map itself is nil, then just make a nil map + var newMap reflect.Value + if m.IsNil() { + newMap = reflect.New(m.Type()) + } else { + newMap = wrapPtr(reflect.MakeMap(m.Type())) + } + + w.cs = append(w.cs, newMap) + w.valPush(newMap) + return nil +} + +func (w *walker) MapElem(m, k, v reflect.Value) error { + return nil +} + +func (w *walker) PointerEnter(v bool) error { + if v { + w.ps[w.depth]++ + } + return nil +} + +func (w *walker) PointerExit(v bool) error { + if v { + w.ps[w.depth]-- + } + return nil +} + +func (w *walker) Pointer(v reflect.Value) error { + if _, ok := w.shallowCopiers[v.Type()]; ok { + // Shallow copy this value. Use the same logic as primitive, then + // return skip. + if err := w.Primitive(v); err != nil { + return err + } + + return reflectwalk.SkipEntry + } + + return nil +} + +func (w *walker) Interface(v reflect.Value) error { + if !v.IsValid() { + return nil + } + if w.ifaceTypes == nil { + w.ifaceTypes = make(map[uint64]reflect.Type) + } + + w.ifaceTypes[ifaceKey(w.ps[w.depth], w.depth)] = v.Type() + return nil +} + +func (w *walker) Primitive(v reflect.Value) error { + if w.ignoring() { + return nil + } + w.lock(v) + + // IsValid verifies the v is non-zero and CanInterface verifies + // that we're allowed to read this value (unexported fields). + var newV reflect.Value + if v.IsValid() && v.CanInterface() { + newV = reflect.New(v.Type()) + newV.Elem().Set(v) + } + + w.valPush(newV) + w.replacePointerMaybe() + return nil +} + +func (w *walker) Slice(s reflect.Value) error { + if w.ignoring() { + return nil + } + w.lock(s) + + var newS reflect.Value + if s.IsNil() { + newS = reflect.New(s.Type()) + } else { + newS = wrapPtr(reflect.MakeSlice(s.Type(), s.Len(), s.Cap())) + } + + w.cs = append(w.cs, newS) + w.valPush(newS) + return nil +} + +func (w *walker) SliceElem(i int, elem reflect.Value) error { + if w.ignoring() { + return nil + } + + // We don't write the slice here because elem might still be + // arbitrarily complex. Just record the index and continue on. + w.valPush(reflect.ValueOf(i)) + + return nil +} + +func (w *walker) Array(a reflect.Value) error { + if w.ignoring() { + return nil + } + w.lock(a) + + newA := reflect.New(a.Type()) + + w.cs = append(w.cs, newA) + w.valPush(newA) + return nil +} + +func (w *walker) ArrayElem(i int, elem reflect.Value) error { + if w.ignoring() { + return nil + } + + // We don't write the array here because elem might still be + // arbitrarily complex. Just record the index and continue on. + w.valPush(reflect.ValueOf(i)) + + return nil +} + +func (w *walker) Struct(s reflect.Value) error { + if w.ignoring() { + return nil + } + w.lock(s) + + var v reflect.Value + if c, ok := w.copiers[s.Type()]; ok { + // We have a Copier for this struct, so we use that copier to + // get the copy, and we ignore anything deeper than this. + w.ignoreDepth = w.depth + + dup, err := c(s.Interface()) + if err != nil { + return err + } + + // We need to put a pointer to the value on the value stack, + // so allocate a new pointer and set it. + v = reflect.New(s.Type()) + reflect.Indirect(v).Set(reflect.ValueOf(dup)) + } else { + // No copier, we copy ourselves and allow reflectwalk to guide + // us deeper into the structure for copying. + v = reflect.New(s.Type()) + } + + // Push the value onto the value stack for setting the struct field, + // and add the struct itself to the containers stack in case we walk + // deeper so that its own fields can be modified. + w.valPush(v) + w.cs = append(w.cs, v) + + return nil +} + +func (w *walker) StructField(f reflect.StructField, v reflect.Value) error { + if w.ignoring() { + return nil + } + + // If PkgPath is non-empty, this is a private (unexported) field. + // We do not set this unexported since the Go runtime doesn't allow us. + if f.PkgPath != "" { + return reflectwalk.SkipEntry + } + + switch f.Tag.Get(tagKey) { + case "shallow": + // If we're shallow copying then assign the value directly to the + // struct and skip the entry. + if v.IsValid() { + s := w.cs[len(w.cs)-1] + sf := reflect.Indirect(s).FieldByName(f.Name) + if sf.CanSet() { + sf.Set(v) + } + } + + return reflectwalk.SkipEntry + + case "ignore": + // Do nothing + return reflectwalk.SkipEntry + } + + // Push the field onto the stack, we'll handle it when we exit + // the struct field in Exit... + w.valPush(reflect.ValueOf(f)) + + return nil +} + +// ignore causes the walker to ignore any more values until we exit this on +func (w *walker) ignore() { + w.ignoreDepth = w.depth +} + +func (w *walker) ignoring() bool { + return w.ignoreDepth > 0 && w.depth >= w.ignoreDepth +} + +func (w *walker) pointerPeek() bool { + return w.ps[w.depth] > 0 +} + +func (w *walker) valPop() reflect.Value { + result := w.vals[len(w.vals)-1] + w.vals = w.vals[:len(w.vals)-1] + + // If we're out of values, that means we popped everything off. In + // this case, we reset the result so the next pushed value becomes + // the result. + if len(w.vals) == 0 { + w.Result = nil + } + + return result +} + +func (w *walker) valPush(v reflect.Value) { + w.vals = append(w.vals, v) + + // If we haven't set the result yet, then this is the result since + // it is the first (outermost) value we're seeing. + if w.Result == nil && v.IsValid() { + w.Result = v.Interface() + } +} + +func (w *walker) replacePointerMaybe() { + // Determine the last pointer value. If it is NOT a pointer, then + // we need to push that onto the stack. + if !w.pointerPeek() { + w.valPush(reflect.Indirect(w.valPop())) + return + } + + v := w.valPop() + + // If the expected type is a pointer to an interface of any depth, + // such as *interface{}, **interface{}, etc., then we need to convert + // the value "v" from *CONCRETE to *interface{} so types match for + // Set. + // + // Example if v is type *Foo where Foo is a struct, v would become + // *interface{} instead. This only happens if we have an interface expectation + // at this depth. + // + // For more info, see GH-16 + if iType, ok := w.ifaceTypes[ifaceKey(w.ps[w.depth], w.depth)]; ok && iType.Kind() == reflect.Interface { + y := reflect.New(iType) // Create *interface{} + y.Elem().Set(reflect.Indirect(v)) // Assign "Foo" to interface{} (dereferenced) + v = y // v is now typed *interface{} (where *v = Foo) + } + + for i := 1; i < w.ps[w.depth]; i++ { + if iType, ok := w.ifaceTypes[ifaceKey(w.ps[w.depth]-i, w.depth)]; ok { + iface := reflect.New(iType).Elem() + iface.Set(v) + v = iface + } + + p := reflect.New(v.Type()) + p.Elem().Set(v) + v = p + } + + w.valPush(v) +} + +// if this value is a Locker, lock it and add it to the locks slice +func (w *walker) lock(v reflect.Value) { + if !w.useLocks { + return + } + + if !v.IsValid() || !v.CanInterface() { + return + } + + type rlocker interface { + RLocker() sync.Locker + } + + var locker sync.Locker + + // We can't call Interface() on a value directly, since that requires + // a copy. This is OK, since the pointer to a value which is a sync.Locker + // is also a sync.Locker. + if v.Kind() == reflect.Ptr { + switch l := v.Interface().(type) { + case rlocker: + // don't lock a mutex directly + if _, ok := l.(*sync.RWMutex); !ok { + locker = l.RLocker() + } + case sync.Locker: + locker = l + } + } else if v.CanAddr() { + switch l := v.Addr().Interface().(type) { + case rlocker: + // don't lock a mutex directly + if _, ok := l.(*sync.RWMutex); !ok { + locker = l.RLocker() + } + case sync.Locker: + locker = l + } + } + + // still no callable locker + if locker == nil { + return + } + + // don't lock a mutex directly + switch locker.(type) { + case *sync.Mutex, *sync.RWMutex: + return + } + + locker.Lock() + w.locks[w.depth] = locker +} + +// wrapPtr is a helper that takes v and always make it *v. copystructure +// stores things internally as pointers until the last moment before unwrapping +func wrapPtr(v reflect.Value) reflect.Value { + if !v.IsValid() { + return v + } + vPtr := reflect.New(v.Type()) + vPtr.Elem().Set(v) + return vPtr +} diff --git a/vendor/github.com/mitchellh/reflectwalk/.travis.yml b/vendor/github.com/mitchellh/reflectwalk/.travis.yml new file mode 100644 index 000000000000..4f2ee4d97338 --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/vendor/github.com/mitchellh/reflectwalk/LICENSE b/vendor/github.com/mitchellh/reflectwalk/LICENSE new file mode 100644 index 000000000000..f9c841a51e0d --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/reflectwalk/README.md b/vendor/github.com/mitchellh/reflectwalk/README.md new file mode 100644 index 000000000000..ac82cd2e159f --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/README.md @@ -0,0 +1,6 @@ +# reflectwalk + +reflectwalk is a Go library for "walking" a value in Go using reflection, +in the same way a directory tree can be "walked" on the filesystem. Walking +a complex structure can allow you to do manipulations on unknown structures +such as those decoded from JSON. diff --git a/vendor/github.com/mitchellh/reflectwalk/location.go b/vendor/github.com/mitchellh/reflectwalk/location.go new file mode 100644 index 000000000000..6a7f176117f9 --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/location.go @@ -0,0 +1,19 @@ +package reflectwalk + +//go:generate stringer -type=Location location.go + +type Location uint + +const ( + None Location = iota + Map + MapKey + MapValue + Slice + SliceElem + Array + ArrayElem + Struct + StructField + WalkLoc +) diff --git a/vendor/github.com/mitchellh/reflectwalk/location_string.go b/vendor/github.com/mitchellh/reflectwalk/location_string.go new file mode 100644 index 000000000000..70760cf4c705 --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/location_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type=Location location.go"; DO NOT EDIT. + +package reflectwalk + +import "fmt" + +const _Location_name = "NoneMapMapKeyMapValueSliceSliceElemArrayArrayElemStructStructFieldWalkLoc" + +var _Location_index = [...]uint8{0, 4, 7, 13, 21, 26, 35, 40, 49, 55, 66, 73} + +func (i Location) String() string { + if i >= Location(len(_Location_index)-1) { + return fmt.Sprintf("Location(%d)", i) + } + return _Location_name[_Location_index[i]:_Location_index[i+1]] +} diff --git a/vendor/github.com/mitchellh/reflectwalk/reflectwalk.go b/vendor/github.com/mitchellh/reflectwalk/reflectwalk.go new file mode 100644 index 000000000000..7fee7b050ba4 --- /dev/null +++ b/vendor/github.com/mitchellh/reflectwalk/reflectwalk.go @@ -0,0 +1,420 @@ +// reflectwalk is a package that allows you to "walk" complex structures +// similar to how you may "walk" a filesystem: visiting every element one +// by one and calling callback functions allowing you to handle and manipulate +// those elements. +package reflectwalk + +import ( + "errors" + "reflect" +) + +// PrimitiveWalker implementations are able to handle primitive values +// within complex structures. Primitive values are numbers, strings, +// booleans, funcs, chans. +// +// These primitive values are often members of more complex +// structures (slices, maps, etc.) that are walkable by other interfaces. +type PrimitiveWalker interface { + Primitive(reflect.Value) error +} + +// InterfaceWalker implementations are able to handle interface values as they +// are encountered during the walk. +type InterfaceWalker interface { + Interface(reflect.Value) error +} + +// MapWalker implementations are able to handle individual elements +// found within a map structure. +type MapWalker interface { + Map(m reflect.Value) error + MapElem(m, k, v reflect.Value) error +} + +// SliceWalker implementations are able to handle slice elements found +// within complex structures. +type SliceWalker interface { + Slice(reflect.Value) error + SliceElem(int, reflect.Value) error +} + +// ArrayWalker implementations are able to handle array elements found +// within complex structures. +type ArrayWalker interface { + Array(reflect.Value) error + ArrayElem(int, reflect.Value) error +} + +// StructWalker is an interface that has methods that are called for +// structs when a Walk is done. +type StructWalker interface { + Struct(reflect.Value) error + StructField(reflect.StructField, reflect.Value) error +} + +// EnterExitWalker implementations are notified before and after +// they walk deeper into complex structures (into struct fields, +// into slice elements, etc.) +type EnterExitWalker interface { + Enter(Location) error + Exit(Location) error +} + +// PointerWalker implementations are notified when the value they're +// walking is a pointer or not. Pointer is called for _every_ value whether +// it is a pointer or not. +type PointerWalker interface { + PointerEnter(bool) error + PointerExit(bool) error +} + +// PointerValueWalker implementations are notified with the value of +// a particular pointer when a pointer is walked. Pointer is called +// right before PointerEnter. +type PointerValueWalker interface { + Pointer(reflect.Value) error +} + +// SkipEntry can be returned from walk functions to skip walking +// the value of this field. This is only valid in the following functions: +// +// - Struct: skips all fields from being walked +// - StructField: skips walking the struct value +// +var SkipEntry = errors.New("skip this entry") + +// Walk takes an arbitrary value and an interface and traverses the +// value, calling callbacks on the interface if they are supported. +// The interface should implement one or more of the walker interfaces +// in this package, such as PrimitiveWalker, StructWalker, etc. +func Walk(data, walker interface{}) (err error) { + v := reflect.ValueOf(data) + ew, ok := walker.(EnterExitWalker) + if ok { + err = ew.Enter(WalkLoc) + } + + if err == nil { + err = walk(v, walker) + } + + if ok && err == nil { + err = ew.Exit(WalkLoc) + } + + return +} + +func walk(v reflect.Value, w interface{}) (err error) { + // Determine if we're receiving a pointer and if so notify the walker. + // The logic here is convoluted but very important (tests will fail if + // almost any part is changed). I will try to explain here. + // + // First, we check if the value is an interface, if so, we really need + // to check the interface's VALUE to see whether it is a pointer. + // + // Check whether the value is then a pointer. If so, then set pointer + // to true to notify the user. + // + // If we still have a pointer or an interface after the indirections, then + // we unwrap another level + // + // At this time, we also set "v" to be the dereferenced value. This is + // because once we've unwrapped the pointer we want to use that value. + pointer := false + pointerV := v + + for { + if pointerV.Kind() == reflect.Interface { + if iw, ok := w.(InterfaceWalker); ok { + if err = iw.Interface(pointerV); err != nil { + return + } + } + + pointerV = pointerV.Elem() + } + + if pointerV.Kind() == reflect.Ptr { + if pw, ok := w.(PointerValueWalker); ok { + if err = pw.Pointer(pointerV); err != nil { + if err == SkipEntry { + // Skip the rest of this entry but clear the error + return nil + } + + return + } + } + + pointer = true + v = reflect.Indirect(pointerV) + } + if pw, ok := w.(PointerWalker); ok { + if err = pw.PointerEnter(pointer); err != nil { + return + } + + defer func(pointer bool) { + if err != nil { + return + } + + err = pw.PointerExit(pointer) + }(pointer) + } + + if pointer { + pointerV = v + } + pointer = false + + // If we still have a pointer or interface we have to indirect another level. + switch pointerV.Kind() { + case reflect.Ptr, reflect.Interface: + continue + } + break + } + + // We preserve the original value here because if it is an interface + // type, we want to pass that directly into the walkPrimitive, so that + // we can set it. + originalV := v + if v.Kind() == reflect.Interface { + v = v.Elem() + } + + k := v.Kind() + if k >= reflect.Int && k <= reflect.Complex128 { + k = reflect.Int + } + + switch k { + // Primitives + case reflect.Bool, reflect.Chan, reflect.Func, reflect.Int, reflect.String, reflect.Invalid: + err = walkPrimitive(originalV, w) + return + case reflect.Map: + err = walkMap(v, w) + return + case reflect.Slice: + err = walkSlice(v, w) + return + case reflect.Struct: + err = walkStruct(v, w) + return + case reflect.Array: + err = walkArray(v, w) + return + default: + panic("unsupported type: " + k.String()) + } +} + +func walkMap(v reflect.Value, w interface{}) error { + ew, ewok := w.(EnterExitWalker) + if ewok { + ew.Enter(Map) + } + + if mw, ok := w.(MapWalker); ok { + if err := mw.Map(v); err != nil { + return err + } + } + + for _, k := range v.MapKeys() { + kv := v.MapIndex(k) + + if mw, ok := w.(MapWalker); ok { + if err := mw.MapElem(v, k, kv); err != nil { + return err + } + } + + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(MapKey) + } + + if err := walk(k, w); err != nil { + return err + } + + if ok { + ew.Exit(MapKey) + ew.Enter(MapValue) + } + + // get the map value again as it may have changed in the MapElem call + if err := walk(v.MapIndex(k), w); err != nil { + return err + } + + if ok { + ew.Exit(MapValue) + } + } + + if ewok { + ew.Exit(Map) + } + + return nil +} + +func walkPrimitive(v reflect.Value, w interface{}) error { + if pw, ok := w.(PrimitiveWalker); ok { + return pw.Primitive(v) + } + + return nil +} + +func walkSlice(v reflect.Value, w interface{}) (err error) { + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(Slice) + } + + if sw, ok := w.(SliceWalker); ok { + if err := sw.Slice(v); err != nil { + return err + } + } + + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + + if sw, ok := w.(SliceWalker); ok { + if err := sw.SliceElem(i, elem); err != nil { + return err + } + } + + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(SliceElem) + } + + if err := walk(elem, w); err != nil { + return err + } + + if ok { + ew.Exit(SliceElem) + } + } + + ew, ok = w.(EnterExitWalker) + if ok { + ew.Exit(Slice) + } + + return nil +} + +func walkArray(v reflect.Value, w interface{}) (err error) { + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(Array) + } + + if aw, ok := w.(ArrayWalker); ok { + if err := aw.Array(v); err != nil { + return err + } + } + + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + + if aw, ok := w.(ArrayWalker); ok { + if err := aw.ArrayElem(i, elem); err != nil { + return err + } + } + + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(ArrayElem) + } + + if err := walk(elem, w); err != nil { + return err + } + + if ok { + ew.Exit(ArrayElem) + } + } + + ew, ok = w.(EnterExitWalker) + if ok { + ew.Exit(Array) + } + + return nil +} + +func walkStruct(v reflect.Value, w interface{}) (err error) { + ew, ewok := w.(EnterExitWalker) + if ewok { + ew.Enter(Struct) + } + + skip := false + if sw, ok := w.(StructWalker); ok { + err = sw.Struct(v) + if err == SkipEntry { + skip = true + err = nil + } + if err != nil { + return + } + } + + if !skip { + vt := v.Type() + for i := 0; i < vt.NumField(); i++ { + sf := vt.Field(i) + f := v.FieldByIndex([]int{i}) + + if sw, ok := w.(StructWalker); ok { + err = sw.StructField(sf, f) + + // SkipEntry just pretends this field doesn't even exist + if err == SkipEntry { + continue + } + + if err != nil { + return + } + } + + ew, ok := w.(EnterExitWalker) + if ok { + ew.Enter(StructField) + } + + err = walk(f, w) + if err != nil { + return + } + + if ok { + ew.Exit(StructField) + } + } + } + + if ewok { + ew.Exit(Struct) + } + + return nil +} diff --git a/vendor/golang.org/x/exp/maps/maps.go b/vendor/golang.org/x/exp/maps/maps.go new file mode 100644 index 000000000000..ecc0dabb74d9 --- /dev/null +++ b/vendor/golang.org/x/exp/maps/maps.go @@ -0,0 +1,94 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package maps defines various functions useful with maps of any type. +package maps + +// Keys returns the keys of the map m. +// The keys will be in an indeterminate order. +func Keys[M ~map[K]V, K comparable, V any](m M) []K { + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} + +// Values returns the values of the map m. +// The values will be in an indeterminate order. +func Values[M ~map[K]V, K comparable, V any](m M) []V { + r := make([]V, 0, len(m)) + for _, v := range m { + r = append(r, v) + } + return r +} + +// Equal reports whether two maps contain the same key/value pairs. +// Values are compared using ==. +func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool { + if len(m1) != len(m2) { + return false + } + for k, v1 := range m1 { + if v2, ok := m2[k]; !ok || v1 != v2 { + return false + } + } + return true +} + +// EqualFunc is like Equal, but compares values using eq. +// Keys are still compared with ==. +func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func(V1, V2) bool) bool { + if len(m1) != len(m2) { + return false + } + for k, v1 := range m1 { + if v2, ok := m2[k]; !ok || !eq(v1, v2) { + return false + } + } + return true +} + +// Clear removes all entries from m, leaving it empty. +func Clear[M ~map[K]V, K comparable, V any](m M) { + for k := range m { + delete(m, k) + } +} + +// Clone returns a copy of m. This is a shallow clone: +// the new keys and values are set using ordinary assignment. +func Clone[M ~map[K]V, K comparable, V any](m M) M { + // Preserve nil in case it matters. + if m == nil { + return nil + } + r := make(M, len(m)) + for k, v := range m { + r[k] = v + } + return r +} + +// Copy copies all key/value pairs in src adding them to dst. +// When a key in src is already present in dst, +// the value in dst will be overwritten by the value associated +// with the key in src. +func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) { + for k, v := range src { + dst[k] = v + } +} + +// DeleteFunc deletes any key/value pairs from m for which del returns true. +func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) { + for k, v := range m { + if del(k, v) { + delete(m, k) + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e8bb3ac0feae..d959f55b70b3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,19 +121,24 @@ github.com/cenkalti/backoff/v4 # github.com/cespare/xxhash/v2 v2.2.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2 -# github.com/compose-spec/compose-go v1.20.0 -## explicit; go 1.19 -github.com/compose-spec/compose-go/cli -github.com/compose-spec/compose-go/consts -github.com/compose-spec/compose-go/dotenv -github.com/compose-spec/compose-go/errdefs -github.com/compose-spec/compose-go/interpolation -github.com/compose-spec/compose-go/loader -github.com/compose-spec/compose-go/schema -github.com/compose-spec/compose-go/template -github.com/compose-spec/compose-go/tree -github.com/compose-spec/compose-go/types -github.com/compose-spec/compose-go/utils +# github.com/compose-spec/compose-go/v2 v2.0.0-rc.2 +## explicit; go 1.20 +github.com/compose-spec/compose-go/v2/cli +github.com/compose-spec/compose-go/v2/consts +github.com/compose-spec/compose-go/v2/dotenv +github.com/compose-spec/compose-go/v2/errdefs +github.com/compose-spec/compose-go/v2/format +github.com/compose-spec/compose-go/v2/interpolation +github.com/compose-spec/compose-go/v2/loader +github.com/compose-spec/compose-go/v2/override +github.com/compose-spec/compose-go/v2/paths +github.com/compose-spec/compose-go/v2/schema +github.com/compose-spec/compose-go/v2/template +github.com/compose-spec/compose-go/v2/transform +github.com/compose-spec/compose-go/v2/tree +github.com/compose-spec/compose-go/v2/types +github.com/compose-spec/compose-go/v2/utils +github.com/compose-spec/compose-go/v2/validation # github.com/containerd/console v1.0.3 ## explicit; go 1.13 github.com/containerd/console @@ -480,12 +485,18 @@ github.com/matttproud/golang_protobuf_extensions/pbutil # github.com/miekg/pkcs11 v1.1.1 ## explicit; go 1.12 github.com/miekg/pkcs11 +# github.com/mitchellh/copystructure v1.2.0 +## explicit; go 1.15 +github.com/mitchellh/copystructure # github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 ## explicit github.com/mitchellh/go-wordwrap # github.com/mitchellh/mapstructure v1.5.0 ## explicit; go 1.14 github.com/mitchellh/mapstructure +# github.com/mitchellh/reflectwalk v1.0.2 +## explicit +github.com/mitchellh/reflectwalk # github.com/moby/buildkit v0.13.0-beta1.0.20240105171016-6e200afad512 ## explicit; go 1.21 github.com/moby/buildkit/api/services/control @@ -819,6 +830,7 @@ golang.org/x/crypto/ssh/internal/bcrypt_pbkdf # golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ## explicit; go 1.20 golang.org/x/exp/constraints +golang.org/x/exp/maps golang.org/x/exp/slices # golang.org/x/mod v0.11.0 ## explicit; go 1.17