From d44eca129fb3cd72a2dc291bb2cf218dca60ceaa Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Aug 2021 13:41:21 +0200 Subject: [PATCH 1/3] cli/compose/schema: Validate(): normalize version before validating Signed-off-by: Sebastiaan van Stijn --- cli/compose/schema/schema.go | 3 +++ cli/compose/schema/schema_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index 3d26d1fbca..f8a1abaf65 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -50,6 +50,8 @@ func Version(config map[string]interface{}) string { func normalizeVersion(version string) string { switch version { + case "": + return defaultVersion case "3": return "3.0" default: @@ -62,6 +64,7 @@ var schemas embed.FS // Validate uses the jsonschema to validate the configuration func Validate(config map[string]interface{}, version string) error { + version = normalizeVersion(version) schemaData, err := schemas.ReadFile("data/config_schema_v" + version + ".json") if err != nil { return errors.Errorf("unsupported Compose file version: %s", version) diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index 1c36294c0d..d90e45f959 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -20,6 +20,9 @@ func TestValidate(t *testing.T) { } assert.NilError(t, Validate(config, "3.0")) + assert.NilError(t, Validate(config, "3")) + assert.ErrorContains(t, Validate(config, ""), "unsupported Compose file version: 1.0") + assert.ErrorContains(t, Validate(config, "12345"), "unsupported Compose file version: 12345") } func TestValidateUndefinedTopLevelOption(t *testing.T) { @@ -84,6 +87,7 @@ func TestValidateCredentialSpecs(t *testing.T) { version string expectedErr string }{ + {version: "3", expectedErr: "credential_spec"}, {version: "3.0", expectedErr: "credential_spec"}, {version: "3.1", expectedErr: "credential_spec"}, {version: "3.2", expectedErr: "credential_spec"}, From a9fd697737e4f9699ff259c3040a163ba7dcfb43 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Aug 2021 13:32:25 +0200 Subject: [PATCH 2/3] cli/compose: add schema 3.10 (no changes with 3.9 yet) Adding a copy of the 3.9 schema, with only the version-string changed. This makes it easier to find changes since 3.9, which are added after this. Signed-off-by: Sebastiaan van Stijn --- .../schema/data/config_schema_v3.10.json | 620 ++++++++++++++++++ cli/compose/schema/schema_test.go | 1 + 2 files changed, 621 insertions(+) create mode 100644 cli/compose/schema/data/config_schema_v3.10.json diff --git a/cli/compose/schema/data/config_schema_v3.10.json b/cli/compose/schema/data/config_schema_v3.10.json new file mode 100644 index 0000000000..53e8ba890a --- /dev/null +++ b/cli/compose/schema/data/config_schema_v3.10.json @@ -0,0 +1,620 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.10.json", + "type": "object", + "required": ["version"], + + "properties": { + "version": { + "type": "string" + }, + + "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": false + } + ] + }, + "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"}, + "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 + } + ] + } + } + }, + "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"} + } + }, + "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 + } + }, + "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"} + }, + "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_test.go b/cli/compose/schema/schema_test.go index d90e45f959..e31828d4ba 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -98,6 +98,7 @@ func TestValidateCredentialSpecs(t *testing.T) { {version: "3.7", expectedErr: "config"}, {version: "3.8"}, {version: "3.9"}, + {version: "3.10"}, } for _, tc := range tests { From a7778806a0e3930b4d27810149be358e07bc3861 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Aug 2021 14:00:37 +0200 Subject: [PATCH 3/3] cli/compose/schema: make version optional, default to "latest" The compose spec (https://compose-spec.io) defines the version to be optional, and implementations of the spec to check for supported attributes instead. While this change does not switch the `docker stack` implementation to use the compose-spec, it makes it function more similar. Previously, omitting a version number would either produce an error (as the field was required), or switched the handling to assume it was version 1.0 (which is deprecated). With this change, compose files without a version number will be handled as the latest version supported by `docker stack` (currently 3.10). This allows users that work with docker-compose or docker compose (v2) to deploy their compose file, without having to re-add a version number. Fields that are not supported by stackes (schema 3.10) will still produce an error. Signed-off-by: Sebastiaan van Stijn --- cli/compose/loader/loader_test.go | 8 ++++++++ cli/compose/schema/data/config_schema_v3.10.json | 4 ++-- cli/compose/schema/schema.go | 5 +++-- cli/compose/schema/schema_test.go | 4 +++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index db57dba24a..052dd6f0bd 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -422,6 +422,14 @@ func TestV1Unsupported(t *testing.T) { foo: image: busybox `) + assert.ErrorContains(t, err, "(root) Additional property foo is not allowed") + + _, err = loadYAML(` +version: "1.0" +foo: + image: busybox +`) + assert.ErrorContains(t, err, "unsupported Compose file version: 1.0") } diff --git a/cli/compose/schema/data/config_schema_v3.10.json b/cli/compose/schema/data/config_schema_v3.10.json index 53e8ba890a..d7efc208ce 100644 --- a/cli/compose/schema/data/config_schema_v3.10.json +++ b/cli/compose/schema/data/config_schema_v3.10.json @@ -2,11 +2,11 @@ "$schema": "http://json-schema.org/draft-04/schema#", "id": "config_schema_v3.10.json", "type": "object", - "required": ["version"], "properties": { "version": { - "type": "string" + "type": "string", + "default": "3.10" }, "services": { diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index f8a1abaf65..a662f4aaf5 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -11,7 +11,7 @@ import ( ) const ( - defaultVersion = "1.0" + defaultVersion = "3.10" versionField = "version" ) @@ -39,7 +39,8 @@ func init() { gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) } -// Version returns the version of the config, defaulting to version 1.0 +// Version returns the version of the config, defaulting to the latest "3.x" +// version (3.10). func Version(config map[string]interface{}) string { version, ok := config[versionField] if !ok { diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index e31828d4ba..ed14e1d0ae 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -21,7 +21,8 @@ func TestValidate(t *testing.T) { assert.NilError(t, Validate(config, "3.0")) assert.NilError(t, Validate(config, "3")) - assert.ErrorContains(t, Validate(config, ""), "unsupported Compose file version: 1.0") + assert.NilError(t, Validate(config, "")) + assert.ErrorContains(t, Validate(config, "1.0"), "unsupported Compose file version: 1.0") assert.ErrorContains(t, Validate(config, "12345"), "unsupported Compose file version: 12345") } @@ -99,6 +100,7 @@ func TestValidateCredentialSpecs(t *testing.T) { {version: "3.8"}, {version: "3.9"}, {version: "3.10"}, + {version: ""}, } for _, tc := range tests {