vendor: update compose to v2.4.1

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2024-10-28 17:26:28 -07:00
parent 181348397c
commit a585faf3d2
No known key found for this signature in database
GPG Key ID: AFA9DE5F8AB7AF39
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/Microsoft/go-winio v0.6.2
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/containerd v1.7.22
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/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA=
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/compose-spec/compose-go/v2 v2.4.1 h1:tEg6Qn/9LZnKg42fZlFmxN4lxSqnCvsiG5TXnxzvI4c=
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/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
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()
}
func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
configPaths, err := o.getConfigPaths()
// ReadConfigFiles reads ConfigFiles and populates the content field
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 {
return nil, err
}
configs := make([][]byte, len(config.ConfigFiles))
var configs []types.ConfigFile
for _, f := range configPaths {
for i, c := range config.ConfigFiles {
var err error
var b []byte
if f == "-" {
if c.Filename == "-" {
b, err = io.ReadAll(os.Stdin)
if err != nil {
return nil, err
}
} else {
f, err := filepath.Abs(f)
f, err := filepath.Abs(c.Filename)
if err != nil {
return nil, err
}
@ -427,27 +429,31 @@ func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
return nil, err
}
}
configs = append(configs, types.ConfigFile{
Filename: f,
Content: b,
})
configs[i] = 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
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
configDetails, err := o.prepare()
config, err := o.prepare(ctx)
if err != nil {
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 {
return nil, err
}
for _, config := range configDetails.ConfigFiles {
for _, config := range config.ConfigFiles {
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
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
configDetails, err := o.prepare()
configDetails, err := o.prepare(ctx)
if err != nil {
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
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) {
configs, err := o.GetConfigFiles()
func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
defaultDir, err := o.GetWorkingDir()
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 {
return types.ConfigDetails{}, err
}
configDetails := types.ConfigDetails{
ConfigFiles: configs,
WorkingDir: workingDir,
Environment: o.Environment,
return configDetails, err
}
o.loadOptions = append(o.loadOptions,
withNamePrecedenceLoad(workingDir, o),
withNamePrecedenceLoad(defaultDir, o),
withConvertWindowsPaths(o),
withListeners(o))
return configDetails, nil
}
@ -502,8 +503,13 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
opts.SetProjectName(nameFromEnv, true)
} 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(
loader.NormalizeProjectName(filepath.Base(absWorkingDir)),
loader.NormalizeProjectName(dirname),
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)
for _, filename := range filenames {
individualEnvMap, individualErr := readFile(filename, lookupFn)
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
if individualErr != nil {
return envMap, individualErr
@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
}
func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename, nil)
envMap, err := ReadFile(filename, nil)
if err != nil {
return err
}
@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
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)
if err != nil {
return nil, err

View File

@ -119,7 +119,7 @@ loop:
offset = i + 1
inherited = rune == '\n'
break loop
case '_', '.', '[', ']':
case '_', '.', '-', '[', ']':
default:
// variable name should match [A-Za-z0-9_.-]
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
@ -136,6 +136,10 @@ loop:
return "", "", inherited, errors.New("zero length string")
}
if inherited && strings.IndexByte(key, ' ') == -1 {
p.line++
}
// trim whitespace
key = strings.TrimRightFunc(key, unicode.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) {
setBindOption(volume, option)
}
// ignore unknown options
// ignore unknown options FIXME why not report an error here?
}
}
return nil

View File

@ -30,6 +30,7 @@ import (
"strings"
"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"
"github.com/compose-spec/compose-go/v2/override"
"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)
}
func (l localResourceLoader) Accept(p string) bool {
_, err := os.Stat(l.abs(p))
return err == nil
func (l localResourceLoader) Accept(_ string) bool {
// LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
return true
}
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
}
// 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.
// Deprecated: use LoadWithContext.
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
}
dict = OmitEmpty(dict)
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
dict, err = override.EnforceUnicity(dict)
return err
@ -675,6 +723,7 @@ func NormalizeProjectName(s string) string {
var userDefinedKeys = []tree.Path{
"services",
"services.*.depends_on",
"volumes",
"networks",
"secrets",
@ -687,7 +736,7 @@ func processExtensions(dict map[string]any, p tree.Path, extensions map[string]a
for key, value := range dict {
skip := false
for _, uk := range userDefinedKeys {
if uk.Matches(p) {
if p.Matches(uk) {
skip = true
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
if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
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 {
// Return a map with the Content field populated
v["Content"] = val
delete(ext, types.SecretConfigXValue)
if len(ext) == 0 {
delete(v, "#extensions")
delete(v, consts.Extensions)
}
}
}

View File

@ -18,6 +18,7 @@ package loader
import (
"fmt"
"path"
"strconv"
"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 {
volumesFrom := n.([]any)
for _, v := range volumesFrom {
@ -123,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
}
services[name] = service
}
dict["services"] = services
}
setNameFromKey(dict)
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 {
target interface{}
paths []tree.Path
target interface{}
paths []tree.Path
visitedNodes map[*yaml.Node]string
}
// UnmarshalYAML implement yaml.Unmarshaler
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
resolved, err := p.resolveReset(value, tree.NewPath())
p.visitedNodes = nil
if err != nil {
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
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 strings.Contains(path.String(), ".<<") {
path = tree.NewPath(strings.Replace(path.String(), ".<<", "", 1))
if strings.Contains(pathStr, ".<<") {
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 node.Kind == yaml.AliasNode {
return p.resolveReset(node.Alias, path)

View File

@ -47,7 +47,7 @@ func init() {
mergeSpecials["services.*.build"] = mergeBuild
mergeSpecials["services.*.build.args"] = 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.*.command"] = override
mergeSpecials["services.*.depends_on"] = mergeDependsOn
@ -58,7 +58,7 @@ func init() {
mergeSpecials["services.*.entrypoint"] = override
mergeSpecials["services.*.env_file"] = mergeToSequence
mergeSpecials["services.*.environment"] = mergeToSequence
mergeSpecials["services.*.extra_hosts"] = mergeToSequence
mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
mergeSpecials["services.*.healthcheck.test"] = override
mergeSpecials["services.*.labels"] = mergeToSequence
mergeSpecials["services.*.logging"] = mergeLogging
@ -163,6 +163,22 @@ func mergeNetworks(c any, o any, path tree.Path) (any, error) {
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) {
right := convertIntoSequence(c)
left := convertIntoSequence(o)
@ -172,15 +188,21 @@ func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
func convertIntoSequence(value any) []any {
switch v := value.(type) {
case map[string]any:
seq := make([]any, len(v))
i := 0
for k, v := range v {
if v == nil {
seq[i] = k
var seq []any
for k, val := range v {
if val == nil {
seq = append(seq, k)
} 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 {
return cmp.Compare(a.(string), b.(string))

View File

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

View File

@ -33,6 +33,7 @@ func init() {
transformers["services.*.extends"] = transformExtends
transformers["services.*.networks"] = transformServiceNetworks
transformers["services.*.volumes.*"] = transformVolumeMount
transformers["services.*.dns"] = transformStringOrList
transformers["services.*.devices.*"] = transformDeviceMapping
transformers["services.*.secrets.*"] = transformFileMount
transformers["services.*.configs.*"] = transformFileMount
@ -48,6 +49,15 @@ func init() {
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
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)

View File

@ -26,6 +26,8 @@ func init() {
defaultValues["services.*.build"] = defaultBuildContext
defaultValues["services.*.secrets.*"] = defaultSecretMount
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

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"`
Count DeviceCount `yaml:"count,omitempty" json:"count,omitempty"`
IDs []string `yaml:"device_ids,omitempty" json:"device_ids,omitempty"`
Options Mapping `yaml:"options,omitempty" json:"options,omitempty"`
}
type DeviceCount int64

View File

@ -23,6 +23,7 @@ import (
type EnvFile struct {
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Required bool `yaml:"required" json:"required"`
Format string `yaml:"format,omitempty" json:"format,omitempty"`
}
// 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 {
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
if envFile.Required {
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
}
continue
}
b, err := os.ReadFile(envFile.Path)
vars, err := loadEnvFile(envFile, resolve)
if err != nil {
return nil, fmt.Errorf("failed to load %s: %w", envFile.Path, err)
return nil, err
}
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())
environment.OverrideBy(vars.ToMappingWithEquals())
}
service.Environment = environment.OverrideBy(service.Environment)
@ -644,6 +633,31 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
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 {
if p == nil {
return nil

View File

@ -20,7 +20,6 @@ import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"github.com/docker/go-connections/nat"
@ -82,6 +81,7 @@ type ServiceConfig struct {
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,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"`
HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
Image string `yaml:"image,omitempty" json:"image,omitempty"`
@ -132,6 +132,8 @@ type ServiceConfig struct {
Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,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:"-"`
}
@ -388,30 +390,6 @@ type Resource struct {
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
// only be an integer (e.g: SSD=3) for a service
type GenericResource struct {
@ -552,9 +530,6 @@ func (s ServiceVolumeConfig) String() string {
if s.Volume != nil && s.Volume.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, ","))
}
@ -581,6 +556,7 @@ type ServiceVolumeBind struct {
SELinux string `yaml:"selinux,omitempty" json:"selinux,omitempty"`
Propagation string `yaml:"propagation,omitempty" json:"propagation,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:"-"`
}

View File

@ -30,6 +30,8 @@ var checks = map[tree.Path]checkerFunc{
"configs.*": checkFileObject("file", "environment", "content"),
"secrets.*": checkFileObject("file", "environment"),
"services.*.develop.watch.*.path": checkPath,
"services.*.deploy.resources.reservations.devices.*": checkDeviceRequest,
"services.*.gpus.*": checkDeviceRequest,
}
func Validate(dict map[string]any) error {
@ -94,3 +96,13 @@ func checkPath(value any, p tree.Path) error {
}
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
## explicit; go 1.11
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
github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts