add //go:build directives to prevent downgrading to go1.16 language
This is a follow-up to 0e73168b7e6d1d029d76d05b843b1aaec46739a8
This repository is not yet a module (i.e., does not have a `go.mod`). This
is not problematic when building the code in GOPATH or "vendor" mode, but
when using the code as a module-dependency (in module-mode), different semantics
are applied since Go1.21, which switches Go _language versions_ on a per-module,
per-package, or even per-file base.
A condensed summary of that logic [is as follows][1]:
- For modules that have a go.mod containing a go version directive; that
version is considered a minimum _required_ version (starting with the
go1.19.13 and go1.20.8 patch releases: before those, it was only a
recommendation).
- For dependencies that don't have a go.mod (not a module), go language
version go1.16 is assumed.
- Likewise, for modules that have a go.mod, but the file does not have a
go version directive, go language version go1.16 is assumed.
- If a go.work file is present, but does not have a go version directive,
language version go1.17 is assumed.
When switching language versions, Go _downgrades_ the language version,
which means that language features (such as generics, and `any`) are not
available, and compilation fails. For example:
# github.com/docker/cli/cli/context/store
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
Note that these fallbacks are per-module, per-package, and can even be
per-file, so _(indirect) dependencies_ can still use modern language
features, as long as their respective go.mod has a version specified.
Unfortunately, these failures do not occur when building locally (using
vendor / GOPATH mode), but will affect consumers of the module.
Obviously, this situation is not ideal, and the ultimate solution is to
move to go modules (add a go.mod), but this comes with a non-insignificant
risk in other areas (due to our complex dependency tree).
We can revert to using go1.16 language features only, but this may be
limiting, and may still be problematic when (e.g.) matching signatures
of dependencies.
There is an escape hatch: adding a `//go:build` directive to files that
make use of go language features. From the [go toolchain docs][2]:
> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a `go.mod` file with a go line such as `go 1.21` or `go 1.21.3`.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding `//go:build go1.22` to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.
This patch adds `//go:build` directives to those files using recent additions
to the language. It's currently using go1.19 as version to match the version
in our "vendor.mod", but we can consider being more permissive ("any" requires
go1.18 or up), or more "optimistic" (force go1.21, which is the version we
currently use to build).
For completeness sake, note that any file _without_ a `//go:build` directive
will continue to use go1.16 language version when used as a module.
[1]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L9-L56
[2]; https://go.dev/doc/toolchain#:~:text=The%20go%20line%20for,file%20to%20Go%201.22
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-12-14 07:51:57 -05:00
|
|
|
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
2024-06-18 06:07:25 -04:00
|
|
|
//go:build go1.21
|
add //go:build directives to prevent downgrading to go1.16 language
This is a follow-up to 0e73168b7e6d1d029d76d05b843b1aaec46739a8
This repository is not yet a module (i.e., does not have a `go.mod`). This
is not problematic when building the code in GOPATH or "vendor" mode, but
when using the code as a module-dependency (in module-mode), different semantics
are applied since Go1.21, which switches Go _language versions_ on a per-module,
per-package, or even per-file base.
A condensed summary of that logic [is as follows][1]:
- For modules that have a go.mod containing a go version directive; that
version is considered a minimum _required_ version (starting with the
go1.19.13 and go1.20.8 patch releases: before those, it was only a
recommendation).
- For dependencies that don't have a go.mod (not a module), go language
version go1.16 is assumed.
- Likewise, for modules that have a go.mod, but the file does not have a
go version directive, go language version go1.16 is assumed.
- If a go.work file is present, but does not have a go version directive,
language version go1.17 is assumed.
When switching language versions, Go _downgrades_ the language version,
which means that language features (such as generics, and `any`) are not
available, and compilation fails. For example:
# github.com/docker/cli/cli/context/store
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
Note that these fallbacks are per-module, per-package, and can even be
per-file, so _(indirect) dependencies_ can still use modern language
features, as long as their respective go.mod has a version specified.
Unfortunately, these failures do not occur when building locally (using
vendor / GOPATH mode), but will affect consumers of the module.
Obviously, this situation is not ideal, and the ultimate solution is to
move to go modules (add a go.mod), but this comes with a non-insignificant
risk in other areas (due to our complex dependency tree).
We can revert to using go1.16 language features only, but this may be
limiting, and may still be problematic when (e.g.) matching signatures
of dependencies.
There is an escape hatch: adding a `//go:build` directive to files that
make use of go language features. From the [go toolchain docs][2]:
> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a `go.mod` file with a go line such as `go 1.21` or `go 1.21.3`.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding `//go:build go1.22` to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.
This patch adds `//go:build` directives to those files using recent additions
to the language. It's currently using go1.19 as version to match the version
in our "vendor.mod", but we can consider being more permissive ("any" requires
go1.18 or up), or more "optimistic" (force go1.21, which is the version we
currently use to build).
For completeness sake, note that any file _without_ a `//go:build` directive
will continue to use go1.16 language version when used as a module.
[1]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L9-L56
[2]; https://go.dev/doc/toolchain#:~:text=The%20go%20line%20for,file%20to%20Go%201.22
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-12-14 07:51:57 -05:00
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
package loader
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"path"
|
2017-05-27 15:29:30 -04:00
|
|
|
"path/filepath"
|
2016-12-20 16:26:49 -05:00
|
|
|
"reflect"
|
|
|
|
"sort"
|
golangci-lint: enable perfsprint linter
cli/compose/types/types.go:568:17: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return []byte(fmt.Sprintf("%v", e.External)), nil
^
cli/command/formatter/buildcache.go:174:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
return fmt.Sprintf("%d", c.v.UsageCount)
^
cli/command/formatter/buildcache.go:178:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%t", c.v.InUse)
^
cli/command/formatter/buildcache.go:182:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%t", c.v.Shared)
^
cli/command/formatter/image.go:259:9: fmt.Sprintf can be replaced with faster strconv.FormatInt (perfsprint)
return fmt.Sprintf("%d", c.i.Containers)
^
cli/command/formatter/tabwriter/tabwriter_test.go:698:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
b.Run(fmt.Sprintf("%d", x), func(b *testing.B) {
^
cli/command/formatter/tabwriter/tabwriter_test.go:720:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
b.Run(fmt.Sprintf("%d", h), func(b *testing.B) {
^
cli/command/image/prune.go:62:31: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
pruneFilters.Add("dangling", fmt.Sprintf("%v", !options.all))
^
cli/command/network/formatter.go:92:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%v", c.n.EnableIPv6)
^
cli/command/network/formatter.go:96:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%v", c.n.Internal)
^
cli/command/service/formatter.go:745:9: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
pub = fmt.Sprintf("%d", pr.pStart)
^
cli/command/service/formatter.go:750:9: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
tgt = fmt.Sprintf("%d", pr.tStart)
^
cli/command/service/opts.go:49:10: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
return fmt.Sprintf("%v", *i.value)
^
cli/compose/loader/loader.go:720:36: fmt.Sprint can be replaced with faster strconv.Itoa (perfsprint)
v, err := toServicePortConfigs(fmt.Sprint(value))
^
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-11-20 10:18:19 -05:00
|
|
|
"strconv"
|
2016-12-20 16:26:49 -05:00
|
|
|
"strings"
|
2018-08-29 17:29:39 -04:00
|
|
|
"time"
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2018-06-25 11:15:26 -04:00
|
|
|
interp "github.com/docker/cli/cli/compose/interpolation"
|
2017-04-17 18:07:56 -04:00
|
|
|
"github.com/docker/cli/cli/compose/schema"
|
|
|
|
"github.com/docker/cli/cli/compose/template"
|
|
|
|
"github.com/docker/cli/cli/compose/types"
|
2017-05-15 08:45:19 -04:00
|
|
|
"github.com/docker/cli/opts"
|
2017-11-06 17:03:43 -05:00
|
|
|
"github.com/docker/docker/api/types/versions"
|
2017-01-31 15:45:45 -05:00
|
|
|
"github.com/docker/go-connections/nat"
|
2016-12-20 16:26:49 -05:00
|
|
|
units "github.com/docker/go-units"
|
2023-12-27 05:33:03 -05:00
|
|
|
"github.com/go-viper/mapstructure/v2"
|
2020-07-20 04:20:42 -04:00
|
|
|
"github.com/google/shlex"
|
2017-03-24 12:24:58 -04:00
|
|
|
"github.com/pkg/errors"
|
2017-08-07 05:52:40 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
2016-12-20 16:26:49 -05:00
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
)
|
|
|
|
|
2018-06-25 11:15:26 -04:00
|
|
|
// Options supported by Load
|
|
|
|
type Options struct {
|
|
|
|
// Skip schema validation
|
|
|
|
SkipValidation bool
|
|
|
|
// Skip interpolation
|
|
|
|
SkipInterpolation bool
|
|
|
|
// Interpolation options
|
|
|
|
Interpolate *interp.Options
|
2019-08-22 11:36:12 -04:00
|
|
|
// Discard 'env_file' entries after resolving to 'environment' section
|
|
|
|
discardEnvFiles bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to
|
|
|
|
// the `environment` section
|
2023-11-20 11:38:50 -05:00
|
|
|
func WithDiscardEnvFiles(options *Options) {
|
|
|
|
options.discardEnvFiles = true
|
2018-06-25 11:15:26 -04:00
|
|
|
}
|
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
|
|
|
|
// structure, and returns it.
|
2023-11-20 12:04:36 -05:00
|
|
|
func ParseYAML(source []byte) (map[string]any, error) {
|
|
|
|
var cfg any
|
2016-12-20 16:26:49 -05:00
|
|
|
if err := yaml.Unmarshal(source, &cfg); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-11-20 12:04:36 -05:00
|
|
|
cfgMap, ok := cfg.(map[any]any)
|
2016-12-20 16:26:49 -05:00
|
|
|
if !ok {
|
2022-11-16 16:21:16 -05:00
|
|
|
return nil, errors.Errorf("top-level object must be a mapping")
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
converted, err := convertToStringKeysRecursive(cfgMap, "")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-11-20 12:04:36 -05:00
|
|
|
return converted.(map[string]any), nil
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Load reads a ConfigDetails and returns a fully loaded configuration
|
2023-11-20 11:38:50 -05:00
|
|
|
func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Config, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
if len(configDetails.ConfigFiles) < 1 {
|
2017-03-09 13:23:45 -05:00
|
|
|
return nil, errors.Errorf("No files specified")
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
options := &Options{
|
2018-06-25 11:15:26 -04:00
|
|
|
Interpolate: &interp.Options{
|
|
|
|
Substitute: template.Substitute,
|
|
|
|
LookupValue: configDetails.LookupEnv,
|
|
|
|
TypeCastMapping: interpolateTypeCastMapping,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
for _, op := range opt {
|
|
|
|
op(options)
|
2018-06-25 11:15:26 -04:00
|
|
|
}
|
|
|
|
|
2017-09-29 08:21:40 -04:00
|
|
|
configs := []*types.Config{}
|
2018-06-25 11:15:26 -04:00
|
|
|
var err error
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2017-09-29 08:21:40 -04:00
|
|
|
for _, file := range configDetails.ConfigFiles {
|
|
|
|
configDict := file.Config
|
|
|
|
version := schema.Version(configDict)
|
|
|
|
if configDetails.Version == "" {
|
|
|
|
configDetails.Version = version
|
|
|
|
}
|
|
|
|
if configDetails.Version != version {
|
|
|
|
return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
|
|
|
|
}
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2017-09-29 08:21:40 -04:00
|
|
|
if err := validateForbidden(configDict); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
if !options.SkipInterpolation {
|
|
|
|
configDict, err = interpolateConfig(configDict, *options.Interpolate)
|
2018-06-25 11:15:26 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-29 08:21:40 -04:00
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
if !options.SkipValidation {
|
2018-06-25 11:15:26 -04:00
|
|
|
if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-29 08:21:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
cfg, err := loadSections(configDict, configDetails)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cfg.Filename = file.Filename
|
2023-11-20 11:38:50 -05:00
|
|
|
if options.discardEnvFiles {
|
2019-08-22 11:36:12 -04:00
|
|
|
for i := range cfg.Services {
|
|
|
|
cfg.Services[i].EnvFile = nil
|
|
|
|
}
|
|
|
|
}
|
2017-09-29 08:21:40 -04:00
|
|
|
|
|
|
|
configs = append(configs, cfg)
|
2017-10-03 18:03:20 -04:00
|
|
|
}
|
2017-09-29 08:21:40 -04:00
|
|
|
|
|
|
|
return merge(configs)
|
2017-05-15 11:19:32 -04:00
|
|
|
}
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func validateForbidden(configDict map[string]any) error {
|
|
|
|
servicesDict, ok := configDict["services"].(map[string]any)
|
2017-10-03 18:03:20 -04:00
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
forbidden := getProperties(servicesDict, types.ForbiddenProperties)
|
|
|
|
if len(forbidden) > 0 {
|
|
|
|
return &ForbiddenPropertiesError{Properties: forbidden}
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
2017-10-03 18:03:20 -04:00
|
|
|
return nil
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func loadSections(config map[string]any, configDetails types.ConfigDetails) (*types.Config, error) {
|
2017-10-04 16:51:48 -04:00
|
|
|
var err error
|
2018-02-21 12:31:52 -05:00
|
|
|
cfg := types.Config{
|
|
|
|
Version: schema.Version(config),
|
|
|
|
}
|
2017-10-04 16:51:48 -04:00
|
|
|
|
2022-09-29 11:21:51 -04:00
|
|
|
loaders := []struct {
|
2017-10-04 16:51:48 -04:00
|
|
|
key string
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc func(config map[string]any) error
|
2017-10-04 16:51:48 -04:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
key: "services",
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc: func(config map[string]any) error {
|
2017-10-04 16:51:48 -04:00
|
|
|
cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)
|
|
|
|
return err
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "networks",
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc: func(config map[string]any) error {
|
2017-11-22 14:21:32 -05:00
|
|
|
cfg.Networks, err = LoadNetworks(config, configDetails.Version)
|
2017-10-04 16:51:48 -04:00
|
|
|
return err
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "volumes",
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc: func(config map[string]any) error {
|
2017-11-06 17:03:43 -05:00
|
|
|
cfg.Volumes, err = LoadVolumes(config, configDetails.Version)
|
2017-10-04 16:51:48 -04:00
|
|
|
return err
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "secrets",
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc: func(config map[string]any) error {
|
2017-11-06 17:30:10 -05:00
|
|
|
cfg.Secrets, err = LoadSecrets(config, configDetails)
|
2017-10-04 16:51:48 -04:00
|
|
|
return err
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "configs",
|
2023-11-20 12:04:36 -05:00
|
|
|
fnc: func(config map[string]any) error {
|
2017-11-06 17:30:10 -05:00
|
|
|
cfg.Configs, err = LoadConfigObjs(config, configDetails)
|
2017-10-04 16:51:48 -04:00
|
|
|
return err
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, loader := range loaders {
|
|
|
|
if err := loader.fnc(getSection(config, loader.key)); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
cfg.Extras = getExtras(config)
|
2017-10-04 16:51:48 -04:00
|
|
|
return &cfg, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func getSection(config map[string]any, key string) map[string]any {
|
2017-10-04 16:51:48 -04:00
|
|
|
section, ok := config[key]
|
|
|
|
if !ok {
|
2023-11-20 12:04:36 -05:00
|
|
|
return make(map[string]any)
|
2017-10-04 16:51:48 -04:00
|
|
|
}
|
2023-11-20 12:04:36 -05:00
|
|
|
return section.(map[string]any)
|
2017-10-04 16:51:48 -04:00
|
|
|
}
|
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
// GetUnsupportedProperties returns the list of any unsupported properties that are
|
|
|
|
// used in the Compose files.
|
2023-11-20 12:04:36 -05:00
|
|
|
func GetUnsupportedProperties(configDicts ...map[string]any) []string {
|
2016-12-20 16:26:49 -05:00
|
|
|
unsupported := map[string]bool{}
|
|
|
|
|
2017-09-29 08:21:40 -04:00
|
|
|
for _, configDict := range configDicts {
|
|
|
|
for _, service := range getServices(configDict) {
|
2023-11-20 12:04:36 -05:00
|
|
|
serviceDict := service.(map[string]any)
|
2017-09-29 08:21:40 -04:00
|
|
|
for _, property := range types.UnsupportedProperties {
|
|
|
|
if _, isSet := serviceDict[property]; isSet {
|
|
|
|
unsupported[property] = true
|
|
|
|
}
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return sortedKeys(unsupported)
|
|
|
|
}
|
|
|
|
|
|
|
|
func sortedKeys(set map[string]bool) []string {
|
2022-09-03 14:07:29 -04:00
|
|
|
keys := make([]string, 0, len(set))
|
2016-12-20 16:26:49 -05:00
|
|
|
for key := range set {
|
|
|
|
keys = append(keys, key)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDeprecatedProperties returns the list of any deprecated properties that
|
|
|
|
// are used in the compose files.
|
2023-11-20 12:04:36 -05:00
|
|
|
func GetDeprecatedProperties(configDicts ...map[string]any) map[string]string {
|
2017-09-29 08:21:40 -04:00
|
|
|
deprecated := map[string]string{}
|
|
|
|
|
|
|
|
for _, configDict := range configDicts {
|
|
|
|
deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
|
|
|
|
for key, value := range deprecatedProperties {
|
|
|
|
deprecated[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return deprecated
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func getProperties(services map[string]any, propertyMap map[string]string) map[string]string {
|
2016-12-20 16:26:49 -05:00
|
|
|
output := map[string]string{}
|
|
|
|
|
|
|
|
for _, service := range services {
|
2023-11-20 12:04:36 -05:00
|
|
|
if serviceDict, ok := service.(map[string]any); ok {
|
2016-12-20 16:26:49 -05:00
|
|
|
for property, description := range propertyMap {
|
|
|
|
if _, isSet := serviceDict[property]; isSet {
|
|
|
|
output[property] = description
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
|
|
|
// ForbiddenPropertiesError is returned when there are properties in the Compose
|
|
|
|
// file that are forbidden.
|
|
|
|
type ForbiddenPropertiesError struct {
|
|
|
|
Properties map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ForbiddenPropertiesError) Error() string {
|
|
|
|
return "Configuration contains forbidden properties"
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func getServices(configDict map[string]any) map[string]any {
|
2016-12-20 16:26:49 -05:00
|
|
|
if services, ok := configDict["services"]; ok {
|
2023-11-20 12:04:36 -05:00
|
|
|
if servicesDict, ok := services.(map[string]any); ok {
|
2016-12-20 16:26:49 -05:00
|
|
|
return servicesDict
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
return map[string]any{}
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2018-10-02 07:00:11 -04:00
|
|
|
// Transform converts the source into the target struct with compose types transformer
|
2018-07-31 03:37:09 -04:00
|
|
|
// and the specified transformers if any.
|
2023-11-20 12:04:36 -05:00
|
|
|
func Transform(source any, target any, additionalTransformers ...Transformer) error {
|
2016-12-20 16:26:49 -05:00
|
|
|
data := mapstructure.Metadata{}
|
|
|
|
config := &mapstructure.DecoderConfig{
|
|
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
2018-07-31 03:37:09 -04:00
|
|
|
createTransformHook(additionalTransformers...),
|
2016-12-20 16:26:49 -05:00
|
|
|
mapstructure.StringToTimeDurationHookFunc()),
|
|
|
|
Result: target,
|
|
|
|
Metadata: &data,
|
|
|
|
}
|
|
|
|
decoder, err := mapstructure.NewDecoder(config)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-04-03 17:42:16 -04:00
|
|
|
return decoder.Decode(source)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2019-10-28 19:10:55 -04:00
|
|
|
// TransformerFunc defines a function to perform the actual transformation
|
2023-11-20 12:04:36 -05:00
|
|
|
type TransformerFunc func(any) (any, error)
|
2019-10-28 19:10:55 -04:00
|
|
|
|
2018-07-31 03:37:09 -04:00
|
|
|
// Transformer defines a map to type transformer
|
|
|
|
type Transformer struct {
|
|
|
|
TypeOf reflect.Type
|
2019-10-28 19:10:55 -04:00
|
|
|
Func TransformerFunc
|
2018-07-31 03:37:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {
|
2023-11-20 12:04:36 -05:00
|
|
|
transforms := map[reflect.Type]func(any) (any, error){
|
2017-06-01 15:22:09 -04:00
|
|
|
reflect.TypeOf(types.External{}): transformExternal,
|
|
|
|
reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest,
|
|
|
|
reflect.TypeOf(types.ShellCommand{}): transformShellCommand,
|
|
|
|
reflect.TypeOf(types.StringList{}): transformStringList,
|
|
|
|
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
|
|
|
reflect.TypeOf(types.UlimitsConfig{}): transformUlimits,
|
|
|
|
reflect.TypeOf(types.UnitBytes(0)): transformSize,
|
|
|
|
reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort,
|
|
|
|
reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap,
|
|
|
|
reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap,
|
|
|
|
reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList,
|
|
|
|
reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,
|
2019-02-12 10:07:07 -05:00
|
|
|
reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false),
|
2017-06-01 15:22:09 -04:00
|
|
|
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
|
|
|
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
|
|
|
|
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
2024-02-07 09:55:01 -05:00
|
|
|
reflect.TypeOf(types.HostsList{}): transformHostsList,
|
2017-06-01 15:22:09 -04:00
|
|
|
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
2017-09-20 05:42:08 -04:00
|
|
|
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
|
2018-08-29 17:29:39 -04:00
|
|
|
reflect.TypeOf(types.Duration(0)): transformStringToDuration,
|
2017-06-01 15:22:09 -04:00
|
|
|
}
|
|
|
|
|
2018-07-31 03:37:09 -04:00
|
|
|
for _, transformer := range additionalTransformers {
|
|
|
|
transforms[transformer.TypeOf] = transformer.Func
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
return func(_ reflect.Type, target reflect.Type, data any) (any, error) {
|
2017-06-01 15:22:09 -04:00
|
|
|
transform, ok := transforms[target]
|
|
|
|
if !ok {
|
|
|
|
return data, nil
|
|
|
|
}
|
|
|
|
return transform(data)
|
|
|
|
}
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// keys needs to be converted to strings for jsonschema
|
2023-11-20 12:04:36 -05:00
|
|
|
func convertToStringKeysRecursive(value any, keyPrefix string) (any, error) {
|
|
|
|
if mapping, ok := value.(map[any]any); ok {
|
|
|
|
dict := make(map[string]any)
|
2016-12-20 16:26:49 -05:00
|
|
|
for key, entry := range mapping {
|
|
|
|
str, ok := key.(string)
|
|
|
|
if !ok {
|
2017-01-18 15:27:02 -05:00
|
|
|
return nil, formatInvalidKeyError(keyPrefix, key)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2023-11-20 12:04:36 -05:00
|
|
|
if list, ok := value.([]any); ok {
|
|
|
|
var convertedList []any
|
2016-12-20 16:26:49 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func formatInvalidKeyError(keyPrefix string, key any) error {
|
2017-01-18 15:27:02 -05:00
|
|
|
var location string
|
|
|
|
if keyPrefix == "" {
|
|
|
|
location = "at top level"
|
|
|
|
} else {
|
2022-11-16 16:21:16 -05:00
|
|
|
location = "in " + keyPrefix
|
2017-01-18 15:27:02 -05:00
|
|
|
}
|
2022-11-16 16:21:16 -05:00
|
|
|
return errors.Errorf("non-string key %s: %#v", location, key)
|
2017-01-18 15:27:02 -05:00
|
|
|
}
|
|
|
|
|
2017-03-01 03:52:00 -05:00
|
|
|
// LoadServices produces a ServiceConfig map from a compose file Dict
|
|
|
|
// the servicesDict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadServices(servicesDict map[string]any, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
|
2022-09-03 14:07:29 -04:00
|
|
|
services := make([]types.ServiceConfig, 0, len(servicesDict))
|
2016-12-20 16:26:49 -05:00
|
|
|
|
|
|
|
for name, serviceDef := range servicesDict {
|
2023-11-20 12:04:36 -05:00
|
|
|
serviceConfig, err := LoadService(name, serviceDef.(map[string]any), workingDir, lookupEnv)
|
2016-12-20 16:26:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
services = append(services, *serviceConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
return services, nil
|
|
|
|
}
|
|
|
|
|
2017-03-01 03:52:00 -05:00
|
|
|
// LoadService produces a single ServiceConfig from a compose file Dict
|
|
|
|
// the serviceDict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadService(name string, serviceDict map[string]any, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
serviceConfig := &types.ServiceConfig{}
|
2018-07-31 03:37:09 -04:00
|
|
|
if err := Transform(serviceDict, serviceConfig); err != nil {
|
2016-12-20 16:26:49 -05:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
serviceConfig.Name = name
|
|
|
|
|
2017-02-07 04:44:47 -05:00
|
|
|
if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
|
2016-12-20 16:26:49 -05:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-01-21 00:22:19 -05:00
|
|
|
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
|
|
|
|
serviceConfig.Extras = getExtras(serviceDict)
|
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
return serviceConfig, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func loadExtras(name string, source map[string]any) map[string]any {
|
|
|
|
if dict, ok := source[name].(map[string]any); ok {
|
2018-06-25 04:51:56 -04:00
|
|
|
return getExtras(dict)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func getExtras(dict map[string]any) map[string]any {
|
|
|
|
extras := map[string]any{}
|
2018-06-25 04:51:56 -04:00
|
|
|
for key, value := range dict {
|
|
|
|
if strings.HasPrefix(key, "x-") {
|
|
|
|
extras[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(extras) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return extras
|
|
|
|
}
|
|
|
|
|
2017-03-14 12:39:26 -04:00
|
|
|
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
|
2017-02-07 04:44:47 -05:00
|
|
|
for k, v := range vars {
|
|
|
|
interpolatedV, ok := lookupEnv(k)
|
2017-03-14 12:39:26 -04:00
|
|
|
if (v == nil || *v == "") && ok {
|
2017-02-07 04:44:47 -05:00
|
|
|
// lookupEnv is prioritized over vars
|
2017-03-14 12:39:26 -04:00
|
|
|
environment[k] = &interpolatedV
|
2017-02-07 04:44:47 -05:00
|
|
|
} else {
|
2017-03-14 12:39:26 -04:00
|
|
|
environment[k] = v
|
2017-02-07 04:44:47 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
|
2017-03-14 12:39:26 -04:00
|
|
|
environment := make(map[string]*string)
|
2016-12-20 16:26:49 -05:00
|
|
|
|
2017-01-18 15:27:02 -05:00
|
|
|
if len(serviceConfig.EnvFile) > 0 {
|
2016-12-20 16:26:49 -05:00
|
|
|
var envVars []string
|
|
|
|
|
2017-01-18 15:27:02 -05:00
|
|
|
for _, file := range serviceConfig.EnvFile {
|
2017-01-10 17:40:53 -05:00
|
|
|
filePath := absPath(workingDir, file)
|
2017-06-05 18:23:21 -04:00
|
|
|
fileVars, err := opts.ParseEnvFile(filePath)
|
2016-12-20 16:26:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
envVars = append(envVars, fileVars...)
|
|
|
|
}
|
2017-03-14 12:39:26 -04:00
|
|
|
updateEnvironment(environment,
|
2017-06-05 18:23:21 -04:00
|
|
|
opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2017-03-14 12:39:26 -04:00
|
|
|
updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
|
|
|
|
serviceConfig.Environment = environment
|
2016-12-20 16:26:49 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-21 00:22:19 -05:00
|
|
|
func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
|
2017-01-24 12:09:53 -05:00
|
|
|
for i, volume := range volumes {
|
|
|
|
if volume.Type != "bind" {
|
2016-12-20 16:26:49 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-01-21 00:22:19 -05:00
|
|
|
if volume.Source == "" {
|
|
|
|
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
|
|
|
|
}
|
|
|
|
|
2017-05-27 15:29:30 -04:00
|
|
|
filePath := expandUser(volume.Source, lookupEnv)
|
2019-07-09 16:13:19 -04:00
|
|
|
// 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(filePath) && !isAbs(filePath) {
|
2017-05-27 15:29:30 -04:00
|
|
|
filePath = absPath(workingDir, filePath)
|
|
|
|
}
|
|
|
|
volume.Source = filePath
|
2017-01-24 12:09:53 -05:00
|
|
|
volumes[i] = volume
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
2018-01-21 00:22:19 -05:00
|
|
|
return nil
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: make this more robust
|
2023-11-20 11:38:50 -05:00
|
|
|
func expandUser(srcPath string, lookupEnv template.Mapping) string {
|
|
|
|
if strings.HasPrefix(srcPath, "~") {
|
2017-02-07 04:44:47 -05:00
|
|
|
home, ok := lookupEnv("HOME")
|
|
|
|
if !ok {
|
|
|
|
logrus.Warn("cannot expand '~', because the environment lacks HOME")
|
2023-11-20 11:38:50 -05:00
|
|
|
return srcPath
|
2017-02-07 04:44:47 -05:00
|
|
|
}
|
2023-11-20 11:38:50 -05:00
|
|
|
return strings.Replace(srcPath, "~", home, 1)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
2023-11-20 11:38:50 -05:00
|
|
|
return srcPath
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func transformUlimits(data any) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case int:
|
|
|
|
return types.UlimitsConfig{Single: value}, nil
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
2016-12-20 16:26:49 -05:00
|
|
|
ulimit := types.UlimitsConfig{}
|
|
|
|
ulimit.Soft = value["soft"].(int)
|
|
|
|
ulimit.Hard = value["hard"].(int)
|
|
|
|
return ulimit, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for ulimits", value)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-01 03:52:00 -05:00
|
|
|
// LoadNetworks produces a NetworkConfig map from a compose file Dict
|
|
|
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadNetworks(source map[string]any, version string) (map[string]types.NetworkConfig, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
networks := make(map[string]types.NetworkConfig)
|
2018-07-31 03:37:09 -04:00
|
|
|
err := Transform(source, &networks)
|
2016-12-20 16:26:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return networks, err
|
|
|
|
}
|
|
|
|
for name, network := range networks {
|
2017-11-22 14:21:32 -05:00
|
|
|
if !network.External.External {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case network.External.Name != "":
|
|
|
|
if network.Name != "" {
|
|
|
|
return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
|
|
|
|
}
|
|
|
|
if versions.GreaterThanOrEqualTo(version, "3.5") {
|
|
|
|
logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
|
|
|
|
}
|
|
|
|
network.Name = network.External.Name
|
|
|
|
network.External.Name = ""
|
|
|
|
case network.Name == "":
|
|
|
|
network.Name = name
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
network.Extras = loadExtras(name, source)
|
2017-11-22 14:21:32 -05:00
|
|
|
networks[name] = network
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
return networks, nil
|
|
|
|
}
|
|
|
|
|
2017-03-24 12:24:58 -04:00
|
|
|
func externalVolumeError(volume, key string) error {
|
|
|
|
return errors.Errorf(
|
|
|
|
"conflicting parameters \"external\" and %q specified for volume %q",
|
|
|
|
key, volume)
|
|
|
|
}
|
|
|
|
|
2017-03-01 03:52:00 -05:00
|
|
|
// LoadVolumes produces a VolumeConfig map from a compose file Dict
|
|
|
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadVolumes(source map[string]any, version string) (map[string]types.VolumeConfig, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
volumes := make(map[string]types.VolumeConfig)
|
2018-07-31 03:37:09 -04:00
|
|
|
if err := Transform(source, &volumes); err != nil {
|
2016-12-20 16:26:49 -05:00
|
|
|
return volumes, err
|
|
|
|
}
|
2017-11-06 17:03:43 -05:00
|
|
|
|
2016-12-20 16:26:49 -05:00
|
|
|
for name, volume := range volumes {
|
2017-11-06 17:03:43 -05:00
|
|
|
if !volume.External.External {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case volume.Driver != "":
|
|
|
|
return nil, externalVolumeError(name, "driver")
|
|
|
|
case len(volume.DriverOpts) > 0:
|
|
|
|
return nil, externalVolumeError(name, "driver_opts")
|
|
|
|
case len(volume.Labels) > 0:
|
|
|
|
return nil, externalVolumeError(name, "labels")
|
|
|
|
case volume.External.Name != "":
|
|
|
|
if volume.Name != "" {
|
|
|
|
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
2017-02-20 01:12:36 -05:00
|
|
|
}
|
2017-11-06 17:03:43 -05:00
|
|
|
if versions.GreaterThanOrEqualTo(version, "3.4") {
|
2017-06-29 19:25:50 -04:00
|
|
|
logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
|
2017-02-20 01:12:36 -05:00
|
|
|
}
|
2017-11-06 17:03:43 -05:00
|
|
|
volume.Name = volume.External.Name
|
|
|
|
volume.External.Name = ""
|
|
|
|
case volume.Name == "":
|
|
|
|
volume.Name = name
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
volume.Extras = loadExtras(name, source)
|
2017-11-06 17:03:43 -05:00
|
|
|
volumes[name] = volume
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
return volumes, nil
|
|
|
|
}
|
|
|
|
|
2017-03-01 03:52:00 -05:00
|
|
|
// LoadSecrets produces a SecretConfig map from a compose file Dict
|
|
|
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadSecrets(source map[string]any, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
|
2017-01-10 17:40:53 -05:00
|
|
|
secrets := make(map[string]types.SecretConfig)
|
2018-07-31 03:37:09 -04:00
|
|
|
if err := Transform(source, &secrets); err != nil {
|
2017-01-10 17:40:53 -05:00
|
|
|
return secrets, err
|
|
|
|
}
|
|
|
|
for name, secret := range secrets {
|
2017-11-22 06:18:05 -05:00
|
|
|
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
|
2017-11-06 17:30:10 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
secretConfig := types.SecretConfig(obj)
|
|
|
|
secretConfig.Extras = loadExtras(name, source)
|
|
|
|
secrets[name] = secretConfig
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
|
|
|
return secrets, nil
|
|
|
|
}
|
|
|
|
|
2017-05-15 11:19:32 -04:00
|
|
|
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
|
|
|
|
// the source Dict is not validated if directly used. Use Load() to enable validation
|
2023-11-20 12:04:36 -05:00
|
|
|
func LoadConfigObjs(source map[string]any, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
|
2017-05-15 11:19:32 -04:00
|
|
|
configs := make(map[string]types.ConfigObjConfig)
|
2018-07-31 03:37:09 -04:00
|
|
|
if err := Transform(source, &configs); err != nil {
|
2017-05-15 11:19:32 -04:00
|
|
|
return configs, err
|
|
|
|
}
|
|
|
|
for name, config := range configs {
|
2017-11-22 06:18:05 -05:00
|
|
|
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
|
2017-11-06 17:30:10 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2017-05-15 11:19:32 -04:00
|
|
|
}
|
2018-06-25 04:51:56 -04:00
|
|
|
configConfig := types.ConfigObjConfig(obj)
|
|
|
|
configConfig.Extras = loadExtras(name, source)
|
|
|
|
configs[name] = configConfig
|
2017-05-15 11:19:32 -04:00
|
|
|
}
|
|
|
|
return configs, nil
|
|
|
|
}
|
|
|
|
|
2017-11-22 06:18:05 -05:00
|
|
|
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
|
|
|
|
// if "external: true"
|
2019-03-28 11:16:54 -04:00
|
|
|
switch {
|
|
|
|
case obj.External.External:
|
2017-11-22 06:18:05 -05:00
|
|
|
// handle deprecated external.name
|
|
|
|
if obj.External.Name != "" {
|
|
|
|
if obj.Name != "" {
|
|
|
|
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
|
|
|
|
}
|
|
|
|
if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
|
|
|
|
logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
|
|
|
|
}
|
|
|
|
obj.Name = obj.External.Name
|
|
|
|
obj.External.Name = ""
|
2023-11-20 07:54:53 -05:00
|
|
|
} else if obj.Name == "" {
|
|
|
|
obj.Name = name
|
2017-11-22 06:18:05 -05:00
|
|
|
}
|
|
|
|
// if not "external: true"
|
2019-03-28 11:16:54 -04:00
|
|
|
case obj.Driver != "":
|
|
|
|
if obj.File != "" {
|
|
|
|
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
|
|
|
|
}
|
|
|
|
default:
|
2017-11-06 17:30:10 -05:00
|
|
|
obj.File = absPath(details.WorkingDir, obj.File)
|
|
|
|
}
|
2017-11-22 06:18:05 -05:00
|
|
|
|
2017-11-06 17:30:10 -05:00
|
|
|
return obj, nil
|
|
|
|
}
|
|
|
|
|
2017-05-27 15:29:30 -04:00
|
|
|
func absPath(workingDir string, filePath string) string {
|
|
|
|
if filepath.IsAbs(filePath) {
|
|
|
|
return filePath
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
2017-05-27 15:29:30 -04:00
|
|
|
return filepath.Join(workingDir, filePath)
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformMapStringString TransformerFunc = func(data any) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
switch value := data.(type) {
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
2017-03-14 12:39:26 -04:00
|
|
|
return toMapStringString(value, false), nil
|
2016-12-20 16:26:49 -05:00
|
|
|
case map[string]string:
|
|
|
|
return value, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for map[string]string", value)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformExternal TransformerFunc = func(data any) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case bool:
|
2023-11-20 12:04:36 -05:00
|
|
|
return map[string]any{"external": value}, nil
|
|
|
|
case map[string]any:
|
|
|
|
return map[string]any{"external": true, "name": value["name"]}, nil
|
2016-12-20 16:26:49 -05:00
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for external", value)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformServicePort TransformerFunc = func(data any) (any, error) {
|
2017-01-31 15:45:45 -05:00
|
|
|
switch entries := data.(type) {
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
2017-01-31 15:45:45 -05:00
|
|
|
// 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.
|
2023-11-20 12:04:36 -05:00
|
|
|
ports := []any{}
|
2017-01-31 15:45:45 -05:00
|
|
|
for _, entry := range entries {
|
|
|
|
switch value := entry.(type) {
|
|
|
|
case int:
|
golangci-lint: enable perfsprint linter
cli/compose/types/types.go:568:17: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return []byte(fmt.Sprintf("%v", e.External)), nil
^
cli/command/formatter/buildcache.go:174:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
return fmt.Sprintf("%d", c.v.UsageCount)
^
cli/command/formatter/buildcache.go:178:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%t", c.v.InUse)
^
cli/command/formatter/buildcache.go:182:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%t", c.v.Shared)
^
cli/command/formatter/image.go:259:9: fmt.Sprintf can be replaced with faster strconv.FormatInt (perfsprint)
return fmt.Sprintf("%d", c.i.Containers)
^
cli/command/formatter/tabwriter/tabwriter_test.go:698:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
b.Run(fmt.Sprintf("%d", x), func(b *testing.B) {
^
cli/command/formatter/tabwriter/tabwriter_test.go:720:9: fmt.Sprintf can be replaced with faster strconv.Itoa (perfsprint)
b.Run(fmt.Sprintf("%d", h), func(b *testing.B) {
^
cli/command/image/prune.go:62:31: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
pruneFilters.Add("dangling", fmt.Sprintf("%v", !options.all))
^
cli/command/network/formatter.go:92:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%v", c.n.EnableIPv6)
^
cli/command/network/formatter.go:96:9: fmt.Sprintf can be replaced with faster strconv.FormatBool (perfsprint)
return fmt.Sprintf("%v", c.n.Internal)
^
cli/command/service/formatter.go:745:9: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
pub = fmt.Sprintf("%d", pr.pStart)
^
cli/command/service/formatter.go:750:9: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
tgt = fmt.Sprintf("%d", pr.tStart)
^
cli/command/service/opts.go:49:10: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
return fmt.Sprintf("%v", *i.value)
^
cli/compose/loader/loader.go:720:36: fmt.Sprint can be replaced with faster strconv.Itoa (perfsprint)
v, err := toServicePortConfigs(fmt.Sprint(value))
^
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-11-20 10:18:19 -05:00
|
|
|
v, err := toServicePortConfigs(strconv.Itoa(value))
|
2017-01-31 15:45:45 -05:00
|
|
|
if err != nil {
|
|
|
|
return data, err
|
|
|
|
}
|
|
|
|
ports = append(ports, v...)
|
|
|
|
case string:
|
|
|
|
v, err := toServicePortConfigs(value)
|
|
|
|
if err != nil {
|
|
|
|
return data, err
|
|
|
|
}
|
|
|
|
ports = append(ports, v...)
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
2017-01-31 15:45:45 -05:00
|
|
|
ports = append(ports, value)
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for port", value)
|
2017-01-31 15:45:45 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return ports, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for port", entries)
|
2017-01-31 15:45:45 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformStringSourceMap TransformerFunc = func(data any) (any, error) {
|
2017-01-10 17:40:53 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case string:
|
2023-11-20 12:04:36 -05:00
|
|
|
return map[string]any{"source": value}, nil
|
|
|
|
case map[string]any:
|
2017-01-10 17:40:53 -05:00
|
|
|
return data, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for secret", value)
|
2017-01-10 17:40:53 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformBuildConfig TransformerFunc = func(data any) (any, error) {
|
2017-09-20 05:42:08 -04:00
|
|
|
switch value := data.(type) {
|
|
|
|
case string:
|
2023-11-20 12:04:36 -05:00
|
|
|
return map[string]any{"context": value}, nil
|
|
|
|
case map[string]any:
|
2017-09-20 05:42:08 -04:00
|
|
|
return data, nil
|
|
|
|
default:
|
|
|
|
return data, errors.Errorf("invalid type %T for service build", value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformServiceVolumeConfig TransformerFunc = func(data any) (any, error) {
|
2017-01-24 12:09:53 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case string:
|
2017-05-09 19:21:17 -04:00
|
|
|
return ParseVolume(value)
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
2017-01-24 12:09:53 -05:00
|
|
|
return data, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for service volume", value)
|
2017-01-24 12:09:53 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformServiceNetworkMap TransformerFunc = func(value any) (any, error) {
|
|
|
|
if list, ok := value.([]any); ok {
|
|
|
|
mapValue := map[any]any{}
|
2016-12-20 16:26:49 -05:00
|
|
|
for _, name := range list {
|
|
|
|
mapValue[name] = nil
|
|
|
|
}
|
|
|
|
return mapValue, nil
|
|
|
|
}
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformStringOrNumberList TransformerFunc = func(value any) (any, error) {
|
|
|
|
list := value.([]any)
|
2016-12-20 16:26:49 -05:00
|
|
|
result := make([]string, len(list))
|
|
|
|
for i, item := range list {
|
|
|
|
result[i] = fmt.Sprint(item)
|
|
|
|
}
|
2017-01-18 15:27:02 -05:00
|
|
|
return result, nil
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformStringList TransformerFunc = func(data any) (any, error) {
|
2017-01-18 15:27:02 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case string:
|
|
|
|
return []string{value}, nil
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
2017-01-18 15:27:02 -05:00
|
|
|
return value, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return data, errors.Errorf("invalid type %T for string list", value)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-07 09:55:01 -05:00
|
|
|
var transformHostsList TransformerFunc = func(data any) (any, error) {
|
|
|
|
hl := transformListOrMapping(data, ":", false, []string{"=", ":"})
|
2017-06-01 15:22:09 -04:00
|
|
|
|
2024-02-07 09:55:01 -05:00
|
|
|
// Remove brackets from IP addresses if present (for example "[::1]" -> "::1").
|
|
|
|
result := make([]string, 0, len(hl))
|
|
|
|
for _, hip := range hl {
|
|
|
|
host, ip, _ := strings.Cut(hip, ":")
|
|
|
|
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
|
|
|
|
ip = ip[1 : len(ip)-1]
|
|
|
|
}
|
|
|
|
result = append(result, fmt.Sprintf("%s:%s", host, ip))
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
}
|
2024-02-07 09:55:01 -05:00
|
|
|
return result, nil
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
}
|
|
|
|
|
2024-02-07 09:55:01 -05:00
|
|
|
// transformListOrMapping transforms pairs of strings that may be represented as
|
|
|
|
// a map, or a list of '=' or ':' separated strings, into a list of ':' separated
|
|
|
|
// strings.
|
|
|
|
func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string {
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
switch value := listOrMapping.(type) {
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
return toStringList(value, sep, allowNil)
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
2024-02-07 09:55:01 -05:00
|
|
|
result := make([]string, 0, len(value))
|
|
|
|
for _, entry := range value {
|
|
|
|
for i, allowSep := range allowSeps {
|
|
|
|
entry := fmt.Sprint(entry)
|
|
|
|
k, v, ok := strings.Cut(entry, allowSep)
|
|
|
|
if ok {
|
|
|
|
// Entry uses this allowed separator. Add it to the result, using
|
|
|
|
// sep as a separator.
|
|
|
|
result = append(result, fmt.Sprintf("%s%s%s", k, sep, v))
|
|
|
|
break
|
|
|
|
} else if i == len(allowSeps)-1 {
|
|
|
|
// No more separators to try, keep the entry if allowNil.
|
|
|
|
if allowNil {
|
|
|
|
result = append(result, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
}
|
|
|
|
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
|
|
|
|
}
|
|
|
|
|
2024-02-07 09:55:01 -05:00
|
|
|
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
|
|
|
|
return func(data any) (any, error) {
|
|
|
|
return transformMappingOrList(data, sep, allowNil), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any {
|
2022-12-27 11:44:59 -05:00
|
|
|
switch values := mappingOrList.(type) {
|
2023-11-20 12:04:36 -05:00
|
|
|
case map[string]any:
|
2022-12-27 11:44:59 -05:00
|
|
|
return toMapStringString(values, allowNil)
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
|
|
|
result := make(map[string]any)
|
2022-12-27 11:44:59 -05:00
|
|
|
for _, v := range values {
|
|
|
|
key, val, hasValue := strings.Cut(v.(string), sep)
|
2017-03-14 12:39:26 -04:00
|
|
|
switch {
|
2022-12-27 11:44:59 -05:00
|
|
|
case !hasValue && allowNil:
|
2017-03-14 12:39:26 -04:00
|
|
|
result[key] = nil
|
2022-12-27 11:44:59 -05:00
|
|
|
case !hasValue && !allowNil:
|
2017-03-14 12:39:26 -04:00
|
|
|
result[key] = ""
|
|
|
|
default:
|
2022-12-27 11:44:59 -05:00
|
|
|
result[key] = val
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
2017-03-09 13:23:45 -05:00
|
|
|
panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformShellCommand TransformerFunc = func(value any) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
if str, ok := value.(string); ok {
|
2020-07-20 04:20:42 -04:00
|
|
|
return shlex.Split(str)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformHealthCheckTest TransformerFunc = func(data any) (any, error) {
|
2017-01-18 15:27:02 -05:00
|
|
|
switch value := data.(type) {
|
|
|
|
case string:
|
|
|
|
return append([]string{"CMD-SHELL"}, value), nil
|
2023-11-20 12:04:36 -05:00
|
|
|
case []any:
|
2017-01-18 15:27:02 -05:00
|
|
|
return value, nil
|
|
|
|
default:
|
2017-03-09 13:23:45 -05:00
|
|
|
return value, errors.Errorf("invalid type %T for healthcheck.test", value)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformSize TransformerFunc = func(value any) (any, error) {
|
2016-12-20 16:26:49 -05:00
|
|
|
switch value := value.(type) {
|
|
|
|
case int:
|
|
|
|
return int64(value), nil
|
|
|
|
case string:
|
|
|
|
return units.RAMInBytes(value)
|
|
|
|
}
|
2017-03-09 13:23:45 -05:00
|
|
|
panic(errors.Errorf("invalid type for size %T", value))
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
var transformStringToDuration TransformerFunc = func(value any) (any, error) {
|
2018-08-29 17:29:39 -04:00
|
|
|
switch value := value.(type) {
|
|
|
|
case string:
|
|
|
|
d, err := time.ParseDuration(value)
|
|
|
|
if err != nil {
|
|
|
|
return value, err
|
|
|
|
}
|
|
|
|
return types.Duration(d), nil
|
|
|
|
default:
|
|
|
|
return value, errors.Errorf("invalid type %T for duration", value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func toServicePortConfigs(value string) ([]any, error) {
|
|
|
|
var portConfigs []any
|
2017-01-31 15:45:45 -05:00
|
|
|
|
|
|
|
ports, portBindings, err := nat.ParsePortSpecs([]string{value})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// We need to sort the key of the ports to make sure it is consistent
|
|
|
|
keys := []string{}
|
|
|
|
for port := range ports {
|
|
|
|
keys = append(keys, string(port))
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
|
|
|
for _, key := range keys {
|
|
|
|
// Reuse ConvertPortToPortConfig so that it is consistent
|
|
|
|
portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for _, p := range portConfig {
|
|
|
|
portConfigs = append(portConfigs, types.ServicePortConfig{
|
|
|
|
Protocol: string(p.Protocol),
|
|
|
|
Target: p.TargetPort,
|
|
|
|
Published: p.PublishedPort,
|
|
|
|
Mode: string(p.PublishMode),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return portConfigs, nil
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func toMapStringString(value map[string]any, allowNil bool) map[string]any {
|
|
|
|
output := make(map[string]any)
|
2016-12-20 16:26:49 -05:00
|
|
|
for key, value := range value {
|
2017-03-14 12:39:26 -04:00
|
|
|
output[key] = toString(value, allowNil)
|
2016-12-20 16:26:49 -05:00
|
|
|
}
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func toString(value any, allowNil bool) any {
|
2017-03-14 12:39:26 -04:00
|
|
|
switch {
|
|
|
|
case value != nil:
|
|
|
|
return fmt.Sprint(value)
|
|
|
|
case allowNil:
|
|
|
|
return nil
|
|
|
|
default:
|
2016-12-20 16:26:49 -05:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
|
2023-11-20 12:04:36 -05:00
|
|
|
func toStringList(value map[string]any, separator string, allowNil bool) []string {
|
Preserve sort-order of extra hosts, and allow duplicate entries
Extra hosts (`extra_hosts` in compose-file, or `--hosts` in services) adds
custom host/ip mappings to the container's `/etc/hosts`.
The current implementation used a `map[string]string{}` as intermediate
storage, and sorted the results alphabetically when converting to a service-spec.
As a result, duplicate hosts were removed, and order of host/ip mappings was not
preserved (in case the compose-file used a list instead of a map).
According to the **host.conf(5)** man page (http://man7.org/linux/man-pages/man5/host.conf.5.html)
multi Valid values are on and off. If set to on, the resolver
library will return all valid addresses for a host that
appears in the /etc/hosts file, instead of only the first.
This is off by default, as it may cause a substantial
performance loss at sites with large hosts files.
Multiple entries for a host are allowed, and even required for some situations,
for example, to add mappings for IPv4 and IPv6 addreses for a host, as illustrated
by the example hosts file in the **hosts(5)** man page (http://man7.org/linux/man-pages/man5/hosts.5.html):
# The following lines are desirable for IPv4 capable hosts
127.0.0.1 localhost
# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1 thishost.mydomain.org thishost
192.168.1.10 foo.mydomain.org foo
192.168.1.13 bar.mydomain.org bar
146.82.138.7 master.debian.org master
209.237.226.90 www.opensource.org
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
This patch changes the intermediate storage format to use a `[]string`, and only
sorts entries if the input format in the compose file is a mapping. If the input
format is a list, the original sort-order is preserved.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-10-29 20:33:23 -04:00
|
|
|
output := []string{}
|
|
|
|
for key, value := range value {
|
|
|
|
if value == nil && !allowNil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
|
|
|
|
}
|
|
|
|
sort.Strings(output)
|
|
|
|
return output
|
|
|
|
}
|