diff --git a/go.mod b/go.mod index 92550f9e3..8c41ac0f3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/ProtonMail/go-crypto v1.3.0 github.com/aws/aws-sdk-go-v2/config v1.32.24 - github.com/compose-spec/compose-go/v2 v2.11.0 + github.com/compose-spec/compose-go/v2 v2.13.0 github.com/containerd/console v1.0.5 github.com/containerd/containerd/v2 v2.2.5 github.com/containerd/continuity v0.5.0 diff --git a/go.sum b/go.sum index 83e1bceb1..04b31a48e 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM= -github.com/compose-spec/compose-go/v2 v2.11.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE= +github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= 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 69ea56543..884ecfc05 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 @@ -383,6 +383,24 @@ func WithoutEnvironmentResolution(o *ProjectOptions) error { return nil } +// WithSelectedServices restricts the loaded project to the given services and their +// dependencies. An empty list means "all services". When set, services not in the +// list are dropped from the project before environment resolution, so their +// `env_file` / `label_file` entries are not loaded from disk. +func WithSelectedServices(services ...string) ProjectOptionsFn { + return func(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, loader.WithSelectedServices(services)) + return nil + } +} + +// WithoutUnnecessaryResources drops networks/volumes/secrets/configs/models that +// are not referenced by services remaining after selection. +func WithoutUnnecessaryResources(o *ProjectOptions) error { + o.loadOptions = append(o.loadOptions, loader.WithoutUnnecessaryResources) + return nil +} + // DefaultFileNames defines the Compose file names for auto-discovery (in order of preference) var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} 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 4f3d59d24..24911eb75 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 @@ -78,6 +78,14 @@ type Options struct { projectNameImperativelySet bool // Profiles set profiles to enable Profiles []string + // SelectedServices restricts the project model to these services (and their dependencies) + // after parsing. An empty slice means "all services". When set, services not in the list + // are dropped from the project before environment resolution, so their env_file / label_file + // entries are not loaded. + SelectedServices []string + // PruneUnnecessaryResources drops networks/volumes/secrets/configs/models that are not + // referenced by active services after service selection. + PruneUnnecessaryResources bool // ResourceLoaders manages support for remote resources ResourceLoaders []ResourceLoader // KnownExtensions manages x-* attribute we know and the corresponding go structs @@ -187,6 +195,8 @@ func (o *Options) clone() *Options { projectName: o.projectName, projectNameImperativelySet: o.projectNameImperativelySet, Profiles: o.Profiles, + SelectedServices: o.SelectedServices, + PruneUnnecessaryResources: o.PruneUnnecessaryResources, ResourceLoaders: o.ResourceLoaders, KnownExtensions: o.KnownExtensions, Listeners: o.Listeners, @@ -260,6 +270,22 @@ func WithProfiles(profiles []string) func(*Options) { } } +// WithSelectedServices restricts the loaded project to the given services and their +// dependencies. An empty slice means "all services". When set, services not in the +// list are dropped from the project before environment resolution: their `env_file` +// and `label_file` entries will not be loaded from disk. +func WithSelectedServices(services []string) func(*Options) { + return func(opts *Options) { + opts.SelectedServices = services + } +} + +// WithoutUnnecessaryResources drops networks/volumes/secrets/configs/models that +// are not referenced by services remaining after selection. +func WithoutUnnecessaryResources(opts *Options) { + opts.PruneUnnecessaryResources = true +} + // PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase // that hardly can be implemented using go-yaml and mapstructure type PostProcessor interface { @@ -603,6 +629,24 @@ func ModelToProject(dict map[string]interface{}, opts *Options, configDetails ty } } + if len(opts.SelectedServices) > 0 { + // WithServicesEnabled must precede WithSelectedServices: the latter walks + // only active services, so any selected service currently sitting in + // DisabledServices (e.g. gated by a profile) would otherwise be invisible. + project, err = project.WithServicesEnabled(opts.SelectedServices...) + if err != nil { + return nil, err + } + project, err = project.WithSelectedServices(opts.SelectedServices) + if err != nil { + return nil, err + } + } + + if opts.PruneUnnecessaryResources { + project = project.WithoutUnnecessaryResources() + } + if !opts.SkipResolveEnvironment { project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles) if err != 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 7b1c4941c..165ce4e56 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 @@ -133,6 +133,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { if len(dependsOn) > 0 { service["depends_on"] = dependsOn } + + inheritPreStartImage(service) + services[name] = service } @@ -143,6 +146,25 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { return dict, nil } +// inheritPreStartImage propagates the parent service's image to any pre_start +// hook that does not declare its own, per compose-spec PR #647. +func inheritPreStartImage(service map[string]any) { + hooks, ok := service["pre_start"].([]any) + if !ok { + return + } + image, ok := service["image"].(string) + if !ok || image == "" { + return + } + for _, h := range hooks { + hook := h.(map[string]any) + if _, set := hook["image"]; !set { + hook["image"] = image + } + } +} + func normalizeNetworks(dict map[string]any) { var networks map[string]any if n, ok := dict["networks"]; ok { diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/reset.go b/vendor/github.com/compose-spec/compose-go/v2/loader/reset.go index 7a07dfeb5..78836b5ca 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/reset.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/reset.go @@ -40,7 +40,7 @@ type nodeCache struct { type ResetProcessor struct { target any paths []tree.Path - visitedNodes map[*yaml.Node][]string + visitedNodes map[*yaml.Node][]tree.Path resolvedNodes map[*yaml.Node]nodeCache visitCount int // maxNodeVisits is the per-document cap; when zero, defaultMaxNodeVisits is used. @@ -49,7 +49,7 @@ type ResetProcessor struct { // UnmarshalYAML implement yaml.Unmarshaler func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error { - p.visitedNodes = make(map[*yaml.Node][]string) + p.visitedNodes = make(map[*yaml.Node][]tree.Path) p.resolvedNodes = make(map[*yaml.Node]nodeCache) p.visitCount = 0 defer func() { @@ -190,7 +190,12 @@ func (p *ResetProcessor) resolveContainer(node *yaml.Node, path tree.Path) (*yam if resolved == nil { continue } - if v.Kind == yaml.AliasNode { + // Under the merge key `<<`, the YAML library only accepts an + // AliasNode value when its target is a MappingNode. An alias to a + // SequenceNode (the spec-allowed "sequence of mappings" form via an + // anchor) is rejected. Substitute the resolved target so the YAML + // library sees the underlying node directly for merge keys. + if v.Kind == yaml.AliasNode && key != "<<" { nodes = append(nodes, node.Content[idx-1], v) } else { nodes = append(nodes, node.Content[idx-1], resolved) @@ -278,36 +283,41 @@ func (p *ResetProcessor) applyNullOverrides(target any, path tree.Path) error { func (p *ResetProcessor) checkForCycle(node *yaml.Node, path tree.Path) error { paths := p.visitedNodes[node] - pathStr := path.String() for _, prevPath := range paths { // If we're visiting the exact same path, it's not a cycle - if pathStr == prevPath { + if path == prevPath { continue } + // Compare on the raw form so dots inside escaped segment names (e.g. + // service names containing ".") aren't conflated with path separators. + pathStr := string(path) + prevStr := string(prevPath) + // If either path is using a merge key, it's legitimate YAML merging - if strings.Contains(prevPath, "<<") || strings.Contains(pathStr, "<<") { + if strings.Contains(prevStr, "<<") || strings.Contains(pathStr, "<<") { continue } // Only consider it a cycle if one path is contained within the other // and they're not in different service definitions - if (strings.HasPrefix(pathStr, prevPath+".") || - strings.HasPrefix(prevPath, pathStr+".")) && - !areInDifferentServices(pathStr, prevPath) { - return fmt.Errorf("cycle detected: node at path %s references node at path %s", pathStr, prevPath) + if (strings.HasPrefix(pathStr, prevStr+".") || + strings.HasPrefix(prevStr, pathStr+".")) && + !areInDifferentServices(path, prevPath) { + return fmt.Errorf("cycle detected: node at path %s references node at path %s", + path.String(), prevPath.String()) } } - p.visitedNodes[node] = append(paths, pathStr) + p.visitedNodes[node] = append(paths, path) return nil } // areInDifferentServices checks if two paths are in different service definitions -func areInDifferentServices(path1, path2 string) bool { - parts1 := strings.Split(path1, ".") - parts2 := strings.Split(path2, ".") +func areInDifferentServices(path1, path2 tree.Path) bool { + parts1 := path1.Parts() + parts2 := path2.Parts() for i := 0; i < len(parts1) && i < len(parts2); i++ { if parts1[i] == "services" && i+1 < len(parts1) && parts2[i] == "services" && i+1 < len(parts2) { 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 8a551d73b..fe0e45d68 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 @@ -688,6 +688,11 @@ }, "uniqueItems": true }, + "pre_start": { + "type": "array", + "items": {"$ref": "#/$defs/pre_start_hook"}, + "description": "Init containers to run to completion before the service container is started. Each step runs in its own ephemeral container, in declared order; a non-zero exit fails the bring-up of the service and its dependents." + }, "post_start": { "type": "array", "items": {"$ref": "#/$defs/service_hook"}, @@ -1657,6 +1662,43 @@ "required": ["command"] }, + "pre_start_hook": { + "type": "object", + "description": "Configuration for a pre_start init container, run to completion before the service container starts.", + "properties": { + "command": { + "$ref": "#/$defs/command", + "description": "Command to execute. Optional when the chosen image's entrypoint already runs the intended command." + }, + "image": { + "type": "string", + "description": "Image used for the ephemeral container. If omitted, the parent service's image is used." + }, + "user": { + "type": "string", + "description": "User to run the command as. Defaults to the user declared in image (or to the service's user when image is omitted)." + }, + "privileged": { + "type": ["boolean", "string"], + "description": "Whether to run the command with extended privileges." + }, + "working_dir": { + "type": "string", + "description": "Working directory for the command. Defaults to the service's working directory." + }, + "environment": { + "$ref": "#/$defs/list_or_dict", + "description": "Environment variables for the command. Appended to or overriding the service environment." + }, + "per_replica": { + "type": ["boolean", "string"], + "description": "Whether the hook runs once per service replica (true), or once for the service as a whole before any replica starts (false, the default)." + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "env_file": { "oneOf": [ { diff --git a/vendor/github.com/compose-spec/compose-go/v2/schema/schema.go b/vendor/github.com/compose-spec/compose-go/v2/schema/schema.go index a73eda245..a765e3378 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/schema/schema.go +++ b/vendor/github.com/compose-spec/compose-go/v2/schema/schema.go @@ -24,6 +24,7 @@ import ( "fmt" "slices" "strings" + "sync" "time" "github.com/santhosh-tekuri/jsonschema/v6" @@ -46,22 +47,45 @@ func durationFormatChecker(input any) error { //go:embed compose-spec.json var Schema string +// compiledSchema is the compose-spec schema compiled once and reused across +// every Validate call. Compiling the schema (which loads and resolves the +// draft 2020-12 meta-schema) is expensive and was previously redone on every +// call; doing it once behind a sync.Once keeps Validate cheap and makes it +// safe to call from several goroutines without sharing any mutable compiler +// state. see https://github.com/docker/compose/issues/13866 +var ( + compiledSchema *jsonschema.Schema + compiledSchemaErr error + compiledSchemaOnce sync.Once +) + +func compileSchema() (*jsonschema.Schema, error) { + compiledSchemaOnce.Do(func() { + compiler := jsonschema.NewCompiler() + shema, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) + if err != nil { + compiledSchemaErr = err + return + } + if err := compiler.AddResource("compose-spec.json", shema); err != nil { + compiledSchemaErr = err + return + } + compiler.RegisterFormat(&jsonschema.Format{ + Name: "duration", + Validate: durationFormatChecker, + }) + compiledSchema, compiledSchemaErr = compiler.Compile("compose-spec.json") + }) + return compiledSchema, compiledSchemaErr +} + // Validate uses the jsonschema to validate the configuration func Validate(config map[string]interface{}) error { - compiler := jsonschema.NewCompiler() - shema, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema)) + schema, err := compileSchema() if err != nil { return err } - err = compiler.AddResource("compose-spec.json", shema) - if err != nil { - return err - } - compiler.RegisterFormat(&jsonschema.Format{ - Name: "duration", - Validate: durationFormatChecker, - }) - schema := compiler.MustCompile("compose-spec.json") // santhosh-tekuri doesn't allow derived types // see https://github.com/santhosh-tekuri/jsonschema/pull/240 diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/derived.gen.go b/vendor/github.com/compose-spec/compose-go/v2/types/derived.gen.go index e284fa9f5..c758d3ff5 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/types/derived.gen.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/derived.gen.go @@ -729,6 +729,24 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { copy(dst.VolumesFrom, src.VolumesFrom) } dst.WorkingDir = src.WorkingDir + if src.PreStart == nil { + dst.PreStart = nil + } else { + if dst.PreStart != nil { + if len(src.PreStart) > len(dst.PreStart) { + if cap(dst.PreStart) >= len(src.PreStart) { + dst.PreStart = (dst.PreStart)[:len(src.PreStart)] + } else { + dst.PreStart = make([]ServiceHook, len(src.PreStart)) + } + } else if len(src.PreStart) < len(dst.PreStart) { + dst.PreStart = (dst.PreStart)[:len(src.PreStart)] + } + } else { + dst.PreStart = make([]ServiceHook, len(src.PreStart)) + } + deriveDeepCopy_26(dst.PreStart, src.PreStart) + } if src.PostStart == nil { dst.PostStart = nil } else { @@ -2101,6 +2119,7 @@ func deriveDeepCopy_49(dst, src *ServiceHook) { } copy(dst.Command, src.Command) } + dst.Image = src.Image dst.User = src.User dst.Privileged = src.Privileged dst.WorkingDir = src.WorkingDir @@ -2110,6 +2129,7 @@ func deriveDeepCopy_49(dst, src *ServiceHook) { } else { dst.Environment = nil } + dst.PerReplica = src.PerReplica if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2139,6 +2159,12 @@ func deriveDeepCopy_50(dst, src *IPAMConfig) { } deriveDeepCopy_60(dst.Config, src.Config) } + if src.Options != nil { + dst.Options = make(map[string]string, len(src.Options)) + deriveDeepCopy_5(dst.Options, src.Options) + } else { + dst.Options = nil + } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/hooks.go b/vendor/github.com/compose-spec/compose-go/v2/types/hooks.go index 4c58c0949..6eca7c94d 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/types/hooks.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/hooks.go @@ -16,13 +16,17 @@ package types -// ServiceHook is a command to exec inside container by some lifecycle events +// ServiceHook is a hook executed at a service lifecycle event: a command exec'd +// inside the service container for post_start/pre_stop, or an ephemeral +// container run before the service starts for pre_start. type ServiceHook struct { Command ShellCommand `yaml:"command,omitempty" json:"command"` + Image string `yaml:"image,omitempty" json:"image,omitempty"` User string `yaml:"user,omitempty" json:"user,omitempty"` Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` + PerReplica bool `yaml:"per_replica,omitempty" json:"per_replica,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } 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 fd4f35136..66987e9fe 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 @@ -138,6 +138,7 @@ type ServiceConfig struct { Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"` VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` + PreStart []ServiceHook `yaml:"pre_start,omitempty" json:"pre_start,omitempty"` PostStart []ServiceHook `yaml:"post_start,omitempty" json:"post_start,omitempty"` PreStop []ServiceHook `yaml:"pre_stop,omitempty" json:"pre_stop,omitempty"` @@ -753,6 +754,7 @@ type NetworkConfig struct { type IPAMConfig struct { Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` Config []*IPAMPool `yaml:"config,omitempty" json:"config,omitempty"` + Options Options `yaml:"options,omitempty" json:"options,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } diff --git a/vendor/modules.txt b/vendor/modules.txt index bac63a2b0..e7b132ec4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -190,7 +190,7 @@ github.com/cloudflare/circl/math/mlsbset github.com/cloudflare/circl/sign github.com/cloudflare/circl/sign/ed25519 github.com/cloudflare/circl/sign/ed448 -# github.com/compose-spec/compose-go/v2 v2.11.0 +# github.com/compose-spec/compose-go/v2 v2.13.0 ## explicit; go 1.24 github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/consts