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/Microsoft/go-winio v0.6.1
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/containerd v1.7.15
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/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.0.2 h1:zhXMV7VWI00Su0LdKt8/sxeXxcjLWhmGmpEyw+ZYznI=
github.com/compose-spec/compose-go/v2 v2.0.2/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc=
github.com/compose-spec/compose-go/v2 v2.1.1 h1:tKuYJwAVgxIryRrsvWJSf1kNviVOQVVqwyHsV6YoIUc=
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/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
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) {
return nil
}
if err != nil {
return err
}
if !s.IsDir() {
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 {
names := utils.MapKeys(v.children)
for _, name := range names {
if i := slices.Index(path, name); i > 0 {
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
if i := slices.Index(path, name); i >= 0 {
return fmt.Errorf("dependency cycle detected: %s -> %s", strings.Join(path[i:], " -> "), name)
}
ch := v.children[name]
err := searchCycle(append(path, name), ch)

View File

@ -29,12 +29,15 @@ import (
"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) {
if source == 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 {
if v, ok := config.(string); ok {
configs[i] = map[string]any{
@ -73,11 +76,19 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
p = path
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 {
if f == 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
}
func (l localResourceLoader) Dir(path string) string {
path = l.abs(filepath.Dir(path))
func (l localResourceLoader) Dir(originalPath string) string {
path := l.abs(originalPath)
if !l.isDir(path) {
path = l.abs(filepath.Dir(originalPath))
}
rel, err := filepath.Rel(l.WorkingDir, path)
if err != nil {
return path
@ -157,6 +160,14 @@ func (l localResourceLoader) Dir(path string) string {
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 {
return &Options{
SkipValidation: o.SkipValidation,
@ -317,19 +328,11 @@ func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetail
return nil, errors.New("No files specified")
}
err := projectName(*configDetails, opts)
err := projectName(configDetails, opts)
if err != nil {
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)
}
@ -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 {
return nil, err
}
@ -584,10 +587,14 @@ func InvalidProjectNameErr(v string) error {
// projectName determines the canonical name to use for the project considering
// the loader Options as well as `name` fields in Compose YAML fields (which
// also support interpolation).
//
// TODO(milas): restructure loading so that we don't need to re-parse the YAML
// here, as it's both wasteful and makes this code error-prone.
func projectName(details types.ConfigDetails, opts *Options) error {
func projectName(details *types.ConfigDetails, opts *Options) error {
defer func() {
if details.Environment == nil {
details.Environment = map[string]string{}
}
details.Environment[consts.ComposeProjectName] = opts.projectName
}()
if opts.projectNameImperativelySet {
if NormalizeProjectName(opts.projectName) != 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
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 {
services := d.(map[string]any)
for name, s := range services {
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 {
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
}
func normalizeNetworks(dict map[string]any) map[string]any {
func normalizeNetworks(dict map[string]any) {
var networks map[string]any
if n, ok := dict["networks"]; ok {
networks = n.(map[string]any)
} else {
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
networks["default"] = nil
}
return networks
if len(networks) > 0 {
dict["networks"] = networks
}
}
func resolve(a any, fn func(s string) (string, bool)) (any, bool) {

View File

@ -19,6 +19,7 @@ package loader
import (
"fmt"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/tree"
"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
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" {
p.paths = append(p.paths, path)
return nil, nil

View File

@ -28,7 +28,6 @@ import (
// checkConsistency validate a compose model is consistent
func checkConsistency(project *types.Project) error {
containerNames := map[string]string{}
for _, s := range project.Services {
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)
@ -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 != "" {
attr := "scale"
if s.Scale == nil {

View File

@ -42,6 +42,7 @@ func init() {
unique["services.*.build.labels"] = keyValueIndexer
unique["services.*.cap_add"] = keyValueIndexer
unique["services.*.cap_drop"] = keyValueIndexer
unique["services.*.devices"] = volumeIndexer
unique["services.*.configs"] = mountIndexer("")
unique["services.*.deploy.labels"] = keyValueIndexer
unique["services.*.dns"] = keyValueIndexer
@ -197,12 +198,15 @@ func portIndexer(y any, p tree.Path) (string, error) {
return "", nil
}
func envFileIndexer(y any, _ tree.Path) (string, error) {
func envFileIndexer(y any, p tree.Path) (string, error) {
switch value := y.(type) {
case string:
return value, nil
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
}

View File

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

View File

@ -271,102 +271,6 @@ func Substitute(template string, mapping Mapping) (string, error) {
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)
func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
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"
)
func transformBuild(data any, p tree.Path) (any, error) {
func transformBuild(data any, p tree.Path, ignoreParseError bool) (any, error) {
switch v := data.(type) {
case map[string]any:
return transformMapping(v, p)
return transformMapping(v, p, ignoreParseError)
case string:
return map[string]any{
"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) {
case map[string]any:
if _, ok := v["context"]; !ok {

View File

@ -20,7 +20,7 @@ import (
"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{}
@ -48,18 +48,18 @@ func init() {
}
// Canonical transforms a compose model into canonical syntax
func Canonical(yaml map[string]any) (map[string]any, error) {
canonical, err := transform(yaml, tree.NewPath())
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
if err != nil {
return nil, err
}
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 {
if p.Matches(pattern) {
t, err := transformer(data, p)
t, err := transformer(data, p, ignoreParseError)
if err != nil {
return nil, err
}
@ -68,13 +68,13 @@ func transform(data any, p tree.Path) (any, error) {
}
switch v := data.(type) {
case map[string]any:
a, err := transformMapping(v, p)
a, err := transformMapping(v, p, ignoreParseError)
if err != nil {
return a, err
}
return v, nil
case []any:
a, err := transformSequence(v, p)
a, err := transformSequence(v, p, ignoreParseError)
if err != nil {
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 {
t, err := transform(e, p.Next("[]"))
t, err := transform(e, p.Next("[]"), ignoreParseError)
if err != nil {
return nil, err
}
@ -95,9 +95,9 @@ func transformSequence(v []any, p tree.Path) ([]any, error) {
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 {
t, err := transform(e, p.Next(k))
t, err := transform(e, p.Next(k), ignoreParseError)
if err != nil {
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) {
for pattern, transformer := range defaultValues {
if p.Matches(pattern) {
t, err := transformer(data, p)
t, err := transformer(data, p, false)
if err != nil {
return nil, err
}

View File

@ -22,7 +22,7 @@ import (
"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) {
case map[string]any:
for i, e := range v {

View File

@ -22,7 +22,7 @@ import (
"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) {
case string:
return []any{

View File

@ -22,10 +22,10 @@ import (
"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) {
case map[string]any:
return transformMapping(v, p)
return transformMapping(v, p, ignoreParseError)
case string:
return map[string]any{
"service": v,

View File

@ -23,11 +23,11 @@ import (
"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 {
return nil, nil
}
resource, err := transformMapping(data.(map[string]any), p)
resource, err := transformMapping(data.(map[string]any), p, ignoreParseError)
if err != nil {
return nil, err
}

View File

@ -22,7 +22,7 @@ import (
"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) {
case map[string]any:
return v, nil

View File

@ -23,7 +23,7 @@ import (
"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) {
case map[string]any:
return v, nil
@ -32,6 +32,9 @@ func transformKeyValue(data any, p tree.Path) (any, error) {
for _, e := range v {
before, after, found := strings.Cut(e.(string), "=")
if !found {
if ignoreParseError {
return data, nil
}
return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e)
}
mapping[before] = after

View File

@ -24,7 +24,7 @@ import (
"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) {
case []any:
// We process the list instead of individual items here.
@ -48,7 +48,10 @@ func transformPorts(data any, p tree.Path) (any, error) {
case string:
parsed, err := types.ParsePortConfig(value)
if err != nil {
return data, nil
if ignoreParseError {
return data, nil
}
return nil, err
}
if err != nil {
return nil, err

View File

@ -22,7 +22,7 @@ import (
"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) {
case map[string]any:
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) {
case map[string]any:
source := v["source"]

View File

@ -20,16 +20,16 @@ import (
"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) {
case map[string]any:
return transformMapping(value, p)
return transformMapping(value, p, ignoreParseError)
default:
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 {
networks := make(map[string]any, len(slice))
for _, net := range slice {

View File

@ -23,7 +23,7 @@ import (
"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) {
case map[string]any:
return v, nil

View File

@ -22,7 +22,7 @@ import (
"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) {
case map[string]any:
return v, nil

View File

@ -24,13 +24,16 @@ import (
"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) {
case map[string]any:
return v, nil
case string:
volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string]
if err != nil {
if ignoreParseError {
return v, nil
}
return nil, err
}
volume.Target = cleanTarget(volume.Target)

View File

@ -659,12 +659,12 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S
name string
service ServiceConfig
}
resultCh := make(chan result)
expect := len(p.Services)
resultCh := make(chan result, expect)
newProject := p.deepCopy()
eg, ctx := errgroup.WithContext(context.Background())
eg.Go(func() error {
expect := len(newProject.Services)
s := Services{}
for expect > 0 {
select {
@ -696,3 +696,17 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S
}
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{}:
list := make([]string, len(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
default:

View File

@ -266,6 +266,7 @@ type BuildConfig struct {
Context string `yaml:"context,omitempty" json:"context,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"`
DockerfileInline string `yaml:"dockerfile_inline,omitempty" json:"dockerfile_inline,omitempty"`
Entitlements []string `yaml:"entitlements,omitempty" json:"entitlements,omitempty"`
Args MappingWithEquals `yaml:"args,omitempty" json:"args,omitempty"`
SSH SSHConfig `yaml:"ssh,omitempty" json:"ssh,omitempty"`
Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
@ -452,6 +453,7 @@ type ServiceNetworkConfig struct {
Ipv6Address string `yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"`
LinkLocalIPs []string `yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"`
MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
DriverOpts Options `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"`
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
## explicit; go 1.11
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
github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts