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 <kevin.zhao@arm.com>
This commit is contained in:
Kevin Zhao 2017-12-26 14:59:28 +08:00
parent 97524cba92
commit d4a5f9c5e9
11 changed files with 529 additions and 98 deletions

View File

@ -164,7 +164,7 @@ Sample capsule:
env: env:
PATH: /usr/local/bin PATH: /usr/local/bin
resources: resources:
allocation: requests:
cpu: 1 cpu: 1
memory: 2GB memory: 2GB
volumes: volumes:
@ -219,7 +219,7 @@ Ports fields:
* protocol(string): TCP or UDP, by default is TCP * protocol(string): TCP or UDP, by default is TCP
RecourcesObject fields: RecourcesObject fields:
* allocation(AllocationObject): the resources that the capsule needed * requests(AllocationObject): the resources that the capsule needed
AllocationObject: AllocationObject:
* cpu(string): cpu resources, cores number * cpu(string): cpu resources, cores number

View File

@ -1,18 +1,17 @@
capsule_template_version: 2017-12-20
# use "-" because that the fields have many items # use "-" because that the fields have many items
capsule_version: beta capsuleVersion: beta
kind: capsule kind: capsule
metadata: metadata:
name: capsule-volume name: capsule-volume
labels: labels:
foo: bar foo: bar
restart_policy: always restartPolicy: always
spec: spec:
containers: containers:
- image: test - image: test
command: command:
- "/bin/bash" - "/bin/bash"
workdir: /root workDir: /root
labels: labels:
app: web app: web
volumeMounts: volumeMounts:

View File

@ -1,33 +1,30 @@
capsule_template_version: 2017-06-21
# use "-" because that the fields have many items # use "-" because that the fields have many items
capsule_version: beta capsuleVersion: beta
kind: capsule kind: capsule
metadata: metadata:
name: capsule-example name: capsule-example
labels: labels:
app: web app: web
nihao: baibai app1: web1
restart_policy: always restartPolicy: always
spec: spec:
containers: containers:
- image: ubuntu - image: ubuntu
command: command:
- "/bin/bash" - "/bin/bash"
image_pull_policy: ifnotpresent imagePullPolicy: ifnotpresent
workdir: /root workDir: /root
labels:
app: web
ports: ports:
- name: nginx-port - name: nginx-port
containerPort: 80 containerPort: 80
hostPort: 80 hostPort: 80
protocol: TCP protocol: TCP
resources: resources:
allocation: requests:
cpu: 1 cpu: 1
memory: 1024 memory: 1024
environment: env:
PATCH: /usr/local/bin ENV1: /usr/local/bin
volumeMounts: volumeMounts:
- name: volume1 - name: volume1
mountPath: /data1 mountPath: /data1
@ -38,10 +35,8 @@ spec:
args: args:
- "Hello" - "Hello"
- "World" - "World"
image_pull_policy: ifnotpresent imagePullPolicy: ifnotpresent
workdir: /root workDir: /root
labels:
app: web01
ports: ports:
- name: nginx-port - name: nginx-port
containerPort: 80 containerPort: 80
@ -52,11 +47,11 @@ spec:
hostPort: 3306 hostPort: 3306
protocol: TCP protocol: TCP
resources: resources:
allocation: requests:
cpu: 1 cpu: 1
memory: 1024 memory: 1024
environment: env:
NWH: /usr/bin/ ENV2: /usr/bin/
volumeMounts: volumeMounts:
- name: volume2 - name: volume2
mountPath: /data2 mountPath: /data2

View File

