Merge pull request #2425 from glours/bump-compose-go-2.1.0

bump compose-go to v2.1.1
This commit is contained in:
thompson-shaun 2024-05-30 12:16:33 -04:00 committed by GitHub
commit 1f28985d20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 329 additions and 180 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.1 github.com/Microsoft/go-winio v0.6.1
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.0.2 github.com/compose-spec/compose-go/v2 v2.1.1
github.com/containerd/console v1.0.4 github.com/containerd/console v1.0.4
github.com/containerd/containerd v1.7.15 github.com/containerd/containerd v1.7.15
github.com/containerd/continuity v0.4.3 github.com/containerd/continuity v0.4.3

4
go.sum
View File

@ -84,8 +84,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb 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.0.2 h1:zhXMV7VWI00Su0LdKt8/sxeXxcjLWhmGmpEyw+ZYznI= github.com/compose-spec/compose-go/v2 v2.1.1 h1:tKuYJwAVgxIryRrsvWJSf1kNviVOQVVqwyHsV6YoIUc=
github.com/compose-spec/compose-go/v2 v2.0.2/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= github.com/compose-spec/compose-go/v2 v2.1.1/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=

View File

@ -283,6 +283,9 @@ func WithEnvFiles(file ...string) ProjectOptionsFn {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil return nil
} }
if err != nil {
return err
}
if !s.IsDir() { if !s.IsDir() {
o.EnvFiles = []string{defaultDotEnv} o.EnvFiles = []string{defaultDotEnv}
} }

View File

@ -50,8 +50,8 @@ func (g *graph[T]) checkCycle() error {
func searchCycle[T any](path []string, v *vertex[T]) error { func searchCycle[T any](path []string, v *vertex[T]) error {
names := utils.MapKeys(v.children) names := utils.MapKeys(v.children)
for _, name := range names { for _, name := range names {
if i := slices.Index(path, name); i > 0 { if i := slices.Index(path, name); i >= 0 {
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> ")) return fmt.Errorf("dependency cycle detected: %s -> %s", strings.Join(path[i:], " -> "), name)
} }
ch := v.children[name] ch := v.children[name]
err := searchCycle(append(path, name), ch) err := searchCycle(append(path, name), ch)

View File

@ -29,12 +29,15 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
) )
// loadIncludeConfig parse the require config from raw yaml // loadIncludeConfig parse the required config from raw yaml
func loadIncludeConfig(source any) ([]types.IncludeConfig, error) { func loadIncludeConfig(source any) ([]types.IncludeConfig, error) {
if source == nil { if source == nil {
return nil, nil return nil, nil
} }
configs := source.([]any) configs, ok := source.([]any)
if !ok {
return nil, fmt.Errorf("`include` must be a list, got %s", source)
}
for i, config := range configs { for i, config := range configs {
if v, ok := config.(string); ok { if v, ok := config.(string); ok {
configs[i] = map[string]any{ configs[i] = map[string]any{
@ -73,11 +76,19 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
p = path p = path
if i == 0 { // This is the "main" file, used to define project-directory. Others are overrides if i == 0 { // This is the "main" file, used to define project-directory. Others are overrides
relworkingdir = loader.Dir(path)
if r.ProjectDirectory == "" {
r.ProjectDirectory = filepath.Dir(path)
}
switch {
case r.ProjectDirectory == "":
relworkingdir = loader.Dir(path)
r.ProjectDirectory = filepath.Dir(path)
case !filepath.IsAbs(r.ProjectDirectory):
relworkingdir = loader.Dir(r.ProjectDirectory)
r.ProjectDirectory = filepath.Join(configDetails.WorkingDir, r.ProjectDirectory)
default:
relworkingdir = r.ProjectDirectory
}
for _, f := range included { for _, f := range included {
if f == path { if f == path {
included = append(included, path) included = append(included, path)

View File

@ -148,8 +148,11 @@ func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
return l.abs(p), nil return l.abs(p), nil
} }
func (l localResourceLoader) Dir(path string) string { func (l localResourceLoader) Dir(originalPath string) string {
path = l.abs(filepath.Dir(path)) path := l.abs(originalPath)
if !l.isDir(path) {
path = l.abs(filepath.Dir(originalPath))
}
rel, err := filepath.Rel(l.WorkingDir, path) rel, err := filepath.Rel(l.WorkingDir, path)
if err != nil { if err != nil {
return path return path
@ -157,6 +160,14 @@ func (l localResourceLoader) Dir(path string) string {
return rel return rel
} }
func (l localResourceLoader) isDir(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}
func (o *Options) clone() *Options { func (o *Options) clone() *Options {
return &Options{ return &Options{
SkipValidation: o.SkipValidation, SkipValidation: o.SkipValidation,
@ -317,19 +328,11 @@ func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetail
return nil, errors.New("No files specified") return nil, errors.New("No files specified")
} }
err := projectName(*configDetails, opts) err := projectName(configDetails, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO(milas): this should probably ALWAYS set (overriding any existing)
if _, ok := configDetails.Environment[consts.ComposeProjectName]; !ok && opts.projectName != "" {
if configDetails.Environment == nil {
configDetails.Environment = map[string]string{}
}
configDetails.Environment[consts.ComposeProjectName] = opts.projectName
}
return load(ctx, *configDetails, opts, nil) return load(ctx, *configDetails, opts, nil)
} }
@ -452,7 +455,7 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option
} }
} }
dict, err = transform.Canonical(dict) dict, err = transform.Canonical(dict, opts.SkipInterpolation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -584,10 +587,14 @@ func InvalidProjectNameErr(v string) error {
// projectName determines the canonical name to use for the project considering // projectName determines the canonical name to use for the project considering
// the loader Options as well as `name` fields in Compose YAML fields (which // the loader Options as well as `name` fields in Compose YAML fields (which
// also support interpolation). // also support interpolation).
// func projectName(details *types.ConfigDetails, opts *Options) error {
// TODO(milas): restructure loading so that we don't need to re-parse the YAML defer func() {
// here, as it's both wasteful and makes this code error-prone. if details.Environment == nil {
func projectName(details types.ConfigDetails, opts *Options) error { details.Environment = map[string]string{}
}
details.Environment[consts.ComposeProjectName] = opts.projectName
}()
if opts.projectNameImperativelySet { if opts.projectNameImperativelySet {
if NormalizeProjectName(opts.projectName) != opts.projectName { if NormalizeProjectName(opts.projectName) != opts.projectName {
return InvalidProjectNameErr(opts.projectName) return InvalidProjectNameErr(opts.projectName)

View File

@ -26,18 +26,12 @@ import (
// Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults // Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
dict["networks"] = normalizeNetworks(dict) normalizeNetworks(dict)
if d, ok := dict["services"]; ok { if d, ok := dict["services"]; ok {
services := d.(map[string]any) services := d.(map[string]any)
for name, s := range services { for name, s := range services {
service := s.(map[string]any) service := s.(map[string]any)
_, hasNetworks := service["networks"]
_, hasNetworkMode := service["network_mode"]
if !hasNetworks && !hasNetworkMode {
// Service without explicit network attachment are implicitly exposed on default network
service["networks"] = map[string]any{"default": nil}
}
if service["pull_policy"] == types.PullPolicyIfNotPresent { if service["pull_policy"] == types.PullPolicyIfNotPresent {
service["pull_policy"] = types.PullPolicyMissing service["pull_policy"] = types.PullPolicyMissing
@ -137,18 +131,51 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
return dict, nil return dict, nil
} }
func normalizeNetworks(dict map[string]any) map[string]any { func normalizeNetworks(dict map[string]any) {
var networks map[string]any var networks map[string]any
if n, ok := dict["networks"]; ok { if n, ok := dict["networks"]; ok {
networks = n.(map[string]any) networks = n.(map[string]any)
} else { } else {
networks = map[string]any{} networks = map[string]any{}
} }
if _, ok := networks["default"]; !ok {
// implicit `default` network must be introduced only if actually used by some service
usesDefaultNetwork := false
if s, ok := dict["services"]; ok {
services := s.(map[string]any)
for name, se := range services {
service := se.(map[string]any)
if _, ok := service["network_mode"]; ok {
continue
}
if n, ok := service["networks"]; !ok {
// If none explicitly declared, service is connected to default network
service["networks"] = map[string]any{"default": nil}
usesDefaultNetwork = true
} else {
net := n.(map[string]any)
if len(net) == 0 {
// networks section declared but empty (corner case)
service["networks"] = map[string]any{"default": nil}
usesDefaultNetwork = true
} else if _, ok := net["default"]; ok {
usesDefaultNetwork = true
}
}
services[name] = service
}
dict["services"] = services
}
if _, ok := networks["default"]; !ok && usesDefaultNetwork {
// If not declared explicitly, Compose model involves an implicit "default" network // If not declared explicitly, Compose model involves an implicit "default" network
networks["default"] = nil networks["default"] = nil
} }
return networks
if len(networks) > 0 {
dict["networks"] = networks
}
} }
func resolve(a any, fn func(s string) (string, bool)) (any, bool) { func resolve(a any, fn func(s string) (string, bool)) (any, bool) {

View File

@ -19,6 +19,7 @@ package loader
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -40,6 +41,15 @@ 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) {
// 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 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)
}
if node.Tag == "!reset" { if node.Tag == "!reset" {
p.paths = append(p.paths, path) p.paths = append(p.paths, path)
return nil, nil return nil, nil

View File

@ -28,7 +28,6 @@ import (
// checkConsistency validate a compose model is consistent // checkConsistency validate a compose model is consistent
func checkConsistency(project *types.Project) error { func checkConsistency(project *types.Project) error {
containerNames := map[string]string{}
for _, s := range project.Services { for _, s := range project.Services {
if s.Build == nil && s.Image == "" { if s.Build == nil && s.Image == "" {
return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid) return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
@ -145,13 +144,6 @@ func checkConsistency(project *types.Project) error {
} }
} }
if s.ContainerName != "" {
if existing, ok := containerNames[s.ContainerName]; ok {
return fmt.Errorf(`"services.%s": container name "%s" is already in use by "services.%s": %w`, s.Name, s.ContainerName, existing, errdefs.ErrInvalid)
}
containerNames[s.ContainerName] = s.Name
}
if s.GetScale() > 1 && s.ContainerName != "" { if s.GetScale() > 1 && s.ContainerName != "" {
attr := "scale" attr := "scale"
if s.Scale == nil { if s.Scale == nil {

View File

@ -42,6 +42,7 @@ func init() {
unique["services.*.build.labels"] = keyValueIndexer unique["services.*.build.labels"] = keyValueIndexer
unique["services.*.cap_add"] = keyValueIndexer unique["services.*.cap_add"] = keyValueIndexer
unique["services.*.cap_drop"] = keyValueIndexer unique["services.*.cap_drop"] = keyValueIndexer
unique["services.*.devices"] = volumeIndexer
unique["services.*.configs"] = mountIndexer("") unique["services.*.configs"] = mountIndexer("")
unique["services.*.deploy.labels"] = keyValueIndexer unique["services.*.deploy.labels"] = keyValueIndexer
unique["services.*.dns"] = keyValueIndexer unique["services.*.dns"] = keyValueIndexer
@ -197,12 +198,15 @@ func portIndexer(y any, p tree.Path) (string, error) {
return "", nil return "", nil
} }
func envFileIndexer(y any, _ tree.Path) (string, error) { func envFileIndexer(y any, p tree.Path) (string, error) {
switch value := y.(type) { switch value := y.(type) {
case string: case string:
return value, nil return value, nil
case map[string]any: case map[string]any:
return value["path"].(string), nil if pathValue, ok := value["path"]; ok {
return pathValue.(string), nil
}
return "", fmt.Errorf("environment path attribut %s is missing", p)
} }
return "", nil return "", nil
} }

View File

@ -104,6 +104,7 @@
"context": {"type": "string"}, "context": {"type": "string"},
"dockerfile": {"type": "string"}, "dockerfile": {"type": "string"},
"dockerfile_inline": {"type": "string"}, "dockerfile_inline": {"type": "string"},
"entitlements": {"type": "array", "items": {"type": "string"}},
"args": {"$ref": "#/definitions/list_or_dict"}, "args": {"$ref": "#/definitions/list_or_dict"},
"ssh": {"$ref": "#/definitions/list_or_dict"}, "ssh": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"},
@ -295,6 +296,12 @@
"ipv6_address": {"type": "string"}, "ipv6_address": {"type": "string"},
"link_local_ips": {"$ref": "#/definitions/list_of_strings"}, "link_local_ips": {"$ref": "#/definitions/list_of_strings"},
"mac_address": {"type": "string"}, "mac_address": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"priority": {"type": "number"} "priority": {"type": "number"}
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -271,102 +271,6 @@ func Substitute(template string, mapping Mapping) (string, error) {
return SubstituteWith(template, mapping, DefaultPattern) return SubstituteWith(template, mapping, DefaultPattern)
} }
// ExtractVariables returns a map of all the variables defined in the specified
// composefile (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
if pattern == nil {
pattern = DefaultPattern
}
return recurseExtract(configDict, pattern)
}
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variable {
m := map[string]Variable{}
switch value := value.(type) {
case string:
if values, is := extractVariable(value, pattern); is {
for _, v := range values {
m[v.Name] = v
}
}
case map[string]interface{}:
for _, elem := range value {
submap := recurseExtract(elem, pattern)
for key, value := range submap {
m[key] = value
}
}
case []interface{}:
for _, elem := range value {
if values, is := extractVariable(elem, pattern); is {
for _, v := range values {
m[v.Name] = v
}
}
}
}
return m
}
type Variable struct {
Name string
DefaultValue string
PresenceValue string
Required bool
}
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
sValue, ok := value.(string)
if !ok {
return []Variable{}, false
}
matches := pattern.FindAllStringSubmatch(sValue, -1)
if len(matches) == 0 {
return []Variable{}, false
}
values := []Variable{}
for _, match := range matches {
groups := matchGroups(match, pattern)
if escaped := groups[groupEscaped]; escaped != "" {
continue
}
val := groups[groupNamed]
if val == "" {
val = groups[groupBraced]
}
name := val
var defaultValue string
var presenceValue string
var required bool
switch {
case strings.Contains(val, ":?"):
name, _ = partition(val, ":?")
required = true
case strings.Contains(val, "?"):
name, _ = partition(val, "?")
required = true
case strings.Contains(val, ":-"):
name, defaultValue = partition(val, ":-")
case strings.Contains(val, "-"):
name, defaultValue = partition(val, "-")
case strings.Contains(val, ":+"):
name, presenceValue = partition(val, ":+")
case strings.Contains(val, "+"):
name, presenceValue = partition(val, "+")
}
values = append(values, Variable{
Name: name,
DefaultValue: defaultValue,
PresenceValue: presenceValue,
Required: required,
})
}
return values, len(values) > 0
}
// Soft default (fall back if unset or empty) // Soft default (fall back if unset or empty)
func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) { func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
return withDefaultWhenAbsence(substitution, mapping, true) return withDefaultWhenAbsence(substitution, mapping, true)

View File

@ -0,0 +1,155 @@
/*
Copyright 2020 The Compose Specification Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package template
import (
"regexp"
"strings"
)
type Variable struct {
Name string
DefaultValue string
PresenceValue string
Required bool
}
// ExtractVariables returns a map of all the variables defined in the specified
// compose file (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
if pattern == nil {
pattern = DefaultPattern
}
return recurseExtract(configDict, pattern)
}
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variable {
m := map[string]Variable{}
switch value := value.(type) {
case string:
if values, is := extractVariable(value, pattern); is {
for _, v := range values {
m[v.Name] = v
}
}
case map[string]interface{}:
for _, elem := range value {
submap := recurseExtract(elem, pattern)
for key, value := range submap {
m[key] = value
}
}
case []interface{}:
for _, elem := range value {
if values, is := extractVariable(elem, pattern); is {
for _, v := range values {
m[v.Name] = v
}
}
}
}
return m
}
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
sValue, ok := value.(string)
if !ok {
return []Variable{}, false
}
matches := pattern.FindAllStringSubmatch(sValue, -1)
if len(matches) == 0 {
return []Variable{}, false
}
values := []Variable{}
for _, match := range matches {
groups := matchGroups(match, pattern)
if escaped := groups[groupEscaped]; escaped != "" {
continue
}
val := groups[groupNamed]
if val == "" {
val = groups[groupBraced]
s := match[0]
i := getFirstBraceClosingIndex(s)
if i > 0 {
val = s[2:i]
if len(s) > i {
if v, b := extractVariable(s[i+1:], pattern); b {
values = append(values, v...)
}
}
}
}
name := val
var defaultValue string
var presenceValue string
var required bool
i := strings.IndexFunc(val, func(r rune) bool {
if r >= 'a' && r <= 'z' {
return false
}
if r >= 'A' && r <= 'Z' {
return false
}
if r == '_' {
return false
}
return true
})
if i > 0 {
name = val[:i]
rest := val[i:]
switch {
case strings.HasPrefix(rest, ":?"):
required = true
case strings.HasPrefix(rest, "?"):
required = true
case strings.HasPrefix(rest, ":-"):
defaultValue = rest[2:]
case strings.HasPrefix(rest, "-"):
defaultValue = rest[1:]
case strings.HasPrefix(rest, ":+"):
presenceValue = rest[2:]
case strings.HasPrefix(rest, "+"):
presenceValue = rest[1:]
}
}
values = append(values, Variable{
Name: name,
DefaultValue: defaultValue,
PresenceValue: presenceValue,
Required: required,
})
if defaultValue != "" {
if v, b := extractVariable(defaultValue, pattern); b {
values = append(values, v...)
}
}
if presenceValue != "" {
if v, b := extractVariable(presenceValue, pattern); b {
values = append(values, v...)
}
}
}
return values, len(values) > 0
}

View File

@ -22,10 +22,10 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformBuild(data any, p tree.Path) (any, error) { func transformBuild(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return transformMapping(v, p) return transformMapping(v, p, ignoreParseError)
case string: case string:
return map[string]any{ return map[string]any{
"context": v, "context": v,
@ -35,7 +35,7 @@ func transformBuild(data any, p tree.Path) (any, error) {
} }
} }
func defaultBuildContext(data any, _ tree.Path) (any, error) { func defaultBuildContext(data any, _ tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
if _, ok := v["context"]; !ok { if _, ok := v["context"]; !ok {

View File

@ -20,7 +20,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
type transformFunc func(data any, p tree.Path) (any, error) type transformFunc func(data any, p tree.Path, ignoreParseError bool) (any, error)
var transformers = map[tree.Path]transformFunc{} var transformers = map[tree.Path]transformFunc{}
@ -48,18 +48,18 @@ func init() {
} }
// Canonical transforms a compose model into canonical syntax // Canonical transforms a compose model into canonical syntax
func Canonical(yaml map[string]any) (map[string]any, error) { func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
canonical, err := transform(yaml, tree.NewPath()) canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return canonical.(map[string]any), nil return canonical.(map[string]any), nil
} }
func transform(data any, p tree.Path) (any, error) { func transform(data any, p tree.Path, ignoreParseError bool) (any, error) {
for pattern, transformer := range transformers { for pattern, transformer := range transformers {
if p.Matches(pattern) { if p.Matches(pattern) {
t, err := transformer(data, p) t, err := transformer(data, p, ignoreParseError)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -68,13 +68,13 @@ func transform(data any, p tree.Path) (any, error) {
} }
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
a, err := transformMapping(v, p) a, err := transformMapping(v, p, ignoreParseError)
if err != nil { if err != nil {
return a, err return a, err
} }
return v, nil return v, nil
case []any: case []any:
a, err := transformSequence(v, p) a, err := transformSequence(v, p, ignoreParseError)
if err != nil { if err != nil {
return a, err return a, err
} }
@ -84,9 +84,9 @@ func transform(data any, p tree.Path) (any, error) {
} }
} }
func transformSequence(v []any, p tree.Path) ([]any, error) { func transformSequence(v []any, p tree.Path, ignoreParseError bool) ([]any, error) {
for i, e := range v { for i, e := range v {
t, err := transform(e, p.Next("[]")) t, err := transform(e, p.Next("[]"), ignoreParseError)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,9 +95,9 @@ func transformSequence(v []any, p tree.Path) ([]any, error) {
return v, nil return v, nil
} }
func transformMapping(v map[string]any, p tree.Path) (map[string]any, error) { func transformMapping(v map[string]any, p tree.Path, ignoreParseError bool) (map[string]any, error) {
for k, e := range v { for k, e := range v {
t, err := transform(e, p.Next(k)) t, err := transform(e, p.Next(k), ignoreParseError)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -39,7 +39,7 @@ func SetDefaultValues(yaml map[string]any) (map[string]any, error) {
func setDefaults(data any, p tree.Path) (any, error) { func setDefaults(data any, p tree.Path) (any, error) {
for pattern, transformer := range defaultValues { for pattern, transformer := range defaultValues {
if p.Matches(pattern) { if p.Matches(pattern) {
t, err := transformer(data, p) t, err := transformer(data, p, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -22,7 +22,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformDependsOn(data any, p tree.Path) (any, error) { func transformDependsOn(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
for i, e := range v { for i, e := range v {

View File

@ -22,7 +22,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformEnvFile(data any, p tree.Path) (any, error) { func transformEnvFile(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case string: case string:
return []any{ return []any{

View File

@ -22,10 +22,10 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformExtends(data any, p tree.Path) (any, error) { func transformExtends(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return transformMapping(v, p) return transformMapping(v, p, ignoreParseError)
case string: case string:
return map[string]any{ return map[string]any{
"service": v, "service": v,

View File

@ -23,11 +23,11 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func transformMaybeExternal(data any, p tree.Path) (any, error) { func transformMaybeExternal(data any, p tree.Path, ignoreParseError bool) (any, error) {
if data == nil { if data == nil {
return nil, nil return nil, nil
} }
resource, err := transformMapping(data.(map[string]any), p) resource, err := transformMapping(data.(map[string]any), p, ignoreParseError)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -22,7 +22,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformInclude(data any, p tree.Path) (any, error) { func transformInclude(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return v, nil return v, nil

View File

@ -23,7 +23,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformKeyValue(data any, p tree.Path) (any, error) { func transformKeyValue(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return v, nil return v, nil
@ -32,6 +32,9 @@ func transformKeyValue(data any, p tree.Path) (any, error) {
for _, e := range v { for _, e := range v {
before, after, found := strings.Cut(e.(string), "=") before, after, found := strings.Cut(e.(string), "=")
if !found { if !found {
if ignoreParseError {
return data, nil
}
return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e) return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e)
} }
mapping[before] = after mapping[before] = after

View File

@ -24,7 +24,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
func transformPorts(data any, p tree.Path) (any, error) { func transformPorts(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch entries := data.(type) { switch entries := data.(type) {
case []any: case []any:
// We process the list instead of individual items here. // We process the list instead of individual items here.
@ -48,7 +48,10 @@ func transformPorts(data any, p tree.Path) (any, error) {
case string: case string:
parsed, err := types.ParsePortConfig(value) parsed, err := types.ParsePortConfig(value)
if err != nil { if err != nil {
return data, nil if ignoreParseError {
return data, nil
}
return nil, err
} }
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -22,7 +22,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformFileMount(data any, p tree.Path) (any, error) { func transformFileMount(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return data, nil return data, nil
@ -35,7 +35,7 @@ func transformFileMount(data any, p tree.Path) (any, error) {
} }
} }
func defaultSecretMount(data any, p tree.Path) (any, error) { func defaultSecretMount(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
source := v["source"] source := v["source"]

View File

@ -20,16 +20,16 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformService(data any, p tree.Path) (any, error) { func transformService(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch value := data.(type) { switch value := data.(type) {
case map[string]any: case map[string]any:
return transformMapping(value, p) return transformMapping(value, p, ignoreParseError)
default: default:
return value, nil return value, nil
} }
} }
func transformServiceNetworks(data any, _ tree.Path) (any, error) { func transformServiceNetworks(data any, _ tree.Path, _ bool) (any, error) {
if slice, ok := data.([]any); ok { if slice, ok := data.([]any); ok {
networks := make(map[string]any, len(slice)) networks := make(map[string]any, len(slice))
for _, net := range slice { for _, net := range slice {

View File

@ -23,7 +23,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformSSH(data any, p tree.Path) (any, error) { func transformSSH(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return v, nil return v, nil

View File

@ -22,7 +22,7 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformUlimits(data any, p tree.Path) (any, error) { func transformUlimits(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return v, nil return v, nil

View File

@ -24,13 +24,16 @@ import (
"github.com/compose-spec/compose-go/v2/tree" "github.com/compose-spec/compose-go/v2/tree"
) )
func transformVolumeMount(data any, p tree.Path) (any, error) { func transformVolumeMount(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch v := data.(type) { switch v := data.(type) {
case map[string]any: case map[string]any:
return v, nil return v, nil
case string: case string:
volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string] volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string]
if err != nil { if err != nil {
if ignoreParseError {
return v, nil
}
return nil, err return nil, err
} }
volume.Target = cleanTarget(volume.Target) volume.Target = cleanTarget(volume.Target)

View File

@ -659,12 +659,12 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S
name string name string
service ServiceConfig service ServiceConfig
} }
resultCh := make(chan result) expect := len(p.Services)
resultCh := make(chan result, expect)
newProject := p.deepCopy() newProject := p.deepCopy()
eg, ctx := errgroup.WithContext(context.Background()) eg, ctx := errgroup.WithContext(context.Background())
eg.Go(func() error { eg.Go(func() error {
expect := len(newProject.Services)
s := Services{} s := Services{}
for expect > 0 { for expect > 0 {
select { select {
@ -696,3 +696,17 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S
} }
return newProject, eg.Wait() return newProject, eg.Wait()
} }
// CheckContainerNameUnicity validate project doesn't have services declaring the same container_name
func (p *Project) CheckContainerNameUnicity() error {
names := utils.Set[string]{}
for name, s := range p.Services {
if s.ContainerName != "" {
if existing, ok := names[s.ContainerName]; ok {
return fmt.Errorf(`services.%s: container name %q is already in use by service %s"`, name, s.ContainerName, existing)
}
names.Add(s.ContainerName)
}
}
return nil
}

View File

@ -28,7 +28,11 @@ func (l *StringList) DecodeMapstructure(value interface{}) error {
case []interface{}: case []interface{}:
list := make([]string, len(v)) list := make([]string, len(v))
for i, e := range v { for i, e := range v {
list[i] = e.(string) val, ok := e.(string)
if !ok {
return fmt.Errorf("invalid type %T for string list", value)
}
list[i] = val
} }
*l = list *l = list
default: default:

View File

@ -266,6 +266,7 @@ type BuildConfig struct {
Context string `yaml:"context,omitempty" json:"context,omitempty"` Context string `yaml:"context,omitempty" json:"context,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"`
DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"` DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"`
Entitlements []string `yaml:"entitlements,omitempty" json:"entitlements,omitempty"`
Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"` Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"`
SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"` SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"`
Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
@ -452,6 +453,7 @@ type ServiceNetworkConfig struct {
Ipv6Address string `yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` Ipv6Address string `yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"`
LinkLocalIPs []string `yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"` LinkLocalIPs []string `yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"`
MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"` MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
DriverOpts Options `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"`
} }

2
vendor/modules.txt vendored
View File

@ -131,7 +131,7 @@ github.com/cenkalti/backoff/v4
# github.com/cespare/xxhash/v2 v2.2.0 # github.com/cespare/xxhash/v2 v2.2.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.0.2 # github.com/compose-spec/compose-go/v2 v2.1.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