Browse Source

Merge pull request #4147 from aanand/version-3.0

Support version 3.0 of the Compose file format
Aanand Prasad 9 years ago
parent
commit
586443ba5d

+ 17 - 4
compose/config/config.py

@@ -15,6 +15,7 @@ from cached_property import cached_property
 from ..const import COMPOSEFILE_V1 as V1
 from ..const import COMPOSEFILE_V2_0 as V2_0
 from ..const import COMPOSEFILE_V2_1 as V2_1
+from ..const import COMPOSEFILE_V3_0 as V3_0
 from ..utils import build_string_dict
 from ..utils import splitdrive
 from .environment import env_vars_from_file
@@ -175,7 +176,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
         if version == '2':
             version = V2_0
 
-        if version not in (V2_0, V2_1):
+        if version == '3':
+            version = V3_0
+
+        if version not in (V2_0, V2_1, V3_0):
             raise ConfigurationError(
                 'Version in "{}" is unsupported. {}'
                 .format(self.filename, VERSION_EXPLANATION))
@@ -326,6 +330,14 @@ def load(config_details):
         for service_dict in service_dicts:
             match_named_volumes(service_dict, volumes)
 
+    services_using_deploy = [s for s in service_dicts if s.get('deploy')]
+    if services_using_deploy:
+        log.warn(
+            "Some services ({}) use the 'deploy' key, which will be ignored. "
+            "Compose does not support deploy configuration - use the experimental "
+            "`docker deploy` command to deploy to a swarm."
+            .format(", ".join(sorted(s['name'] for s in services_using_deploy))))
+
     return Config(main_file.version, service_dicts, volumes, networks)
 
 
@@ -433,7 +445,7 @@ def process_config_file(config_file, environment, service_name=None):
         'service',
         environment)
 
-    if config_file.version in (V2_0, V2_1):
+    if config_file.version in (V2_0, V2_1, V3_0):
         processed_config = dict(config_file.config)
         processed_config['services'] = services
         processed_config['volumes'] = interpolate_config_section(
@@ -446,9 +458,10 @@ def process_config_file(config_file, environment, service_name=None):
             config_file.get_networks(),
             'network',
             environment)
-
-    if config_file.version == V1:
+    elif config_file.version == V1:
         processed_config = services
+    else:
+        raise Exception("Unsupported version: {}".format(repr(config_file.version)))
 
     config_file = config_file._replace(config=processed_config)
     validate_against_config_schema(config_file)

+ 378 - 0
compose/config/config_schema_v3.0.json

@@ -0,0 +1,378 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "config_schema_v3.0.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
+    }
+  },
+
+  "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"}
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cgroup_parent": {"type": "string"},
+        "command": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "container_name": {"type": "string"},
+        "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"},
+        "ipc": {"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": {
+            "type": ["string", "number"],
+            "format": "ports"
+          },
+          "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"]},
+        "stdin_open": {"type": "boolean"},
+        "stop_signal": {"type": "string"},
+        "stop_grace_period": {"type": "string", "format": "duration"},
+        "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"},
+        "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "working_dir": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "healthcheck": {
+      "id": "#/definitions/healthcheck",
+      "type": ["object", "null"],
+      "properties": {
+        "interval": {"type":"string"},
+        "timeout": {"type":"string"},
+        "retries": {"type": "number"},
+        "command": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "deployment": {
+      "id": "#/definitions/deployment",
+      "type": ["object", "null"],
+      "properties": {
+        "mode": {"type": "string"},
+        "replicas": {"type": "integer"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "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"}
+          },
+          "additionalProperties": false
+        },
+        "resources": {
+          "type": "object",
+          "properties": {
+            "limits": {"$ref": "#/definitions/resource"},
+            "reservations": {"$ref": "#/definitions/resource"}
+          }
+        },
+        "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"}}
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "resource": {
+      "id": "#/definitions/resource",
+      "type": "object",
+      "properties": {
+        "cpus": {"type": "string"},
+        "memory": {"type": "string"}
+      },
+      "additionaProperties": false
+    },
+
+    "network": {
+      "id": "#/definitions/network",
+      "type": ["object", "null"],
+      "properties": {
+        "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
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "additionalProperties": false
+    },
+
+    "volume": {
+      "id": "#/definitions/volume",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          }
+        }
+      },
+      "labels": {"$ref": "#/definitions/list_or_dict"},
+      "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"]
+          }
+        }
+      }
+    }
+  }
+}

+ 2 - 2
compose/config/errors.py