@ -131,8 +131,8 @@ class CapsuleController(base.Controller):
action="capsule:create") action="capsule:create")
# Abstract the capsule specification # Abstract the capsule specification
capsules_spec = capsule_dict['spec'] capsules_template = capsule_dict.get('template')
spec_content = utils.check_capsule_template(capsules_spec) spec_content = utils.check_capsule_template(capsules_template)
containers_spec = utils.capsule_get_container_spec(spec_content) containers_spec = utils.capsule_get_container_spec(spec_content)
volumes_spec = utils.capsule_get_volume_spec(spec_content) volumes_spec = utils.capsule_get_volume_spec(spec_content)
@ -148,10 +148,11 @@ class CapsuleController(base.Controller):
capsule_need_memory = 0 capsule_need_memory = 0
container_volume_requests = [] 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) metadata_info = capsules_template.get('metadata', None)
requested_networks_info = capsules_spec.get('nets', []) requested_networks_info = capsules_template.get('nets', [])
requested_networks = \ requested_networks = \
utils.build_requested_networks(context, requested_networks_info) utils.build_requested_networks(context, requested_networks_info)
@ -203,7 +204,7 @@ class CapsuleController(base.Controller):
if container_dict.get('resources'): if container_dict.get('resources'):
resources_list = container_dict.get('resources') resources_list = container_dict.get('resources')
allocation = resources_list.get('allocation') allocation = resources_list.get('requests')
if allocation.get('cpu'): if allocation.get('cpu'):
capsule_need_cpu += allocation.get('cpu') capsule_need_cpu += allocation.get('cpu')
container_dict['cpu'] = allocation.get('cpu') container_dict['cpu'] = allocation.get('cpu')

View File

@ -15,12 +15,12 @@
from zun.common.validation import parameter_types from zun.common.validation import parameter_types
_capsule_properties = { _capsule_properties = {
'spec': parameter_types.spec 'template': parameter_types.capsule_template
} }
capsule_create = { capsule_create = {
'type': 'object', 'type': 'object',
'properties': _capsule_properties, 'properties': _capsule_properties,
'required': ['spec'], 'required': ['template'],
'additionalProperties': False 'additionalProperties': False
} }

View File

@ -76,6 +76,28 @@ VALID_STATES = {
consts.PAUSED] 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): def validate_container_state(container, action):
if container.status not in VALID_STATES[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']: if kind_field not in ['capsule', 'Capsule']:
raise exception.InvalidCapsuleTemplate("kind fields need to be " raise exception.InvalidCapsuleTemplate("kind fields need to be "
"set as capsule or Capsule") "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') spec_field = tpl.get('spec')
if spec_field is None: if spec_field is None:
raise exception.InvalidCapsuleTemplate("No Spec found") raise exception.InvalidCapsuleTemplate("No Spec found")
@ -360,10 +388,15 @@ def capsule_get_container_spec(spec_field):
"container at least") "container at least")
for i in range(0, containers_num): for i in range(0, containers_num):
container_image = containers_spec[i].get('image') container_spec = containers_spec[i]
if container_image is None: if 'image' not in container_spec.keys():
raise exception.InvalidCapsuleTemplate("Container " raise exception.InvalidCapsuleTemplate("Container "
"image is needed") "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 return containers_spec

View File

@ -256,6 +256,176 @@ security_groups = {
} }
} }
spec = { capsule_kind = {
'type': ['object'], "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']
} }

View File

