Merge pull request #2760 from tonistiigi/update-compose-v2.4.1

vendor: update compose to v2.4.1
This commit is contained in:
CrazyMax 2024-10-29 10:28:24 +01:00 committed by GitHub
commit 704b2cc52d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 907 additions and 379 deletions

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.26.6 github.com/aws/aws-sdk-go-v2/config v1.26.6
github.com/compose-spec/compose-go/v2 v2.2.0 github.com/compose-spec/compose-go/v2 v2.4.1
github.com/containerd/console v1.0.4 github.com/containerd/console v1.0.4
github.com/containerd/containerd v1.7.22 github.com/containerd/containerd v1.7.22
github.com/containerd/continuity v0.4.4 github.com/containerd/continuity v0.4.4

4
go.sum
View File

@ -83,8 +83,8 @@ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnTh
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= 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/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA= github.com/compose-spec/compose-go/v2 v2.4.1 h1:tEg6Qn/9LZnKg42fZlFmxN4lxSqnCvsiG5TXnxzvI4c=
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= github.com/compose-spec/compose-go/v2 v2.4.1/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=

View File

@ -403,22 +403,24 @@ func (o *ProjectOptions) GetWorkingDir() (string, error) {
return os.Getwd() return os.Getwd()
} }
func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) { // ReadConfigFiles reads ConfigFiles and populates the content field
configPaths, err := o.getConfigPaths() func (o *ProjectOptions) ReadConfigFiles(ctx context.Context, workingDir string, options *ProjectOptions) (*types.ConfigDetails, error) {
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, options.loadOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configs := make([][]byte, len(config.ConfigFiles))
var configs []types.ConfigFile for i, c := range config.ConfigFiles {
for _, f := range configPaths { var err error
var b []byte var b []byte
if f == "-" { if c.Filename == "-" {
b, err = io.ReadAll(os.Stdin) b, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
f, err := filepath.Abs(f) f, err := filepath.Abs(c.Filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -427,27 +429,31 @@ func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
return nil, err return nil, err
} }
} }
configs = append(configs, types.ConfigFile{ configs[i] = b
Filename: f,
Content: b,
})
} }
return configs, err for i, c := range configs {
config.ConfigFiles[i].Content = c
}
return config, nil
} }
// LoadProject loads compose file according to options and bind to types.Project go structs // LoadProject loads compose file according to options and bind to types.Project go structs
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) { func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
configDetails, err := o.prepare() config, err := o.prepare(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
project, err := loader.LoadWithContext(ctx, configDetails, o.loadOptions...) project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
ConfigFiles: config.ConfigFiles,
WorkingDir: config.WorkingDir,
Environment: o.Environment,
}, o.loadOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, config := range configDetails.ConfigFiles { for _, config := range config.ConfigFiles {
project.ComposeFiles = append(project.ComposeFiles, config.Filename) project.ComposeFiles = append(project.ComposeFiles, config.Filename)
} }
@ -456,36 +462,31 @@ func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error
// LoadModel loads compose file according to options and returns a raw (yaml tree) model // LoadModel loads compose file according to options and returns a raw (yaml tree) model
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) { func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
configDetails, err := o.prepare() configDetails, err := o.prepare(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return loader.LoadModelWithContext(ctx, configDetails, o.loadOptions...) return loader.LoadModelWithContext(ctx, *configDetails, o.loadOptions...)
} }
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options // prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) { func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
configs, err := o.GetConfigFiles() defaultDir, err := o.GetWorkingDir()
if err != nil { if err != nil {
return types.ConfigDetails{}, err return &types.ConfigDetails{}, err
} }
workingDir, err := o.GetWorkingDir() configDetails, err := o.ReadConfigFiles(ctx, defaultDir, o)
if err != nil { if err != nil {
return types.ConfigDetails{}, err return configDetails, err
}
configDetails := types.ConfigDetails{
ConfigFiles: configs,
WorkingDir: workingDir,
Environment: o.Environment,
} }
o.loadOptions = append(o.loadOptions, o.loadOptions = append(o.loadOptions,
withNamePrecedenceLoad(workingDir, o), withNamePrecedenceLoad(defaultDir, o),
withConvertWindowsPaths(o), withConvertWindowsPaths(o),
withListeners(o)) withListeners(o))
return configDetails, nil return configDetails, nil
} }
@ -502,8 +503,13 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" { } else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
opts.SetProjectName(nameFromEnv, true) opts.SetProjectName(nameFromEnv, true)
} else { } else {
dirname := filepath.Base(absWorkingDir)
symlink, err := filepath.EvalSymlinks(absWorkingDir)
if err == nil && filepath.Base(symlink) != dirname {
logrus.Warnf("project has been loaded without an explicit name from a symlink. Using name %q", dirname)
}
opts.SetProjectName( opts.SetProjectName(
loader.NormalizeProjectName(filepath.Base(absWorkingDir)), loader.NormalizeProjectName(dirname),
false, false,
) )
} }

View File

@ -0,0 +1,38 @@
/*
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 dotenv
import (
"fmt"
"io"
)
var formats = map[string]Parser{}
type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error)
func RegisterFormat(format string, p Parser) {
formats[format] = p
}
func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) {
parser, ok := formats[format]
if !ok {
return nil, fmt.Errorf("unsupported env_file format %q", format)
}
return parser(r, filename, resolve)
}

View File

@ -86,7 +86,7 @@ func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string,
envMap := make(map[string]string) envMap := make(map[string]string)
for _, filename := range filenames { for _, filename := range filenames {
individualEnvMap, individualErr := readFile(filename, lookupFn) individualEnvMap, individualErr := ReadFile(filename, lookupFn)
if individualErr != nil { if individualErr != nil {
return envMap, individualErr return envMap, individualErr
@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
} }
func loadFile(filename string, overload bool) error { func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename, nil) envMap, err := ReadFile(filename, nil)
if err != nil { if err != nil {
return err return err
} }
@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
return nil return nil
} }
func readFile(filename string, lookupFn LookupFn) (map[string]string, error) { func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -119,7 +119,7 @@ loop:
offset = i + 1 offset = i + 1
inherited = rune == '\n' inherited = rune == '\n'
break loop break loop
case '_', '.', '[', ']': case '_', '.', '-', '[', ']':
default: default:
// variable name should match [A-Za-z0-9_.-] // variable name should match [A-Za-z0-9_.-]
if unicode.IsLetter(rune) || unicode.IsNumber(rune) { if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
@ -136,6 +136,10 @@ loop:
return "", "", inherited, errors.New("zero length string") return "", "", inherited, errors.New("zero length string")
} }
if inherited && strings.IndexByte(key, ' ') == -1 {
p.line++
}
// trim whitespace // trim whitespace
key = strings.TrimRightFunc(key, unicode.IsSpace) key = strings.TrimRightFunc(key, unicode.IsSpace)
cutset := strings.TrimLeftFunc(src[offset:], isSpace) cutset := strings.TrimLeftFunc(src[offset:], isSpace)

View File

@ -95,7 +95,7 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu
if isBindOption(option) { if isBindOption(option) {
setBindOption(volume, option) setBindOption(volume, option)
} }
// ignore unknown options // ignore unknown options FIXME why not report an error here?
} }
} }
return nil return nil

View File

@ -30,6 +30,7 @@ import (
"strings" "strings"
"github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/consts"
"github.com/compose-spec/compose-go/v2/errdefs"
interp "github.com/compose-spec/compose-go/v2/interpolation" interp "github.com/compose-spec/compose-go/v2/interpolation"
"github.com/compose-spec/compose-go/v2/override" "github.com/compose-spec/compose-go/v2/override"
"github.com/compose-spec/compose-go/v2/paths" "github.com/compose-spec/compose-go/v2/paths"
@ -139,9 +140,9 @@ func (l localResourceLoader) abs(p string) string {
return filepath.Join(l.WorkingDir, p) return filepath.Join(l.WorkingDir, p)
} }
func (l localResourceLoader) Accept(p string) bool { func (l localResourceLoader) Accept(_ string) bool {
_, err := os.Stat(l.abs(p)) // LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
return err == nil return true
} }
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) { func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
@ -300,6 +301,51 @@ func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, er
return converted.(map[string]interface{}), &processor, nil return converted.(map[string]interface{}), &processor, nil
} }
// LoadConfigFiles ingests config files with ResourceLoader and returns config details with paths to local copies
func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
if len(configFiles) < 1 {
return &types.ConfigDetails{}, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
}
opts := &Options{}
config := &types.ConfigDetails{
ConfigFiles: make([]types.ConfigFile, len(configFiles)),
}
for _, op := range options {
op(opts)
}
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{})
for i, p := range configFiles {
for _, loader := range opts.ResourceLoaders {
_, isLocalResourceLoader := loader.(localResourceLoader)
if !loader.Accept(p) {
continue
}
local, err := loader.Load(ctx, p)
if err != nil {
return nil, err
}
if config.WorkingDir == "" && !isLocalResourceLoader {
config.WorkingDir = filepath.Dir(local)
}
abs, err := filepath.Abs(local)
if err != nil {
abs = local
}
config.ConfigFiles[i] = types.ConfigFile{
Filename: abs,
}
break
}
}
if config.WorkingDir == "" {
config.WorkingDir = workingDir
}
return config, nil
}
// Load reads a ConfigDetails and returns a fully loaded configuration. // Load reads a ConfigDetails and returns a fully loaded configuration.
// Deprecated: use LoadWithContext. // Deprecated: use LoadWithContext.
func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
@ -470,6 +516,8 @@ func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, wor
return err return err
} }
dict = OmitEmpty(dict)
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override // Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
dict, err = override.EnforceUnicity(dict) dict, err = override.EnforceUnicity(dict)
return err return err
@ -675,6 +723,7 @@ func NormalizeProjectName(s string) string {
var userDefinedKeys = []tree.Path{ var userDefinedKeys = []tree.Path{
"services", "services",
"services.*.depends_on",
"volumes", "volumes",
"networks", "networks",
"secrets", "secrets",
@ -687,7 +736,7 @@ func processExtensions(dict map[string]any, p tree.Path, extensions map[string]a
for key, value := range dict { for key, value := range dict {
skip := false skip := false
for _, uk := range userDefinedKeys { for _, uk := range userDefinedKeys {
if uk.Matches(p) { if p.Matches(uk) {
skip = true skip = true
break break
} }
@ -770,14 +819,14 @@ func secretConfigDecoderHook(from, to reflect.Type, data interface{}) (interface
// Check if the input is a map and we're decoding into a SecretConfig // Check if the input is a map and we're decoding into a SecretConfig
if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) { if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
if v, ok := data.(map[string]interface{}); ok { if v, ok := data.(map[string]interface{}); ok {
if ext, ok := v["#extensions"].(map[string]interface{}); ok { if ext, ok := v[consts.Extensions].(map[string]interface{}); ok {
if val, ok := ext[types.SecretConfigXValue].(string); ok { if val, ok := ext[types.SecretConfigXValue].(string); ok {
// Return a map with the Content field populated // Return a map with the Content field populated
v["Content"] = val v["Content"] = val
delete(ext, types.SecretConfigXValue) delete(ext, types.SecretConfigXValue)
if len(ext) == 0 { if len(ext) == 0 {
delete(v, "#extensions") delete(v, consts.Extensions)
} }
} }
} }

View File

@ -18,6 +18,7 @@ package loader
import ( import (
"fmt" "fmt"
"path"
"strconv" "strconv"
"strings" "strings"
@ -102,6 +103,17 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
} }
} }
if v, ok := service["volumes"]; ok {
volumes := v.([]any)
for i, volume := range volumes {
vol := volume.(map[string]any)
target := vol["target"].(string)
vol["target"] = path.Clean(target)
volumes[i] = vol
}
service["volumes"] = volumes
}
if n, ok := service["volumes_from"]; ok { if n, ok := service["volumes_from"]; ok {
volumesFrom := n.([]any) volumesFrom := n.([]any)
for _, v := range volumesFrom { for _, v := range volumesFrom {
@ -123,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
} }
services[name] = service services[name] = service
} }
dict["services"] = services dict["services"] = services
} }
setNameFromKey(dict) setNameFromKey(dict)
return dict, nil return dict, nil

View File

@ -0,0 +1,74 @@
/*
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 "github.com/compose-spec/compose-go/v2/tree"
var omitempty = []tree.Path{
"services.*.dns"}
// OmitEmpty removes empty attributes which are irrelevant when unset
func OmitEmpty(yaml map[string]any) map[string]any {
cleaned := omitEmpty(yaml, tree.NewPath())
return cleaned.(map[string]any)
}
func omitEmpty(data any, p tree.Path) any {
switch v := data.(type) {
case map[string]any:
for k, e := range v {
if isEmpty(e) && mustOmit(p) {
delete(v, k)
continue
}
v[k] = omitEmpty(e, p.Next(k))
}
return v
case []any:
var c []any
for _, e := range v {
if isEmpty(e) && mustOmit(p) {
continue
}
c = append(c, omitEmpty(e, p.Next("[]")))
}
return c
default:
return data
}
}
func mustOmit(p tree.Path) bool {
for _, pattern := range omitempty {
if p.Matches(pattern) {
return true
}
}
return false
}
func isEmpty(e any) bool {
if e == nil {
return true
}
if v, ok := e.(string); ok && v == "" {
return true
}
return false
}

View File

@ -26,13 +26,15 @@ import (
) )
type ResetProcessor struct { type ResetProcessor struct {
target interface{} target interface{}
paths []tree.Path paths []tree.Path
visitedNodes map[*yaml.Node]string
} }
// UnmarshalYAML implement yaml.Unmarshaler // UnmarshalYAML implement yaml.Unmarshaler
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error { func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
resolved, err := p.resolveReset(value, tree.NewPath()) resolved, err := p.resolveReset(value, tree.NewPath())
p.visitedNodes = nil
if err != nil { if err != nil {
return err return err
} }
@ -41,10 +43,28 @@ func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
// resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree // resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree
func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) { func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) {
pathStr := path.String()
// If the path contains "<<", removing the "<<" element and merging the path // If the path contains "<<", removing the "<<" element and merging the path
if strings.Contains(path.String(), ".<<") { if strings.Contains(pathStr, ".<<") {
path = tree.NewPath(strings.Replace(path.String(), ".<<", "", 1)) path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 1))
} }
// Check for cycle
if p.visitedNodes == nil {
p.visitedNodes = make(map[*yaml.Node]string)
}
// Check for cycle by seeing if the node has already been visited at this path
if previousPath, found := p.visitedNodes[node]; found {
// If the current node has been visited, we have a cycle if the previous path is a prefix
if strings.HasPrefix(pathStr, previousPath) {
return nil, fmt.Errorf("cycle detected at path: %s", pathStr)
}
}
// Mark the current node as visited
p.visitedNodes[node] = pathStr
// If the node is an alias, We need to process the alias field in order to consider the !override and !reset tags // If the node is an alias, We need to process the alias field in order to consider the !override and !reset tags
if node.Kind == yaml.AliasNode { if node.Kind == yaml.AliasNode {
return p.resolveReset(node.Alias, path) return p.resolveReset(node.Alias, path)

View File

@ -47,7 +47,7 @@ func init() {
mergeSpecials["services.*.build"] = mergeBuild mergeSpecials["services.*.build"] = mergeBuild
mergeSpecials["services.*.build.args"] = mergeToSequence mergeSpecials["services.*.build.args"] = mergeToSequence
mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence
mergeSpecials["services.*.build.extra_hosts"] = mergeToSequence mergeSpecials["services.*.build.extra_hosts"] = mergeExtraHosts
mergeSpecials["services.*.build.labels"] = mergeToSequence mergeSpecials["services.*.build.labels"] = mergeToSequence
mergeSpecials["services.*.command"] = override mergeSpecials["services.*.command"] = override
mergeSpecials["services.*.depends_on"] = mergeDependsOn mergeSpecials["services.*.depends_on"] = mergeDependsOn
@ -58,7 +58,7 @@ func init() {
mergeSpecials["services.*.entrypoint"] = override mergeSpecials["services.*.entrypoint"] = override
mergeSpecials["services.*.env_file"] = mergeToSequence mergeSpecials["services.*.env_file"] = mergeToSequence
mergeSpecials["services.*.environment"] = mergeToSequence mergeSpecials["services.*.environment"] = mergeToSequence
mergeSpecials["services.*.extra_hosts"] = mergeToSequence mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
mergeSpecials["services.*.healthcheck.test"] = override mergeSpecials["services.*.healthcheck.test"] = override
mergeSpecials["services.*.labels"] = mergeToSequence mergeSpecials["services.*.labels"] = mergeToSequence
mergeSpecials["services.*.logging"] = mergeLogging mergeSpecials["services.*.logging"] = mergeLogging
@ -163,6 +163,22 @@ func mergeNetworks(c any, o any, path tree.Path) (any, error) {
return mergeMappings(right, left, path) return mergeMappings(right, left, path)
} }
func mergeExtraHosts(c any, o any, _ tree.Path) (any, error) {
right := convertIntoSequence(c)
left := convertIntoSequence(o)
// Rewrite content of left slice to remove duplicate elements
i := 0
for _, v := range left {
if !slices.Contains(right, v) {
left[i] = v
i++
}
}
// keep only not duplicated elements from left slice
left = left[:i]
return append(right, left...), nil
}
func mergeToSequence(c any, o any, _ tree.Path) (any, error) { func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
right := convertIntoSequence(c) right := convertIntoSequence(c)
left := convertIntoSequence(o) left := convertIntoSequence(o)
@ -172,15 +188,21 @@ func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
func convertIntoSequence(value any) []any { func convertIntoSequence(value any) []any {
switch v := value.(type) { switch v := value.(type) {
case map[string]any: case map[string]any:
seq := make([]any, len(v)) var seq []any
i := 0 for k, val := range v {
for k, v := range v { if val == nil {
if v == nil { seq = append(seq, k)
seq[i] = k
} else { } else {
seq[i] = fmt.Sprintf("%s=%v", k, v) switch vl := val.(type) {
// if val is an array we need to add the key with each value one by one
case []any:
for _, vlv := range vl {
seq = append(seq, fmt.Sprintf("%s=%v", k, vlv))
}
default:
seq = append(seq, fmt.Sprintf("%s=%v", k, val))
}
} }
i++
} }
slices.SortFunc(seq, func(a, b any) int { slices.SortFunc(seq, func(a, b any) int {
return cmp.Compare(a.(string), b.(string)) return cmp.Compare(a.(string), b.(string))

View File

@ -267,6 +267,7 @@
}, },
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/extra_hosts"}, "extra_hosts": {"$ref": "#/definitions/extra_hosts"},
"gpus": {"$ref": "#/definitions/gpus"},
"group_add": { "group_add": {
"type": "array", "type": "array",
"items": { "items": {
@ -370,6 +371,8 @@
}, },
"uniqueItems": true "uniqueItems": true
}, },
"post_start": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
"pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
"privileged": {"type": ["boolean", "string"]}, "privileged": {"type": ["boolean", "string"]},
"profiles": {"$ref": "#/definitions/list_of_strings"}, "profiles": {"$ref": "#/definitions/list_of_strings"},
"pull_policy": {"type": "string", "enum": [ "pull_policy": {"type": "string", "enum": [
@ -416,6 +419,7 @@
"properties": { "properties": {
"propagation": {"type": "string"}, "propagation": {"type": "string"},
"create_host_path": {"type": ["boolean", "string"]}, "create_host_path": {"type": ["boolean", "string"]},
"recursive": {"type": "string", "enum": ["enabled", "disabled", "writable", "readonly"]},
"selinux": {"type": "string", "enum": ["z", "Z"]} "selinux": {"type": "string", "enum": ["z", "Z"]}
}, },
"additionalProperties": false, "additionalProperties": false,
@ -500,11 +504,11 @@
}, },
"additionalProperties": false, "additionalProperties": false,
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
}, }
"additionalProperties": false,
"patternProperties": {"^x-": {}}
} }
} },
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}, },
"deployment": { "deployment": {
"id": "#/definitions/deployment", "id": "#/definitions/deployment",
@ -632,6 +636,26 @@
"devices": { "devices": {
"id": "#/definitions/devices", "id": "#/definitions/devices",
"type": "array", "type": "array",
"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"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}},
"required": [
"capabilities"
]
}
},
"gpus": {
"id": "#/definitions/gpus",
"type": "array",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -813,6 +837,20 @@
] ]
}, },
"service_hook": {
"id": "#/definitions/service_hook",
"type": "object",
"properties": {
"command": {"$ref": "#/definitions/command"},
"user": {"type": "string"},
"privileged": {"type": ["boolean", "string"]},
"working_dir": {"type": "string"},
"environment": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"env_file": { "env_file": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},
@ -828,6 +866,9 @@
"path": { "path": {
"type": "string" "type": "string"
}, },
"format": {
"type": "string"
},
"required": { "required": {
"type": ["boolean", "string"], "type": ["boolean", "string"],
"default": true "default": true
@ -878,7 +919,8 @@
"patternProperties": { "patternProperties": {
".+": { ".+": {
"type": ["string", "array"] "type": ["string", "array"]
} },
"uniqueItems": false
}, },
"additionalProperties": false "additionalProperties": false
}, },

View File

@ -33,6 +33,7 @@ func init() {
transformers["services.*.extends"] = transformExtends transformers["services.*.extends"] = transformExtends
transformers["services.*.networks"] = transformServiceNetworks transformers["services.*.networks"] = transformServiceNetworks
transformers["services.*.volumes.*"] = transformVolumeMount transformers["services.*.volumes.*"] = transformVolumeMount
transformers["services.*.dns"] = transformStringOrList
transformers["services.*.devices.*"] = transformDeviceMapping transformers["services.*.devices.*"] = transformDeviceMapping
transformers["services.*.secrets.*"] = transformFileMount transformers["services.*.secrets.*"] = transformFileMount
transformers["services.*.configs.*"] = transformFileMount transformers["services.*.configs.*"] = transformFileMount
@ -48,6 +49,15 @@ func init() {
transformers["include.*"] = transformInclude transformers["include.*"] = transformInclude
} }
func transformStringOrList(data any, _ tree.Path, _ bool) (any, error) {
switch t := data.(type) {
case string:
return []any{t}, nil
default:
return data, nil
}
}
// Canonical transforms a compose model into canonical syntax // Canonical transforms a compose model into canonical syntax
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) { func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError) canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)

View File

@ -26,6 +26,8 @@ func init() {
defaultValues["services.*.build"] = defaultBuildContext defaultValues["services.*.build"] = defaultBuildContext
defaultValues["services.*.secrets.*"] = defaultSecretMount defaultValues["services.*.secrets.*"] = defaultSecretMount
defaultValues["services.*.ports.*"] = portDefaults defaultValues["services.*.ports.*"] = portDefaults
defaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults
defaultValues["services.*.gpus.*"] = deviceRequestDefaults
} }
// SetDefaultValues transforms a compose model to set default values to missing attributes // SetDefaultValues transforms a compose model to set default values to missing attributes

View File

@ -0,0 +1,36 @@
/*
Copyright 2020 The Compose Specification Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package transform
import (
"fmt"
"github.com/compose-spec/compose-go/v2/tree"
)
func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) {
v, ok := data.(map[string]any)
if !ok {
return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
}
_, hasCount := v["count"]
_, hasIds := v["device_ids"]
if !hasCount && !hasIds {
v["count"] = "all"
}
return v, nil
}

View File

@ -0,0 +1,48 @@
/*
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"
"strconv"
)
type NanoCPUs float32
func (n *NanoCPUs) DecodeMapstructure(a any) error {
switch v := a.(type) {
case string:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
*n = NanoCPUs(f)
case int:
*n = NanoCPUs(v)
case float32:
*n = NanoCPUs(v)
case float64:
*n = NanoCPUs(v)
default:
return fmt.Errorf("unexpected value type %T for cpus", v)
}
return nil
}
func (n *NanoCPUs) Value() float32 {
return float32(*n)
}

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ type DeviceRequest struct {
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"` Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"`
IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"` IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"`
Options Mapping `yaml:"options,omitempty" json:"options,omitempty"`
} }
type DeviceCount int64 type DeviceCount int64

View File

@ -23,6 +23,7 @@ import (
type EnvFile struct { type EnvFile struct {
Path string `yaml:"path,omitempty" json:"path,omitempty"` Path string `yaml:"path,omitempty" json:"path,omitempty"`
Required bool `yaml:"required" json:"required"` Required bool `yaml:"required" json:"required"`
Format string `yaml:"format,omitempty" json:"format,omitempty"`
} }
// MarshalYAML makes EnvFile implement yaml.Marshaler // MarshalYAML makes EnvFile implement yaml.Marshaler

View File

@ -0,0 +1,28 @@
/*
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
// ServiceHook is a command to exec inside container by some lifecycle events
type ServiceHook struct {
Command ShellCommand `yaml:"command,omitempty" json:"command"`
User string `yaml:"user,omitempty" json:"user,omitempty"`
Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
}

View File

@ -616,22 +616,11 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
} }
for _, envFile := range service.EnvFiles { for _, envFile := range service.EnvFiles {
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) { vars, err := loadEnvFile(envFile, resolve)
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 { if err != nil {
return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err) return nil, err
} }
environment.OverrideBy(vars.ToMappingWithEquals())
fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), resolve)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", envFile.Path, err)
}
environment.OverrideBy(Mapping(fileVars).ToMappingWithEquals())
} }
service.Environment = environment.OverrideBy(service.Environment) service.Environment = environment.OverrideBy(service.Environment)
@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
return newProject, nil return newProject, nil
} }
func loadEnvFile(envFile EnvFile, resolve dotenv.LookupFn) (Mapping, error) {
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)
}
return nil, nil
}
file, err := os.Open(envFile.Path)
if err != nil {
return nil, err
}
defer file.Close() //nolint:errcheck
var fileVars map[string]string
if envFile.Format != "" {
fileVars, err = dotenv.ParseWithFormat(file, envFile.Path, resolve, envFile.Format)
} else {
fileVars, err = dotenv.ParseWithLookup(file, resolve)
}
if err != nil {
return nil, err
}
return fileVars, nil
}
func (p *Project) deepCopy() *Project { func (p *Project) deepCopy() *Project {
if p == nil { if p == nil {
return nil return nil

View File

@ -20,7 +20,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
@ -82,6 +81,7 @@ type ServiceConfig struct {
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"` ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"` GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"`
Gpus []DeviceRequest `yaml:"gpus,omitempty" json:"gpus,omitempty"`
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"` Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"` HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
Image string `yaml:"image,omitempty" json:"image,omitempty"` Image string `yaml:"image,omitempty" json:"image,omitempty"`
@ -132,6 +132,8 @@ type ServiceConfig struct {
Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"` Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
PostStart []ServiceHook `yaml:"post_start,omitempty" json:"post_start,omitempty"`
PreStop []ServiceHook `yaml:"pre_stop,omitempty" json:"pre_stop,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
} }
@ -388,30 +390,6 @@ type Resource struct {
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
} }
type NanoCPUs float32
func (n *NanoCPUs) DecodeMapstructure(a any) error {
switch v := a.(type) {
case string:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
*n = NanoCPUs(f)
case float32:
*n = NanoCPUs(v)
case float64:
*n = NanoCPUs(v)
default:
return fmt.Errorf("unexpected value type %T for cpus", v)
}
return nil
}
func (n *NanoCPUs) Value() float32 {
return float32(*n)
}
// GenericResource represents a "user defined" resource which can // GenericResource represents a "user defined" resource which can
// only be an integer (e.g: SSD=3) for a service // only be an integer (e.g: SSD=3) for a service
type GenericResource struct { type GenericResource struct {
@ -552,9 +530,6 @@ func (s ServiceVolumeConfig) String() string {
if s.Volume != nil && s.Volume.NoCopy { if s.Volume != nil && s.Volume.NoCopy {
options = append(options, "nocopy") options = append(options, "nocopy")
} }
if s.Volume != nil && s.Volume.Subpath != "" {
options = append(options, s.Volume.Subpath)
}
return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ",")) return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ","))
} }
@ -581,6 +556,7 @@ type ServiceVolumeBind struct {
SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"` SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"`
Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"` Propagation string `yaml:"propagation,omitempty" json:"propagation,omitempty"`
CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"` CreateHostPath bool `yaml:"create_host_path,omitempty" json:"create_host_path,omitempty"`
Recursive string `yaml:"recursive,omitempty" json:"recursive,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
} }

View File

@ -30,6 +30,8 @@ var checks = map[tree.Path]checkerFunc{
"configs.*": checkFileObject("file", "environment", "content"), "configs.*": checkFileObject("file", "environment", "content"),
"secrets.*": checkFileObject("file", "environment"), "secrets.*": checkFileObject("file", "environment"),
"services.*.develop.watch.*.path": checkPath, "services.*.develop.watch.*.path": checkPath,
"services.*.deploy.resources.reservations.devices.*": checkDeviceRequest,
"services.*.gpus.*": checkDeviceRequest,
} }
func Validate(dict map[string]any) error { func Validate(dict map[string]any) error {
@ -94,3 +96,13 @@ func checkPath(value any, p tree.Path) error {
} }
return nil return nil
} }
func checkDeviceRequest(value any, p tree.Path) error {
v := value.(map[string]any)
_, hasCount := v["count"]
_, hasIds := v["device_ids"]
if hasCount && hasIds {
return fmt.Errorf(`%s: "count" and "device_ids" attributes are exclusive`, p)
}
return nil
}

2
vendor/modules.txt vendored
View File

@ -128,7 +128,7 @@ github.com/cenkalti/backoff/v4
# github.com/cespare/xxhash/v2 v2.3.0 # github.com/cespare/xxhash/v2 v2.3.0
## explicit; go 1.11 ## explicit; go 1.11
github.com/cespare/xxhash/v2 github.com/cespare/xxhash/v2
# github.com/compose-spec/compose-go/v2 v2.2.0 # github.com/compose-spec/compose-go/v2 v2.4.1
## explicit; go 1.21 ## explicit; go 1.21
github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts github.com/compose-spec/compose-go/v2/consts