diff --git a/promenade/config.py b/promenade/config.py index cc7bd52c..00e7e026 100644 --- a/promenade/config.py +++ b/promenade/config.py @@ -1,5 +1,5 @@ from . import exceptions, logging, validation -from .design_ref import get_documents +from . import design_ref as dr import jinja2 import jsonpath_ng import yaml @@ -63,7 +63,7 @@ class Configuration: @classmethod def from_design_ref(cls, design_ref, **kwargs): - documents, use_dh_engine = get_documents(design_ref) + documents, use_dh_engine = dr.get_documents(design_ref) return cls( documents=documents, diff --git a/promenade/control/validatedesign.py b/promenade/control/validatedesign.py index 3e378c78..b367432e 100644 --- a/promenade/control/validatedesign.py +++ b/promenade/control/validatedesign.py @@ -26,19 +26,17 @@ LOG = logging.getLogger(__name__) class ValidateDesignResource(base.BaseResource): @policy.ApiEnforcer('kubernetes_provisioner:post_validatedesign') def on_post(self, req, resp): - + result = ValidationMessage() try: json_data = self.req_json(req) href = json_data.get('href', None) config = Configuration.from_design_ref( href, allow_missing_substitutions=False) result = validation.check_design(config) - except (exceptions.InvalidFormatError, - exceptions.DeckhandException) as e: - if isinstance(e, exceptions.InvalidFormatError): - msg = "Invalid JSON Format: %s" % str(e) - else: - msg = str(e) - result = ValidationMessage() + except exceptions.InvalidFormatError as e: + msg = "Invalid JSON Format: %s" % str(e) result.add_error_message(msg, name=e.title) - return result.get_output() + except Exception as e: + result.add_error_message(str(e), name=e.title) + + result.update_response(resp) diff --git a/promenade/utils/validation_message.py b/promenade/utils/validation_message.py index a3fad97d..24c4010b 100644 --- a/promenade/utils/validation_message.py +++ b/promenade/utils/validation_message.py @@ -87,3 +87,8 @@ class ValidationMessage(object): :rtype: json """ return json.dumps(self.output, indent=2) + + def update_response(self, resp): + output = self.get_output() + resp.status = output['code'] + resp.body = json.dumps(output) diff --git a/promenade/validation.py b/promenade/validation.py index 9454488d..3705eeda 100644 --- a/promenade/validation.py +++ b/promenade/validation.py @@ -26,16 +26,18 @@ LOG = logging.getLogger(__name__) def check_design(config): - kinds = ['Docker', 'HostSystem', 'Kubelet', 'KubernetesNetwork'] + kinds = ['Docker', 'Genesis', 'HostSystem', 'Kubelet', 'KubernetesNetwork'] validation_msg = ValidationMessage() for kind in kinds: count = 0 + schema = None + name = None for doc in config.documents: schema = doc.get('schema', None) if not schema: msg = '"schema" is a required document key.' - validation_msg.add_error_message( - msg, name=exceptions.ValidationException(msg)) + exc = exceptions.ValidationException(msg) + validation_msg.add_error_message(str(exc), name=exc.title) return validation_msg name = schema.split('/')[1] if name == kind: @@ -43,11 +45,9 @@ def check_design(config): if count != 1: msg = ('There are {0} {1} documents. However, there should be one.' ).format(count, kind) + exc = exceptions.ValidationException(msg) validation_msg.add_error_message( - msg, - name=exceptions.ValidationException(msg), - schema=schema, - doc_name=kind) + str(exc), name=exc.title, schema=schema, doc_name=kind) return validation_msg diff --git a/tests/unit/api/test_validatedesign.py b/tests/unit/api/test_validatedesign.py new file mode 100644 index 00000000..0bc117e9 --- /dev/null +++ b/tests/unit/api/test_validatedesign.py @@ -0,0 +1,271 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from falcon import testing +from promenade import promenade +from promenade.control import health_api +from unittest import mock +import copy +import falcon +import json +import pytest + + +@pytest.fixture() +def client(): + return testing.TestClient(promenade.start_promenade(disable='keystone')) + + +@pytest.fixture() +def std_headers(): + return { + 'Content-Type': 'application/json', + 'X-IDENTITY-STATUS': 'Confirmed', + 'X-USER-NAME': 'Test', + 'X-ROLES': 'admin' + } + + +@pytest.fixture() +def std_body(): + return json.dumps({ + 'rel': 'design', + 'href': 'http://localhost:9999', + 'type': 'application/x-yaml', + }) + + +def test_post_validatedesign_empty_docs(client, std_body, std_headers): + with mock.patch('promenade.design_ref.get_documents') as gd: + gd.return_value = ([], False) + response = client.simulate_post( + '/api/v1.0/validatedesign', headers=std_headers, body=std_body) + assert response.status == falcon.HTTP_400 + assert response.json['details']['errorCount'] == 5 + + +VALID_DOCS = [ + { + 'data': { + 'config': { + 'insecure-registries': ['registry:5000'], + 'live-restore': True, + 'max-concurrent-downloads': 10, + 'oom-score-adjust': -999, + 'storage-driver': 'overlay2' + } + }, + 'metadata': { + 'layeringDefinition': { + 'abstract': False, + 'layer': 'site' + }, + 'name': 'docker', + 'schema': 'metadata/Document/v1', + 'storagePolicy': 'cleartext' + }, + 'schema': 'promenade/Docker/v1' + }, + { + 'data': { + 'apiserver': { + 'command_prefix': [ + '/apiserver', '--authorization-mode=Node,RBAC', + '--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds', + '--service-cluster-ip-range=10.96.0.0/16', + '--endpoint-reconciler-type=lease' + ] + }, + 'armada': { + 'target_manifest': 'cluster-bootstrap' + }, + 'files': [{ + 'content': + '# placeholder for triggering calico etcd bootstrapping', + 'mode': + 420, + 'path': + '/var/lib/anchor/calico-etcd-bootstrap' + }], + 'hostname': + 'n0', + 'images': { + 'armada': 'quay.io/airshipit/armada:master', + 'helm': { + 'tiller': 'gcr.io/kubernetes-helm/tiller:v2.7.2' + }, + 'kubernetes': { + 'apiserver': + 'gcr.io/google_containers/hyperkube-amd64:v1.10.2', + 'controller-manager': + 'gcr.io/google_containers/hyperkube-amd64:v1.10.2', + 'etcd': + 'quay.io/coreos/etcd:v3.2.14', + 'scheduler': + 'gcr.io/google_containers/hyperkube-amd64:v1.10.2' + } + }, + 'ip': + '192.168.77.10', + 'labels': { + 'dynamic': [ + 'calico-etcd=enabled', 'coredns=enabled', + 'kubernetes-apiserver=enabled', + 'kubernetes-controller-manager=enabled', + 'kubernetes-etcd=enabled', 'kubernetes-scheduler=enabled', + 'promenade-genesis=enabled', 'ucp-control-plane=enabled' + ] + } + }, + 'metadata': { + 'layeringDefinition': { + 'abstract': False, + 'layer': 'site' + }, + 'name': 'genesis', + 'schema': 'metadata/Document/v1' + }, + 'schema': 'promenade/Genesis/v1' + }, + { + 'data': { + 'files': [{ + 'mode': + 365, + 'path': + '/opt/kubernetes/bin/kubelet', + 'tar_path': + 'kubernetes/node/bin/kubelet', + 'tar_url': + 'https://dl.k8s.io/v1.10.2/kubernetes-node-linux-amd64.tar.gz' + }, { + 'content': + '/var/lib/docker/containers/*/*-json.log\n{\n compress\n copytruncate\n create 0644 root root\n daily\n dateext\n dateformat -%Y%m%d-%s\n maxsize 10M\n missingok\n notifempty\n su root root\n rotate 1\n}', + 'mode': + 292, + 'path': + '/etc/logrotate.d/json-logrotate' + }], + 'images': { + 'haproxy': 'haproxy:1.8.3', + 'helm': { + 'helm': 'lachlanevenson/k8s-helm:v2.7.2' + }, + 'kubernetes': { + 'kubectl': + 'gcr.io/google_containers/hyperkube-amd64:v1.10.2' + } + }, + 'packages': { + 'additional': ['curl', 'jq'], + 'keys': [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o\nddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R\nmYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn\nTGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK\ndHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT\nX+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG\nHOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c\nNKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ\nhT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U\n65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM\nzHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB\ntDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv\nY2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe\nAQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n\nAk40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I\n1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl\nuyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv\n0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8\nL5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD\nYBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR\n7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc\njk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP\nHXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL\nMXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ\nTvBR8Q==\n=Fm3p\n-----END PGP PUBLIC KEY BLOCK-----' + ], + 'repositories': + ['deb http://apt.dockerproject.org/repo ubuntu-xenial main'], + 'required': { + 'docker': 'docker-engine=1.13.1-0~ubuntu-xenial', + 'socat': 'socat=1.7.3.1-1' + } + }, + 'validation': { + 'pod_logs': { + 'image': 'busybox:1.28.3' + } + } + }, + 'metadata': { + 'layeringDefinition': { + 'abstract': False, + 'layer': 'site' + }, + 'name': 'host-system', + 'schema': 'metadata/Document/v1' + }, + 'schema': 'promenade/HostSystem/v1' + }, + { + 'data': { + 'arguments': [ + '--cni-bin-dir=/opt/cni/bin', '--cni-conf-dir=/etc/cni/net.d', + '--eviction-max-pod-grace-period=-1', '--network-plugin=cni', + '--node-status-update-frequency=5s', + '--serialize-image-pulls=false', '--v=5' + ], + 'images': { + 'pause': 'gcr.io/google_containers/pause-amd64:3.0' + } + }, + 'metadata': { + 'layeringDefinition': { + 'abstract': False, + 'layer': 'site' + }, + 'name': 'kubelet', + 'schema': 'metadata/Document/v1', + 'storagePolicy': 'cleartext' + }, + 'schema': 'promenade/Kubelet/v1' + }, + { + 'data': { + 'dns': { + 'bootstrap_validation_checks': [ + 'calico-etcd.kube-system.svc.cluster.local', 'google.com', + 'kubernetes-etcd.kube-system.svc.cluster.local', + 'kubernetes.default.svc.cluster.local' + ], + 'cluster_domain': + 'cluster.local', + 'service_ip': + '10.96.0.10', + 'upstream_servers': ['8.8.8.8', '8.8.4.4'] + }, + 'etcd': { + 'container_port': 2379, + 'haproxy_port': 2378 + }, + 'hosts_entries': [{ + 'ip': '192.168.77.1', + 'names': ['registry'] + }], + 'kubernetes': { + 'apiserver_port': 6443, + 'haproxy_port': 6553, + 'pod_cidr': '10.97.0.0/16', + 'service_cidr': '10.96.0.0/16', + 'service_ip': '10.96.0.1' + } + }, + 'metadata': { + 'layeringDefinition': { + 'abstract': False, + 'layer': 'site' + }, + 'name': 'kubernetes-network', + 'schema': 'metadata/Document/v1', + 'storagePolicy': 'cleartext' + }, + 'schema': 'promenade/KubernetesNetwork/v1' + }, +] + + +def test_post_validatedesign_valid_docs(client, std_body, std_headers): + with mock.patch('promenade.design_ref.get_documents') as gd: + gd.return_value = (VALID_DOCS, False) + response = client.simulate_post( + '/api/v1.0/validatedesign', headers=std_headers, body=std_body) + assert response.status == falcon.HTTP_200 + assert response.json['details']['errorCount'] == 0 diff --git a/tox.ini b/tox.ini index 0380bf65..86ae66f8 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = deps = -r{toxinidir}/requirements-frozen.txt -r{toxinidir}/test-requirements.txt commands = - pytest + pytest {posargs} [testenv:py36] setenv = @@ -19,7 +19,7 @@ setenv = deps = -r{toxinidir}/requirements-frozen.txt -r{toxinidir}/test-requirements.txt commands = - pytest + pytest {posargs} [testenv:bandit] deps = bandit==1.4.0