@ -28,14 +28,14 @@ class TestCapsuleController(api_base.FunctionalTest):
def test_create_capsule(self, mock_capsule_create, def test_create_capsule(self, mock_capsule_create,
mock_neutron_get_network): mock_neutron_get_network):
params = ('{' params = ('{'
'"spec": ' '"template": '
'{"kind": "capsule",' '{"kind": "capsule",'
' "spec": {' ' "spec": {'
' "containers":' ' "containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' ' [{"env": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, ' ' "image": "test",'
' "image_driver": "docker", "resources": ' ' "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}}' ' {"requests": {"cpu": 1, "memory": 1024}}'
' }]' ' }]'
' }, ' ' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' ' "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, def test_create_capsule_two_containers(self, mock_capsule_create,
mock_neutron_get_network): mock_neutron_get_network):
params = ('{' params = ('{'
'"spec": ' '"template": '
'{"kind": "capsule",' '{"kind": "capsule",'
' "spec": {' ' "spec": {'
' "containers":' ' "containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' ' [{"image": "test", "resources": '
' "image": "test", "labels": {"app": "web"}, ' ' {"requests": {"cpu": 1, "memory": 1024}}}, '
' "image_driver": "docker", "resources": ' ' {"image": "test1", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}}},' ' {"requests": {"cpu": 1, "memory": 1024}}}]'
' {"environment": {"ROOT_PASSWORD": "foo1"}, '
' "image": "test1", "labels": {"app1": "web1"}, '
' "image_driver": "docker", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}}}'
' ]'
' }, ' ' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' ' "metadata": {"labels": {"foo0": "bar0"},'
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
@ -89,7 +84,7 @@ class TestCapsuleController(api_base.FunctionalTest):
content_type='application/json') content_type='application/json')
return_value = response.json return_value = response.json
expected_meta_name = "capsule-example" expected_meta_name = "capsule-example"
expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"} expected_meta_labels = {"foo0": "bar0"}
expected_memory = '2048M' expected_memory = '2048M'
expected_cpu = 2.0 expected_cpu = 2.0
expected_container_num = 3 expected_container_num = 3
@ -109,12 +104,11 @@ class TestCapsuleController(api_base.FunctionalTest):
@patch('zun.common.utils.check_capsule_template') @patch('zun.common.utils.check_capsule_template')
def test_create_capsule_wrong_kind_set(self, mock_check_template, def test_create_capsule_wrong_kind_set(self, mock_check_template,
mock_capsule_create): mock_capsule_create):
params = ('{"spec": {"kind": "test",' params = ('{"template": {"kind": "test",'
'"spec": {"containers":' '"spec": {"containers":'
'[{"environment": {"ROOT_PASSWORD": "foo0"}, ' '[{"environment": {"ROOT_PASSWORD": "foo0"}, '
'"image": "test1", "labels": {"app0": "web0"}, ' '"image": "test1", "resources": '
'"image_driver": "docker", "resources": ' '{"requests": {"cpu": 1, "memory": 1024}}}]}, '
'{"allocation": {"cpu": 1, "memory": 1024}}}]}, '
'"metadata": {"labels": {"foo0": "bar0"}, ' '"metadata": {"labels": {"foo0": "bar0"}, '
'"name": "capsule-example"}}}') '"name": "capsule-example"}}}')
mock_check_template.side_effect = exception.InvalidCapsuleTemplate( mock_check_template.side_effect = exception.InvalidCapsuleTemplate(
@ -127,7 +121,7 @@ class TestCapsuleController(api_base.FunctionalTest):
@patch('zun.common.utils.check_capsule_template') @patch('zun.common.utils.check_capsule_template')
def test_create_capsule_less_than_one_container(self, mock_check_template, def test_create_capsule_less_than_one_container(self, mock_check_template,
mock_capsule_create): mock_capsule_create):
params = ('{"spec": {"kind": "capsule",' params = ('{"template": {"kind": "capsule",'
'"spec": {container:[]}, ' '"spec": {container:[]}, '
'"metadata": {"labels": {"foo0": "bar0"}, ' '"metadata": {"labels": {"foo0": "bar0"}, '
'"name": "capsule-example"}}}') '"name": "capsule-example"}}}')
@ -141,7 +135,7 @@ class TestCapsuleController(api_base.FunctionalTest):
@patch('zun.common.utils.check_capsule_template') @patch('zun.common.utils.check_capsule_template')
def test_create_capsule_no_container_field(self, mock_check_template, def test_create_capsule_no_container_field(self, mock_check_template,
mock_capsule_create): mock_capsule_create):
params = ('{"spec": {"kind": "capsule",' params = ('{"template": {"kind": "capsule",'
'"spec": {}, ' '"spec": {}, '
'"metadata": {"labels": {"foo0": "bar0"}, ' '"metadata": {"labels": {"foo0": "bar0"}, '
'"name": "capsule-example"}}}') '"name": "capsule-example"}}}')
@ -155,8 +149,8 @@ class TestCapsuleController(api_base.FunctionalTest):
@patch('zun.common.utils.check_capsule_template') @patch('zun.common.utils.check_capsule_template')
def test_create_capsule_no_container_image(self, mock_check_template, def test_create_capsule_no_container_image(self, mock_check_template,
mock_capsule_create): mock_capsule_create):
params = ('{"spec": {"kind": "capsule",' params = ('{"template": {"kind": "capsule",'
'"spec": {container:[{"environment": ' '"spec": {container:[{"env": '
'{"ROOT_PASSWORD": "foo1"}]}, ' '{"ROOT_PASSWORD": "foo1"}]}, '
'"metadata": {"labels": {"foo0": "bar0"}, ' '"metadata": {"labels": {"foo0": "bar0"}, '
'"name": "capsule-example"}}}') '"name": "capsule-example"}}}')
@ -174,18 +168,17 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_neutron_get_network, mock_neutron_get_network,
mock_create_volume, mock_create_volume,
mock_ensure_volume_usable): mock_ensure_volume_usable):
fake_volume_id = 'fakevolid' fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2'
fake_volume = mock.Mock(id=fake_volume_id) fake_volume = mock.Mock(id=fake_volume_id)
mock_create_volume.return_value = fake_volume mock_create_volume.return_value = fake_volume
params = ('{' params = ('{'
'"spec":' '"template":'
'{"kind": "capsule",' '{"kind": "capsule",'
' "spec":' ' "spec":'
' {"containers":' ' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' ' [{"env": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, ' ' "image": "test", "resources": '
' "image_driver": "docker", "resources": ' ' {"requests": {"cpu": 1, "memory": 1024}},'
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", ' ' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"}]' ' "mountPath": "/data1"}]'
' }' ' }'
@ -195,7 +188,8 @@ class TestCapsuleController(api_base.FunctionalTest):
' "cinder": {"size": 3, "autoRemove": "True"}' ' "cinder": {"size": 3, "autoRemove": "True"}'
' }]' ' }]'
' }, ' ' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},' ' "metadata": {"labels": '
' {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
@ -227,28 +221,29 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_neutron_get_network, mock_neutron_get_network,
mock_search_volume, mock_search_volume,
mock_ensure_volume_usable): mock_ensure_volume_usable):
fake_volume_id = 'fakevolid' fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2'
fake_volume = mock.Mock(id=fake_volume_id) fake_volume = mock.Mock(id=fake_volume_id)
mock_search_volume.return_value = fake_volume mock_search_volume.return_value = fake_volume
params = ('{' params = ('{'
'"spec":' '"template":'
'{"kind": "capsule",' '{"kind": "capsule",'
' "spec":' ' "spec":'
' {"containers":' ' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' ' [{"env": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, ' ' "image": "test", "resources": '
' "image_driver": "docker", "resources": ' ' {"requests": {"cpu": 1, "memory": 1024}},'
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", ' ' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"}]' ' "mountPath": "/data1"}]'
' }' ' }'
' ],' ' ],'
' "volumes":' ' "volumes":'
' [{"name": "volume1",' ' [{"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"}' ' "name": "capsule-example"}'
' }' ' }'
'}') '}')
@ -283,30 +278,30 @@ class TestCapsuleController(api_base.FunctionalTest):
mock_search_volume, mock_search_volume,
mock_ensure_volume_usable, mock_ensure_volume_usable,
mock_create_volume): mock_create_volume):
fake_volume_id1 = 'fakevolid1' fake_volume_id1 = '3259309d-659c-4e20-b354-ee712e64b3b2'
fake_volume = mock.Mock(id=fake_volume_id1) fake_volume = mock.Mock(id=fake_volume_id1)
mock_search_volume.return_value = fake_volume 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) fake_volume = mock.Mock(id=fake_volume_id2)
mock_create_volume.return_value = fake_volume mock_create_volume.return_value = fake_volume
params = ('{' params = ('{'
'"spec":' '"template":'
'{"kind": "capsule",' '{"kind": "capsule",'
' "spec":' ' "spec":'
' {"containers":' ' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, ' ' [{"env": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, ' ' "image": "test", "resources": '
' "image_driver": "docker", "resources": ' ' {"requests": {"cpu": 1, "memory": 1024}},'
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", ' ' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"},' ' "mountPath": "/data1"},'
' {"name": "volume2", ' ' {"name": "volume2", '
' "mountPath": "/data2"}]' ' "mountPath": "/data2"}]'
' }' ' }'
' ],' ' ],'
' "volumes":' ' "volumes":'
' [{"name": "volume1",' ' [{"name": "volume1",'
' "cinder": {"volumeID": "fakevolid1"}},' ' "cinder": {"volumeID": '
' "3259309d-659c-4e20-b354-ee712e64b3b2"}},'
' {"name": "volume2",' ' {"name": "volume2",'
' "cinder": {"size": 3, "autoRemove": "True"}' ' "cinder": {"size": 3, "autoRemove": "True"}'
' }]' ' }]'

