From d4a5f9c5e9416cc7ce32986ea458ec1bdf866324 Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Tue, 26 Dec 2017 14:59:28 +0800 Subject: [PATCH] Add detailed parameters for Capsule create Currently we don't have detailed parameters checking when creating a capsule, this patch add the volume parameters type checking and several test cases. Also modify the related template file and design document. Change some parameters in the template to Kubernetes friendly. Modify the field check from "spec" to "template" in check_capsule_template, since there already a low level spec field in capsule yaml. Will also modify the python-zunclient. Part of blueprint introduce-compose Change-Id: I88c1c248d83d0a27f5a291fcf9d952bb70234dff Signed-off-by: Kevin Zhao --- specs/container-composition.rst | 4 +- template/capsule/capsule-volume.yaml | 7 +- template/capsule/capsule.yaml | 31 ++- zun/api/controllers/experimental/capsules.py | 13 +- .../experimental/schemas/capsules.py | 4 +- zun/common/utils.py | 37 ++- zun/common/validation/parameter_types.py | 174 +++++++++++++- .../controllers/experimental/test_capsules.py | 93 ++++---- zun/tests/unit/common/test_utils.py | 13 ++ zun/tests/unit/common/test_validations.py | 219 ++++++++++++++++++ zun/tests/unit/db/utils.py | 32 +-- 11 files changed, 529 insertions(+), 98 deletions(-) diff --git a/specs/container-composition.rst b/specs/container-composition.rst index 9d5836044..21945348f 100644 --- a/specs/container-composition.rst +++ b/specs/container-composition.rst @@ -164,7 +164,7 @@ Sample capsule: env: PATH: /usr/local/bin resources: - allocation: + requests: cpu: 1 memory: 2GB volumes: @@ -219,7 +219,7 @@ Ports fields: * protocol(string): TCP or UDP, by default is TCP RecourcesObject fields: -* allocation(AllocationObject): the resources that the capsule needed +* requests(AllocationObject): the resources that the capsule needed AllocationObject: * cpu(string): cpu resources, cores number diff --git a/template/capsule/capsule-volume.yaml b/template/capsule/capsule-volume.yaml index cdd0eedfe..208805969 100644 --- a/template/capsule/capsule-volume.yaml +++ b/template/capsule/capsule-volume.yaml @@ -1,18 +1,17 @@ -capsule_template_version: 2017-12-20 # use "-" because that the fields have many items -capsule_version: beta +capsuleVersion: beta kind: capsule metadata: name: capsule-volume labels: foo: bar -restart_policy: always +restartPolicy: always spec: containers: - image: test command: - "/bin/bash" - workdir: /root + workDir: /root labels: app: web volumeMounts: diff --git a/template/capsule/capsule.yaml b/template/capsule/capsule.yaml index 06b072fd2..c5a1d5455 100644 --- a/template/capsule/capsule.yaml +++ b/template/capsule/capsule.yaml @@ -1,33 +1,30 @@ -capsule_template_version: 2017-06-21 # use "-" because that the fields have many items -capsule_version: beta +capsuleVersion: beta kind: capsule metadata: name: capsule-example labels: app: web - nihao: baibai -restart_policy: always + app1: web1 +restartPolicy: always spec: containers: - image: ubuntu command: - "/bin/bash" - image_pull_policy: ifnotpresent - workdir: /root - labels: - app: web + imagePullPolicy: ifnotpresent + workDir: /root ports: - name: nginx-port containerPort: 80 hostPort: 80 protocol: TCP resources: - allocation: + requests: cpu: 1 memory: 1024 - environment: - PATCH: /usr/local/bin + env: + ENV1: /usr/local/bin volumeMounts: - name: volume1 mountPath: /data1 @@ -38,10 +35,8 @@ spec: args: - "Hello" - "World" - image_pull_policy: ifnotpresent - workdir: /root - labels: - app: web01 + imagePullPolicy: ifnotpresent + workDir: /root ports: - name: nginx-port containerPort: 80 @@ -52,11 +47,11 @@ spec: hostPort: 3306 protocol: TCP resources: - allocation: + requests: cpu: 1 memory: 1024 - environment: - NWH: /usr/bin/ + env: + ENV2: /usr/bin/ volumeMounts: - name: volume2 mountPath: /data2 diff --git a/zun/api/controllers/experimental/capsules.py b/zun/api/controllers/experimental/capsules.py index 602640d7f..c171985e6 100644 --- a/zun/api/controllers/experimental/capsules.py +++ b/zun/api/controllers/experimental/capsules.py @@ -131,8 +131,8 @@ class CapsuleController(base.Controller): action="capsule:create") # Abstract the capsule specification - capsules_spec = capsule_dict['spec'] - spec_content = utils.check_capsule_template(capsules_spec) + capsules_template = capsule_dict.get('template') + spec_content = utils.check_capsule_template(capsules_template) containers_spec = utils.capsule_get_container_spec(spec_content) volumes_spec = utils.capsule_get_volume_spec(spec_content) @@ -148,10 +148,11 @@ class CapsuleController(base.Controller): capsule_need_memory = 0 container_volume_requests = [] - capsule_restart_policy = capsules_spec.get('restart_policy', 'always') + capsule_restart_policy = capsules_template.get('restart_policy', + 'always') - metadata_info = capsules_spec.get('metadata', None) - requested_networks_info = capsules_spec.get('nets', []) + metadata_info = capsules_template.get('metadata', None) + requested_networks_info = capsules_template.get('nets', []) requested_networks = \ utils.build_requested_networks(context, requested_networks_info) @@ -203,7 +204,7 @@ class CapsuleController(base.Controller): if container_dict.get('resources'): resources_list = container_dict.get('resources') - allocation = resources_list.get('allocation') + allocation = resources_list.get('requests') if allocation.get('cpu'): capsule_need_cpu += allocation.get('cpu') container_dict['cpu'] = allocation.get('cpu') diff --git a/zun/api/controllers/experimental/schemas/capsules.py b/zun/api/controllers/experimental/schemas/capsules.py index 3e9abb7a5..91f3dbeec 100644 --- a/zun/api/controllers/experimental/schemas/capsules.py +++ b/zun/api/controllers/experimental/schemas/capsules.py @@ -15,12 +15,12 @@ from zun.common.validation import parameter_types _capsule_properties = { - 'spec': parameter_types.spec + 'template': parameter_types.capsule_template } capsule_create = { 'type': 'object', 'properties': _capsule_properties, - 'required': ['spec'], + 'required': ['template'], 'additionalProperties': False } diff --git a/zun/common/utils.py b/zun/common/utils.py index bbddc4cee..6951886fe 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -76,6 +76,28 @@ VALID_STATES = { consts.PAUSED] } +VALID_CONTAINER_FILED = { + 'image': 'image', + 'command': 'command', + 'args': 'args', + 'resources': 'resources', + 'ports': 'ports', + 'volumeMounts': 'volumeMounts', + 'env': 'environment', + 'workDir': 'workdir', + 'imagePullPolicy': 'image_pull_policy', +} + +VALID_CAPSULE_FIELD = { + 'restartPolicy': 'restart_policy', +} + +VALID_CAPSULE_RESTART_POLICY = { + 'Never': 'no', + 'Always': 'always', + 'OnFailure': 'on-failure', +} + def validate_container_state(container, action): if container.status not in VALID_STATES[action]: @@ -344,6 +366,12 @@ def check_capsule_template(tpl): if kind_field not in ['capsule', 'Capsule']: raise exception.InvalidCapsuleTemplate("kind fields need to be " "set as capsule or Capsule") + # Align the Capsule restartPolicy with container restart_policy + if 'restartPolicy' in tpl.keys(): + tpl['restartPolicy'] = \ + VALID_CAPSULE_RESTART_POLICY[tpl['restartPolicy']] + tpl[VALID_CAPSULE_FIELD['restartPolicy']] = tpl.pop('restartPolicy') + spec_field = tpl.get('spec') if spec_field is None: raise exception.InvalidCapsuleTemplate("No Spec found") @@ -360,10 +388,15 @@ def capsule_get_container_spec(spec_field): "container at least") for i in range(0, containers_num): - container_image = containers_spec[i].get('image') - if container_image is None: + container_spec = containers_spec[i] + if 'image' not in container_spec.keys(): raise exception.InvalidCapsuleTemplate("Container " "image is needed") + # Remap the Capsule's container fields to native Zun container fields. + for key in list(container_spec.keys()): + container_spec[VALID_CONTAINER_FILED[key]] = \ + container_spec.pop(key) + return containers_spec diff --git a/zun/common/validation/parameter_types.py b/zun/common/validation/parameter_types.py index eacbf81ad..8dd04c6c8 100644 --- a/zun/common/validation/parameter_types.py +++ b/zun/common/validation/parameter_types.py @@ -256,6 +256,176 @@ security_groups = { } } -spec = { - 'type': ['object'], +capsule_kind = { + "type": ["string"], + 'enum': ['capsule', 'Capsule'] +} + +capsule_version = { + "type": ["string"], + 'enum': ['beta', 'Beta'] +} + +capsule_metadata = { + "type": ["object"], + "properties": { + "labels": labels, + # use the same format as container name + "name": container_name, + } +} + +capsule_restart_policy = { + "type": ["string"], + "enum": ['Always', 'OnFailure', 'Never'] +} + +capsule_container_command = { + 'type': ['array'], + 'items': command +} + +capsule_container_args = capsule_container_command + +capsule_container_resources = { + 'type': ['object'], + 'properties': { + 'requests': { + "type": ["object"], + 'properties': { + 'cpu': cpu, + 'memory': memory, + }, + 'additionalProperties': False, + }, + }, + "additionalProperties": False, + "required": ['requests'] +} + +capsule_port_protocol = { + "type": ["string"], + 'enum': ['TCP', 'UDP'] +} + +capsule_container_ports = { + 'type': ['array'], + 'items': { + 'type': 'object', + 'properties': { + 'name': container_name, + 'containerPort': non_negative_integer, + 'hostPort': non_negative_integer, + 'protocol': capsule_port_protocol, + }, + 'additionalProperties': False, + 'required': ['containerPort', 'hostPort'] + } +} + +volume_name = { + 'type': ['string'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' +} + +capsule_volume_path = { + 'type': ['string'] +} + +capsule_container_volume_list = { + 'type': ['array'], + 'items': { + 'type': 'object', + 'properties': { + 'name': volume_name, + 'mountPath': capsule_volume_path, + 'readOnly': boolean, + }, + 'additionalProperties': False, + 'required': ['name', 'mountPath'] + } +} + +capsule_containers_list = { + 'type': ['array'], + 'items': { + 'type': 'object', + 'properties': { + 'image': image_name, + 'command': capsule_container_command, + 'args': capsule_container_args, + 'resources': capsule_container_resources, + 'ports': capsule_container_ports, + 'volumeMounts': capsule_container_volume_list, + 'env': environment, + 'workDir': workdir, + 'imagePullPolicy': image_pull_policy, + }, + 'additionalProperties': False, + 'required': ['image'] + } +} + +volume_size = { + 'type': ['number'], + 'pattern': '^[0-9]*$', + 'minLength': 1 +} + +volume_auto_remove = { + 'type': boolean, +} + +volume_uuid = { + 'type': 'string', + 'maxLength': 36, + 'minLength': 36 +} + +capsule_cinder_volume = { + 'type': 'object', + 'properties': { + 'volumeID': volume_uuid, + 'size': volume_size, + 'autoRemove': boolean, + }, + 'additionalProperties': False, +} + +capsule_volumes_list = { + 'type': ['array', 'null'], + 'items': { + 'type': 'object', + 'properties': { + 'name': image_name, + 'cinder': capsule_cinder_volume, + }, + 'additionalProperties': True, + 'required': ['name', 'cinder'] + } +} + +capsule_spec = { + 'type': ['object'], + "properties": { + "containers": capsule_containers_list, + "volumes": capsule_volumes_list, + }, + "additionalProperties": True, + "required": ['containers'] +} + +capsule_template = { + 'type': ['object'], + "properties": { + "kind": capsule_kind, + "capsuleVersion": capsule_version, + "metadata": capsule_metadata, + "restartPolicy": capsule_restart_policy, + "spec": capsule_spec, + }, + "additionalProperties": False, + "required": ['kind', 'spec', 'metadata'] } diff --git a/zun/tests/unit/api/controllers/experimental/test_capsules.py b/zun/tests/unit/api/controllers/experimental/test_capsules.py index 899f2e6c4..4f08882c9 100644 --- a/zun/tests/unit/api/controllers/experimental/test_capsules.py +++ b/zun/tests/unit/api/controllers/experimental/test_capsules.py @@ -28,14 +28,14 @@ class TestCapsuleController(api_base.FunctionalTest): def test_create_capsule(self, mock_capsule_create, mock_neutron_get_network): params = ('{' - '"spec": ' + '"template": ' '{"kind": "capsule",' ' "spec": {' ' "containers":' - ' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' - ' "image": "test", "labels": {"app": "web"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}}' + ' [{"env": {"ROOT_PASSWORD": "foo0"}, ' + ' "image": "test",' + ' "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}}' ' }]' ' }, ' ' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' @@ -66,21 +66,16 @@ class TestCapsuleController(api_base.FunctionalTest): def test_create_capsule_two_containers(self, mock_capsule_create, mock_neutron_get_network): params = ('{' - '"spec": ' + '"template": ' '{"kind": "capsule",' ' "spec": {' ' "containers":' - ' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' - ' "image": "test", "labels": {"app": "web"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}}},' - ' {"environment": {"ROOT_PASSWORD": "foo1"}, ' - ' "image": "test1", "labels": {"app1": "web1"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}}}' - ' ]' + ' [{"image": "test", "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}}}, ' + ' {"image": "test1", "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}}}]' ' }, ' - ' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' + ' "metadata": {"labels": {"foo0": "bar0"},' ' "name": "capsule-example"}' ' }' '}') @@ -89,7 +84,7 @@ class TestCapsuleController(api_base.FunctionalTest): content_type='application/json') return_value = response.json expected_meta_name = "capsule-example" - expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"} + expected_meta_labels = {"foo0": "bar0"} expected_memory = '2048M' expected_cpu = 2.0 expected_container_num = 3 @@ -109,12 +104,11 @@ class TestCapsuleController(api_base.FunctionalTest): @patch('zun.common.utils.check_capsule_template') def test_create_capsule_wrong_kind_set(self, mock_check_template, mock_capsule_create): - params = ('{"spec": {"kind": "test",' + params = ('{"template": {"kind": "test",' '"spec": {"containers":' '[{"environment": {"ROOT_PASSWORD": "foo0"}, ' - '"image": "test1", "labels": {"app0": "web0"}, ' - '"image_driver": "docker", "resources": ' - '{"allocation": {"cpu": 1, "memory": 1024}}}]}, ' + '"image": "test1", "resources": ' + '{"requests": {"cpu": 1, "memory": 1024}}}]}, ' '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') mock_check_template.side_effect = exception.InvalidCapsuleTemplate( @@ -127,7 +121,7 @@ class TestCapsuleController(api_base.FunctionalTest): @patch('zun.common.utils.check_capsule_template') def test_create_capsule_less_than_one_container(self, mock_check_template, mock_capsule_create): - params = ('{"spec": {"kind": "capsule",' + params = ('{"template": {"kind": "capsule",' '"spec": {container:[]}, ' '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') @@ -141,7 +135,7 @@ class TestCapsuleController(api_base.FunctionalTest): @patch('zun.common.utils.check_capsule_template') def test_create_capsule_no_container_field(self, mock_check_template, mock_capsule_create): - params = ('{"spec": {"kind": "capsule",' + params = ('{"template": {"kind": "capsule",' '"spec": {}, ' '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') @@ -155,8 +149,8 @@ class TestCapsuleController(api_base.FunctionalTest): @patch('zun.common.utils.check_capsule_template') def test_create_capsule_no_container_image(self, mock_check_template, mock_capsule_create): - params = ('{"spec": {"kind": "capsule",' - '"spec": {container:[{"environment": ' + params = ('{"template": {"kind": "capsule",' + '"spec": {container:[{"env": ' '{"ROOT_PASSWORD": "foo1"}]}, ' '"metadata": {"labels": {"foo0": "bar0"}, ' '"name": "capsule-example"}}}') @@ -174,18 +168,17 @@ class TestCapsuleController(api_base.FunctionalTest): mock_neutron_get_network, mock_create_volume, mock_ensure_volume_usable): - fake_volume_id = 'fakevolid' + fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2' fake_volume = mock.Mock(id=fake_volume_id) mock_create_volume.return_value = fake_volume params = ('{' - '"spec":' + '"template":' '{"kind": "capsule",' ' "spec":' ' {"containers":' - ' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' - ' "image": "test", "labels": {"app": "web"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}},' + ' [{"env": {"ROOT_PASSWORD": "foo0"}, ' + ' "image": "test", "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}},' ' "volumeMounts": [{"name": "volume1", ' ' "mountPath": "/data1"}]' ' }' @@ -195,7 +188,8 @@ class TestCapsuleController(api_base.FunctionalTest): ' "cinder": {"size": 3, "autoRemove": "True"}' ' }]' ' }, ' - ' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' + ' "metadata": {"labels": ' + ' {"foo0": "bar0", "foo1": "bar1"},' ' "name": "capsule-example"}' ' }' '}') @@ -227,28 +221,29 @@ class TestCapsuleController(api_base.FunctionalTest): mock_neutron_get_network, mock_search_volume, mock_ensure_volume_usable): - fake_volume_id = 'fakevolid' + fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2' fake_volume = mock.Mock(id=fake_volume_id) mock_search_volume.return_value = fake_volume params = ('{' - '"spec":' + '"template":' '{"kind": "capsule",' ' "spec":' ' {"containers":' - ' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' - ' "image": "test", "labels": {"app": "web"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}},' + ' [{"env": {"ROOT_PASSWORD": "foo0"}, ' + ' "image": "test", "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}},' ' "volumeMounts": [{"name": "volume1", ' ' "mountPath": "/data1"}]' ' }' ' ],' ' "volumes":' ' [{"name": "volume1",' - ' "cinder": {"volumeID": "fakevolid"}' + ' "cinder": {"volumeID": ' + ' "3259309d-659c-4e20-b354-ee712e64b3b2"}' ' }]' ' }, ' - ' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' + ' "metadata": {"labels": ' + ' {"foo0": "bar0", "foo1": "bar1"},' ' "name": "capsule-example"}' ' }' '}') @@ -283,30 +278,30 @@ class TestCapsuleController(api_base.FunctionalTest): mock_search_volume, mock_ensure_volume_usable, mock_create_volume): - fake_volume_id1 = 'fakevolid1' + fake_volume_id1 = '3259309d-659c-4e20-b354-ee712e64b3b2' fake_volume = mock.Mock(id=fake_volume_id1) mock_search_volume.return_value = fake_volume - fake_volume_id2 = 'fakevolid2' + fake_volume_id2 = 'ef770cfb-349a-483a-97f6-b86e46e344b8' fake_volume = mock.Mock(id=fake_volume_id2) mock_create_volume.return_value = fake_volume params = ('{' - '"spec":' + '"template":' '{"kind": "capsule",' ' "spec":' ' {"containers":' - ' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' - ' "image": "test", "labels": {"app": "web"}, ' - ' "image_driver": "docker", "resources": ' - ' {"allocation": {"cpu": 1, "memory": 1024}},' + ' [{"env": {"ROOT_PASSWORD": "foo0"}, ' + ' "image": "test", "resources": ' + ' {"requests": {"cpu": 1, "memory": 1024}},' ' "volumeMounts": [{"name": "volume1", ' ' "mountPath": "/data1"},' - ' {"name": "volume2", ' + ' {"name": "volume2", ' ' "mountPath": "/data2"}]' ' }' ' ],' ' "volumes":' ' [{"name": "volume1",' - ' "cinder": {"volumeID": "fakevolid1"}},' + ' "cinder": {"volumeID": ' + ' "3259309d-659c-4e20-b354-ee712e64b3b2"}},' ' {"name": "volume2",' ' "cinder": {"size": 3, "autoRemove": "True"}' ' }]' diff --git a/zun/tests/unit/common/test_utils.py b/zun/tests/unit/common/test_utils.py index 3ef44acf6..e40bc259f 100644 --- a/zun/tests/unit/common/test_utils.py +++ b/zun/tests/unit/common/test_utils.py @@ -143,6 +143,12 @@ class TestUtils(base.TestCase): params = ({"kind": "capsule", "spec": {}}) utils.check_capsule_template(params) + params = ({"kind": "capsule", "restartPolicy": "Always", "spec": { + "containers": [{"image": "test1"}] + }}) + utils.check_capsule_template(params) + self.assertEqual(params["restart_policy"], "always") + def test_capsule_get_container_spec(self): with self.assertRaisesRegex( exception.InvalidCapsuleTemplate, @@ -163,6 +169,13 @@ class TestUtils(base.TestCase): {"environment": {"ROOT_PASSWORD": "foo0"}}]}) utils.capsule_get_container_spec(params) + params = ({"containers": [ + {"image": "test1", "env": {"ROOT_PASSWORD": "foo0"}}]}) + utils.capsule_get_container_spec(params) + self.assertEqual(params.get("containers")[0].get("environment"), + {"ROOT_PASSWORD": "foo0"}) + self.assertNotIn("env", params.get("containers")) + def test_capsule_get_volume_spec(self): with self.assertRaisesRegex( exception.InvalidCapsuleTemplate, diff --git a/zun/tests/unit/common/test_validations.py b/zun/tests/unit/common/test_validations.py index 177703e4c..6eca861b2 100644 --- a/zun/tests/unit/common/test_validations.py +++ b/zun/tests/unit/common/test_validations.py @@ -37,6 +37,15 @@ CONTAINER_CREATE = { 'additionalProperties': False, } +CAPSULE_CREATE = { + 'type': 'object', + 'properties': { + 'template': parameter_types.capsule_template + }, + 'required': ['template'], + 'additionalProperties': False +} + class TestSchemaValidations(base.BaseTestCase): def setUp(self): @@ -194,3 +203,213 @@ class TestSchemaValidations(base.BaseTestCase): "Invalid input for field" " 'runtime'"): self.schema_validator.validate(request_to_validate) + + +class TestCapsuleSchemaValidations(base.BaseTestCase): + def setUp(self): + super(TestCapsuleSchemaValidations, self).setUp() + self.schema_validator = validators.SchemaValidator(CAPSULE_CREATE) + + def test_create_schema_with_all_valid_parameters(self): + request_to_validate = \ + {"template": + {"kind": "capsule", + "capsuleVersion": "beta", + "metadata": { + "labels": {"app": "web"}, + "name": "template"}, + "restartPolicy": "Always", + "spec": { + "containers": [ + {"workDir": "/root", "image": "ubuntu", + "volumeMounts": [{"readOnly": True, + "mountPath": "/data1", + "name": "volume1"}], + "command": ["/bin/bash"], + "env": {"ENV2": "/usr/bin"}, + "imagePullPolicy": "ifnotpresent", + "ports": [{"containerPort": 80, + "protocol": "TCP", + "name": "nginx-port", + "hostPort": 80}], + "resources": {"requests": {"cpu": 1, + "memory": 1024}}}], + "volumes": [ + {"cinder": {"autoRemove": True, "size": 5}, + "name": "volume1"} + ]}}} + self.schema_validator.validate(request_to_validate) + + def test_create_schema_kind_missing(self): + request_to_validate = \ + {"template": + {"capsuleVersion": "beta", + "metadata": { + "labels": {"app": "web"}, + "name": "template"}, + "spec": {"containers": [{"image": "test"}]} + }} + with self.assertRaisesRegex(exception.SchemaValidationError, + "'kind' is a required property"): + self.schema_validator.validate(request_to_validate) + + def test_create_schema_metadata_missing(self): + request_to_validate = \ + {"template": + {"capsuleVersion": "beta", + "kind": "capsule", + "spec": {"containers": [{"image": "test"}]} + }} + with self.assertRaisesRegex(exception.SchemaValidationError, + "'metadata' is a required property"): + self.schema_validator.validate(request_to_validate) + + def test_create_schema_spec_missing(self): + request_to_validate = \ + {"template": + {"capsuleVersion": "beta", + "kind": "capsule", + "metadata": { + "labels": {"app": "web"}, + "name": "template"}, + }} + with self.assertRaisesRegex(exception.SchemaValidationError, + "'spec' is a required property"): + self.schema_validator.validate(request_to_validate) + + def test_create_schema_with_all_essential_params(self): + request_to_validate = \ + {"template": + {"kind": "capsule", + "capsuleVersion": "beta", + "metadata": { + "labels": {}, + "name": "test-essential"}, + "spec": { + "containers": [ + {"image": "test"}] + }}} + self.schema_validator.validate(request_to_validate) + + def test_create_schema_capsule_restart_policy(self): + valid_restart_policy = ["Always", "OnFailure", "Never"] + invalid_restart_policy = ["always", "4", "onfailure", "never"] + for restart_policy in valid_restart_policy: + request_to_validate = \ + {"template": + {"capsuleVersion": "beta", + "kind": "capsule", + "metadata": { + "labels": {"app": "web"}, + "name": "template"}, + "restartPolicy": restart_policy, + "spec": {"containers": [{"image": "test"}]} + }} + self.schema_validator.validate(request_to_validate) + for restart_policy in invalid_restart_policy: + request_to_validate = \ + {"template": + {"capsuleVersion": "beta", + "kind": "capsule", + "metadata": { + "labels": {"app": "web"}, + "name": "template"}, + "restartPolicy": restart_policy, + "spec": {"containers": [{"image": "test"}]} + }} + with self.assertRaisesRegex(exception.SchemaValidationError, + "Invalid input for field " + "'restartPolicy'."): + self.schema_validator.validate(request_to_validate) + + def test_create_schema_capsule_existed_volume_mounts(self): + request_to_validate = { + "template": { + "kind": "capsule", + "metadata": {}, + "spec": { + "containers": [ + {"image": "test", + "volumeMounts": [{ + "name": "volume1", + "mountPath": "/data"}] + }], + "volumes": [ + {"name": "volume1", + "cinder": { + "volumeID": + "d2a28af0-e243-4525-adf9-2d091466e43d"} + } + ] + } + } + } + self.schema_validator.validate(request_to_validate) + + def test_create_schema_capsule_new_volume_mounts(self): + request_to_validate = { + "template": { + "kind": "capsule", + "metadata": {}, + "spec": { + "containers": [ + {"image": "test", + "volumeMounts": [{ + "name": "volume1", + "mountPath": "/data"}] + }], + "volumes": [ + {"name": "volume1", + "cinder": { + "size": 5, + "autoRemove": True} + } + ] + } + } + } + self.schema_validator.validate(request_to_validate) + + def test_create_schema_capsule_volume_no_cinder(self): + request_to_validate = { + "template": { + "kind": "capsule", + "metadata": {}, + "spec": { + "containers": [ + {"image": "test"}], + "volumes": [ + {"name": "volume1", + "no-cinder-driver": { + "size": 5, + "autoRemove": True} + } + ] + } + } + } + with self.assertRaisesRegex(exception.SchemaValidationError, + "'cinder' is a required property"): + self.schema_validator.validate(request_to_validate) + + def test_create_schema_capsule_volume_no_name(self): + request_to_validate = { + "template": { + "kind": "capsule", + "metadata": {}, + "spec": { + "containers": [ + {"image": "test"}], + "volumes": [ + { + "cinder": { + "size": 5, + "autoRemove": True} + } + ] + } + } + } + with self.assertRaisesRegex(exception.SchemaValidationError, + "'name' is a required property"): + self.schema_validator.validate(request_to_validate) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 53a5f65dc..434c9a8e6 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -20,20 +20,26 @@ from zun.db import api as db_api CONF = cfg.CONF -CAPSULE_SPEC = {"kind": "capsule", "capsule_template_version": "2017-06-21", - "capsule_version": "beta", "restart_policy": "always", +CAPSULE_SPEC = {"kind": "capsule", + "capsuleVersion": "beta", + "restartPolicy": "Always", "spec": {"containers": - [{"environment": {"MYSQL_ROOT_PASSWORD": "password"}, - "image": "mysql", "labels": {"app": "web"}, - "image_driver": "docker", "resources": - {"allocation": {"cpu": 1, - "memory": 1024}}}], - "volumes": [{"name": "volume1", - "image": "ubuntu-xenial", - "drivers": "cinder", - "volumeType": "type1", - "driverOptions": "options", - "size": "5GB"}]}} + [{"env": {"TEST": "password"}, + "image": "test", + "resources": + {"requests": {"cpu": 1, "memory": 1024}}, + "volumeMounts": [ + {"name": "volume1", "mountPath": "/data1"}, + {"name": "volume2", "mountPath": "/data2"}] + }], + "volumes": [ + {"name": "volume1", + "cinder": { + "volumeID": + "9600e785-9320-4d3f-ba02-04e3d43fddec"} + }, + {"name": "volume2", + "cinder": {"size": 5}}]}} def get_test_container(**kwargs):