diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 36c8dbb641c0..742db95ee90e 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -235,15 +235,17 @@ type resourceOptions struct { resCPU opts.NanoCPUs resMemBytes opts.MemBytes resGenericResources []string + swapBytes opts.MemBytes + memSwappiness int64 } -func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) { +func (r *resourceOptions) ToResourceRequirements(flags *pflag.FlagSet) (*swarm.ResourceRequirements, error) { generic, err := ParseGenericResources(r.resGenericResources) if err != nil { return nil, err } - return &swarm.ResourceRequirements{ + resreq := &swarm.ResourceRequirements{ Limits: &swarm.Limit{ NanoCPUs: r.limitCPU.Value(), MemoryBytes: r.limitMemBytes.Value(), @@ -254,7 +256,20 @@ func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, MemoryBytes: r.resMemBytes.Value(), GenericResources: generic, }, - }, nil + } + + // SwapBytes and MemorySwappiness are *int64 (pointers), so we need to have + // a variable we can take a pointer to. Additionally, we need to ensure + // that these values are only set if they are set as options. + if flags.Changed(flagSwapBytes) { + swapBytes := r.swapBytes.Value() + resreq.SwapBytes = &swapBytes + } + if flags.Changed(flagMemSwappiness) { + resreq.MemorySwappiness = &r.memSwappiness + } + + return resreq, nil } type restartPolicyOptions struct { @@ -734,7 +749,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N return networks[i].Target < networks[j].Target }) - resources, err := options.resources.ToResourceRequirements() + resources, err := options.resources.ToResourceRequirements(flags) if err != nil { return service, err } @@ -889,6 +904,10 @@ func addServiceFlags(flags *pflag.FlagSet, options *serviceOptions, defaultFlagV flags.Var(&options.resources.resMemBytes, flagReserveMemory, "Reserve Memory") flags.Int64Var(&options.resources.limitPids, flagLimitPids, 0, "Limit maximum number of processes (default 0 = unlimited)") flags.SetAnnotation(flagLimitPids, "version", []string{"1.41"}) + flags.Var(&options.resources.swapBytes, flagSwapBytes, "Swap Bytes (-1 for unlimited)") + flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"}) + flags.Int64Var(&options.resources.memSwappiness, flagMemSwappiness, -1, "Tune memory swappiness (0-100), -1 to reset to default") + flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"}) flags.Var(&options.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) flags.Var(&options.replicas, flagReplicas, "Number of tasks") @@ -1073,6 +1092,8 @@ const ( flagUlimitAdd = "ulimit-add" flagUlimitRemove = "ulimit-rm" flagOomScoreAdj = "oom-score-adj" + flagSwapBytes = "memory-swap" + flagMemSwappiness = "memory-swappiness" ) func toNetipAddrSlice(ips []string) []netip.Addr { diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go index 563c42e9a42c..d11d9a2e1aa9 100644 --- a/cli/command/service/opts_test.go +++ b/cli/command/service/opts_test.go @@ -163,8 +163,10 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) { }, } + flags := newCreateCommand(nil).Flags() + for _, opt := range incorrectOptions { - _, err := opt.ToResourceRequirements() + _, err := opt.ToResourceRequirements(flags) assert.Check(t, is.ErrorContains(err, "")) } @@ -178,12 +180,41 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) { } for _, opt := range correctOptions { - r, err := opt.ToResourceRequirements() + r, err := opt.ToResourceRequirements(flags) assert.NilError(t, err) assert.Check(t, is.Len(r.Reservations.GenericResources, len(opt.resGenericResources))) } } +func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) { + // first, check that no flag set means no field set in the return + flags := newCreateCommand(nil).Flags() + + // These should be the default values of the field. + swapOptions := resourceOptions{ + swapBytes: 0, + memSwappiness: -1, + } + + r, err := swapOptions.ToResourceRequirements(flags) + assert.NilError(t, err) + assert.Check(t, is.Nil(r.SwapBytes)) + assert.Check(t, is.Nil(r.MemorySwappiness)) + + // now set the flags and some values + flags.Set(flagSwapBytes, "86000") + flags.Set(flagMemSwappiness, "23") + swapOptions.swapBytes = 86000 + swapOptions.memSwappiness = 23 + + r, err = swapOptions.ToResourceRequirements(flags) + assert.NilError(t, err) + assert.Check(t, r.SwapBytes != nil) + assert.Check(t, is.Equal(*(r.SwapBytes), int64(86000))) + assert.Check(t, r.MemorySwappiness != nil) + assert.Check(t, is.Equal(*(r.MemorySwappiness), int64(23))) +} + func TestToServiceNetwork(t *testing.T) { nws := []network.Inspect{ { diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 5fbc289d830e..ee14f088dfa8 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -560,6 +560,10 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement GenericResources: generic, } } + // These fields are themselves pointers -- we can simply assign, no need to + // nil-check them. Nil is nil. + resources.SwapBytes = source.MemswapLimit + resources.MemorySwappiness = source.MemSwappiness return resources, nil } diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index b5999fe6ea8e..5ce1690401c3 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -81,6 +81,9 @@ func TestConvertExtraHosts(t *testing.T) { } func TestConvertResourcesFull(t *testing.T) { + // create some variables so we can get pointers + memswap := int64(72090) + swappiness := int64(27) source := composetypes.Resources{ Limits: &composetypes.ResourceLimit{ NanoCPUs: "0.003", @@ -90,6 +93,8 @@ func TestConvertResourcesFull(t *testing.T) { NanoCPUs: "0.002", MemoryBytes: composetypes.UnitBytes(200000000), }, + MemswapLimit: &memswap, + MemSwappiness: &swappiness, } resources, err := convertResources(source) assert.NilError(t, err) @@ -103,6 +108,8 @@ func TestConvertResourcesFull(t *testing.T) { NanoCPUs: 2000000, MemoryBytes: 200000000, }, + SwapBytes: &memswap, + MemorySwappiness: &swappiness, } assert.Check(t, is.DeepEqual(expected, resources)) } diff --git a/cli/compose/loader/full-example.yml b/cli/compose/loader/full-example.yml index 36ebf833e708..0a008fe5c7b7 100644 --- a/cli/compose/loader/full-example.yml +++ b/cli/compose/loader/full-example.yml @@ -1,4 +1,4 @@ -version: "3.13" +version: "3.14" services: foo: @@ -79,6 +79,8 @@ services: - discrete_resource_spec: kind: 'ssd' value: 1 + memswap_limit: 86000 + mem_swappiness: 27 restart_policy: condition: on-failure delay: 5s diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index 5d420c872bcf..060a6d40bf6d 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -11,7 +11,7 @@ import ( func fullExampleConfig(workingDir, homeDir string) *types.Config { return &types.Config{ - Version: "3.13", + Version: "3.14", Services: services(workingDir, homeDir), Networks: networks(), Volumes: volumes(), @@ -108,6 +108,8 @@ func services(workingDir, homeDir string) []types.ServiceConfig { }, }, }, + MemswapLimit: int64Ptr(86000), + MemSwappiness: int64Ptr(27), }, RestartPolicy: &types.RestartPolicy{ Condition: "on-failure", diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index fd9a644726ae..dabf50a38f81 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -184,7 +184,7 @@ func strPtr(val string) *string { } var sampleConfig = types.Config{ - Version: "3.13", + Version: "3.14", Services: []types.ServiceConfig{ { Name: "foo", @@ -970,6 +970,10 @@ func uint32Ptr(value uint32) *uint32 { return &value } +func int64Ptr(value int64) *int64 { + return &value +} + func TestFullExample(t *testing.T) { skip.If(t, runtime.GOOS == "windows", "FIXME: substitutes platform-specific HOME-dirs and requires platform-specific golden files; see https://github.com/docker/cli/pull/4610") diff --git a/cli/compose/loader/testdata/full-example.json.golden b/cli/compose/loader/testdata/full-example.json.golden index c0ef39dabe37..4f0a08d97009 100644 --- a/cli/compose/loader/testdata/full-example.json.golden +++ b/cli/compose/loader/testdata/full-example.json.golden @@ -181,7 +181,9 @@ } } ] - } + }, + "memswap_limit": 86000, + "mem_swappiness": 27 }, "restart_policy": { "condition": "on-failure", @@ -513,7 +515,7 @@ "working_dir": "/code" } }, - "version": "3.13", + "version": "3.14", "volumes": { "another-volume": { "name": "user_specified_name", diff --git a/cli/compose/loader/testdata/full-example.yaml.golden b/cli/compose/loader/testdata/full-example.yaml.golden index dfc57f15419f..a69b7d2f31e3 100644 --- a/cli/compose/loader/testdata/full-example.yaml.golden +++ b/cli/compose/loader/testdata/full-example.yaml.golden @@ -1,4 +1,4 @@ -version: "3.13" +version: "3.14" services: foo: build: @@ -73,6 +73,8 @@ services: - discrete_resource_spec: kind: ssd value: 1 + memswap_limit: 86000 + mem_swappiness: 27 restart_policy: condition: on-failure delay: 5s diff --git a/cli/compose/schema/data/config_schema_v3.14.json b/cli/compose/schema/data/config_schema_v3.14.json new file mode 100644 index 000000000000..5189f8d0186f --- /dev/null +++ b/cli/compose/schema/data/config_schema_v3.14.json @@ -0,0 +1,686 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.14.json", + "type": "object", + + "properties": { + "version": { + "type": "string", + "default": "3.14" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + }, + + "configs": { + "id": "#/properties/configs", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/config" + } + }, + "additionalProperties": false + } + }, + + "patternProperties": {"^x-": {}}, + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "cache_from": {"$ref": "#/definitions/list_of_strings"}, + "network": {"type": "string"}, + "target": {"type": "string"}, + "shm_size": {"type": ["integer", "string"]}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": true + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroupns_mode": {"type": "string"}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "configs": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "container_name": {"type": "string"}, + "credential_spec": { + "type": "object", + "properties": { + "config": {"type": "string"}, + "file": {"type": "string"}, + "registry": {"type": "string"} + }, + "additionalProperties": false + }, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "init": {"type": "boolean"}, + "ipc": {"type": "string"}, + "isolation": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": { "type": ["string", "number"] } + } + }, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": "integer"}, + "protocol": {"type": "string"} + }, + "additionalProperties": false + } + ] + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "secrets": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "oom_score_adj": {"type": "integer"}, + "user": {"type": "string"}, + "userns_mode": {"type": "string"}, + "volumes": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "source": {"type": "string"}, + "target": {"type": "string"}, + "read_only": {"type": "boolean"}, + "consistency": {"type": "string"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"} + } + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + } + }, + "tmpfs": { + "type": "object", + "properties": { + "size": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "additionalProperties": false + } + ], + "uniqueItems": true + } + }, + "working_dir": {"type": "string"} + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": "object", + "additionalProperties": false, + "properties": { + "disable": {"type": "boolean"}, + "interval": {"type": "string", "format": "duration"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "timeout": {"type": "string", "format": "duration"}, + "start_period": {"type": "string", "format": "duration"}, + "start_interval": {"type": "string", "format": "duration"} + } + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "endpoint_mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "rollback_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"}, + "order": {"type": "string", "enum": [ + "start-first", "stop-first" + ]} + }, + "additionalProperties": false + }, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"}, + "order": {"type": "string", "enum": [ + "start-first", "stop-first" + ]} + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"}, + "pids": {"type": "integer"} + }, + "additionalProperties": false + }, + "reservations": { + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"}, + "generic_resources": {"$ref": "#/definitions/generic_resources"} + }, + "additionalProperties": false + }, + "memswap_limit": { + "type": "integer" + }, + "mem_swappiness": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}}, + "preferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "spread": {"type": "string"} + }, + "additionalProperties": false + } + }, + "max_replicas_per_node": {"type": "integer"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "generic_resources": { + "id": "#/definitions/generic_resources", + "type": "array", + "items": { + "type": "object", + "properties": { + "discrete_resource_spec": { + "type": "object", + "properties": { + "kind": {"type": "string"}, + "value": {"type": "number"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "name": {"type": "string"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "internal": {"type": "boolean"}, + "attachable": {"type": "boolean"}, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "name": {"type": "string"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "x-cluster-spec": { + "type": "object", + "properties": { + "group": {"type": "string"}, + "access_mode": { + "type": "object", + "properties": { + "scope": {"type": "string"}, + "sharing": {"type": "string"}, + "block_volume": {"type": "object"}, + "mount_volume": { + "type": "object", + "properties": { + "fs_type": {"type": "string"}, + "mount_flags": {"type": "array", "items": {"type": "string"}} + } + } + } + }, + "accessibility_requirements": { + "type": "object", + "properties": { + "requisite": { + "type": "array", + "items": { + "type": "object", + "properties": { + "segments": {"$ref": "#/definitions/list_or_dict"} + } + } + }, + "preferred": { + "type": "array", + "items": { + "type": "object", + "properties": { + "segments": {"$ref": "#/definitions/list_or_dict"} + } + } + } + } + }, + "capacity_range": { + "type": "object", + "properties": { + "required_bytes": {"type": "string"}, + "limit_bytes": {"type": "string"} + } + }, + "availability": {"type": "string"} + } + } + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "name": {"type": "string"}, + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "template_driver": {"type": "string"} + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "config": { + "id": "#/definitions/config", + "type": "object", + "properties": { + "name": {"type": "string"}, + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "template_driver": {"type": "string"} + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index ea8543a85957..2725ae243427 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -15,7 +15,7 @@ import ( ) const ( - defaultVersion = "3.13" + defaultVersion = "3.14" versionField = "version" ) @@ -52,7 +52,7 @@ func init() { } // Version returns the version of the config, defaulting to the latest "3.x" -// version (3.13). If only the major version "3" is specified, it is used as +// version (3.14). If only the major version "3" is specified, it is used as // version "3.x" and returns the default version (latest 3.x). func Version(config map[string]any) string { version, ok := config[versionField] diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index 8d039a6a8ccb..3733fc09fbff 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -210,6 +210,7 @@ func TestValidateCredentialSpecs(t *testing.T) { {version: "3.11"}, {version: "3.12"}, {version: "3.13"}, + {version: "3.14"}, {version: "3"}, {version: ""}, } diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index fdc0eb44feda..a55eec1d2a93 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -307,8 +307,10 @@ type UpdateConfig struct { // Resources the resource limits and reservations type Resources struct { - Limits *ResourceLimit `yaml:",omitempty" json:"limits,omitempty"` - Reservations *Resource `yaml:",omitempty" json:"reservations,omitempty"` + Limits *ResourceLimit `yaml:",omitempty" json:"limits,omitempty"` + Reservations *Resource `yaml:",omitempty" json:"reservations,omitempty"` + MemswapLimit *int64 `mapstructure:"memswap_limit" yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"` + MemSwappiness *int64 `mapstructure:"mem_swappiness" yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"` } // ResourceLimit is a resource to be limited diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 2fd9d6d9d04c..fb550533c445 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -40,6 +40,8 @@ Create a new service | `--log-driver` | `string` | | Logging driver for service | | `--log-opt` | `list` | | Logging driver options | | `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) | +| `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) | +| `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default | | `--mode` | `string` | `replicated` | Service mode (`replicated`, `global`, `replicated-job`, `global-job`) | | [`--mount`](#mount) | `mount` | | Attach a filesystem mount to the service | | `--name` | `string` | | Service name | diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index f3564bcc1f24..64f81b07e80f 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -53,6 +53,8 @@ Update a service | `--log-driver` | `string` | | Logging driver for service | | `--log-opt` | `list` | | Logging driver options | | `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) | +| `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) | +| `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default | | [`--mount-add`](#mount-add) | `mount` | | Add or update a mount on a service | | `--mount-rm` | `list` | | Remove a mount by its target path | | [`--network-add`](#network-add) | `network` | | Add a network |