diff --git a/go.mod b/go.mod index 41a055cb..3bd6bfee 100644 --- a/go.mod +++ b/go.mod @@ -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.0-rc.8 + github.com/compose-spec/compose-go/v2 v2.0.2 github.com/containerd/console v1.0.4 github.com/containerd/containerd v1.7.14 github.com/containerd/continuity v0.4.3 diff --git a/go.sum b/go.sum index e075a787..a26aa418 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,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.0-rc.8 h1:b7l+GqFF+2W4M4kLQUDRTGhqmTiRwT3bYd9X7xrxp5Q= -github.com/compose-spec/compose-go/v2 v2.0.0-rc.8/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= +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/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= diff --git a/vendor/github.com/compose-spec/compose-go/v2/cli/options.go b/vendor/github.com/compose-spec/compose-go/v2/cli/options.go index adb2f75e..2c5ee1a3 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/cli/options.go +++ b/vendor/github.com/compose-spec/compose-go/v2/cli/options.go @@ -217,7 +217,10 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn { // profiles specified via the COMPOSE_PROFILES environment variable otherwise. func WithDefaultProfiles(profile ...string) ProjectOptionsFn { if len(profile) == 0 { - profile = strings.Split(os.Getenv(consts.ComposeProfiles), ",") + for _, s := range strings.Split(os.Getenv(consts.ComposeProfiles), ",") { + profile = append(profile, strings.TrimSpace(s)) + } + } return WithProfiles(profile) } @@ -379,7 +382,7 @@ var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.y // DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference) var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"} -func (o ProjectOptions) GetWorkingDir() (string, error) { +func (o *ProjectOptions) GetWorkingDir() (string, error) { if o.WorkingDir != "" { return filepath.Abs(o.WorkingDir) } @@ -395,7 +398,7 @@ func (o ProjectOptions) GetWorkingDir() (string, error) { return os.Getwd() } -func (o ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) { +func (o *ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) { configPaths, err := o.getConfigPaths() if err != nil { return nil, err @@ -427,39 +430,66 @@ func (o ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) { return configs, err } -// ProjectFromOptions load a compose project based on command line options -func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) { - configs, err := options.GeConfigFiles() +// 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() if err != nil { return nil, err } - workingDir, err := options.GetWorkingDir() + project, err := loader.LoadWithContext(ctx, configDetails, o.loadOptions...) if err != nil { return nil, err } - options.loadOptions = append(options.loadOptions, - withNamePrecedenceLoad(workingDir, options), - withConvertWindowsPaths(options), - withListeners(options)) - - project, err := loader.LoadWithContext(ctx, types.ConfigDetails{ - ConfigFiles: configs, - WorkingDir: workingDir, - Environment: options.Environment, - }, options.loadOptions...) - if err != nil { - return nil, err - } - - for _, config := range configs { + for _, config := range configDetails.ConfigFiles { project.ComposeFiles = append(project.ComposeFiles, config.Filename) } return project, nil } +// 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() + if err != nil { + return nil, err + } + + 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.GeConfigFiles() + if err != nil { + return types.ConfigDetails{}, err + } + + workingDir, err := o.GetWorkingDir() + if err != nil { + return types.ConfigDetails{}, err + } + + configDetails := types.ConfigDetails{ + ConfigFiles: configs, + WorkingDir: workingDir, + Environment: o.Environment, + } + + o.loadOptions = append(o.loadOptions, + withNamePrecedenceLoad(workingDir, o), + withConvertWindowsPaths(o), + withListeners(o)) + return configDetails, nil +} + +// ProjectFromOptions load a compose project based on command line options +// Deprecated: use ProjectOptions.LoadProject or ProjectOptions.LoadModel +func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) { + return options.LoadProject(ctx) +} + func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(*loader.Options) { return func(opts *loader.Options) { if options.Name != "" { diff --git a/vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go b/vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go index a5440700..1990ddd2 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go +++ b/vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go @@ -30,6 +30,9 @@ var ( // ErrIncompatible is returned when a compose project uses an incompatible attribute ErrIncompatible = errors.New("incompatible attribute") + + // ErrDisabled is returned when a resource was found in model but is disabled + ErrDisabled = errors.New("disabled") ) // IsNotFoundError returns true if the unwrapped error is ErrNotFound diff --git a/vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go b/vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go new file mode 100644 index 00000000..d96785cf --- /dev/null +++ b/vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go @@ -0,0 +1,63 @@ +/* + 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 graph + +import ( + "fmt" + "strings" + + "github.com/compose-spec/compose-go/v2/types" + "github.com/compose-spec/compose-go/v2/utils" + "golang.org/x/exp/slices" +) + +// CheckCycle analyze project's depends_on relation and report an error on cycle detection +func CheckCycle(project *types.Project) error { + g, err := newGraph(project) + if err != nil { + return err + } + return g.checkCycle() +} + +func (g *graph[T]) checkCycle() error { + // iterate on vertices in a name-order to render a predicable error message + // this is required by tests and enforce command reproducibility by user, which otherwise could be confusing + names := utils.MapKeys(g.vertices) + for _, name := range names { + err := searchCycle([]string{name}, g.vertices[name]) + if err != nil { + return err + } + } + return nil +} + +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:], " -> ")) + } + ch := v.children[name] + err := searchCycle(append(path, name), ch) + if err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/graph/graph.go b/vendor/github.com/compose-spec/compose-go/v2/graph/graph.go index 41123370..de4e9e10 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/graph/graph.go +++ b/vendor/github.com/compose-spec/compose-go/v2/graph/graph.go @@ -16,14 +16,6 @@ package graph -import ( - "fmt" - "strings" - - "github.com/compose-spec/compose-go/v2/utils" - "golang.org/x/exp/slices" -) - // graph represents project as service dependencies type graph[T any] struct { vertices map[string]*vertex[T] @@ -72,34 +64,6 @@ func (g *graph[T]) leaves() []*vertex[T] { return res } -func (g *graph[T]) checkCycle() error { - // iterate on vertices in a name-order to render a predicable error message - // this is required by tests and enforce command reproducibility by user, which otherwise could be confusing - names := utils.MapKeys(g.vertices) - for _, name := range names { - err := searchCycle([]string{name}, g.vertices[name]) - if err != nil { - return err - } - } - return nil -} - -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:], " -> ")) - } - ch := v.children[name] - err := searchCycle(append(path, name), ch) - if err != nil { - return err - } - } - return nil -} - // descendents return all descendents for a vertex, might contain duplicates func (v *vertex[T]) descendents() []string { var vx []string diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go b/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go index 07bf32ba..cdfed509 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/extends.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "path/filepath" - "strings" "github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/override" @@ -106,11 +105,6 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a } source := deepClone(base).(map[string]any) - err = validateExtendSource(source, ref) - if err != nil { - return nil, err - } - for _, processor := range post { processor.Apply(map[string]any{ "services": map[string]any{ @@ -127,30 +121,6 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a return merged, nil } -// validateExtendSource check the source for `extends` doesn't refer to another container/service -func validateExtendSource(source map[string]any, ref string) error { - forbidden := []string{"links", "volumes_from", "depends_on"} - for _, key := range forbidden { - if _, ok := source[key]; ok { - return fmt.Errorf("service %q can't be used with `extends` as it declare `%s`", ref, key) - } - } - - sharedNamespace := []string{"network_mode", "ipc", "pid", "net", "cgroup", "userns_mode", "uts"} - for _, key := range sharedNamespace { - if v, ok := source[key]; ok { - val := v.(string) - if strings.HasPrefix(val, types.ContainerPrefix) { - return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another container", ref, key) - } - if strings.HasPrefix(val, types.ServicePrefix) { - return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another service", ref, key) - } - } - } - return nil -} - func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) { for _, loader := range opts.ResourceLoaders { if !loader.Accept(path) { diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/include.go b/vendor/github.com/compose-spec/compose-go/v2/loader/include.go index a42e0d0d..e7631415 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/include.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/include.go @@ -60,43 +60,41 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model }) } + var relworkingdir string for i, p := range r.Path { for _, loader := range options.ResourceLoaders { - if loader.Accept(p) { - path, err := loader.Load(ctx, p) - if err != nil { - return err + if !loader.Accept(p) { + continue + } + path, err := loader.Load(ctx, p) + if err != nil { + return err + } + 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) + } + + for _, f := range included { + if f == path { + included = append(included, path) + return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include ")) + } } - p = path - break } } r.Path[i] = p } - mainFile := r.Path[0] - for _, f := range included { - if f == mainFile { - included = append(included, mainFile) - return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include ")) - } - } - - if r.ProjectDirectory == "" { - r.ProjectDirectory = filepath.Dir(mainFile) - } - relworkingdir, err := filepath.Rel(configDetails.WorkingDir, r.ProjectDirectory) - if err != nil { - // included file path is not inside project working directory => use absolute path - relworkingdir = r.ProjectDirectory - } - loadOptions := options.clone() loadOptions.ResolvePaths = true loadOptions.SkipNormalization = true loadOptions.SkipConsistencyCheck = true loadOptions.ResourceLoaders = append(loadOptions.RemoteResourceLoaders(), localResourceLoader{ - WorkingDir: relworkingdir, + WorkingDir: r.ProjectDirectory, }) if len(r.EnvFile) == 0 { diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go b/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go index 985b4b54..1ed3977a 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go @@ -41,6 +41,7 @@ import ( "github.com/compose-spec/compose-go/v2/validation" "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -84,6 +85,15 @@ type Options struct { Listeners []Listener } +var versionWarning []string + +func (o *Options) warnObsoleteVersion(file string) { + if !slices.Contains(versionWarning, file) { + logrus.Warning(fmt.Sprintf("%s: `version` is obsolete", file)) + } + versionWarning = append(versionWarning, file) +} + type Listener = func(event string, metadata map[string]any) // Invoke all listeners for an event @@ -285,12 +295,45 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types. return LoadWithContext(context.Background(), configDetails, options...) } -// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration +// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration as a compose-go Project func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { + opts := toOptions(&configDetails, options) + dict, err := loadModelWithContext(ctx, &configDetails, opts) + if err != nil { + return nil, err + } + return modelToProject(dict, opts, configDetails) +} + +// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary +func LoadModelWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (map[string]any, error) { + opts := toOptions(&configDetails, options) + return loadModelWithContext(ctx, &configDetails, opts) +} + +// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary +func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetails, opts *Options) (map[string]any, error) { if len(configDetails.ConfigFiles) < 1 { return nil, errors.New("No files specified") } + 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) +} + +func toOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options { opts := &Options{ Interpolate: &interp.Options{ Substitute: template.Substitute, @@ -304,21 +347,7 @@ func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, opt op(opts) } opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir}) - - 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) + return opts } func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options, ct *cycleTracker, included []string) (map[string]interface{}, error) { @@ -390,6 +419,10 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option if err := schema.Validate(dict); err != nil { return fmt.Errorf("validating %s: %w", file.Filename, err) } + if _, ok := dict["version"]; ok { + opts.warnObsoleteVersion(file.Filename) + delete(dict, "version") + } } return err @@ -458,7 +491,7 @@ func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Option return dict, nil } -func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (*types.Project, error) { +func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (map[string]interface{}, error) { mainFile := configDetails.ConfigFiles[0].Filename for _, f := range loaded { if f == mainFile { @@ -481,6 +514,19 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, return nil, errors.New("project name must not be empty") } + if !opts.SkipNormalization { + dict["name"] = opts.projectName + dict, err = Normalize(dict, configDetails.Environment) + if err != nil { + return nil, err + } + } + + return dict, nil +} + +// modelToProject binds a canonical yaml dict into compose-go structs +func modelToProject(dict map[string]interface{}, opts *Options, configDetails types.ConfigDetails) (*types.Project, error) { project := &types.Project{ Name: opts.projectName, WorkingDir: configDetails.WorkingDir, @@ -488,6 +534,7 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, } delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName + var err error dict, err = processExtensions(dict, tree.NewPath(), opts.KnownExtensions) if err != nil { return nil, err @@ -498,13 +545,6 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, return nil, err } - if !opts.SkipNormalization { - err := Normalize(project) - if err != nil { - return nil, err - } - } - if opts.ConvertWindowsPaths { for i, service := range project.Services { for j, volume := range service.Volumes { @@ -514,6 +554,10 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, } } + if project, err = project.WithProfiles(opts.Profiles); err != nil { + return nil, err + } + if !opts.SkipConsistencyCheck { err := checkConsistency(project) if err != nil { @@ -521,17 +565,12 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, } } - if project, err = project.WithProfiles(opts.Profiles); err != nil { - return nil, err - } - if !opts.SkipResolveEnvironment { project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles) if err != nil { return nil, err } } - return project, nil } diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go b/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go index e1f0beeb..551b4be6 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go @@ -18,266 +18,202 @@ package loader import ( "fmt" + "strconv" "strings" - "github.com/compose-spec/compose-go/v2/errdefs" "github.com/compose-spec/compose-go/v2/types" - "github.com/sirupsen/logrus" ) // Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults -func Normalize(project *types.Project) error { - if project.Networks == nil { - project.Networks = make(map[string]types.NetworkConfig) +func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { + dict["networks"] = 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 + } + + fn := func(s string) (string, bool) { + v, ok := env[s] + return v, ok + } + + if b, ok := service["build"]; ok { + build := b.(map[string]any) + if build["context"] == nil { + build["context"] = "." + } + if build["dockerfile"] == nil && build["dockerfile_inline"] == nil { + build["dockerfile"] = "Dockerfile" + } + + if a, ok := build["args"]; ok { + build["args"], _ = resolve(a, fn) + } + + service["build"] = build + } + + if e, ok := service["environment"]; ok { + service["environment"], _ = resolve(e, fn) + } + + var dependsOn map[string]any + if d, ok := service["depends_on"]; ok { + dependsOn = d.(map[string]any) + } else { + dependsOn = map[string]any{} + } + if l, ok := service["links"]; ok { + links := l.([]any) + for _, e := range links { + link := e.(string) + parts := strings.Split(link, ":") + if len(parts) == 2 { + link = parts[0] + } + if _, ok := dependsOn[link]; !ok { + dependsOn[link] = map[string]any{ + "condition": types.ServiceConditionStarted, + "restart": true, + "required": true, + } + } + } + } + + for _, namespace := range []string{"network_mode", "ipc", "pid", "uts", "cgroup"} { + if n, ok := service[namespace]; ok { + ref := n.(string) + if strings.HasPrefix(ref, types.ServicePrefix) { + shared := ref[len(types.ServicePrefix):] + if _, ok := dependsOn[shared]; !ok { + dependsOn[shared] = map[string]any{ + "condition": types.ServiceConditionStarted, + "restart": true, + "required": true, + } + } + } + } + } + + if n, ok := service["volumes_from"]; ok { + volumesFrom := n.([]any) + for _, v := range volumesFrom { + vol := v.(string) + if !strings.HasPrefix(vol, types.ContainerPrefix) { + spec := strings.Split(vol, ":") + if _, ok := dependsOn[spec[0]]; !ok { + dependsOn[spec[0]] = map[string]any{ + "condition": types.ServiceConditionStarted, + "restart": false, + "required": true, + } + } + } + } + } + if len(dependsOn) > 0 { + service["depends_on"] = dependsOn + } + services[name] = service + } + dict["services"] = services } - // If not declared explicitly, Compose model involves an implicit "default" network - if _, ok := project.Networks["default"]; !ok { - project.Networks["default"] = types.NetworkConfig{} - } + setNameFromKey(dict) - for name, s := range project.Services { - if len(s.Networks) == 0 && s.NetworkMode == "" { - // Service without explicit network attachment are implicitly exposed on default network - s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} - } - - if s.PullPolicy == types.PullPolicyIfNotPresent { - s.PullPolicy = types.PullPolicyMissing - } - - fn := func(s string) (string, bool) { - v, ok := project.Environment[s] - return v, ok - } - - if s.Build != nil { - if s.Build.Context == "" { - s.Build.Context = "." - } - if s.Build.Dockerfile == "" && s.Build.DockerfileInline == "" { - s.Build.Dockerfile = "Dockerfile" - } - s.Build.Args = s.Build.Args.Resolve(fn) - } - s.Environment = s.Environment.Resolve(fn) - - for _, link := range s.Links { - parts := strings.Split(link, ":") - if len(parts) == 2 { - link = parts[0] - } - s.DependsOn = setIfMissing(s.DependsOn, link, types.ServiceDependency{ - Condition: types.ServiceConditionStarted, - Restart: true, - Required: true, - }) - } - - for _, namespace := range []string{s.NetworkMode, s.Ipc, s.Pid, s.Uts, s.Cgroup} { - if strings.HasPrefix(namespace, types.ServicePrefix) { - name := namespace[len(types.ServicePrefix):] - s.DependsOn = setIfMissing(s.DependsOn, name, types.ServiceDependency{ - Condition: types.ServiceConditionStarted, - Restart: true, - Required: true, - }) - } - } - - for _, vol := range s.VolumesFrom { - if !strings.HasPrefix(vol, types.ContainerPrefix) { - spec := strings.Split(vol, ":") - s.DependsOn = setIfMissing(s.DependsOn, spec[0], types.ServiceDependency{ - Condition: types.ServiceConditionStarted, - Restart: false, - Required: true, - }) - } - } - - err := relocateLogDriver(&s) - if err != nil { - return err - } - - err = relocateLogOpt(&s) - if err != nil { - return err - } - - err = relocateDockerfile(&s) - if err != nil { - return err - } - - inferImplicitDependencies(&s) - - project.Services[name] = s - } - - setNameFromKey(project) - - return nil + return dict, nil } -// IsServiceDependency check the relation set by ref refers to a service -func IsServiceDependency(ref string) (string, bool) { - if strings.HasPrefix( - ref, - types.ServicePrefix, - ) { - return ref[len(types.ServicePrefix):], true +func normalizeNetworks(dict map[string]any) map[string]any { + var networks map[string]any + if n, ok := dict["networks"]; ok { + networks = n.(map[string]any) + } else { + networks = map[string]any{} } - return "", false + if _, ok := networks["default"]; !ok { + // If not declared explicitly, Compose model involves an implicit "default" network + networks["default"] = nil + } + return networks } -func inferImplicitDependencies(service *types.ServiceConfig) { - var dependencies []string - - maybeReferences := []string{ - service.NetworkMode, - service.Ipc, - service.Pid, - service.Uts, - service.Cgroup, - } - for _, ref := range maybeReferences { - if dep, ok := IsServiceDependency(ref); ok { - dependencies = append(dependencies, dep) - } - } - - for _, vol := range service.VolumesFrom { - spec := strings.Split(vol, ":") - if len(spec) == 0 { - continue - } - if spec[0] == "container" { - continue - } - dependencies = append(dependencies, spec[0]) - } - - for _, link := range service.Links { - dependencies = append(dependencies, strings.Split(link, ":")[0]) - } - - if len(dependencies) > 0 && service.DependsOn == nil { - service.DependsOn = make(types.DependsOnConfig) - } - - for _, d := range dependencies { - if _, ok := service.DependsOn[d]; !ok { - service.DependsOn[d] = types.ServiceDependency{ - Condition: types.ServiceConditionStarted, - Required: true, +func resolve(a any, fn func(s string) (string, bool)) (any, bool) { + switch v := a.(type) { + case []any: + var resolved []any + for _, val := range v { + if r, ok := resolve(val, fn); ok { + resolved = append(resolved, r) } } + return resolved, true + case map[string]any: + resolved := map[string]any{} + for key, val := range v { + if val != nil { + resolved[key] = val + continue + } + if s, ok := fn(key); ok { + resolved[key] = s + } + } + return resolved, true + case string: + if !strings.Contains(v, "=") { + if val, ok := fn(v); ok { + return fmt.Sprintf("%s=%s", v, val), true + } + return "", false + } + return v, true + default: + return v, false } } -// setIfMissing adds a ServiceDependency for service if not already defined -func setIfMissing(d types.DependsOnConfig, service string, dep types.ServiceDependency) types.DependsOnConfig { - if d == nil { - d = types.DependsOnConfig{} - } - if _, ok := d[service]; !ok { - d[service] = dep - } - return d -} - // Resources with no explicit name are actually named by their key in map -func setNameFromKey(project *types.Project) { - for key, n := range project.Networks { - if n.Name == "" { - if n.External { - n.Name = key - } else { - n.Name = fmt.Sprintf("%s_%s", project.Name, key) - } - project.Networks[key] = n +func setNameFromKey(dict map[string]any) { + for _, r := range []string{"networks", "volumes", "configs", "secrets"} { + a, ok := dict[r] + if !ok { + continue } - } - - for key, v := range project.Volumes { - if v.Name == "" { - if v.External { - v.Name = key + toplevel := a.(map[string]any) + for key, r := range toplevel { + var resource map[string]any + if r != nil { + resource = r.(map[string]any) } else { - v.Name = fmt.Sprintf("%s_%s", project.Name, key) + resource = map[string]any{} } - project.Volumes[key] = v - } - } - - for key, c := range project.Configs { - if c.Name == "" { - if c.External { - c.Name = key - } else { - c.Name = fmt.Sprintf("%s_%s", project.Name, key) + if resource["name"] == nil { + if x, ok := resource["external"]; ok && isTrue(x) { + resource["name"] = key + } else { + resource["name"] = fmt.Sprintf("%s_%s", dict["name"], key) + } } - project.Configs[key] = c - } - } - - for key, s := range project.Secrets { - if s.Name == "" { - if s.External { - s.Name = key - } else { - s.Name = fmt.Sprintf("%s_%s", project.Name, key) - } - project.Secrets[key] = s + toplevel[key] = resource } } } -func relocateLogOpt(s *types.ServiceConfig) error { - if len(s.LogOpt) != 0 { - logrus.Warn("`log_opts` is deprecated. Use the `logging` element") - if s.Logging == nil { - s.Logging = &types.LoggingConfig{} - } - for k, v := range s.LogOpt { - if _, ok := s.Logging.Options[k]; !ok { - s.Logging.Options[k] = v - } else { - return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options': %w", errdefs.ErrInvalid) - } - } - } - return nil -} - -func relocateLogDriver(s *types.ServiceConfig) error { - if s.LogDriver != "" { - logrus.Warn("`log_driver` is deprecated. Use the `logging` element") - if s.Logging == nil { - s.Logging = &types.LoggingConfig{} - } - if s.Logging.Driver == "" { - s.Logging.Driver = s.LogDriver - } else { - return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver': %w", errdefs.ErrInvalid) - } - } - return nil -} - -func relocateDockerfile(s *types.ServiceConfig) error { - if s.Dockerfile != "" { - logrus.Warn("`dockerfile` is deprecated. Use the `build` element") - if s.Build == nil { - s.Build = &types.BuildConfig{} - } - if s.Dockerfile == "" { - s.Build.Dockerfile = s.Dockerfile - } else { - return fmt.Errorf("can't use both 'dockerfile' (deprecated) and 'build.dockerfile': %w", errdefs.ErrInvalid) - } - } - return nil +func isTrue(x any) bool { + parseBool, _ := strconv.ParseBool(fmt.Sprint(x)) + return parseBool } diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go b/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go index f7f6e2aa..973e5213 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/validate.go @@ -17,7 +17,6 @@ package loader import ( - "context" "errors" "fmt" "strings" @@ -71,17 +70,14 @@ func checkConsistency(project *types.Project) error { } } - for dependedService := range s.DependsOn { + for dependedService, cfg := range s.DependsOn { if _, err := project.GetService(dependedService); err != nil { - return fmt.Errorf("service %q depends on undefined service %s: %w", s.Name, dependedService, errdefs.ErrInvalid) + if errors.Is(err, errdefs.ErrDisabled) && !cfg.Required { + continue + } + return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid) } } - // Check there isn't a cycle in depends_on declarations - if err := graph.InDependencyOrder(context.Background(), project, func(ctx context.Context, s string, config types.ServiceConfig) error { - return nil - }); err != nil { - return err - } if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) { serviceName := s.NetworkMode[len(types.ServicePrefix):] @@ -124,6 +120,31 @@ func checkConsistency(project *types.Project) error { s.Deploy.Replicas = s.Scale } + if s.CPUS != 0 && s.Deploy != nil { + if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.NanoCPUs.Value() != s.CPUS { + return fmt.Errorf("services.%s: can't set distinct values on 'cpus' and 'deploy.resources.limits.cpus': %w", + s.Name, errdefs.ErrInvalid) + } + } + if s.MemLimit != 0 && s.Deploy != nil { + if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.MemoryBytes != s.MemLimit { + return fmt.Errorf("services.%s: can't set distinct values on 'mem_limit' and 'deploy.resources.limits.memory': %w", + s.Name, errdefs.ErrInvalid) + } + } + if s.MemReservation != 0 && s.Deploy != nil { + if s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != s.MemReservation { + return fmt.Errorf("services.%s: can't set distinct values on 'mem_reservation' and 'deploy.resources.reservations.memory': %w", + s.Name, errdefs.ErrInvalid) + } + } + if s.PidsLimit != 0 && s.Deploy != nil { + if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.Pids != s.PidsLimit { + return fmt.Errorf("services.%s: can't set distinct values on 'pids_limit' and 'deploy.resources.limits.pids': %w", + s.Name, errdefs.ErrInvalid) + } + } + 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) @@ -159,5 +180,5 @@ func checkConsistency(project *types.Project) error { } } - return nil + return graph.CheckCycle(project) } diff --git a/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json b/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json index e62ca35e..3a4ee727 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json +++ b/vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json @@ -322,11 +322,13 @@ { "type": "object", "properties": { + "name": {"type": "string"}, "mode": {"type": "string"}, "host_ip": {"type": "string"}, "target": {"type": "integer"}, "published": {"type": ["string", "integer"]}, - "protocol": {"type": "string"} + "protocol": {"type": "string"}, + "app_protocol": {"type": "string"} }, "additionalProperties": false, "patternProperties": {"^x-": {}} @@ -389,7 +391,8 @@ "volume": { "type": "object", "properties": { - "nocopy": {"type": "boolean"} + "nocopy": {"type": "boolean"}, + "subpath": {"type": "string"} }, "additionalProperties": false, "patternProperties": {"^x-": {}} diff --git a/vendor/github.com/compose-spec/compose-go/v2/template/template.go b/vendor/github.com/compose-spec/compose-go/v2/template/template.go index 63ae61cc..0d9c17ef 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/template/template.go +++ b/vendor/github.com/compose-spec/compose-go/v2/template/template.go @@ -44,7 +44,7 @@ var patternString = fmt.Sprintf( groupInvalid, ) -var defaultPattern = regexp.MustCompile(patternString) +var DefaultPattern = regexp.MustCompile(patternString) // InvalidTemplateError is returned when a variable template is not in a valid // format @@ -121,7 +121,7 @@ func SubstituteWithOptions(template string, mapping Mapping, options ...Option) var returnErr error cfg := &Config{ - pattern: defaultPattern, + pattern: DefaultPattern, replacementFunc: DefaultReplacementFunc, logging: true, } @@ -268,14 +268,14 @@ func getFirstBraceClosingIndex(s string) int { // Substitute variables in the string with their values 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 + pattern = DefaultPattern } return recurseExtract(configDict, pattern) } diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/project.go b/vendor/github.com/compose-spec/compose-go/v2/types/project.go index afd787ef..780dd91c 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/types/project.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/project.go @@ -18,6 +18,7 @@ package types import ( "bytes" + "context" "encoding/json" "fmt" "os" @@ -25,6 +26,7 @@ import ( "sort" "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/errdefs" "github.com/compose-spec/compose-go/v2/utils" "github.com/distribution/reference" "github.com/mitchellh/copystructure" @@ -215,9 +217,9 @@ func (p *Project) GetService(name string) (ServiceConfig, error) { if !ok { _, ok := p.DisabledServices[name] if ok { - return ServiceConfig{}, fmt.Errorf("service %s is disabled", name) + return ServiceConfig{}, fmt.Errorf("no such service: %s: %w", name, errdefs.ErrDisabled) } - return ServiceConfig{}, fmt.Errorf("no such service: %s", name) + return ServiceConfig{}, fmt.Errorf("no such service: %s: %w", name, errdefs.ErrNotFound) } return service, nil } @@ -331,6 +333,9 @@ func (s ServiceConfig) HasProfile(profiles []string) bool { return true } for _, p := range profiles { + if p == "*" { + return true + } for _, sp := range s.Profiles { if sp == p { return true @@ -344,11 +349,6 @@ func (s ServiceConfig) HasProfile(profiles []string) bool { // It returns a new Project instance with the changes and keep the original Project unchanged func (p *Project) WithProfiles(profiles []string) (*Project, error) { newProject := p.deepCopy() - for _, p := range profiles { - if p == "*" { - return newProject, nil - } - } enabled := Services{} disabled := Services{} for name, service := range newProject.AllServices() { @@ -536,39 +536,29 @@ func (p *Project) WithServicesDisabled(names ...string) *Project { // WithImagesResolved updates services images to include digest computed by a resolver function // It returns a new Project instance with the changes and keep the original Project unchanged func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godigest.Digest, error)) (*Project, error) { - newProject := p.deepCopy() - eg := errgroup.Group{} - for i, s := range newProject.Services { - idx := i - service := s - + return p.WithServicesTransform(func(name string, service ServiceConfig) (ServiceConfig, error) { if service.Image == "" { - continue + return service, nil } - eg.Go(func() error { - named, err := reference.ParseDockerRef(service.Image) + named, err := reference.ParseDockerRef(service.Image) + if err != nil { + return service, err + } + + if _, ok := named.(reference.Canonical); !ok { + // image is named but not digested reference + digest, err := resolver(named) if err != nil { - return err + return service, err } - - if _, ok := named.(reference.Canonical); !ok { - // image is named but not digested reference - digest, err := resolver(named) - if err != nil { - return err - } - named, err = reference.WithDigest(named, digest) - if err != nil { - return err - } + named, err = reference.WithDigest(named, digest) + if err != nil { + return service, err } - - service.Image = named.String() - newProject.Services[idx] = service - return nil - }) - } - return newProject, eg.Wait() + } + service.Image = named.String() + return service, nil + }) } // MarshalYAML marshal Project into a yaml tree @@ -606,7 +596,7 @@ func (p *Project) MarshalJSON() ([]byte, error) { for k, v := range p.Extensions { m[k] = v } - return json.Marshal(m) + return json.MarshalIndent(m, "", " ") } // WithServicesEnvironmentResolved parses env_files set for services to resolve the actual environment map for services @@ -662,3 +652,47 @@ func (p *Project) deepCopy() *Project { } return instance.(*Project) } + +// WithServicesTransform applies a transformation to project services and return a new project with transformation results +func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (ServiceConfig, error)) (*Project, error) { + type result struct { + name string + service ServiceConfig + } + resultCh := make(chan result) + newProject := p.deepCopy() + + eg, ctx := errgroup.WithContext(context.Background()) + eg.Go(func() error { + expect := len(newProject.Services) + s := Services{} + for expect > 0 { + select { + case <-ctx.Done(): + // interrupted as some goroutine returned an error + return nil + case r := <-resultCh: + s[r.name] = r.service + expect-- + } + } + newProject.Services = s + return nil + }) + for n, s := range newProject.Services { + name := n + service := s + eg.Go(func() error { + updated, err := fn(name, service) + if err != nil { + return err + } + resultCh <- result{ + name: name, + service: updated, + } + return nil + }) + } + return newProject, eg.Wait() +} diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/types.go b/vendor/github.com/compose-spec/compose-go/v2/types/types.go index 35c4a4e3..2ee7f4c4 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/types/types.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/types.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "sort" + "strconv" "strings" "github.com/docker/go-connections/nat" @@ -365,7 +366,7 @@ type Resources struct { // Resource is a resource to be limited or reserved type Resource struct { // TODO: types to convert from units and ratios - NanoCPUs string `yaml:"cpus,omitempty" json:"cpus,omitempty"` + NanoCPUs NanoCPUs `yaml:"cpus,omitempty" json:"cpus,omitempty"` MemoryBytes UnitBytes `yaml:"memory,omitempty" json:"memory,omitempty"` Pids int64 `yaml:"pids,omitempty" json:"pids,omitempty"` Devices []DeviceRequest `yaml:"devices,omitempty" json:"devices,omitempty"` @@ -374,6 +375,30 @@ 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 { @@ -433,11 +458,13 @@ type ServiceNetworkConfig struct { // ServicePortConfig is the port configuration for a service type ServicePortConfig struct { - Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` - HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"` - Target uint32 `yaml:"target,omitempty" json:"target,omitempty"` - Published string `yaml:"published,omitempty" json:"published,omitempty"` - Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` + HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"` + Target uint32 `yaml:"target,omitempty" json:"target,omitempty"` + Published string `yaml:"published,omitempty" json:"published,omitempty"` + Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` + AppProtocol string `yaml:"app_protocol,omitempty" json:"app_protocol,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } @@ -511,6 +538,9 @@ 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, ",")) } @@ -567,7 +597,8 @@ const ( // ServiceVolumeVolume are options for a service volume of type volume type ServiceVolumeVolume struct { - NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"` + NoCopy bool `yaml:"nocopy,omitempty" json:"nocopy,omitempty"` + Subpath string `yaml:"subpath,omitempty" json:"subpath,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } diff --git a/vendor/modules.txt b/vendor/modules.txt index bdc7fb02..b314e61b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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.0-rc.8 +# github.com/compose-spec/compose-go/v2 v2.0.2 ## explicit; go 1.21 github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/consts