Merge pull request #2205 from crazy-max/bump-compose-go

vendor: update compose-go to v2.0.0-rc.3
This commit is contained in:
CrazyMax 2024-01-31 14:31:50 +01:00 committed by GitHub
commit 4b408c79fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 5777 additions and 2738 deletions

View File

@ -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"

View File

@ -297,9 +297,6 @@ services:
ctx := context.TODO()
cwd, err := os.Getwd()
require.NoError(t, err)
m, g, err := ReadTargets(ctx, []File{fp, fp2, fp3}, []string{"default"}, nil, nil)
require.NoError(t, err)
@ -308,7 +305,7 @@ services:
require.True(t, ok)
require.Equal(t, "Dockerfile.webapp", *m["webapp"].Dockerfile)
require.Equal(t, cwd, *m["webapp"].Context)
require.Equal(t, ".", *m["webapp"].Context)
require.Equal(t, ptrstr("1"), m["webapp"].Args["buildno"])
require.Equal(t, ptrstr("12"), m["webapp"].Args["buildno2"])
@ -347,9 +344,6 @@ services:
ctx := context.TODO()
cwd, err := os.Getwd()
require.NoError(t, err)
m, _, err := ReadTargets(ctx, []File{fp}, []string{"web.app"}, nil, nil)
require.NoError(t, err)
require.Equal(t, 1, len(m))
@ -372,7 +366,7 @@ services:
_, ok = m["web_app"]
require.True(t, ok)
require.Equal(t, "Dockerfile.webapp", *m["web_app"].Dockerfile)
require.Equal(t, cwd, *m["web_app"].Context)
require.Equal(t, ".", *m["web_app"].Context)
require.Equal(t, ptrstr("1"), m["web_app"].Args["buildno"])
require.Equal(t, ptrstr("12"), m["web_app"].Args["buildno2"])
@ -581,9 +575,6 @@ services:
ctx := context.TODO()
cwd, err := os.Getwd()
require.NoError(t, err)
m, _, err := ReadTargets(ctx, []File{fp, fp2}, []string{"app1", "app2"}, nil, nil)
require.NoError(t, err)
@ -596,7 +587,7 @@ services:
require.Equal(t, "Dockerfile", *m["app1"].Dockerfile)
require.Equal(t, ".", *m["app1"].Context)
require.Equal(t, "Dockerfile", *m["app2"].Dockerfile)
require.Equal(t, cwd, *m["app2"].Context)
require.Equal(t, ".", *m["app2"].Context)
}
func TestReadContextFromTargetChain(t *testing.T) {

View File

@ -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)
}

View File

@ -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"
)
@ -49,10 +49,7 @@ secrets:
file: /root/.aws/credentials
`)
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))
@ -65,12 +62,12 @@ secrets:
return c.Targets[i].Name < c.Targets[j].Name
})
require.Equal(t, "db", c.Targets[0].Name)
require.Equal(t, filepath.Join(cwd, "db"), *c.Targets[0].Context)
require.Equal(t, "db", *c.Targets[0].Context)
require.Equal(t, []string{"docker.io/tonistiigi/db"}, c.Targets[0].Tags)
require.Equal(t, "webapp", c.Targets[1].Name)
require.Equal(t, filepath.Join(cwd, "dir"), *c.Targets[1].Context)
require.Equal(t, map[string]string{"foo": filepath.Join(cwd, "bar")}, c.Targets[1].Contexts)
require.Equal(t, "dir", *c.Targets[1].Context)
require.Equal(t, map[string]string{"foo": "bar"}, c.Targets[1].Contexts)
require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile)
require.Equal(t, 1, len(c.Targets[1].Args))
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
@ -83,7 +80,7 @@ secrets:
}, c.Targets[1].Secrets)
require.Equal(t, "webapp2", c.Targets[2].Name)
require.Equal(t, filepath.Join(cwd, "dir"), *c.Targets[2].Context)
require.Equal(t, "dir", *c.Targets[2].Context)
require.Equal(t, "FROM alpine\n", *c.Targets[2].DockerfileInline)
}
@ -95,7 +92,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 +111,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 +136,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 +167,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 +181,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 +206,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 +223,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 +260,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 +313,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 +357,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 +390,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)
}
@ -439,7 +436,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 +482,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 +553,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 {
@ -654,7 +651,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 +670,7 @@ services:
build:
context: .
`)
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
require.NoError(t, err)
}
@ -704,7 +701,7 @@ services:
chdir(t, tmpdir)
c, err := ParseComposeFiles([]File{{
Name: "compose.yml",
Name: "composetypes.yml",
Data: dt,
}})
require.NoError(t, err)
@ -734,7 +731,7 @@ services:
- node_modules/
`)
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
require.NoError(t, err)
}

4
go.mod
View File

@ -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.3
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.7.12
github.com/containerd/continuity v0.4.2
@ -114,8 +114,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

8
go.sum
View File

@ -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.3 h1:t0qajSNkH3zR4HEN2CM+GVU7GBx5AwqiYJk5w800M7w=
github.com/compose-spec/compose-go/v2 v2.0.0-rc.3/go.mod h1:r7CJHU0GaLtRVLm2ch8RCNkJh3GHyaqqc2rSti7VP44=
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.20240126101002-6bd81372ad6f h1:weCt2sfZGVAeThzpVyv4ibC0oFfvSxtbiTE7W77wXpc=
github.com/moby/buildkit v0.13.0-beta1.0.20240126101002-6bd81372ad6f/go.mod h1:vEcIVw63dZyhTgbcyQWXlZrtrKnvFoSI8LhfV+Vj0Jg=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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 {

View File

@ -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{}

View File

@ -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

View File

@ -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")

View File

@ -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 {
previousCharIsEscape = false
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

View File

@ -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:

View File

@ -0,0 +1,111 @@
/*
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 graph
import (
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/utils"
"golang.org/x/exp/slices"
)
// graph represents project as service dependencies
type graph[T any] struct {
vertices map[string]*vertex[T]
}
// vertex represents a service in the dependencies structure
type vertex[T any] struct {
key string
service *T
children map[string]*vertex[T]
parents map[string]*vertex[T]
}
func (g *graph[T]) addVertex(name string, service T) {
g.vertices[name] = &vertex[T]{
key: name,
service: &service,
parents: map[string]*vertex[T]{},
children: map[string]*vertex[T]{},
}
}
func (g *graph[T]) addEdge(src, dest string) {
g.vertices[src].children[dest] = g.vertices[dest]
g.vertices[dest].parents[src] = g.vertices[src]
}
func (g *graph[T]) roots() []*vertex[T] {
var res []*vertex[T]
for _, v := range g.vertices {
if len(v.parents) == 0 {
res = append(res, v)
}
}
return res
}
func (g *graph[T]) leaves() []*vertex[T] {
var res []*vertex[T]
for _, v := range g.vertices {
if len(v.children) == 0 {
res = append(res, v)
}
}
return res
}
func (g *graph[T]) checkCycle() error {
// iterate on vertices in a name-order to render a predicable error message
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
names := utils.MapKeys(g.vertices)
for _, name := range names {
err := searchCycle([]string{name}, g.vertices[name])
if err != nil {
return err
}
}
return nil
}
func searchCycle[T any](path []string, v *vertex[T]) error {
names := utils.MapKeys(v.children)
for _, name := range names {
if i := slices.Index(path, name); i > 0 {
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
}
ch := v.children[name]
err := searchCycle(append(path, name), ch)
if err != nil {
return err
}
}
return nil
}
// descendents return all descendents for a vertex, might contain duplicates
func (v *vertex[T]) descendents() []string {
var vx []string
for _, n := range v.children {
vx = append(vx, n.key)
vx = append(vx, n.descendents()...)
}
return vx
}

View File

@ -0,0 +1,80 @@
/*
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 graph
import (
"context"
"fmt"
"github.com/compose-spec/compose-go/v2/types"
)
// InDependencyOrder walk the service graph an invoke VisitorFn in respect to dependency order
func InDependencyOrder(ctx context.Context, project *types.Project, fn VisitorFn[types.ServiceConfig], options ...func(*Options)) error {
_, err := CollectInDependencyOrder[any](ctx, project, func(ctx context.Context, s string, config types.ServiceConfig) (any, error) {
return nil, fn(ctx, s, config)
}, options...)
return err
}
// CollectInDependencyOrder walk the service graph an invoke CollectorFn in respect to dependency order, then return result for each call
func CollectInDependencyOrder[T any](ctx context.Context, project *types.Project, fn CollectorFn[types.ServiceConfig, T], options ...func(*Options)) (map[string]T, error) {
graph, err := newGraph(project)
if err != nil {
return nil, err
}
t := newTraversal(fn)
for _, option := range options {
option(t.Options)
}
err = walk(ctx, graph, t)
return t.results, err
}
// newGraph creates a service graph from project
func newGraph(project *types.Project) (*graph[types.ServiceConfig], error) {
g := &graph[types.ServiceConfig]{
vertices: map[string]*vertex[types.ServiceConfig]{},
}
for name, s := range project.Services {
g.addVertex(name, s)
}
for name, s := range project.Services {
src := g.vertices[name]
for dep, condition := range s.DependsOn {
dest, ok := g.vertices[dep]
if !ok {
if condition.Required {
if ds, exists := project.DisabledServices[dep]; exists {
return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles)
}
return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep)
}
delete(s.DependsOn, name)
project.Services[name] = s
continue
}
src.children[dep] = dest
dest.parents[name] = src
}
}
err := g.checkCycle()
return g, err
}

View File

@ -0,0 +1,211 @@
/*
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 graph
import (
"context"
"sync"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
)
// CollectorFn executes on each graph vertex based on visit order and return associated value
type CollectorFn[S any, T any] func(context.Context, string, S) (T, error)
// VisitorFn executes on each graph nodes based on visit order
type VisitorFn[S any] func(context.Context, string, S) error
type traversal[S any, T any] struct {
*Options
visitor CollectorFn[S, T]
mu sync.Mutex
status map[string]int
results map[string]T
}
type Options struct {
// inverse reverse the traversal direction
inverse bool
// maxConcurrency limit the concurrent execution of visitorFn while walking the graph
maxConcurrency int
// after marks a set of node as starting points walking the graph
after []string
}
const (
vertexEntered = iota
vertexVisited
)
func newTraversal[S, T any](fn CollectorFn[S, T]) *traversal[S, T] {
return &traversal[S, T]{
Options: &Options{},
status: map[string]int{},
results: map[string]T{},
visitor: fn,
}
}
// WithMaxConcurrency configure traversal to limit concurrency walking graph nodes
func WithMaxConcurrency(max int) func(*Options) {
return func(o *Options) {
o.maxConcurrency = max
}
}
// InReverseOrder configure traversal to walk the graph in reverse dependency order
func InReverseOrder(o *Options) {
o.inverse = true
}
// WithRootNodesAndDown creates a graphTraversal to start from selected nodes
func WithRootNodesAndDown(nodes []string) func(*Options) {
return func(o *Options) {
o.after = nodes
}
}
func walk[S, T any](ctx context.Context, g *graph[S], t *traversal[S, T]) error {
expect := len(g.vertices)
if expect == 0 {
return nil
}
// nodeCh need to allow n=expect writers while reader goroutine could have returned after ctx.Done
nodeCh := make(chan *vertex[S], expect)
defer close(nodeCh)
eg, ctx := errgroup.WithContext(ctx)
if t.maxConcurrency > 0 {
eg.SetLimit(t.maxConcurrency + 1)
}
eg.Go(func() error {
for {
select {
case <-ctx.Done():
return nil
case node := <-nodeCh:
expect--
if expect == 0 {
return nil
}
for _, adj := range t.adjacentNodes(node) {
t.visit(ctx, eg, adj, nodeCh)
}
}
}
})
// select nodes to start walking the graph based on traversal.direction
for _, node := range t.extremityNodes(g) {
t.visit(ctx, eg, node, nodeCh)
}
return eg.Wait()
}
func (t *traversal[S, T]) visit(ctx context.Context, eg *errgroup.Group, node *vertex[S], nodeCh chan *vertex[S]) {
if !t.ready(node) {
// don't visit this service yet as dependencies haven't been visited
return
}
if !t.enter(node) {
// another worker already acquired this node
return
}
eg.Go(func() error {
var (
err error
result T
)
if !t.skip(node) {
result, err = t.visitor(ctx, node.key, *node.service)
}
t.done(node, result)
nodeCh <- node
return err
})
}
func (t *traversal[S, T]) extremityNodes(g *graph[S]) []*vertex[S] {
if t.inverse {
return g.roots()
}
return g.leaves()
}
func (t *traversal[S, T]) adjacentNodes(v *vertex[S]) map[string]*vertex[S] {
if t.inverse {
return v.children
}
return v.parents
}
func (t *traversal[S, T]) ready(v *vertex[S]) bool {
t.mu.Lock()
defer t.mu.Unlock()
depends := v.children
if t.inverse {
depends = v.parents
}
for name := range depends {
if t.status[name] != vertexVisited {
return false
}
}
return true
}
func (t *traversal[S, T]) enter(v *vertex[S]) bool {
t.mu.Lock()
defer t.mu.Unlock()
if _, ok := t.status[v.key]; ok {
return false
}
t.status[v.key] = vertexEntered
return true
}
func (t *traversal[S, T]) done(v *vertex[S], result T) {
t.mu.Lock()
defer t.mu.Unlock()
t.status[v.key] = vertexVisited
t.results[v.key] = result
}
func (t *traversal[S, T]) skip(node *vertex[S]) bool {
if len(t.after) == 0 {
return false
}
if slices.Contains(t.after, node.key) {
return false
}
// is none of our starting node is a descendent, skip visit
ancestors := node.descendents()
for _, name := range t.after {
if slices.Contains(ancestors, name) {
return false
}
}
return true
}

View File

@ -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)
}
}

View File

@ -0,0 +1,176 @@
/*
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 := range services {
merged, err := applyServiceExtends(ctx, name, services, opts, tracker, post...)
if err != nil {
return err
}
services[name] = merged
}
dict["services"] = services
return nil
}
func applyServiceExtends(ctx context.Context, name string, services map[string]any, opts *Options, tracker *cycleTracker, post ...PostProcessor) (any, error) {
s := services[name]
if s == nil {
return nil, nil
}
service, ok := s.(map[string]any)
if !ok {
return nil, fmt.Errorf("services.%s must be a mapping", name)
}
extends, ok := service["extends"]
if !ok {
return s, nil
}
filename := ctx.Value(consts.ComposeFileKey{}).(string)
tracker, err := tracker.Add(filename, name)
if err != nil {
return nil, err
}
var (
ref string
file any
)
switch v := extends.(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)
services, err = getExtendsBaseFromFile(ctx, ref, path, opts, tracker)
if err != nil {
return nil, err
}
} else {
_, ok := services[ref]
if !ok {
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, filename)
}
}
// recursively apply `extends`
base, err = applyServiceExtends(ctx, ref, services, opts, tracker, post...)
if err != nil {
return nil, err
}
if base == nil {
return service, nil
}
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 nil, err
}
delete(merged, "extends")
return merged, nil
}
func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) {
for _, loader := range opts.ResourceLoaders {
if !loader.Accept(path) {
continue
}
local, err := loader.Load(ctx, path)
if err != nil {
return nil, err
}
localdir := filepath.Dir(local)
relworkingdir := loader.Dir(path)
extendsOpts := opts.clone()
// replace localResourceLoader with a new flavour, using extended file base path
extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{
WorkingDir: localdir,
})
extendsOpts.ResolvePaths = true
extendsOpts.SkipNormalization = true
extendsOpts.SkipConsistencyCheck = true
extendsOpts.SkipInclude = true
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
extendsOpts.SkipValidation = true // we validate the merge result
source, err := loadYamlModel(ctx, types.ConfigDetails{
WorkingDir: relworkingdir,
ConfigFiles: []types.ConfigFile{
{Filename: local},
},
}, extendsOpts, ct, nil)
if err != nil {
return nil, err
}
services := source["services"].(map[string]any)
_, ok := services[name]
if !ok {
return nil, fmt.Errorf("cannot extend service %q in %s: service not found", name, path)
}
return services, nil
}
return nil, fmt.Errorf("cannot read %s", path)
}
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
}
}

View File

@ -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
}

View File

@ -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"
@ -271,7 +273,8 @@ services:
stop_grace_period: 20s
stop_signal: SIGUSR1
storage_opt:
size: "20G"
sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0
@ -295,22 +298,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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,742 @@
/*
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"
"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 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
}
// RemoteResourceLoaders excludes localResourceLoader from ResourceLoaders
func (o Options) RemoteResourceLoaders() []ResourceLoader {
var loaders []ResourceLoader
for i, loader := range o.ResourceLoaders {
if _, ok := loader.(localResourceLoader); ok {
if i != len(o.ResourceLoaders)-1 {
logrus.Warning("misconfiguration of ResourceLoaders: localResourceLoader should be last")
}
continue
}
loaders = append(loaders, loader)
}
return loaders
}
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 {
var remotes []paths.RemoteResource
for _, loader := range opts.RemoteResourceLoaders() {
remotes = append(remotes, loader.Accept)
}
err = paths.ResolveRelativePaths(dict, config.WorkingDir, remotes)
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
}

View File

@ -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
}

View File

@ -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
if n.External {
n.Name = key
} else {
n.Name = fmt.Sprintf("%s_%s", project.Name, key)
}
project.Networks[key] = n
}
}
for i, v := range project.Volumes {
for key, v := range project.Volumes {
if v.Name == "" {
v.Name = fmt.Sprintf("%s_%s", project.Name, i)
project.Volumes[i] = v
if v.External {
v.Name = key
} else {
v.Name = fmt.Sprintf("%s_%s", project.Name, key)
}
project.Volumes[key] = v
}
}
for i, c := range project.Configs {
for key, c := range project.Configs {
if c.Name == "" {
c.Name = fmt.Sprintf("%s_%s", project.Name, i)
project.Configs[i] = c
if c.External {
c.Name = key
} else {
c.Name = fmt.Sprintf("%s_%s", project.Name, key)
}
project.Configs[key] = c
}
}
for i, s := range project.Secrets {
for key, 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 s.External {
s.Name = key
} else {
s.Name = fmt.Sprintf("%s_%s", project.Name, key)
}
n.Name = n.External.Name
project.Secrets[key] = s
}
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'")
}
v.Name = v.External.Name
}
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'")
}
s.Name = s.External.Name
}
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'")
}
c.Name = c.External.Name
}
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

View File

@ -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

View File

@ -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,81 @@ 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
}
if node.Tag == "!override" {
p.paths = append(p.paths, path)
return node, 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)
func (p *ResetProcessor) applyNullOverrides(target any, path tree.Path) error {
switch v := target.(type) {
case map[string]any:
KEYS:
for k, e := range v {
next := path.Next(k)
for _, pattern := range p.paths {
if next.Matches(pattern) {
delete(v, k)
continue KEYS
}
}
err := p.applyNullOverrides(e, next)
if err != nil {
return err
}
}
case typ.Kind() == reflect.Map:
iter := val.MapRange()
KEYS:
for iter.Next() {
k := iter.Key()
next := path.Next(k.String())
for _, pattern := range p.paths {
if next.Matches(pattern) {
val.SetMapIndex(k, reflect.Value{})
continue KEYS
}
}
return p.applyNullOverrides(iter.Value(), next)
}
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
}

View File

@ -17,24 +17,26 @@
package loader
import (
"context"
"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/graph"
"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 +48,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,9 +72,15 @@ 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)
}
}
// Check there isn't a cycle in depends_on declarations
if err := graph.InDependencyOrder(context.Background(), project, func(ctx context.Context, s string, config types.ServiceConfig) error {
return nil
}); err != nil {
return err
}
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
serviceName := s.NetworkMode[len(types.ServicePrefix):]
@ -84,36 +92,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)
}
}

View File

@ -0,0 +1,27 @@
/*
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 "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
}

View File

@ -0,0 +1,252 @@
/*
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["networks.*.ipam.config"] = mergeIPAMConfig
mergeSpecials["services.*.annotations"] = mergeToSequence
mergeSpecials["services.*.build"] = mergeBuild
mergeSpecials["services.*.build.args"] = mergeToSequence
mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence
mergeSpecials["services.*.build.labels"] = mergeToSequence
mergeSpecials["services.*.command"] = override
mergeSpecials["services.*.depends_on"] = mergeDependsOn
mergeSpecials["services.*.deploy.labels"] = mergeToSequence
mergeSpecials["services.*.dns"] = mergeToSequence
mergeSpecials["services.*.dns_opt"] = mergeToSequence
mergeSpecials["services.*.dns_search"] = mergeToSequence
mergeSpecials["services.*.entrypoint"] = override
mergeSpecials["services.*.env_file"] = mergeToSequence
mergeSpecials["services.*.environment"] = mergeToSequence
mergeSpecials["services.*.extra_hosts"] = mergeToSequence
mergeSpecials["services.*.healthcheck.test"] = override
mergeSpecials["services.*.labels"] = mergeToSequence
mergeSpecials["services.*.logging"] = mergeLogging
mergeSpecials["services.*.networks"] = mergeNetworks
mergeSpecials["services.*.sysctls"] = mergeToSequence
mergeSpecials["services.*.tmpfs"] = 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
}
}
if o == nil {
return e, 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("cannot 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
case string:
return []any{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 mergeIPAMConfig(c any, o any, path tree.Path) (any, error) {
var ipamConfigs []any
for _, original := range c.([]any) {
right := convertIntoMapping(original, nil)
for _, override := range o.([]any) {
left := convertIntoMapping(override, nil)
if left["subnet"] != right["subnet"] {
// check if left is already in ipamConfigs, add it if not and continue with the next config
if !slices.ContainsFunc(ipamConfigs, func(a any) bool {
return a.(map[string]any)["subnet"] == left["subnet"]
}) {
ipamConfigs = append(ipamConfigs, left)
continue
}
}
merged, err := mergeMappings(right, left, path)
if err != nil {
return nil, err
}
// find index of potential previous config with the same subnet in ipamConfigs
indexIfExist := slices.IndexFunc(ipamConfigs, func(a any) bool {
return a.(map[string]any)["subnet"] == merged["subnet"]
})
// if a previous config is already in ipamConfigs, replace it
if indexIfExist >= 0 {
ipamConfigs[indexIfExist] = merged
} else {
// or add the new config to ipamConfigs
ipamConfigs = append(ipamConfigs, merged)
}
}
}
return ipamConfigs, 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
}

View File

@ -0,0 +1,204 @@
/*
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["networks.*.labels"] = keyValueIndexer
unique["networks.*.ipam.options"] = keyValueIndexer
unique["services.*.annotations"] = keyValueIndexer
unique["services.*.build.args"] = keyValueIndexer
unique["services.*.build.additional_contexts"] = keyValueIndexer
unique["services.*.build.extra_hosts"] = keyValueIndexer
unique["services.*.build.platform"] = keyValueIndexer
unique["services.*.build.tags"] = keyValueIndexer
unique["services.*.build.labels"] = keyValueIndexer
unique["services.*.cap_add"] = keyValueIndexer
unique["services.*.cap_drop"] = keyValueIndexer
unique["services.*.configs"] = mountIndexer("")
unique["services.*.deploy.labels"] = keyValueIndexer
unique["services.*.dns"] = keyValueIndexer
unique["services.*.dns_opt"] = keyValueIndexer
unique["services.*.dns_search"] = keyValueIndexer
unique["services.*.environment"] = keyValueIndexer
unique["services.*.env_file"] = envFileIndexer
unique["services.*.expose"] = exposeIndexer
unique["services.*.extra_hosts"] = keyValueIndexer
unique["services.*.labels"] = keyValueIndexer
unique["services.*.links"] = keyValueIndexer
unique["services.*.networks.*.aliases"] = keyValueIndexer
unique["services.*.networks.*.link_local_ips"] = keyValueIndexer
unique["services.*.ports"] = portIndexer
unique["services.*.profiles"] = keyValueIndexer
unique["services.*.secrets"] = mountIndexer("/run/secrets")
unique["services.*.sysctls"] = keyValueIndexer
unique["services.*.tmpfs"] = keyValueIndexer
unique["services.*.volumes"] = volumeIndexer
}
// 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
}
func envFileIndexer(y any, _ tree.Path) (string, error) {
switch value := y.(type) {
case string:
return value, nil
case map[string]any:
return value["path"].(string), nil
}
return "", nil
}

View File

@ -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
}

View File

@ -14,11 +14,12 @@
limitations under the License.
*/
package consts
package paths
const (
ComposeProjectName = "COMPOSE_PROJECT_NAME"
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
ComposeFilePath = "COMPOSE_FILE"
ComposeProfiles = "COMPOSE_PROFILES"
)
func (r *relativePathsResolver) absExtendsPath(value any) (any, error) {
v := value.(string)
if r.isRemoteResource(v) {
return v, nil
}
return r.absPath(v)
}

View File

@ -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
}

View File

@ -0,0 +1,161 @@
/*
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, remotes []RemoteResource) error {
r := relativePathsResolver{
workingDir: base,
remotes: remotes,
}
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.absExtendsPath,
"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 RemoteResource func(path string) bool
type relativePathsResolver struct {
workingDir string
remotes []RemoteResource
resolvers map[tree.Path]resolver
}
func (r *relativePathsResolver) isRemoteResource(path string) bool {
for _, remote := range r.remotes {
if remote(path) {
return true
}
}
return false
}
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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 @@
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,41 @@
/*
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) {
switch value := data.(type) {
case map[string]any:
return transformMapping(value, p)
default:
return value, nil
}
}
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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -38,7 +38,7 @@ type ConfigDetails struct {
}
// LookupEnv provides a lookup function for environment variables
func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
func (cd *ConfigDetails) LookupEnv(key string) (string, bool) {
v, ok := cd.Environment[key]
if !isCaseInsensitiveEnvVars || ok {
return v, ok

View File

@ -18,6 +18,8 @@ package types
type DevelopConfig struct {
Watch []Trigger `json:"watch,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
type WatchAction string

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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...)
}
newProject, err := newProject.WithProfiles(profiles)
if err != nil {
return newProject, err
}
profiles := p.Profiles
PROFILES:
for _, profile := range enabled {
for _, p := range profiles {
if p == profile {
continue PROFILES
}
}
profiles = append(profiles, profile)
}
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
}
// ForServices restrict the project model to selected services and dependencies
func (p *Project) ForServices(names []string, options ...DependencyOption) error {
func IncludeDependents(options *withServicesOptions) {
options.dependencyPolicy = includeDependents
}
func IgnoreDependencies(options *withServicesOptions) {
options.dependencyPolicy = ignoreDependencies
}
// 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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,13 +113,14 @@ 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"`
StdinOpen bool `yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"`
StopGracePeriod *Duration `yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"`
StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`
StorageOpt map[string]string `yaml:"storage_opt,omitempty" json:"storage_opt,omitempty"`
Sysctls Mapping `yaml:"sysctls,omitempty" json:"sysctls,omitempty"`
Tmpfs StringList `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"`
Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"`
@ -152,25 +133,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 +171,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 +257,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"`
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" json:"-"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}
// BlkioConfig define blkio config
@ -308,7 +293,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 +301,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 +309,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 +336,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 +348,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 +356,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 +368,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 +376,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 +387,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 +397,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 +406,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 +423,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 +436,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 +489,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 +535,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 +566,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 +575,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 +586,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 +601,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 +663,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 +689,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 +709,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 +734,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"`
}

View File

@ -16,13 +16,15 @@
package utils
import "golang.org/x/exp/slices"
import (
"golang.org/x/exp/constraints"
"golang.org/x/exp/maps"
"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)
}
func MapKeys[T constraints.Ordered, U any](theMap map[T]U) []T {
result := maps.Keys(theMap)
slices.Sort(result)
return result
}

View File

@ -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
}

View File

@ -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)))

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

21
vendor/github.com/mitchellh/copystructure/LICENSE generated vendored Normal file
View File

@ -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.

21
vendor/github.com/mitchellh/copystructure/README.md generated vendored Normal file
View File

@ -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.

View File

@ -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
}

View File

@ -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
}

1
vendor/github.com/mitchellh/reflectwalk/.travis.yml generated vendored Normal file
View File

@ -0,0 +1 @@
language: go

21
vendor/github.com/mitchellh/reflectwalk/LICENSE generated vendored Normal file
View File

@ -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.

6
vendor/github.com/mitchellh/reflectwalk/README.md generated vendored Normal file
View File

@ -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.

19
vendor/github.com/mitchellh/reflectwalk/location.go generated vendored Normal file
View File

@ -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
)

View File

@ -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]]
}

420
vendor/github.com/mitchellh/reflectwalk/reflectwalk.go generated vendored Normal file
View File

@ -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
}

94
vendor/golang.org/x/exp/maps/maps.go generated vendored Normal file
View File

@ -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)
}
}
}

39
vendor/modules.txt vendored
View File

@ -121,19 +121,25 @@ 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.3
## 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/graph
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
@ -481,12 +487,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.20240126101002-6bd81372ad6f
## explicit; go 1.21
github.com/moby/buildkit/api/services/control
@ -820,6 +832,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.13.0
## explicit; go 1.18