Repository: compose-spec/compose-go
Pull request #652 (2024-07-01 11:52:03)

attributes also can be declared as string (using variables)
This removes constraints on compose.yaml attributes as those can be set by variables, so we can't require value to be a number or format to be a duration, as `${VARIABLE}` also is a valid value


Patch:
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 33a79e9..a6a9a1f 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -13,7 +13,6 @@
 
     "name": {
       "type": "string",
-      "pattern": "^[a-z0-9][a-z0-9_-]*$",
       "description": "define the Compose project name, until user defines one explicitly."
     },
 
@@ -94,7 +93,7 @@
         "develop": {"$ref": "#/definitions/development"},
         "deploy": {"$ref": "#/definitions/deployment"},
         "annotations": {"$ref": "#/definitions/list_or_dict"},
-        "attach": {"type": "boolean"},
+        "attach": {"type": ["boolean", "string"]},
         "build": {
           "oneOf": [
             {"type": "string"},
@@ -110,15 +109,15 @@
                 "labels": {"$ref": "#/definitions/list_or_dict"},
                 "cache_from": {"type": "array", "items": {"type": "string"}},
                 "cache_to": {"type": "array", "items": {"type": "string"}},
-                "no_cache": {"type": "boolean"},
+                "no_cache": {"type": ["boolean", "string"]},
                 "additional_contexts": {"$ref": "#/definitions/list_or_dict"},
                 "network": {"type": "string"},
-                "pull": {"type": "boolean"},
+                "pull": {"type": ["boolean", "string"]},
                 "target": {"type": "string"},
                 "shm_size": {"type": ["integer", "string"]},
                 "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
                 "isolation": {"type": "string"},
-                "privileged": {"type": "boolean"},
+                "privileged": {"type": ["boolean", "string"]},
                 "secrets": {"$ref": "#/definitions/service_config_or_secret"},
                 "tags": {"type": "array", "items": {"type": "string"}},
                 "ulimits": {"$ref": "#/definitions/ulimits"},
@@ -148,7 +147,7 @@
               "type": "array",
               "items": {"$ref": "#/definitions/blkio_limit"}
             },
-            "weight": {"type": "integer"},
+            "weight": {"type": ["integer", "string"]},
             "weight_device": {
               "type": "array",
               "items": {"$ref": "#/definitions/blkio_weight"}
@@ -163,8 +162,14 @@
         "command": {"$ref": "#/definitions/command"},
         "configs": {"$ref": "#/definitions/service_config_or_secret"},
         "container_name": {"type": "string"},
-        "cpu_count": {"type": "integer", "minimum": 0},
-        "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
+        "cpu_count": {"oneOf": [
+          {"type": "string"},
+          {"type": "integer", "minimum": 0}
+        ]},
+        "cpu_percent": {"oneOf": [
+          {"type": "string"},
+          {"type": "integer", "minimum": 0, "maximum": 100}
+        ]},
         "cpu_shares": {"type": ["number", "string"]},
         "cpu_quota": {"type": ["number", "string"]},
         "cpu_period": {"type": ["number", "string"]},
@@ -193,7 +198,7 @@
                   "type": "object",
                   "additionalProperties": false,
                   "properties": {
-                    "restart": {"type": "boolean"},
+                    "restart": {"type": ["boolean", "string"]},
                     "required": {
                       "type":  "boolean",
                       "default": true
@@ -254,7 +259,7 @@
         "healthcheck": {"$ref": "#/definitions/healthcheck"},
         "hostname": {"type": "string"},
         "image": {"type": "string"},
-        "init": {"type": "boolean"},
+        "init": {"type": ["boolean", "string"]},
         "ipc": {"type": "string"},
         "isolation": {"type": "string"},
         "labels": {"$ref": "#/definitions/list_or_dict"},
@@ -277,7 +282,7 @@
         "mac_address": {"type": "string"},
         "mem_limit": {"type": ["number", "string"]},
         "mem_reservation": {"type": ["string", "integer"]},
-        "mem_swappiness": {"type": "integer"},
+        "mem_swappiness": {"type": ["integer", "string"]},
         "memswap_limit": {"type": ["number", "string"]},
         "network_mode": {"type": "string"},
         "networks": {
@@ -315,8 +320,11 @@
             }
           ]
         },
-        "oom_kill_disable": {"type": "boolean"},
-        "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
+        "oom_kill_disable": {"type": ["boolean", "string"]},
+        "oom_score_adj": {"oneOf": [
+          {"type": "string"},
+          {"type": "integer", "minimum": -1000, "maximum": 1000}
+        ]},
         "pid": {"type": ["string", "null"]},
         "pids_limit": {"type": ["number", "string"]},
         "platform": {"type": "string"},
@@ -324,15 +332,15 @@
           "type": "array",
           "items": {
             "oneOf": [
-              {"type": "number", "format": "ports"},
-              {"type": "string", "format": "ports"},
+              {"type": "number"},
+              {"type": "string"},
               {
                 "type": "object",
                 "properties": {
                   "name": {"type": "string"},
                   "mode": {"type": "string"},
                   "host_ip": {"type": "string"},
-                  "target": {"type": "integer"},
+                  "target": {"type": ["integer", "string"]},
                   "published": {"type": ["string", "integer"]},
                   "protocol": {"type": "string"},
                   "app_protocol": {"type": "string"}
@@ -344,29 +352,29 @@
           },
           "uniqueItems": true
         },
-        "privileged": {"type": "boolean"},
+        "privileged": {"type": ["boolean", "string"]},
         "profiles": {"$ref": "#/definitions/list_of_strings"},
         "pull_policy": {"type": "string", "enum": [
           "always", "never", "if_not_present", "build", "missing"
         ]},
-        "read_only": {"type": "boolean"},
+        "read_only": {"type": ["boolean", "string"]},
         "restart": {"type": "string"},
         "runtime": {
           "type": "string"
         },
         "scale": {
-          "type": "integer"
+          "type": ["integer", "string"]
         },
         "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
         "shm_size": {"type": ["number", "string"]},
         "secrets": {"$ref": "#/definitions/service_config_or_secret"},
         "sysctls": {"$ref": "#/definitions/list_or_dict"},
-        "stdin_open": {"type": "boolean"},
-        "stop_grace_period": {"type": "string", "format": "duration"},
+        "stdin_open": {"type": ["boolean", "string"]},
+        "stop_grace_period": {"type": "string"},
         "stop_signal": {"type": "string"},
         "storage_opt": {"type": "object"},
         "tmpfs": {"$ref": "#/definitions/string_or_list"},
-        "tty": {"type": "boolean"},
+        "tty": {"type": ["boolean", "string"]},
         "ulimits": {"$ref": "#/definitions/ulimits"},
         "user": {"type": "string"},
         "uts": {"type": "string"},
@@ -383,13 +391,13 @@
                   "type": {"type": "string"},
                   "source": {"type": "string"},
                   "target": {"type": "string"},
-                  "read_only": {"type": "boolean"},
+                  "read_only": {"type": ["boolean", "string"]},
                   "consistency": {"type": "string"},
                   "bind": {
                     "type": "object",
                     "properties": {
                       "propagation": {"type": "string"},
-                      "create_host_path": {"type": "boolean"},
+                      "create_host_path": {"type": ["boolean", "string"]},
                       "selinux": {"type": "string", "enum": ["z", "Z"]}
                     },
                     "additionalProperties": false,
@@ -398,7 +406,7 @@
                   "volume": {
                     "type": "object",
                     "properties": {
-                      "nocopy": {"type": "boolean"},
+                      "nocopy": {"type": ["boolean", "string"]},
                       "subpath": {"type": "string"}
                     },
                     "additionalProperties": false,
@@ -413,7 +421,7 @@
                           {"type": "string"}
                         ]
                       },
-                      "mode": {"type": "number"}
+                      "mode": {"type": ["number", "string"]}
                     },
                     "additionalProperties": false,
                     "patternProperties": {"^x-": {}}
@@ -441,18 +449,18 @@
       "id": "#/definitions/healthcheck",
       "type": "object",
       "properties": {
-        "disable": {"type": "boolean"},
-        "interval": {"type": "string", "format": "duration"},
-        "retries": {"type": "number"},
+        "disable": {"type": ["boolean", "string"]},
+        "interval": {"type": "string"},
+        "retries": {"type": ["number", "string"]},
         "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"}
+        "timeout": {"type": "string"},
+        "start_period": {"type": "string"},
+        "start_interval": {"type": "string"}
       },
       "additionalProperties": false,
       "patternProperties": {"^x-": {}}
@@ -484,16 +492,16 @@
       "properties": {
         "mode": {"type": "string"},
         "endpoint_mode": {"type": "string"},
-        "replicas": {"type": "integer"},
+        "replicas": {"type": ["integer", "string"]},
         "labels": {"$ref": "#/definitions/list_or_dict"},
         "rollback_config": {
           "type": "object",
           "properties": {
-            "parallelism": {"type": "integer"},
-            "delay": {"type": "string", "format": "duration"},
+            "parallelism": {"type": ["integer", "string"]},
+            "delay": {"type": "string"},
             "failure_action": {"type": "string"},
-            "monitor": {"type": "string", "format": "duration"},
-            "max_failure_ratio": {"type": "number"},
+            "monitor": {"type": "string"},
+            "max_failure_ratio": {"type": ["number", "string"]},
             "order": {"type": "string", "enum": [
               "start-first", "stop-first"
             ]}
@@ -504,11 +512,11 @@
         "update_config": {
           "type": "object",
           "properties": {
-            "parallelism": {"type": "integer"},
-            "delay": {"type": "string", "format": "duration"},
+            "parallelism": {"type": ["integer", "string"]},
+            "delay": {"type": "string"},
             "failure_action": {"type": "string"},
-            "monitor": {"type": "string", "format": "duration"},
-            "max_failure_ratio": {"type": "number"},
+            "monitor": {"type": "string"},
+            "max_failure_ratio": {"type": ["number", "string"]},
             "order": {"type": "string", "enum": [
               "start-first", "stop-first"
             ]}
@@ -524,7 +532,7 @@
               "properties": {
                 "cpus": {"type": ["number", "string"]},
                 "memory": {"type": "string"},
-                "pids": {"type": "integer"}
+                "pids": {"type": ["integer", "string"]}
               },
               "additionalProperties": false,
               "patternProperties": {"^x-": {}}
@@ -548,9 +556,9 @@
           "type": "object",
           "properties": {
             "condition": {"type": "string"},
-            "delay": {"type": "string", "format": "duration"},
-            "max_attempts": {"type": "integer"},
-            "window": {"type": "string", "format": "duration"}
+            "delay": {"type": "string"},
+            "max_attempts": {"type": ["integer", "string"]},
+            "window": {"type": "string"}
           },
           "additionalProperties": false,
           "patternProperties": {"^x-": {}}
@@ -570,7 +578,7 @@
                 "patternProperties": {"^x-": {}}
               }
             },
-            "max_replicas_per_node": {"type": "integer"}
+            "max_replicas_per_node": {"type": ["integer", "string"]}
           },
           "additionalProperties": false,
           "patternProperties": {"^x-": {}}
@@ -590,7 +598,7 @@
             "type": "object",
             "properties": {
               "kind": {"type": "string"},
-              "value": {"type": "number"}
+              "value": {"type": ["number", "string"]}
             },
             "additionalProperties": false,
             "patternProperties": {"^x-": {}}
@@ -655,7 +663,7 @@
               "items": {
                 "type": "object",
                 "properties": {
-                  "subnet": {"type": "string", "format": "subnet_ip_address"},
+                  "subnet": {"type": "string"},
                   "ip_range": {"type": "string"},
                   "gateway": {"type": "string"},
                   "aux_addresses": {
@@ -678,7 +686,7 @@
           "patternProperties": {"^x-": {}}
         },
         "external": {
-          "type": ["boolean", "object"],
+          "type": ["boolean", "string", "object"],
           "properties": {
             "name": {
               "deprecated": true,
@@ -688,9 +696,9 @@
           "additionalProperties": false,
           "patternProperties": {"^x-": {}}
         },
-        "internal": {"type": "boolean"},
-        "enable_ipv6": {"type": "boolean"},
-        "attachable": {"type": "boolean"},
+        "internal": {"type": ["boolean", "string"]},
+        "enable_ipv6": {"type": ["boolean", "string"]},
+        "attachable": {"type": ["boolean", "string"]},
         "labels": {"$ref": "#/definitions/list_or_dict"}
       },
       "additionalProperties": false,
@@ -710,7 +718,7 @@
           }
         },
         "external": {
-          "type": ["boolean", "object"],
+          "type": ["boolean", "string", "object"],
           "properties": {
             "name": {
               "deprecated": true,
@@ -734,7 +742,7 @@
         "environment": {"type": "string"},
         "file": {"type": "string"},
         "external": {
-          "type": ["boolean", "object"],
+          "type": ["boolean", "string", "object"],
           "properties": {
             "name": {"type": "string"}
           }
@@ -762,7 +770,7 @@
         "environment": {"type": "string"},
         "file": {"type": "string"},
         "external": {
-          "type": ["boolean", "object"],
+          "type": ["boolean", "string", "object"],
           "properties": {
             "name": {
               "deprecated": true,
@@ -801,7 +809,7 @@
                     "type": "string"
                   },
                   "required": {
-                    "type": "boolean",
+                    "type": ["boolean", "string"],
                     "default": true
                   }
                 },
@@ -855,7 +863,7 @@
       "type": "object",
       "properties": {
         "path": {"type": "string"},
-        "weight": {"type": "integer"}
+        "weight": {"type": ["integer", "string"]}
       },
       "additionalProperties": false
     },
@@ -871,7 +879,7 @@
               "target": {"type": "string"},
               "uid": {"type": "string"},
               "gid": {"type": "string"},
-              "mode": {"type": "number"}
+              "mode": {"type": ["number", "string"]}
             },
             "additionalProperties": false,
             "patternProperties": {"^x-": {}}
@@ -884,12 +892,12 @@
       "patternProperties": {
         "^[a-z]+$": {
           "oneOf": [
-            {"type": "integer"},
+            {"type": ["integer", "string"]},
             {
               "type": "object",
               "properties": {
-                "hard": {"type": "integer"},
-                "soft": {"type": "integer"}
+                "hard": {"type": ["integer", "string"]},
+                "soft": {"type": ["integer", "string"]}
               },
               "required": ["soft", "hard"],
               "additionalProperties": false,
diff --git a/schema/using-variables.yaml b/schema/using-variables.yaml
new file mode 100644
index 0000000..3f302cd
--- /dev/null
+++ b/schema/using-variables.yaml
@@ -0,0 +1,123 @@
+name: ${VARIABLE}
+services:
+  foo:
+    deploy:
+      mode: ${VARIABLE}
+      replicas: ${VARIABLE}
+      rollback_config:
+        parallelism: ${VARIABLE}
+        delay: ${VARIABLE}
+        failure_action: ${VARIABLE}
+        monitor: ${VARIABLE}
+        max_failure_ratio: ${VARIABLE}
+      update_config:
+        parallelism: ${VARIABLE}
+        delay: ${VARIABLE}
+        failure_action: ${VARIABLE}
+        monitor: ${VARIABLE}
+        max_failure_ratio: ${VARIABLE}
+      resources:
+        limits:
+          memory: ${VARIABLE}
+        reservations:
+          memory: ${VARIABLE}
+          generic_resources:
+            - discrete_resource_spec:
+                kind: ${VARIABLE}
+                value: ${VARIABLE}
+            - discrete_resource_spec:
+                kind: ${VARIABLE}
+                value: ${VARIABLE}
+      restart_policy:
+        condition: ${VARIABLE}
+        delay: ${VARIABLE}
+        max_attempts: ${VARIABLE}
+        window: ${VARIABLE}
+      placement:
+        max_replicas_per_node: ${VARIABLE}
+        preferences:
+          - spread: ${VARIABLE}
+      endpoint_mode: ${VARIABLE}
+    expose:
+      - ${VARIABLE}
+    external_links:
+      - ${VARIABLE}
+    extra_hosts:
+      - ${VARIABLE}
+    hostname: ${VARIABLE}
+
+    healthcheck:
+      test: ${VARIABLE}
+      interval: ${VARIABLE}
+      timeout: ${VARIABLE}
+      retries: ${VARIABLE}
+      start_period: ${VARIABLE}
+      start_interval: ${VARIABLE}
+    image: ${VARIABLE}
+    mac_address: ${VARIABLE}
+    networks:
+      some-network:
+        aliases:
+          - ${VARIABLE}
+      other-network:
+        ipv4_address: ${VARIABLE}
+        ipv6_address: ${VARIABLE}
+        mac_address: ${VARIABLE}
+    ports:
+      - ${VARIABLE}
+    privileged: ${VARIABLE}
+    read_only: ${VARIABLE}
+    restart: ${VARIABLE}
+    secrets:
+      - source: ${VARIABLE}
+        target: ${VARIABLE}
+        uid: ${VARIABLE}
+        gid: ${VARIABLE}
+        mode: ${VARIABLE}
+    stdin_open: ${VARIABLE}
+    stop_grace_period: ${VARIABLE}
+    stop_signal: ${VARIABLE}
+    storage_opt:
+      size: ${VARIABLE}
+    sysctls:
+      net.core.somaxconn: ${VARIABLE}
+    tmpfs:
+      - ${VARIABLE}
+    tty: ${VARIABLE}
+    ulimits:
+      nproc: ${VARIABLE}
+      nofile:
+        soft: ${VARIABLE}
+        hard: ${VARIABLE}
+    user: ${VARIABLE}
+    volumes:
+      - ${VARIABLE}:${VARIABLE}
+      - type: tmpfs
+        target: ${VARIABLE}
+        tmpfs:
+          size: ${VARIABLE}
+
+networks:
+  network:
+    ipam:
+      driver: ${VARIABLE}
+      config:
+        - subnet: ${VARIABLE}
+          ip_range: ${VARIABLE}
+          gateway: ${VARIABLE}
+          aux_addresses:
+            host1: ${VARIABLE}
+  external-network:
+    external: ${VARIABLE}
+
+volumes:
+  external-volume:
+    external: ${VARIABLE}
+
+configs:
+  config1:
+    external: ${VARIABLE}
+
+secrets:
+  secret1:
+    external: ${VARIABLE}


Test patch:
diff --git a/schema/schema_test.go b/schema/schema_test.go
index 8467cde..902805b 100644
--- a/schema/schema_test.go
+++ b/schema/schema_test.go
@@ -17,8 +17,10 @@
 package schema
 
 import (
+	"os"
 	"testing"
 
+	"gopkg.in/yaml.v3"
 	"gotest.tools/v3/assert"
 )
 
@@ -233,3 +235,12 @@ func TestValidateRollbackConfigWithUpdateConfigFull(t *testing.T) {
 	assert.NilError(t, Validate(config))
 	assert.NilError(t, Validate(config))
 }
+
+func TestValidateVariables(t *testing.T) {
+	bytes, err := os.ReadFile("using-variables.yaml")
+	assert.NilError(t, err)
+	var config dict
+	err = yaml.Unmarshal(bytes, &config)
+	assert.NilError(t, err)
+	assert.NilError(t, Validate(config))
+}