View File

@ -143,6 +143,12 @@ class TestUtils(base.TestCase):
params = ({"kind": "capsule", "spec": {}}) params = ({"kind": "capsule", "spec": {}})
utils.check_capsule_template(params) 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): def test_capsule_get_container_spec(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, exception.InvalidCapsuleTemplate,
@ -163,6 +169,13 @@ class TestUtils(base.TestCase):
{"environment": {"ROOT_PASSWORD": "foo0"}}]}) {"environment": {"ROOT_PASSWORD": "foo0"}}]})
utils.capsule_get_container_spec(params) 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): def test_capsule_get_volume_spec(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, exception.InvalidCapsuleTemplate,

View File

@ -37,6 +37,15 @@ CONTAINER_CREATE = {
'additionalProperties': False, 'additionalProperties': False,
} }
CAPSULE_CREATE = {
'type': 'object',
'properties': {
'template': parameter_types.capsule_template
},
'required': ['template'],
'additionalProperties': False
}
class TestSchemaValidations(base.BaseTestCase): class TestSchemaValidations(base.BaseTestCase):
def setUp(self): def setUp(self):
@ -194,3 +203,213 @@ class TestSchemaValidations(base.BaseTestCase):
"Invalid input for field" "Invalid input for field"
" 'runtime'"): " 'runtime'"):
self.schema_validator.validate(request_to_validate) 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)