@@ -3,8 +3,8 @@ from __future__ import unicode_literals
 
 
 VERSION_EXPLANATION = (
-    'You might be seeing this error because you\'re using the wrong Compose '
-    'file version. Either specify a version of "2" (or "2.0") and place your '
+    'You might be seeing this error because you\'re using the wrong Compose file version. '
+    'Either specify a supported version ("2.0", "2.1", "3.0") and place your '
     'service definitions under the `services` key, or omit the `version` key '
     'and place your service definitions at the root of the file to use '
     'version 1.\nFor more on the Compose file format versions, see '

+ 1 - 2
compose/config/serialize.py

@@ -6,7 +6,6 @@ import yaml
 
 from compose.config import types
 from compose.config.config import V1
-from compose.config.config import V2_0
 from compose.config.config import V2_1
 
 
@@ -34,7 +33,7 @@ def denormalize_config(config):
             del net_conf['external_name']
 
     version = config.version
-    if version not in (V2_0, V2_1):
+    if version == V1:
         version = V2_1
 
     return {

+ 3 - 0
compose/const.py

@@ -17,15 +17,18 @@ LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
 COMPOSEFILE_V1 = '1'
 COMPOSEFILE_V2_0 = '2.0'
 COMPOSEFILE_V2_1 = '2.1'
+COMPOSEFILE_V3_0 = '3.0'
 
 API_VERSIONS = {
     COMPOSEFILE_V1: '1.21',
     COMPOSEFILE_V2_0: '1.22',
     COMPOSEFILE_V2_1: '1.24',
+    COMPOSEFILE_V3_0: '1.24',
 }
 
 API_VERSION_TO_ENGINE_VERSION = {
     API_VERSIONS[COMPOSEFILE_V1]: '1.9.0',
     API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0',
     API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0',
+    API_VERSIONS[COMPOSEFILE_V3_0]: '1.12.0',
 }

+ 55 - 0
tests/acceptance/cli_test.py

@@ -285,6 +285,61 @@ class CLITestCase(DockerClientTestCase):
             'volumes': {},
         }
 
+    def test_config_v3(self):
+        self.base_dir = 'tests/fixtures/v3-full'
+        result = self.dispatch(['config'])
+
+        assert yaml.load(result.stdout) == {
+            'version': '3.0',
+            'networks': {},
+            'volumes': {},
+            'services': {
+                'web': {
+                    'image': 'busybox',
+                    'deploy': {
+                        'mode': 'replicated',
+                        'replicas': 6,
+                        'labels': ['FOO=BAR'],
+                        'update_config': {
+                            'parallelism': 3,
+                            'delay': '10s',
+                            'failure_action': 'continue',
+                            'monitor': '60s',
+                            'max_failure_ratio': 0.3,
+                        },
+                        'resources': {
+                            'limits': {
+                                'cpus': '0.001',
+                                'memory': '50M',
+                            },
+                            'reservations': {
+                                    'cpus': '0.0001',
+                                    'memory': '20M',
+                            },
+                        },
+                        'restart_policy': {
+                            'condition': 'on_failure',
+                            'delay': '5s',
+                            'max_attempts': 3,
+                            'window': '120s',
+                        },
+                        'placement': {
+                            'constraints': ['node=foo'],
+                        },
+                    },
+
+                    'healthcheck': {
+                        'command': 'cat /etc/passwd',
+                        'interval': '10s',
+                        'timeout': '1s',
+                        'retries': 5,
+                    },
+
+                    'stop_grace_period': '20s',
+                },
+            },
+        }
+
     def test_ps(self):
         self.project.get_service('simple').create_container()
         result = self.dispatch(['ps'])

+ 37 - 0
tests/fixtures/v3-full/docker-compose.yml

@@ -0,0 +1,37 @@
+version: "3"
+services:
+  web:
+    image: busybox
+
+    deploy:
+      mode: replicated
+      replicas: 6
+      labels: [FOO=BAR]
+      update_config:
+        parallelism: 3
+        delay: 10s
+        failure_action: continue
+        monitor: 60s
+        max_failure_ratio: 0.3
+      resources:
+        limits:
+          cpus: '0.001'
+          memory: 50M
+        reservations:
+          cpus: '0.0001'
+          memory: 20M
+      restart_policy:
+        condition: on_failure
+        delay: 5s
+        max_attempts: 3
+        window: 120s
+      placement:
+        constraints: [node=foo]
+
+    healthcheck:
+      command: cat /etc/passwd
+      interval: 10s
+      timeout: 1s
+      retries: 5
+
+    stop_grace_period: 20s

+ 6 - 0
tests/unit/config/config_test.py

@@ -18,6 +18,7 @@ from compose.config.config import resolve_environment
 from compose.config.config import V1
 from compose.config.config import V2_0
 from compose.config.config import V2_1
+from compose.config.config import V3_0
 from compose.config.environment import Environment
 from compose.config.errors import ConfigurationError
 from compose.config.errors import VERSION_EXPLANATION
@@ -156,9 +157,14 @@ class ConfigTest(unittest.TestCase):
         for version in ['2', '2.0']:
             cfg = config.load(build_config_details({'version': version}))
             assert cfg.version == V2_0
+
         cfg = config.load(build_config_details({'version': '2.1'}))
         assert cfg.version == V2_1
 
+        for version in ['3', '3.0']:
+            cfg = config.load(build_config_details({'version': version}))
+            assert cfg.version == V3_0
+
     def test_v1_file_version(self):
         cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
         assert cfg.version == V1