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):