View File

@ -20,20 +20,26 @@ from zun.db import api as db_api
CONF = cfg.CONF CONF = cfg.CONF
CAPSULE_SPEC = {"kind": "capsule", "capsule_template_version": "2017-06-21", CAPSULE_SPEC = {"kind": "capsule",
"capsule_version": "beta", "restart_policy": "always", "capsuleVersion": "beta",
"restartPolicy": "Always",
"spec": {"containers": "spec": {"containers":
[{"environment": {"MYSQL_ROOT_PASSWORD": "password"}, [{"env": {"TEST": "password"},
"image": "mysql", "labels": {"app": "web"}, "image": "test",
"image_driver": "docker", "resources": "resources":
{"allocation": {"cpu": 1, {"requests": {"cpu": 1, "memory": 1024}},
"memory": 1024}}}], "volumeMounts": [
"volumes": [{"name": "volume1", {"name": "volume1", "mountPath": "/data1"},
"image": "ubuntu-xenial", {"name": "volume2", "mountPath": "/data2"}]
"drivers": "cinder", }],
"volumeType": "type1", "volumes": [
"driverOptions": "options", {"name": "volume1",
"size": "5GB"}]}} "cinder": {
"volumeID":
"9600e785-9320-4d3f-ba02-04e3d43fddec"}
},
{"name": "volume2",
"cinder": {"size": 5}}]}}
def get_test_container(**kwargs): def get_test_container(**kwargs):