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")
}
// 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 {
+1 -1
View File
@@ -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
+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/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=
+12 -6
View File
@@ -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
+1 -1
View File
@@ -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
+142 -20
View File
@@ -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,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
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.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" {
p.paths = append(p.paths, path)
return nil, nil
@@ -68,7 +89,69 @@ func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.No
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 {
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]
}
}
+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",
"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|<id>[=<socket>|<key>[,<key>]]'."},
"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|<id>[=<socket>|<key>[,<key>]]'."},
"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 <service_name>:<alias>."
},
"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."
}
],
+45 -3
View File
@@ -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
}
+1 -1
View File
@@ -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