Merge pull request #3898 from crazy-max/compose-go-2.11.0

vendor: update compose-go to v2.11.0
This commit is contained in:
Tõnis Tiigi
2026-06-12 15:40:38 -07:00
committed by GitHub
9 changed files with 276 additions and 127 deletions
-21
View File
@@ -266,10 +266,6 @@ func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, op
return nil, errors.New("empty compose file") 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 { if err := composeschema.Validate(filtered); err != nil {
return nil, err 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) { func validateComposeFile(dt []byte, fn string, envOverrides map[string]string) (bool, error) {
envs, err := composeEnv(envOverrides) envs, err := composeEnv(envOverrides)
if err != nil { if err != nil {
+1 -1
View File
@@ -7,7 +7,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/go-crypto v1.3.0
github.com/aws/aws-sdk-go-v2/config v1.32.24 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/console v1.0.5
github.com/containerd/containerd/v2 v2.2.4 github.com/containerd/containerd/v2 v2.2.4
github.com/containerd/continuity v0.5.0 github.com/containerd/continuity v0.5.0
+2 -2
View File
@@ -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/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 h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.10.2 h1:USa1NUbDcl/cjb8T9iwnuFsnO79H+2ho2L5SjFKz3uI= github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM=
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/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= 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/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
+12 -6
View File
@@ -84,6 +84,9 @@ type Options struct {
KnownExtensions map[string]any KnownExtensions map[string]any
// Metada for telemetry // Metada for telemetry
Listeners []Listener 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 var versionWarning []string
@@ -446,8 +449,10 @@ func loadYamlFile(ctx context.Context,
fixEmptyNotNull(cfg) fixEmptyNotNull(cfg)
if !opts.SkipExtends { // Process includes first so that extended services have all merged attributes
err = ApplyExtends(ctx, cfg, opts, ct, processor) if !opts.SkipInclude {
included = append(included, file.Filename)
err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor)
if err != nil { if err != nil {
return err return err
} }
@@ -457,12 +462,13 @@ func loadYamlFile(ctx context.Context,
return err return err
} }
if !opts.SkipInclude { // Process extends after includes so base services are fully merged
included = append(included, file.Filename) if !opts.SkipExtends {
err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor) err = ApplyExtends(ctx, cfg, opts, ct, processor)
if err != nil { if err != nil {
return err return err
} }
} }
dict, err = override.Merge(dict, cfg) dict, err = override.Merge(dict, cfg)
@@ -503,7 +509,7 @@ func loadYamlFile(ctx context.Context,
decoder := yaml.NewDecoder(r) decoder := yaml.NewDecoder(r)
for { for {
var raw interface{} var raw interface{}
reset := &ResetProcessor{target: &raw} reset := &ResetProcessor{target: &raw, maxNodeVisits: opts.MaxNodeVisits}
err := decoder.Decode(reset) err := decoder.Decode(reset)
if err != nil && errors.Is(err, io.EOF) { if err != nil && errors.Is(err, io.EOF) {
break break
+1 -1
View File
@@ -41,7 +41,7 @@ func omitEmpty(data any, p tree.Path) any {
} }
return v return v
case []any: case []any:
var c []any c := make([]any, 0, len(v))
for _, e := range v { for _, e := range v {
if isEmpty(e) && mustOmit(p) { if isEmpty(e) && mustOmit(p) {
continue continue
+142 -20
View File
@@ -25,17 +25,38 @@ import (
"go.yaml.in/yaml/v4" "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 { type ResetProcessor struct {
target interface{} target any
paths []tree.Path paths []tree.Path
visitedNodes map[*yaml.Node][]string 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 // UnmarshalYAML implement yaml.Unmarshaler
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error { func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
p.visitedNodes = make(map[*yaml.Node][]string) 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()) resolved, err := p.resolveReset(value, tree.NewPath())
p.visitedNodes = nil
if err != nil { if err != nil {
return err return err
} }
@@ -44,21 +65,21 @@ func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
// resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree // resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree
func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) { func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) {
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() pathStr := path.String()
// If the path contains "<<", removing the "<<" element and merging the path // If the path contains "<<", removing the "<<" element and merging the path
if strings.Contains(pathStr, ".<<") { if strings.Contains(pathStr, ".<<") {
path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 1)) 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.Kind == yaml.AliasNode {
if err := p.checkForCycle(node.Alias, path); err != nil {
return nil, err
}
return p.resolveReset(node.Alias, path)
}
if node.Tag == "!reset" { if node.Tag == "!reset" {
p.paths = append(p.paths, path) p.paths = append(p.paths, path)
return nil, nil return nil, nil
@@ -68,7 +89,69 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No
return node, nil return node, nil
} }
keys := map[string]int{} // 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)
}
// 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)
}
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
}
startIdx := len(p.paths)
resolved, err := p.resolveContainer(node, path)
if err != nil {
return nil, err
}
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 { switch node.Kind {
case yaml.SequenceNode: case yaml.SequenceNode:
var nodes []*yaml.Node var nodes []*yaml.Node
@@ -78,12 +161,18 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resolved != nil { if resolved == nil {
continue
}
if v.Kind == yaml.AliasNode {
nodes = append(nodes, v)
} else {
nodes = append(nodes, resolved) nodes = append(nodes, resolved)
} }
} }
node.Content = nodes node.Content = nodes
case yaml.MappingNode: case yaml.MappingNode:
keys := map[string]int{}
var key string var key string
var nodes []*yaml.Node var nodes []*yaml.Node
for idx, v := range node.Content { 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 { if err != nil {
return nil, err 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) 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 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 // Apply finds the go attributes matching recorded paths and reset them to zero value
func (p *ResetProcessor) Apply(target any) error { func (p *ResetProcessor) Apply(target any) error {
return p.applyNullOverrides(target, tree.NewPath()) 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 // areInDifferentServices checks if two paths are in different service definitions
func areInDifferentServices(path1, path2 string) bool { func areInDifferentServices(path1, path2 string) bool {
// Split paths into components
parts1 := strings.Split(path1, ".") parts1 := strings.Split(path1, ".")
parts2 := strings.Split(path2, ".") parts2 := strings.Split(path2, ".")
// Look for the services component and compare the service names
for i := 0; i < len(parts1) && i < len(parts2); i++ { for i := 0; i < len(parts1) && i < len(parts2); i++ {
if parts1[i] == "services" && i+1 < len(parts1) && if parts1[i] == "services" && i+1 < len(parts1) &&
parts2[i] == "services" && i+1 < len(parts2) { parts2[i] == "services" && i+1 < len(parts2) {
// If they're different services, it's not a cycle
return parts1[i+1] != parts2[i+1] return parts1[i+1] != parts2[i+1]
} }
} }
+72 -72
View File
@@ -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", "$id": "compose_spec.json",
"type": "object", "type": "object",
"title": "Compose Specification", "title": "Compose Specification",
@@ -20,7 +20,7 @@
"include": { "include": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/include" "$ref": "#/$defs/include"
}, },
"description": "compose sub-projects to be included." "description": "compose sub-projects to be included."
}, },
@@ -29,7 +29,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service" "$ref": "#/$defs/service"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -40,7 +40,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/model" "$ref": "#/$defs/model"
} }
}, },
"description": "Language models that will be used by your application." "description": "Language models that will be used by your application."
@@ -51,7 +51,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network" "$ref": "#/$defs/network"
} }
}, },
"description": "Networks that are shared among multiple services." "description": "Networks that are shared among multiple services."
@@ -61,7 +61,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume" "$ref": "#/$defs/volume"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -72,7 +72,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/secret" "$ref": "#/$defs/secret"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -83,7 +83,7 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/config" "$ref": "#/$defs/config"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -94,15 +94,15 @@
"patternProperties": {"^x-": {}}, "patternProperties": {"^x-": {}},
"additionalProperties": false, "additionalProperties": false,
"definitions": { "$defs": {
"service": { "service": {
"type": "object", "type": "object",
"description": "Configuration for a service.", "description": "Configuration for a service.",
"properties": { "properties": {
"develop": {"$ref": "#/definitions/development"}, "develop": {"$ref": "#/$defs/development"},
"deploy": {"$ref": "#/definitions/deployment"}, "deploy": {"$ref": "#/$defs/deployment"},
"annotations": {"$ref": "#/definitions/list_or_dict"}, "annotations": {"$ref": "#/$defs/list_or_dict"},
"attach": {"type": ["boolean", "string"]}, "attach": {"type": ["boolean", "string"]},
"build": { "build": {
"description": "Configuration options for building the service's image.", "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": {"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."}, "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."}, "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."}, "args": {"$ref": "#/$defs/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|<id>[=<socket>|<key>[,<key>]]'."}, "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|<id>[=<socket>|<key>[,<key>]]'."},
"labels": {"$ref": "#/definitions/list_or_dict", "description": "Labels to apply to the built image."}, "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_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."}, "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": {"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."}, "no_cache_filter": {"$ref": "#/$defs/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."}, "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."}, "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"}, "provenance": {"type": ["string","boolean"], "description": "Add a provenance attestation"},
"sbom": {"type": ["string","boolean"], "description": "Add a SBOM 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."}, "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."}, "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."}, "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."}, "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."}, "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."}, "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'."} "platforms": {"type": "array", "items": {"type": "string"}, "description": "Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."}
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -149,22 +149,22 @@
"device_read_bps": { "device_read_bps": {
"type": "array", "type": "array",
"description": "Limit read rate (bytes per second) from a device.", "description": "Limit read rate (bytes per second) from a device.",
"items": {"$ref": "#/definitions/blkio_limit"} "items": {"$ref": "#/$defs/blkio_limit"}
}, },
"device_read_iops": { "device_read_iops": {
"type": "array", "type": "array",
"description": "Limit read rate (IO per second) from a device.", "description": "Limit read rate (IO per second) from a device.",
"items": {"$ref": "#/definitions/blkio_limit"} "items": {"$ref": "#/$defs/blkio_limit"}
}, },
"device_write_bps": { "device_write_bps": {
"type": "array", "type": "array",
"description": "Limit write rate (bytes per second) to a device.", "description": "Limit write rate (bytes per second) to a device.",
"items": {"$ref": "#/definitions/blkio_limit"} "items": {"$ref": "#/$defs/blkio_limit"}
}, },
"device_write_iops": { "device_write_iops": {
"type": "array", "type": "array",
"description": "Limit write rate (IO per second) to a device.", "description": "Limit write rate (IO per second) to a device.",
"items": {"$ref": "#/definitions/blkio_limit"} "items": {"$ref": "#/$defs/blkio_limit"}
}, },
"weight": { "weight": {
"type": ["integer", "string"], "type": ["integer", "string"],
@@ -173,7 +173,7 @@
"weight_device": { "weight_device": {
"type": "array", "type": "array",
"description": "Block IO weight (relative weight) for specific devices.", "description": "Block IO weight (relative weight) for specific devices.",
"items": {"$ref": "#/definitions/blkio_weight"} "items": {"$ref": "#/$defs/blkio_weight"}
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -200,11 +200,11 @@
"description": "Specify an optional parent cgroup for the container." "description": "Specify an optional parent cgroup for the container."
}, },
"command": { "command": {
"$ref": "#/definitions/command", "$ref": "#/$defs/command",
"description": "Override the default command declared by the container image, for example 'CMD' in Dockerfile." "description": "Override the default command declared by the container image, for example 'CMD' in Dockerfile."
}, },
"configs": { "configs": {
"$ref": "#/definitions/service_config_or_secret", "$ref": "#/$defs/service_config_or_secret",
"description": "Grant access to Configs on a per-service basis." "description": "Grant access to Configs on a per-service basis."
}, },
"container_name": { "container_name": {
@@ -276,7 +276,7 @@
}, },
"depends_on": { "depends_on": {
"oneOf": [ "oneOf": [
{"$ref": "#/definitions/list_of_strings"}, {"$ref": "#/$defs/list_of_strings"},
{ {
"type": "object", "type": "object",
"additionalProperties": false, "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." "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": { "device_cgroup_rules": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "Add rules to the cgroup allowed devices list." "description": "Add rules to the cgroup allowed devices list."
}, },
"devices": { "devices": {
@@ -342,7 +342,7 @@
} }
}, },
"dns": { "dns": {
"$ref": "#/definitions/string_or_list", "$ref": "#/$defs/string_or_list",
"description": "Custom DNS servers to set for the service container." "description": "Custom DNS servers to set for the service container."
}, },
"dns_opt": { "dns_opt": {
@@ -352,7 +352,7 @@
"description": "Custom DNS options to be passed to the container's DNS resolver." "description": "Custom DNS options to be passed to the container's DNS resolver."
}, },
"dns_search": { "dns_search": {
"$ref": "#/definitions/string_or_list", "$ref": "#/$defs/string_or_list",
"description": "Custom DNS search domains to set on the service container." "description": "Custom DNS search domains to set on the service container."
}, },
"domainname": { "domainname": {
@@ -360,19 +360,19 @@
"description": "Custom domain name to use for the service container." "description": "Custom domain name to use for the service container."
}, },
"entrypoint": { "entrypoint": {
"$ref": "#/definitions/command", "$ref": "#/$defs/command",
"description": "Override the default entrypoint declared by the container image, for example 'ENTRYPOINT' in Dockerfile." "description": "Override the default entrypoint declared by the container image, for example 'ENTRYPOINT' in Dockerfile."
}, },
"env_file": { "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." "description": "Add environment variables from a file or multiple files. Can be a single file path or a list of file paths."
}, },
"label_file": { "label_file": {
"$ref": "#/definitions/label_file", "$ref": "#/$defs/label_file",
"description": "Add metadata to containers using files containing Docker labels." "description": "Add metadata to containers using files containing Docker labels."
}, },
"environment": { "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." "description": "Add environment variables. You can use either an array or a list of KEY=VAL pairs."
}, },
"expose": { "expose": {
@@ -434,11 +434,11 @@
"description": "Link to services started outside this Compose application. Specify services as <service_name>:<alias>." "description": "Link to services started outside this Compose application. Specify services as <service_name>:<alias>."
}, },
"extra_hosts": { "extra_hosts": {
"$ref": "#/definitions/extra_hosts", "$ref": "#/$defs/extra_hosts",
"description": "Add hostname mappings to the container network interface configuration." "description": "Add hostname mappings to the container network interface configuration."
}, },
"gpus": { "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." "description": "Define GPU devices to use. Can be set to 'all' to use all GPUs, or a list of specific GPU devices."
}, },
"group_add": { "group_add": {
@@ -450,7 +450,7 @@
"description": "Add additional groups which user inside the container should be member of." "description": "Add additional groups which user inside the container should be member of."
}, },
"healthcheck": { "healthcheck": {
"$ref": "#/definitions/healthcheck", "$ref": "#/$defs/healthcheck",
"description": "Configure a health check for the container to monitor its health status." "description": "Configure a health check for the container to monitor its health status."
}, },
"hostname": { "hostname": {
@@ -474,7 +474,7 @@
"description": "Container isolation technology to use. Supported values are platform-specific." "description": "Container isolation technology to use. Supported values are platform-specific."
}, },
"labels": { "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." "description": "Add metadata to containers using Docker labels. You can use either an array or a list."
}, },
"links": { "links": {
@@ -528,7 +528,7 @@
}, },
"models": { "models": {
"oneOf": [ "oneOf": [
{"$ref": "#/definitions/list_of_strings"}, {"$ref": "#/$defs/list_of_strings"},
{"type": "object", {"type": "object",
"patternProperties": { "patternProperties": {
"^[a-zA-Z0-9._-]+$": { "^[a-zA-Z0-9._-]+$": {
@@ -558,7 +558,7 @@
}, },
"networks": { "networks": {
"oneOf": [ "oneOf": [
{"$ref": "#/definitions/list_of_strings"}, {"$ref": "#/$defs/list_of_strings"},
{ {
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
@@ -568,7 +568,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"aliases": { "aliases": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "Alternative hostnames for this service on the network." "description": "Alternative hostnames for this service on the network."
}, },
"interface_name": { "interface_name": {
@@ -584,7 +584,7 @@
"description": "Specify a static IPv6 address for this service on this network." "description": "Specify a static IPv6 address for this service on this network."
}, },
"link_local_ips": { "link_local_ips": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "List of link-local IPs." "description": "List of link-local IPs."
}, },
"mac_address": { "mac_address": {
@@ -690,12 +690,12 @@
}, },
"post_start": { "post_start": {
"type": "array", "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." "description": "Commands to run after the container starts. If any command fails, the container stops."
}, },
"pre_stop": { "pre_stop": {
"type": "array", "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." "description": "Commands to run before the container stops. If any command fails, the container stop is aborted."
}, },
"privileged": { "privileged": {
@@ -703,7 +703,7 @@
"description": "Give extended privileges to the service container." "description": "Give extended privileges to the service container."
}, },
"profiles": { "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." "description": "List of profiles for this service. When profiles are specified, services are only started when the profile is activated."
}, },
"pull_policy": { "pull_policy": {
@@ -742,11 +742,11 @@
"description": "Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes." "description": "Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes."
}, },
"secrets": { "secrets": {
"$ref": "#/definitions/service_config_or_secret", "$ref": "#/$defs/service_config_or_secret",
"description": "Grant access to Secrets on a per-service basis." "description": "Grant access to Secrets on a per-service basis."
}, },
"sysctls": { "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." "description": "Kernel parameters to set in the container. You can use either an array or a list."
}, },
"stdin_open": { "stdin_open": {
@@ -766,7 +766,7 @@
"description": "Storage driver options for the container." "description": "Storage driver options for the container."
}, },
"tmpfs": { "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." "description": "Mount a temporary filesystem (tmpfs) into the container. Can be a single value or a list."
}, },
"tty": { "tty": {
@@ -774,7 +774,7 @@
"description": "Allocate a pseudo-TTY to service container." "description": "Allocate a pseudo-TTY to service container."
}, },
"ulimits": { "ulimits": {
"$ref": "#/definitions/ulimits", "$ref": "#/$defs/ulimits",
"description": "Override the default ulimits for a container." "description": "Override the default ulimits for a container."
}, },
"use_api_socket": { "use_api_socket": {
@@ -855,7 +855,7 @@
"description": "Configuration specific to volume mounts.", "description": "Configuration specific to volume mounts.",
"properties": { "properties": {
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Labels to apply to the volume." "description": "Labels to apply to the volume."
}, },
"nocopy": { "nocopy": {
@@ -975,11 +975,11 @@
"required": ["path", "action"], "required": ["path", "action"],
"properties": { "properties": {
"ignore": { "ignore": {
"$ref": "#/definitions/string_or_list", "$ref": "#/$defs/string_or_list",
"description": "Patterns to exclude from watching." "description": "Patterns to exclude from watching."
}, },
"include": { "include": {
"$ref": "#/definitions/string_or_list", "$ref": "#/$defs/string_or_list",
"description": "Patterns to include in watching." "description": "Patterns to include in watching."
}, },
"path": { "path": {
@@ -996,7 +996,7 @@
"description": "Target path in the container for sync operations." "description": "Target path in the container for sync operations."
}, },
"exec": { "exec": {
"$ref": "#/definitions/service_hook", "$ref": "#/$defs/service_hook",
"description": "Command to execute when a change is detected and action is sync+exec." "description": "Command to execute when a change is detected and action is sync+exec."
}, },
"initial_sync": { "initial_sync": {
@@ -1029,7 +1029,7 @@
"description": "Number of replicas of the service container to run." "description": "Number of replicas of the service container to run."
}, },
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Labels to apply to the service." "description": "Labels to apply to the service."
}, },
"rollback_config": { "rollback_config": {
@@ -1135,11 +1135,11 @@
"description": "Reservation on the amount of memory a container can allocate (e.g., '1g', '1024m')." "description": "Reservation on the amount of memory a container can allocate (e.g., '1g', '1024m')."
}, },
"generic_resources": { "generic_resources": {
"$ref": "#/definitions/generic_resources", "$ref": "#/$defs/generic_resources",
"description": "User-defined resources to reserve." "description": "User-defined resources to reserve."
}, },
"devices": { "devices": {
"$ref": "#/definitions/devices", "$ref": "#/$defs/devices",
"description": "Device reservations for the container." "description": "Device reservations for the container."
} }
}, },
@@ -1246,7 +1246,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"capabilities": { "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')." "description": "List of capabilities the device needs to have (e.g., 'gpu', 'compute', 'utility')."
}, },
"count": { "count": {
@@ -1254,7 +1254,7 @@
"description": "Number of devices of this type to reserve." "description": "Number of devices of this type to reserve."
}, },
"device_ids": { "device_ids": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "List of specific device IDs to reserve." "description": "List of specific device IDs to reserve."
}, },
"driver": { "driver": {
@@ -1262,7 +1262,7 @@
"description": "Device driver to use (e.g., 'nvidia')." "description": "Device driver to use (e.g., 'nvidia')."
}, },
"options": { "options": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Driver-specific options for the device." "description": "Driver-specific options for the device."
} }
}, },
@@ -1288,7 +1288,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"capabilities": { "capabilities": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "List of capabilities the GPU needs to have (e.g., 'compute', 'utility')." "description": "List of capabilities the GPU needs to have (e.g., 'compute', 'utility')."
}, },
"count": { "count": {
@@ -1296,7 +1296,7 @@
"description": "Number of GPUs to use." "description": "Number of GPUs to use."
}, },
"device_ids": { "device_ids": {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "List of specific GPU device IDs to use." "description": "List of specific GPU device IDs to use."
}, },
"driver": { "driver": {
@@ -1304,7 +1304,7 @@
"description": "GPU driver to use (e.g., 'nvidia')." "description": "GPU driver to use (e.g., 'nvidia')."
}, },
"options": { "options": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Driver-specific options for the GPU." "description": "Driver-specific options for the GPU."
} }
} }
@@ -1323,11 +1323,11 @@
"type": "object", "type": "object",
"properties": { "properties": {
"path": { "path": {
"$ref": "#/definitions/string_or_list", "$ref": "#/$defs/string_or_list",
"description": "Path to the Compose application or sub-project files to include." "description": "Path to the Compose application or sub-project files to include."
}, },
"env_file": { "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." "description": "Path to the environment files to use to define default values when interpolating variables in the Compose files being parsed."
}, },
"project_directory": { "project_directory": {
@@ -1436,7 +1436,7 @@
"description": "If true, standalone containers can attach to this network." "description": "If true, standalone containers can attach to this network."
}, },
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Add metadata to the network using labels." "description": "Add metadata to the network using labels."
} }
}, },
@@ -1477,7 +1477,7 @@
"patternProperties": {"^x-": {}} "patternProperties": {"^x-": {}}
}, },
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Add metadata to the volume using labels." "description": "Add metadata to the volume using labels."
} }
}, },
@@ -1512,7 +1512,7 @@
} }
}, },
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Add metadata to the secret using labels." "description": "Add metadata to the secret using labels."
}, },
"driver": { "driver": {
@@ -1567,7 +1567,7 @@
} }
}, },
"labels": { "labels": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Add metadata to the config using labels." "description": "Add metadata to the config using labels."
}, },
"template_driver": { "template_driver": {
@@ -1632,7 +1632,7 @@
"description": "Configuration for service lifecycle hooks, which are commands executed at specific points in a container's lifecycle.", "description": "Configuration for service lifecycle hooks, which are commands executed at specific points in a container's lifecycle.",
"properties": { "properties": {
"command": { "command": {
"$ref": "#/definitions/command", "$ref": "#/$defs/command",
"description": "Command to execute as part of the hook." "description": "Command to execute as part of the hook."
}, },
"user": { "user": {
@@ -1648,7 +1648,7 @@
"description": "Working directory for the command." "description": "Working directory for the command."
}, },
"environment": { "environment": {
"$ref": "#/definitions/list_or_dict", "$ref": "#/$defs/list_or_dict",
"description": "Environment variables for the command." "description": "Environment variables for the command."
} }
}, },
@@ -1725,7 +1725,7 @@
"description": "A single string value." "description": "A single string value."
}, },
{ {
"$ref": "#/definitions/list_of_strings", "$ref": "#/$defs/list_of_strings",
"description": "A list of string values." "description": "A list of string values."
} }
], ],
+45 -3
View File
@@ -17,9 +17,12 @@
package types package types
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv"
"github.com/docker/go-units" "github.com/docker/go-units"
"go.yaml.in/yaml/v4"
) )
// UnitBytes is the bytes type // UnitBytes is the bytes type
@@ -35,14 +38,53 @@ func (u UnitBytes) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%d"`, u)), nil 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 { func (u *UnitBytes) DecodeMapstructure(value interface{}) error {
switch v := value.(type) { switch v := value.(type) {
case int: case int:
*u = UnitBytes(v) *u = UnitBytes(v)
case string: case string:
b, err := units.RAMInBytes(fmt.Sprint(value)) return u.parseString(v)
*u = UnitBytes(b)
return err
} }
return nil return nil
} }
+1 -1
View File
@@ -190,7 +190,7 @@ github.com/cloudflare/circl/math/mlsbset
github.com/cloudflare/circl/sign github.com/cloudflare/circl/sign
github.com/cloudflare/circl/sign/ed25519 github.com/cloudflare/circl/sign/ed25519
github.com/cloudflare/circl/sign/ed448 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 ## explicit; go 1.24
github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts github.com/compose-spec/compose-go/v2/consts