mirror of https://github.com/docker/buildx.git
vendor: update compose-go to v2.0.0-rc.3
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
parent
d0c4bed484
commit
13beda8b11
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"sort"
|
||||
"testing"
|
||||
|
||||
compose "github.com/compose-spec/compose-go/types"
|
||||
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ secrets:
|
|||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Groups))
|
||||
|
@ -95,7 +95,7 @@ services:
|
|||
webapp:
|
||||
build: ./db
|
||||
`)
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Groups))
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
|
@ -114,7 +114,7 @@ services:
|
|||
target: webapp
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
|
@ -139,7 +139,7 @@ services:
|
|||
target: webapp
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
sort.Slice(c.Targets, func(i, j int) bool {
|
||||
|
@ -170,7 +170,7 @@ services:
|
|||
t.Setenv("BAR", "foo")
|
||||
t.Setenv("ZZZ_BAR", "zzz_foo")
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, sliceToMap(os.Environ()))
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, sliceToMap(os.Environ()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ptrstr("bar"), c.Targets[0].Args["FOO"])
|
||||
require.Equal(t, ptrstr("zzz_foo"), c.Targets[0].Args["BAR"])
|
||||
|
@ -184,7 +184,7 @@ services:
|
|||
entrypoint: echo 1
|
||||
`)
|
||||
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,7 @@ networks:
|
|||
gateway: 10.5.0.254
|
||||
`)
|
||||
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -226,7 +226,7 @@ services:
|
|||
- bar
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"foo", "bar"}, c.Targets[0].Tags)
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ networks:
|
|||
name: test-net
|
||||
`)
|
||||
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -316,7 +316,7 @@ services:
|
|||
no-cache: true
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
sort.Slice(c.Targets, func(i, j int) bool {
|
||||
|
@ -360,7 +360,7 @@ services:
|
|||
- type=local,dest=path/to/cache
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags)
|
||||
|
@ -393,7 +393,7 @@ services:
|
|||
- ` + envf.Name() + `
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "FOO": ptrstr("bsdf -csdf"), "NODE_ENV": ptrstr("test")}, c.Targets[0].Args)
|
||||
}
|
||||
|
@ -439,7 +439,7 @@ services:
|
|||
published: "3306"
|
||||
protocol: tcp
|
||||
`)
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -485,7 +485,7 @@ func TestServiceName(t *testing.T) {
|
|||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.svc, func(t *testing.T) {
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: []byte(`
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: []byte(`
|
||||
services:
|
||||
` + tt.svc + `:
|
||||
build:
|
||||
|
@ -556,7 +556,7 @@ services:
|
|||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: tt.dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: tt.dt}}, nil)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
|
@ -654,7 +654,7 @@ services:
|
|||
bar: "baz"
|
||||
`)
|
||||
|
||||
c, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
c, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]*string{"bar": ptrstr("baz")}, c.Targets[0].Args)
|
||||
}
|
||||
|
@ -673,7 +673,7 @@ services:
|
|||
build:
|
||||
context: .
|
||||
`)
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -704,7 +704,7 @@ services:
|
|||
|
||||
chdir(t, tmpdir)
|
||||
c, err := ParseComposeFiles([]File{{
|
||||
Name: "compose.yml",
|
||||
Name: "composetypes.yml",
|
||||
Data: dt,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
@ -734,7 +734,7 @@ services:
|
|||
- node_modules/
|
||||
`)
|
||||
|
||||
_, err := ParseCompose([]compose.ConfigFile{{Content: dt}}, nil)
|
||||
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
4
go.mod
4
go.mod
|
@ -5,7 +5,7 @@ go 1.21
|
|||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.16
|
||||
github.com/compose-spec/compose-go v1.20.0
|
||||
github.com/compose-spec/compose-go/v2 v2.0.0-rc.3
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.7.12
|
||||
github.com/containerd/continuity v0.4.2
|
||||
|
@ -113,8 +113,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
8
go.sum
|
@ -86,8 +86,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
|
|||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go v1.20.0 h1:h4ZKOst1EF/DwZp7dWkb+wbTVE4nEyT9Lc89to84Ol4=
|
||||
github.com/compose-spec/compose-go v1.20.0/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM=
|
||||
github.com/compose-spec/compose-go/v2 v2.0.0-rc.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=
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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 {
|
|
@ -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{}
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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:
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
107
vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go
generated
vendored
Normal file
107
vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go
generated
vendored
Normal 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
|
||||
}
|
53
vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go
generated
vendored
Normal file
53
vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
54
vendor/github.com/compose-spec/compose-go/v2/transform/external.go
generated
vendored
Normal file
54
vendor/github.com/compose-spec/compose-go/v2/transform/external.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
41
vendor/github.com/compose-spec/compose-go/v2/transform/services.go
generated
vendored
Normal file
41
vendor/github.com/compose-spec/compose-go/v2/transform/services.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -18,6 +18,8 @@ package types
|
|||
|
||||
type DevelopConfig struct {
|
||||
Watch []Trigger `json:"watch,omitempty"`
|
||||
|
||||
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
type WatchAction string
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)))
|
49
vendor/github.com/compose-spec/compose-go/v2/validation/external.go
generated
vendored
Normal file
49
vendor/github.com/compose-spec/compose-go/v2/validation/external.go
generated
vendored
Normal 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
|
||||
}
|
96
vendor/github.com/compose-spec/compose-go/v2/validation/validation.go
generated
vendored
Normal file
96
vendor/github.com/compose-spec/compose-go/v2/validation/validation.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
language: go
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
)
|
|
@ -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]]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue