diff --git a/bake/compose.go b/bake/compose.go index 8841d96bfb29..528e1fd05a2b 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -266,10 +266,6 @@ func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, op return nil, errors.New("empty compose file") } - // compose-go schema validation does a JSON round trip that converts nil slices - // from YAML [] values into null, so keep empty lists as arrays before validation. - // buildx#3849 - normalizeEmptyLists(filtered) if err := composeschema.Validate(filtered); err != nil { return nil, err } @@ -283,23 +279,6 @@ func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, op }) } -func normalizeEmptyLists(value any) any { - switch v := value.(type) { - case []any: - if v == nil { - return []any{} - } - for i, e := range v { - v[i] = normalizeEmptyLists(e) - } - case map[string]any: - for k, e := range v { - v[k] = normalizeEmptyLists(e) - } - } - return value -} - func validateComposeFile(dt []byte, fn string, envOverrides map[string]string) (bool, error) { envs, err := composeEnv(envOverrides) if err != nil { diff --git a/go.mod b/go.mod index 0410b39b1372..9214b09177e5 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.10.2 + github.com/compose-spec/compose-go/v2 v2.11.0 github.com/containerd/console v1.0.5 github.com/containerd/containerd/v2 v2.2.4 github.com/containerd/continuity v0.5.0 diff --git a/go.sum b/go.sum index 63542cfbd208..edb193abf900 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.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= -github.com/compose-spec/compose-go/v2 v2.10.2/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= +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/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/loader/loader.go b/vendor/github.com/compose-spec/compose-go/v2/loader/loader.go index f73ad92e80db..4f3d59d249d5 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 @@ -84,6 +84,9 @@ type Options struct { KnownExtensions map[string]any // Metada for telemetry Listeners []Listener + // MaxNodeVisits caps total YAML node visits during reset/override resolution. + // Zero means use the default. Useful for very large compose files that exceed the default cap. + MaxNodeVisits int } var versionWarning []string @@ -446,8 +449,10 @@ func loadYamlFile(ctx context.Context, fixEmptyNotNull(cfg) - if !opts.SkipExtends { - err = ApplyExtends(ctx, cfg, opts, ct, processor) + // Process includes first so that extended services have all merged attributes + if !opts.SkipInclude { + included = append(included, file.Filename) + err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor) if err != nil { return err } @@ -457,12 +462,13 @@ func loadYamlFile(ctx context.Context, return err } - if !opts.SkipInclude { - included = append(included, file.Filename) - err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor) + // Process extends after includes so base services are fully merged + if !opts.SkipExtends { + err = ApplyExtends(ctx, cfg, opts, ct, processor) if err != nil { return err } + } dict, err = override.Merge(dict, cfg) @@ -503,7 +509,7 @@ func loadYamlFile(ctx context.Context, decoder := yaml.NewDecoder(r) for { var raw interface{} - reset := &ResetProcessor{target: &raw} + reset := &ResetProcessor{target: &raw, maxNodeVisits: opts.MaxNodeVisits} err := decoder.Decode(reset) if err != nil && errors.Is(err, io.EOF) { break diff --git a/vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go b/vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go index eef6be8c56ce..fd2d8e8650b2 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go +++ b/vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go @@ -41,7 +41,7 @@ func omitEmpty(data any, p tree.Path) any { } return v case []any: - var c []any + c := make([]any, 0, len(v)) for _, e := range v { if isEmpty(e) && mustOmit(p) { continue 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 ed1fc0c3f211..7a07dfeb59a6 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 @@ -25,17 +25,38 @@ import ( "go.yaml.in/yaml/v4" ) +// defaultMaxNodeVisits caps total resolveReset calls per document. +// Sized to accommodate large real-world compose files while rejecting documents that would +// cause unbounded traversal. Callers can override this via Options.MaxNodeVisits. +const defaultMaxNodeVisits = 100_000 + +// nodeCache stores a resolved node and the relative sub-paths within its subtree that +// carried !reset/!override tags, so cache hits at different call sites can replay them. +type nodeCache struct { + node *yaml.Node + relativePaths []tree.Path +} + type ResetProcessor struct { - target interface{} - paths []tree.Path - visitedNodes map[*yaml.Node][]string + target any + paths []tree.Path + visitedNodes map[*yaml.Node][]string + resolvedNodes map[*yaml.Node]nodeCache + visitCount int + // maxNodeVisits is the per-document cap; when zero, defaultMaxNodeVisits is used. + maxNodeVisits int } // UnmarshalYAML implement yaml.Unmarshaler func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error { p.visitedNodes = make(map[*yaml.Node][]string) + p.resolvedNodes = make(map[*yaml.Node]nodeCache) + p.visitCount = 0 + defer func() { + p.visitedNodes = nil + p.resolvedNodes = nil + }() resolved, err := p.resolveReset(value, tree.NewPath()) - p.visitedNodes = nil if err != nil { return err } @@ -44,31 +65,93 @@ 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) { + p.visitCount++ + limit := p.maxNodeVisits + if limit <= 0 { + limit = defaultMaxNodeVisits + } + if p.visitCount > limit { + return nil, fmt.Errorf("compose file exceeds maximum node visit limit (%d)", limit) + } + pathStr := path.String() // If the path contains "<<", removing the "<<" element and merging the path if strings.Contains(pathStr, ".<<") { path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 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.Tag == "!reset" { + p.paths = append(p.paths, path) + return nil, nil + } + if node.Tag == "!override" { + p.paths = append(p.paths, path) + return node, nil + } + + // If the node is an alias, process the alias target via the cache so each anchor is + // processed at most once. if node.Kind == yaml.AliasNode { if err := p.checkForCycle(node.Alias, path); err != nil { return nil, err } + // Handle !reset/!override on the alias target before delegating to the cache, + // keeping all tag-handling logic in resolveReset rather than split across functions. + target := node.Alias + if target.Tag == "!reset" { + p.paths = append(p.paths, path) + return nil, nil + } + if target.Tag == "!override" { + p.paths = append(p.paths, path) + return target, nil + } + return p.cachedResolve(target, path) + } - return p.resolveReset(node.Alias, path) + // Container nodes are resolved through the cache, ensuring resolved containers are + // not re-traversed. + if node.Kind == yaml.SequenceNode || node.Kind == yaml.MappingNode { + return p.cachedResolve(node, path) } - if node.Tag == "!reset" { - p.paths = append(p.paths, path) - return nil, nil + return node, nil +} + +// cachedResolve resolves node (a container without !reset/!override), serving from cache on +// repeat visits to prevent re-traversal. It is only called after tag checks are done in +// resolveReset, so it never receives !reset/!override-tagged nodes. +func (p *ResetProcessor) cachedResolve(node *yaml.Node, path tree.Path) (*yaml.Node, error) { + if cached, ok := p.resolvedNodes[node]; ok { + for _, rel := range cached.relativePaths { + p.paths = append(p.paths, joinPath(path, rel)) + } + return cached.node, nil } - if node.Tag == "!override" { - p.paths = append(p.paths, path) - return node, nil + + startIdx := len(p.paths) + resolved, err := p.resolveContainer(node, path) + if err != nil { + return nil, err } - keys := map[string]int{} + var relPaths []tree.Path + for _, addedPath := range p.paths[startIdx:] { + rel, err := subPath(addedPath, path) + if err != nil { + return nil, err + } + relPaths = append(relPaths, rel) + } + p.resolvedNodes[node] = nodeCache{node: resolved, relativePaths: relPaths} + return resolved, nil +} + +// resolveContainer processes the children of a Sequence or Mapping node. +// AliasNodes must be kept as-is in the output Content; the resolved value is used only +// for tag inspection. Changing this will affect how the YAML library handles the document +// during decoding. +func (p *ResetProcessor) resolveContainer(node *yaml.Node, path tree.Path) (*yaml.Node, error) { switch node.Kind { case yaml.SequenceNode: var nodes []*yaml.Node @@ -78,12 +161,18 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No if err != nil { return nil, err } - if resolved != nil { + if resolved == nil { + continue + } + if v.Kind == yaml.AliasNode { + nodes = append(nodes, v) + } else { nodes = append(nodes, resolved) } } node.Content = nodes case yaml.MappingNode: + keys := map[string]int{} var key string var nodes []*yaml.Node for idx, v := range node.Content { @@ -98,7 +187,12 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No if err != nil { return nil, err } - if resolved != nil { + if resolved == nil { + continue + } + if v.Kind == yaml.AliasNode { + nodes = append(nodes, node.Content[idx-1], v) + } else { nodes = append(nodes, node.Content[idx-1], resolved) } } @@ -108,6 +202,38 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No return node, nil } +// subPath strips base from full to produce a relative path for cache storage. +// Returns "" when full == base (the !reset/!override tag is on the node root itself). +// Returns an error when full is not rooted at base, which would indicate a logic error +// in resolveReset/cachedResolve. +func subPath(full, base tree.Path) (tree.Path, error) { + if base == "" { + return full, nil + } + fullStr := string(full) + baseStr := string(base) + if fullStr == baseStr { + return "", nil + } + prefix := baseStr + "." + if strings.HasPrefix(fullStr, prefix) { + return tree.Path(fullStr[len(prefix):]), nil + } + return "", fmt.Errorf("internal error: path %q is not a sub-path of %q", fullStr, baseStr) +} + +// joinPath reconstructs an absolute path from a call-site base and a cached relative path. +// A relative path of "" means the tag was on the node root, so base is returned unchanged. +func joinPath(base, rel tree.Path) tree.Path { + if rel == "" { + return base + } + if base == "" { + return rel + } + return tree.Path(string(base) + "." + string(rel)) +} + // Apply finds the go attributes matching recorded paths and reset them to zero value func (p *ResetProcessor) Apply(target any) error { return p.applyNullOverrides(target, tree.NewPath()) @@ -180,15 +306,11 @@ func (p *ResetProcessor) checkForCycle(node *yaml.Node, path tree.Path) error { // areInDifferentServices checks if two paths are in different service definitions func areInDifferentServices(path1, path2 string) bool { - // Split paths into components parts1 := strings.Split(path1, ".") parts2 := strings.Split(path2, ".") - - // Look for the services component and compare the service names 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) { - // If they're different services, it's not a cycle return parts1[i+1] != parts2[i+1] } } 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 462de285c642..8a551d73b07f 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 @@ -1,5 +1,5 @@ { - "$schema": "https://json-schema.org/draft-07/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "compose_spec.json", "type": "object", "title": "Compose Specification", @@ -20,7 +20,7 @@ "include": { "type": "array", "items": { - "$ref": "#/definitions/include" + "$ref": "#/$defs/include" }, "description": "compose sub-projects to be included." }, @@ -29,7 +29,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" + "$ref": "#/$defs/service" } }, "additionalProperties": false, @@ -40,7 +40,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/model" + "$ref": "#/$defs/model" } }, "description": "Language models that will be used by your application." @@ -51,7 +51,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" + "$ref": "#/$defs/network" } }, "description": "Networks that are shared among multiple services." @@ -61,7 +61,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" + "$ref": "#/$defs/volume" } }, "additionalProperties": false, @@ -72,7 +72,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" + "$ref": "#/$defs/secret" } }, "additionalProperties": false, @@ -83,7 +83,7 @@ "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/config" + "$ref": "#/$defs/config" } }, "additionalProperties": false, @@ -94,15 +94,15 @@ "patternProperties": {"^x-": {}}, "additionalProperties": false, - "definitions": { + "$defs": { "service": { "type": "object", "description": "Configuration for a service.", "properties": { - "develop": {"$ref": "#/definitions/development"}, - "deploy": {"$ref": "#/definitions/deployment"}, - "annotations": {"$ref": "#/definitions/list_or_dict"}, + "develop": {"$ref": "#/$defs/development"}, + "deploy": {"$ref": "#/$defs/deployment"}, + "annotations": {"$ref": "#/$defs/list_or_dict"}, "attach": {"type": ["boolean", "string"]}, "build": { "description": "Configuration options for building the service's image.", @@ -115,26 +115,26 @@ "dockerfile": {"type": "string", "description": "Name of the Dockerfile to use for building the image."}, "dockerfile_inline": {"type": "string", "description": "Inline Dockerfile content to use instead of a Dockerfile from the build context."}, "entitlements": {"type": "array", "items": {"type": "string"}, "description": "List of extra privileged entitlements to grant to the build process."}, - "args": {"$ref": "#/definitions/list_or_dict", "description": "Build-time variables, specified as a map or a list of KEY=VAL pairs."}, - "ssh": {"$ref": "#/definitions/list_or_dict", "description": "SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|[=|[,]]'."}, - "labels": {"$ref": "#/definitions/list_or_dict", "description": "Labels to apply to the built image."}, + "args": {"$ref": "#/$defs/list_or_dict", "description": "Build-time variables, specified as a map or a list of KEY=VAL pairs."}, + "ssh": {"$ref": "#/$defs/list_or_dict", "description": "SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|[=|[,]]'."}, + "labels": {"$ref": "#/$defs/list_or_dict", "description": "Labels to apply to the built image."}, "cache_from": {"type": "array", "items": {"type": "string"}, "description": "List of sources the image builder should use for cache resolution"}, "cache_to": {"type": "array", "items": {"type": "string"}, "description": "Cache destinations for the build cache."}, "no_cache": {"type": ["boolean", "string"], "description": "Do not use cache when building the image."}, - "no_cache_filter": {"$ref": "#/definitions/string_or_list", "description": "Do not use build cache for the specified stages."}, - "additional_contexts": {"$ref": "#/definitions/list_or_dict", "description": "Additional build contexts to use, specified as a map of name to context path or URL."}, + "no_cache_filter": {"$ref": "#/$defs/string_or_list", "description": "Do not use build cache for the specified stages."}, + "additional_contexts": {"$ref": "#/$defs/list_or_dict", "description": "Additional build contexts to use, specified as a map of name to context path or URL."}, "network": {"type": "string", "description": "Network mode to use for the build. Options include 'default', 'none', 'host', or a network name."}, "provenance": {"type": ["string","boolean"], "description": "Add a provenance attestation"}, "sbom": {"type": ["string","boolean"], "description": "Add a SBOM attestation"}, "pull": {"type": ["boolean", "string"], "description": "Always attempt to pull a newer version of the image."}, "target": {"type": "string", "description": "Build stage to target in a multi-stage Dockerfile."}, "shm_size": {"type": ["integer", "string"], "description": "Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes."}, - "extra_hosts": {"$ref": "#/definitions/extra_hosts", "description": "Add hostname mappings for the build container."}, + "extra_hosts": {"$ref": "#/$defs/extra_hosts", "description": "Add hostname mappings for the build container."}, "isolation": {"type": "string", "description": "Container isolation technology to use for the build process."}, "privileged": {"type": ["boolean", "string"], "description": "Give extended privileges to the build container."}, - "secrets": {"$ref": "#/definitions/service_config_or_secret", "description": "Secrets to expose to the build. These are accessible at build-time."}, + "secrets": {"$ref": "#/$defs/service_config_or_secret", "description": "Secrets to expose to the build. These are accessible at build-time."}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Additional tags to apply to the built image."}, - "ulimits": {"$ref": "#/definitions/ulimits", "description": "Override the default ulimits for the build container."}, + "ulimits": {"$ref": "#/$defs/ulimits", "description": "Override the default ulimits for the build container."}, "platforms": {"type": "array", "items": {"type": "string"}, "description": "Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."} }, "additionalProperties": false, @@ -149,22 +149,22 @@ "device_read_bps": { "type": "array", "description": "Limit read rate (bytes per second) from a device.", - "items": {"$ref": "#/definitions/blkio_limit"} + "items": {"$ref": "#/$defs/blkio_limit"} }, "device_read_iops": { "type": "array", "description": "Limit read rate (IO per second) from a device.", - "items": {"$ref": "#/definitions/blkio_limit"} + "items": {"$ref": "#/$defs/blkio_limit"} }, "device_write_bps": { "type": "array", "description": "Limit write rate (bytes per second) to a device.", - "items": {"$ref": "#/definitions/blkio_limit"} + "items": {"$ref": "#/$defs/blkio_limit"} }, "device_write_iops": { "type": "array", "description": "Limit write rate (IO per second) to a device.", - "items": {"$ref": "#/definitions/blkio_limit"} + "items": {"$ref": "#/$defs/blkio_limit"} }, "weight": { "type": ["integer", "string"], @@ -173,7 +173,7 @@ "weight_device": { "type": "array", "description": "Block IO weight (relative weight) for specific devices.", - "items": {"$ref": "#/definitions/blkio_weight"} + "items": {"$ref": "#/$defs/blkio_weight"} } }, "additionalProperties": false @@ -200,11 +200,11 @@ "description": "Specify an optional parent cgroup for the container." }, "command": { - "$ref": "#/definitions/command", + "$ref": "#/$defs/command", "description": "Override the default command declared by the container image, for example 'CMD' in Dockerfile." }, "configs": { - "$ref": "#/definitions/service_config_or_secret", + "$ref": "#/$defs/service_config_or_secret", "description": "Grant access to Configs on a per-service basis." }, "container_name": { @@ -276,7 +276,7 @@ }, "depends_on": { "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, + {"$ref": "#/$defs/list_of_strings"}, { "type": "object", "additionalProperties": false, @@ -309,7 +309,7 @@ "description": "Express dependency between services. Service dependencies cause services to be started in dependency order. The dependent service will wait for the dependency to be ready before starting." }, "device_cgroup_rules": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "Add rules to the cgroup allowed devices list." }, "devices": { @@ -342,7 +342,7 @@ } }, "dns": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Custom DNS servers to set for the service container." }, "dns_opt": { @@ -352,7 +352,7 @@ "description": "Custom DNS options to be passed to the container's DNS resolver." }, "dns_search": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Custom DNS search domains to set on the service container." }, "domainname": { @@ -360,19 +360,19 @@ "description": "Custom domain name to use for the service container." }, "entrypoint": { - "$ref": "#/definitions/command", + "$ref": "#/$defs/command", "description": "Override the default entrypoint declared by the container image, for example 'ENTRYPOINT' in Dockerfile." }, "env_file": { - "$ref": "#/definitions/env_file", + "$ref": "#/$defs/env_file", "description": "Add environment variables from a file or multiple files. Can be a single file path or a list of file paths." }, "label_file": { - "$ref": "#/definitions/label_file", + "$ref": "#/$defs/label_file", "description": "Add metadata to containers using files containing Docker labels." }, "environment": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add environment variables. You can use either an array or a list of KEY=VAL pairs." }, "expose": { @@ -434,11 +434,11 @@ "description": "Link to services started outside this Compose application. Specify services as :." }, "extra_hosts": { - "$ref": "#/definitions/extra_hosts", + "$ref": "#/$defs/extra_hosts", "description": "Add hostname mappings to the container network interface configuration." }, "gpus": { - "$ref": "#/definitions/gpus", + "$ref": "#/$defs/gpus", "description": "Define GPU devices to use. Can be set to 'all' to use all GPUs, or a list of specific GPU devices." }, "group_add": { @@ -450,7 +450,7 @@ "description": "Add additional groups which user inside the container should be member of." }, "healthcheck": { - "$ref": "#/definitions/healthcheck", + "$ref": "#/$defs/healthcheck", "description": "Configure a health check for the container to monitor its health status." }, "hostname": { @@ -474,7 +474,7 @@ "description": "Container isolation technology to use. Supported values are platform-specific." }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add metadata to containers using Docker labels. You can use either an array or a list." }, "links": { @@ -528,7 +528,7 @@ }, "models": { "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, + {"$ref": "#/$defs/list_of_strings"}, {"type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { @@ -558,7 +558,7 @@ }, "networks": { "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, + {"$ref": "#/$defs/list_of_strings"}, { "type": "object", "patternProperties": { @@ -568,7 +568,7 @@ "type": "object", "properties": { "aliases": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "Alternative hostnames for this service on the network." }, "interface_name": { @@ -584,7 +584,7 @@ "description": "Specify a static IPv6 address for this service on this network." }, "link_local_ips": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of link-local IPs." }, "mac_address": { @@ -690,12 +690,12 @@ }, "post_start": { "type": "array", - "items": {"$ref": "#/definitions/service_hook"}, + "items": {"$ref": "#/$defs/service_hook"}, "description": "Commands to run after the container starts. If any command fails, the container stops." }, "pre_stop": { "type": "array", - "items": {"$ref": "#/definitions/service_hook"}, + "items": {"$ref": "#/$defs/service_hook"}, "description": "Commands to run before the container stops. If any command fails, the container stop is aborted." }, "privileged": { @@ -703,7 +703,7 @@ "description": "Give extended privileges to the service container." }, "profiles": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of profiles for this service. When profiles are specified, services are only started when the profile is activated." }, "pull_policy": { @@ -742,11 +742,11 @@ "description": "Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes." }, "secrets": { - "$ref": "#/definitions/service_config_or_secret", + "$ref": "#/$defs/service_config_or_secret", "description": "Grant access to Secrets on a per-service basis." }, "sysctls": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Kernel parameters to set in the container. You can use either an array or a list." }, "stdin_open": { @@ -766,7 +766,7 @@ "description": "Storage driver options for the container." }, "tmpfs": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Mount a temporary filesystem (tmpfs) into the container. Can be a single value or a list." }, "tty": { @@ -774,7 +774,7 @@ "description": "Allocate a pseudo-TTY to service container." }, "ulimits": { - "$ref": "#/definitions/ulimits", + "$ref": "#/$defs/ulimits", "description": "Override the default ulimits for a container." }, "use_api_socket": { @@ -855,7 +855,7 @@ "description": "Configuration specific to volume mounts.", "properties": { "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Labels to apply to the volume." }, "nocopy": { @@ -975,11 +975,11 @@ "required": ["path", "action"], "properties": { "ignore": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Patterns to exclude from watching." }, "include": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Patterns to include in watching." }, "path": { @@ -996,7 +996,7 @@ "description": "Target path in the container for sync operations." }, "exec": { - "$ref": "#/definitions/service_hook", + "$ref": "#/$defs/service_hook", "description": "Command to execute when a change is detected and action is sync+exec." }, "initial_sync": { @@ -1029,7 +1029,7 @@ "description": "Number of replicas of the service container to run." }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Labels to apply to the service." }, "rollback_config": { @@ -1135,11 +1135,11 @@ "description": "Reservation on the amount of memory a container can allocate (e.g., '1g', '1024m')." }, "generic_resources": { - "$ref": "#/definitions/generic_resources", + "$ref": "#/$defs/generic_resources", "description": "User-defined resources to reserve." }, "devices": { - "$ref": "#/definitions/devices", + "$ref": "#/$defs/devices", "description": "Device reservations for the container." } }, @@ -1246,7 +1246,7 @@ "type": "object", "properties": { "capabilities": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of capabilities the device needs to have (e.g., 'gpu', 'compute', 'utility')." }, "count": { @@ -1254,7 +1254,7 @@ "description": "Number of devices of this type to reserve." }, "device_ids": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of specific device IDs to reserve." }, "driver": { @@ -1262,7 +1262,7 @@ "description": "Device driver to use (e.g., 'nvidia')." }, "options": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Driver-specific options for the device." } }, @@ -1288,7 +1288,7 @@ "type": "object", "properties": { "capabilities": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of capabilities the GPU needs to have (e.g., 'compute', 'utility')." }, "count": { @@ -1296,7 +1296,7 @@ "description": "Number of GPUs to use." }, "device_ids": { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "List of specific GPU device IDs to use." }, "driver": { @@ -1304,7 +1304,7 @@ "description": "GPU driver to use (e.g., 'nvidia')." }, "options": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Driver-specific options for the GPU." } } @@ -1323,11 +1323,11 @@ "type": "object", "properties": { "path": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Path to the Compose application or sub-project files to include." }, "env_file": { - "$ref": "#/definitions/string_or_list", + "$ref": "#/$defs/string_or_list", "description": "Path to the environment files to use to define default values when interpolating variables in the Compose files being parsed." }, "project_directory": { @@ -1436,7 +1436,7 @@ "description": "If true, standalone containers can attach to this network." }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add metadata to the network using labels." } }, @@ -1477,7 +1477,7 @@ "patternProperties": {"^x-": {}} }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add metadata to the volume using labels." } }, @@ -1512,7 +1512,7 @@ } }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add metadata to the secret using labels." }, "driver": { @@ -1567,7 +1567,7 @@ } }, "labels": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Add metadata to the config using labels." }, "template_driver": { @@ -1632,7 +1632,7 @@ "description": "Configuration for service lifecycle hooks, which are commands executed at specific points in a container's lifecycle.", "properties": { "command": { - "$ref": "#/definitions/command", + "$ref": "#/$defs/command", "description": "Command to execute as part of the hook." }, "user": { @@ -1648,7 +1648,7 @@ "description": "Working directory for the command." }, "environment": { - "$ref": "#/definitions/list_or_dict", + "$ref": "#/$defs/list_or_dict", "description": "Environment variables for the command." } }, @@ -1725,7 +1725,7 @@ "description": "A single string value." }, { - "$ref": "#/definitions/list_of_strings", + "$ref": "#/$defs/list_of_strings", "description": "A list of string values." } ], diff --git a/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go b/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go index 1b2cd4196bcf..0f039ab76778 100644 --- a/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go +++ b/vendor/github.com/compose-spec/compose-go/v2/types/bytes.go @@ -17,9 +17,12 @@ package types import ( + "encoding/json" "fmt" + "strconv" "github.com/docker/go-units" + "go.yaml.in/yaml/v4" ) // UnitBytes is the bytes type @@ -35,14 +38,53 @@ func (u UnitBytes) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%d"`, u)), nil } +// parseString parses a string into a UnitBytes value, supporting plain +// integers, negative values (e.g., "-1"), and human-readable byte units +// (e.g., "1g", "512m"). +func (u *UnitBytes) parseString(s string) error { + if n, err := strconv.ParseInt(s, 10, 64); err == nil { + *u = UnitBytes(n) + return nil + } + b, err := units.RAMInBytes(s) + *u = UnitBytes(b) + return err +} + +// UnmarshalJSON makes UnitBytes implement json.Unmarshaler +func (u *UnitBytes) UnmarshalJSON(data []byte) error { + var v int64 + if err := json.Unmarshal(data, &v); err == nil { + *u = UnitBytes(v) + return nil + } + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return u.parseString(s) +} + +// UnmarshalYAML makes UnitBytes implement yaml.Unmarshaler +func (u *UnitBytes) UnmarshalYAML(value *yaml.Node) error { + var v int64 + if err := value.Decode(&v); err == nil { + *u = UnitBytes(v) + return nil + } + var s string + if err := value.Decode(&s); err != nil { + return err + } + return u.parseString(s) +} + func (u *UnitBytes) DecodeMapstructure(value interface{}) error { switch v := value.(type) { case int: *u = UnitBytes(v) case string: - b, err := units.RAMInBytes(fmt.Sprint(value)) - *u = UnitBytes(b) - return err + return u.parseString(v) } return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 8ae025765265..df6921cdc724 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.10.2 +# github.com/compose-spec/compose-go/v2 v2.11.0 ## explicit; go 1.24 github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/consts