From dec56fbf3c723e6a55ce382f66fdbbd359b58ec6 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 29 Mar 2016 19:21:31 +0530 Subject: [PATCH 01/21] Initial work on test suite for providers: - base test suite for generic provider - base test suite for kubernetes provider. --- tests/system/base.py | 260 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 tests/system/base.py diff --git a/tests/system/base.py b/tests/system/base.py new file mode 100644 index 00000000..ec3b7353 --- /dev/null +++ b/tests/system/base.py @@ -0,0 +1,260 @@ +import os +import anymarkup +import datetime +import unittest +import subprocess +from collections import OrderedDict +import tempfile + + +class BaseProviderTestSuite(unittest.TestCase): + """ + Base test suite for a provider: docker, kubernetes, etc. + """ + + def setUp(self): + self.get_initial_state() + + def tearDown(self): + self.restore_initial_state() + + def get_initial_state(self): + raise NotImplementedError + + def restore_initial_state(self): + raise NotImplementedError + + def deploy(self, app_spec, answers): + """ + Deploy to provider + """ + raise NotImplementedError + + def undeploy(self, app_spec, answers): + """ + Undeploy from provider + """ + raise NotImplementedError + + def get_tmp_answers_file(self, answers): + f = tempfile.NamedTemporaryFile(delete=False, suffix='.conf') + f.close() + anymarkup.serialize_file(answers, f.name, format='ini') + return f.name + + @property + def nulecule_lib(self): + return os.environ['NULECULE_LIB'] + + +class DockerProviderTestSuite(BaseProviderTestSuite): + pass + + +class KubernetesProviderTestSuite(BaseProviderTestSuite): + """ + Base test suite for Kubernetes. + """ + + def tearDown(self): + pods = self._get_pods() + services = self._get_services() + rcs = self._get_rcs() + + # clean up newly created pods + for pod in pods: + if pod not in self._pods: + subprocess.check_output(['/usr/bin/kubectl', 'delete', 'pod', pod]) + + # clean up newly created services + for service in services: + if service not in self._services: + subprocess.check_output(['/usr/bin/kubectl', 'delete', 'service', service]) + + # clean up newly created rcs + for rc in rcs: + if rc not in self._rcs: + subprocess.check_output(['/usr/bin/kubectl', 'delete', 'rc', rc]) + + def deploy(self, app_spec, answers): + """ + Deploy app to kuberntes + + Args: + app_spec (str): image name or path to application + answers (dict): Answers data + + Returns: + Path of the deployed dir. + """ + destination = tempfile.mkdtemp() + answers_path = self.get_tmp_answers_file(answers) + cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, + '--provider=kubernetes', + '--destination=%s' % destination, app_spec] + subprocess.check_output(cmd) + return destination + + def undeploy(self, workdir): + """ + Undeploy app from kubernetes. + + Args: + workdir (str): Path to deployed application dir + """ + cmd = ['atomicapp', 'stop', workdir] + subprocess.check_output(cmd) + + def assertPod(self, name, exists=True, status=None, timeout=1): + """ + Assert a kubernetes pod, if it exists, what's its status. + + We can also set a timeout to wait for the pod to + get to the desired state. + """ + start = datetime.datetime.now() + cmd = ['/usr/bin/kubectl', 'get', 'pod', name] + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + if exists is False: + return True + continue + for line in output.splitlines()[1:]: + pod = self._get_pod_details(line) + result = True + if status is not None: + if pod['status'] == status: + result = result and True + else: + result = result and False + if result: + return True + else: + if exists is False: + return True + if exists: + message = "Pod: %s does not exist" % name + if status is not None: + message += ' with status: %s' % status + else: + message = "Pod: %s exists." % name + raise AssertionError(message) + + def assertService(self, name, exists=True, timeout=1): + """ + Assert a kubernetes service, if it exists. + + We can also set a timeout to wait for the service to + get to the desired state. + """ + cmd = ['/usr/bin/kubectl', 'get', 'service', name] + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + if exists is False: + return True + continue + for line in output.splitlines()[1:]: + return True + else: + if exists is False: + return True + if exists: + message = "Service: %s does not exist" % name + else: + message = "Service: %s exists." % name + raise AssertionError(message) + + def assertRc(self, name, exists=True, timeout=1): + """ + Assert a kubernetes rc, if it exists. + + We can also set a timeout to wait for the rc to + get to the desired state. + """ + cmd = ['/usr/bin/kubectl', 'get', 'rc', name] + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + if exists is False: + return True + continue + for line in output.splitlines()[1:]: + return True + else: + if exists is False: + return True + if exists: + message = "RC: %s does not exist" % name + else: + message = "RC: %s exists." % name + raise AssertionError(message) + + def get_initial_state(self): + """Save initial state of the provider""" + self._services = self._get_services() + self._pods = self._get_pods() + self._rcs = self._get_rcs() + + def _get_services(self): + output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'services']) + services = OrderedDict() + for line in output.splitlines()[1:]: + service = self._get_service_details(line) + services[service['name']] = service + return services + + def _get_service_details(self, line): + name, labels, selector, ips, ports = line.split() + service = { + 'name': name, + 'labels': labels, + 'selector': selector, + 'ips': ips, + 'ports': ports + } + return service + + def _get_pods(self): + output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'pods']) + pods = OrderedDict() + for line in output.splitlines()[1:]: + pod = self._get_pod_details(line) + pods[pod['name']] = pod + return pods + + def _get_pod_details(self, line): + name, ready, status, restarts, age = line.split() + pod = { + 'name': name, + 'ready': ready, + 'status': status, + 'restarts': restarts, + 'age': age + } + return pod + + def _get_rcs(self): + output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'rc']) + rcs = OrderedDict() + for line in output.splitlines()[1:]: + rc = self._get_rc_details(line) + rcs[rc['controller']] = rc + return rcs + + def _get_rc_details(self, line): + controller, container, image, selector, replicas = line.split() + rc = { + 'controller': controller, + 'container': container, + 'image': image, + 'selector': selector, + 'replicas': replicas + } + return rc From dd8b006826dbc0e42be8dd61d14a2e8be98b1794 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 29 Mar 2016 19:22:37 +0530 Subject: [PATCH 02/21] Added functional tests for wordpress atomicapp on k8s. --- tests/system/test_kubernetes_provider.py | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/system/test_kubernetes_provider.py diff --git a/tests/system/test_kubernetes_provider.py b/tests/system/test_kubernetes_provider.py new file mode 100644 index 00000000..f520fc74 --- /dev/null +++ b/tests/system/test_kubernetes_provider.py @@ -0,0 +1,52 @@ +import os + +from base import KubernetesProviderTestSuite + + +class TestWordpress(KubernetesProviderTestSuite): + """ + Test Wordpress Atomic App on Kubernetes Provider + """ + answers = { + 'general': { + 'namespace': 'default' + }, + 'mariadb-atomicapp': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + }, + 'wordpress': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + } + } + + def _run(self): + app_spec = os.path.join( + self.nulecule_lib, 'wordpress-centos7-atomicapp') + return self.deploy(app_spec, self.answers) + + def test_wordpress_run(self): + self._run() + + self.assertPod('wordpress', status='Running', timeout=10) + self.assertPod('mariadb', status='Running', timeout=10) + + self.assertService('wordpress') + self.assertService('mariadb') + + def test_wordpress_stop(self): + workdir = self._run() + + self.assertPod('wordpress', timeout=10) + self.assertPod('mariadb', timeout=10) + + self.assertService('wordpress') + self.assertService('mariadb') + + self.undeploy(workdir) + + self.assertPod('wordpress', exists=False) + self.assertPod('mariadb', exists=False) From 2adaa249d65929350e2af3d588f3f0ea6ae2dc2d Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 29 Mar 2016 19:22:57 +0530 Subject: [PATCH 03/21] Added README for functional tests. --- tests/system/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/system/README.md diff --git a/tests/system/README.md b/tests/system/README.md new file mode 100644 index 00000000..31592499 --- /dev/null +++ b/tests/system/README.md @@ -0,0 +1,8 @@ +## System tests for atomicapp + +### Usage +From root directory of ``atomicapp`` repo, do: + +``` +sudo NULECULE_LIB= py.test tests/system +``` From 96251ba04e9c12f69ef6dcbbb1ab28bf7af3d0ff Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Fri, 8 Apr 2016 15:24:56 +0530 Subject: [PATCH 04/21] Added functional test suite for Docker provider. --- tests/system/base.py | 87 +++++++++++++++++++++++++++- tests/system/test_docker_provider.py | 42 ++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/system/test_docker_provider.py diff --git a/tests/system/base.py b/tests/system/base.py index ec3b7353..dcfd3b3b 100644 --- a/tests/system/base.py +++ b/tests/system/base.py @@ -1,4 +1,5 @@ import os +import re import anymarkup import datetime import unittest @@ -48,7 +49,91 @@ def nulecule_lib(self): class DockerProviderTestSuite(BaseProviderTestSuite): - pass + """ + Base test suite for Docker. + """ + + def tearDown(self): + _containers = self._get_containers(all=True) + + for container in _containers: + if container not in self._containers: + cmd = ['docker', 'rm', '-f', container] + print cmd + subprocess.check_output(cmd) + + def deploy(self, app_spec, answers): + """ + Deploy app to Docker + + Args: + app_spec (str): image name or path to application + answers (dict): Answers data + + Returns: + Path of the deployed dir. + """ + destination = tempfile.mkdtemp() + answers_path = self.get_tmp_answers_file(answers) + cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, + '--provider=docker', + '--destination=%s' % destination, app_spec] + subprocess.check_output(cmd) + return destination + + def undeploy(self, workdir): + """ + Undeploy app from Docker. + + Args: + workdir (str): Path to deployed application dir + """ + cmd = ['atomicapp', 'stop', workdir] + subprocess.check_output(cmd) + + def assertContainerRunning(self, name): + containers = self._get_containers() + for _id, container in containers.items(): + if container['names'] == name: + return True + raise AssertionError('Container: %s not running.' % name) + + def assertContainerNotRunning(self, name): + containers = self._get_containers() + for _id, container in containers.items(): + if container['name'] == name: + raise AssertionError('Container: %s is running' % name) + return True + + def get_initial_state(self): + self._containers = self._get_containers(all=True) + + def _get_containers(self, all=False): + cmd = ['docker', 'ps'] + if all: + cmd.append('-a') + output = subprocess.check_output(cmd) + _containers = OrderedDict() + + for line in output.splitlines()[1:]: + container = self._get_container(line) + _containers[container['id']] = container + return _containers + + def _get_container(self, line): + words = re.split(' {2,}', line) + if len(words) == 6: + words = words[:-1] + [''] + words[-1:] + container_id, image, command, created, status, ports, names = words + return { + 'id': container_id, + 'image': image, + 'command': command, + 'created': created, + 'status': status, + 'ports': ports, + 'names': names + } class KubernetesProviderTestSuite(BaseProviderTestSuite): diff --git a/tests/system/test_docker_provider.py b/tests/system/test_docker_provider.py new file mode 100644 index 00000000..e242503c --- /dev/null +++ b/tests/system/test_docker_provider.py @@ -0,0 +1,42 @@ +import os + +from base import DockerProviderTestSuite + + +class TestWordpress(DockerProviderTestSuite): + """ + Test Wordpress Atomic App on Kubernetes Provider + """ + answers = { + 'general': { + 'namespace': 'default' + }, + 'mariadb-atomicapp': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + }, + 'wordpress': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + } + } + + def _run(self): + app_spec = os.path.join( + self.nulecule_lib, 'wordpress-centos7-atomicapp') + return self.deploy(app_spec, self.answers) + + def test_wordpress_run(self): + self._run() + self.assertContainerRunning('wordpress-atomicapp') + self.assertContainerRunning('mariadb-atomicapp-app') + + def test_wordpress_stop(self): + workdir = self._run() + + self.undeploy(workdir) + + self.assertContainerNotRunning('wordpress-atomicapp') + self.assertContainerNotRunning('mariadb-atomicapp-app') From 6d3b94b364a2e5e65bb3a491fa337d2711a1579c Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Thu, 18 Aug 2016 15:30:50 +0530 Subject: [PATCH 05/21] Added setup code for openshift, kubernetes for functional tests. --- tests/system/providers/__init__.py | 0 tests/system/providers/kubernetes.py | 77 ++++++++++++++++++ tests/system/providers/openshift.py | 115 +++++++++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 tests/system/providers/__init__.py create mode 100644 tests/system/providers/kubernetes.py create mode 100644 tests/system/providers/openshift.py diff --git a/tests/system/providers/__init__.py b/tests/system/providers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/system/providers/kubernetes.py b/tests/system/providers/kubernetes.py new file mode 100644 index 00000000..bb7f8551 --- /dev/null +++ b/tests/system/providers/kubernetes.py @@ -0,0 +1,77 @@ +import os +import subprocess +import sys +import time +import urllib + + +def start(): + if not (os.path.exists('/usr/bin/kubectl') or + os.path.exists('/usr/local/bin/kubectl')): + print "No kubectl bin exists? Please install." + return + + K8S_VERSION = '1.3.4' + + cmd = ( + "docker run " + "--volume=/:/rootfs:ro " + "--volume=/sys:/sys:ro " + "--volume=/var/lib/docker/:/var/lib/docker:rw " + "--volume=/var/lib/kubelet/:/var/lib/kubelet:rw " + "--volume=/var/run:/var/run:rw " + "--net=host " + "--pid=host " + "--privileged=true " + "-d " + "gcr.io/google_containers/hyperkube-amd64:v{k8s_version} " + "/hyperkube kubelet " + ( + "--containerized " + "--hostname-override=\"127.0.0.1\" " + "--address=\"0.0.0.0\" " + "--api-servers=http://localhost:8080 " + "--config=/etc/kubernetes/manifests " + "--cluster-dns=10.0.0.10 " + "--cluster-domain=cluster.local " + "--allow-privileged=true --v=2") + ).format(k8s_version=K8S_VERSION) + + output = subprocess.check_output(cmd, shell=True) + print output + + wait_until_k8s_is_up() + + +def stop(): + cmd = """ +for run in {0..2} +do + docker ps -a | grep 'k8s_' | awk '{print $1}' | xargs --no-run-if-empty docker rm -f + docker ps -a | grep 'gcr.io/google_containers/hyperkube-amd64' | awk '{print $1}' | xargs --no-run-if-empty docker rm -f +done""" + output = subprocess.check_output(cmd, shell=True) + print output + + +def answers(): + return """ +[general] +provider = kubernetes +namespace = default +""" + + +def wait_until_k8s_is_up(): + while True: + try: + resp = urllib.urlopen('http://127.0.0.1:8080') + if resp.getcode() == 200: + break + except IOError: + pass + print '...' + time.sleep(1) + time.sleep(5) + +if __name__ == '__main__': + exec(sys.argv[1] + '()') diff --git a/tests/system/providers/openshift.py b/tests/system/providers/openshift.py new file mode 100644 index 00000000..d4911e3c --- /dev/null +++ b/tests/system/providers/openshift.py @@ -0,0 +1,115 @@ +import base64 +import os +import subprocess +import sys +import time +import urllib2 + + +def start(): + print "Stopping existing origin container, if any..." + try: + subprocess.check_call('docker rm -f origin', shell=True) + except subprocess.CalledProcessError: + pass + if not (os.path.exists('/usr/bin/kubectl') or + os.path.exists('/usr/local/bin/kubectl')): + print "No kubectl bin exists? Please install." + sys.exit(1) + + cmd = """ +docker run -d --name "origin" \ + --privileged --pid=host --net=host \ + -v /:/rootfs:ro -v /var/run:/var/run:rw -v /sys:/sys -v /var/lib/docker:/var/lib/docker:rw \ + -v /var/lib/origin/openshift.local.volumes:/var/lib/origin/openshift.local.volumes \ + openshift/origin start""" + output = subprocess.check_output(cmd, shell=True) + print output + wait_for_os() + + +def answers(): + req = urllib2.Request( + 'https://localhost:8443/oauth/authorize?' + 'response_type=token&client_id=openshift-challenging-client', + headers={'X-CSRF-Token': 1} + ) + base64string = base64.encodestring('openshift:openshift').replace( + '\n', '') + req.add_header('Authorization', 'Basic %s' % base64string) + f = urllib2.urlopen(req) + api_key = f.geturl().split('access_token=')[1].split('&')[0] + subprocess.check_call('docker exec -ti origin oc config set-credentials openshift --token={api_key}'.format(api_key=api_key), shell=True) + subprocess.check_call( + 'docker exec -ti origin oc config set-cluster openshift1 ' + '--server=https://localhost:8443 --insecure-skip-tls-verify=true', + shell=True) + subprocess.check_call( + 'docker exec -ti origin oc config set-context openshift ' + '--cluster=openshift1 --user=openshift', shell=True) + subprocess.check_call( + 'docker exec -ti origin oc config use-context openshift', shell=True) + subprocess.check_call( + 'docker exec -ti origin oc config set contexts.openshift.namespace foo', + shell=True) + + time.sleep(3) + subprocess.check_call( + 'docker exec -ti origin oc new-project foo', shell=True) + time.sleep(3) + + answers = """ +[general] +provider = openshift +provider-api = https://localhost:8443 +provider-auth = {api_key} +namespace = foo +provider-tlsverify = False""".format(api_key=api_key) + print answers + return answers + + +def stop(): + try: + subprocess.check_output('docker rm -f origin', shell=True) + except subprocess.CalledProcessError: + return + + +def wait_for_os(): + while True: + try: + resp = urllib2.urlopen('https://127.0.0.1:8443') + if resp.getcode() == 200: + break + except IOError: + pass + print '...' + time.sleep(1) + time.sleep(5) + + +def wait(): + cmd = """ + echo "Waiting for oc po/svc/rc to finish terminating..." + docker exec -it origin oc get po,svc,rc + sleep 3 # give kubectl chance to catch up to api call + while [ 1 ] + do + oc=`docker exec -it origin oc get po,svc,rc | grep Terminating` + if [[ $oc == "" ]] + then + echo "oc po/svc/rc terminated!" + break + else + echo "..." + fi + sleep 1 + done""" + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError: + pass + +if __name__ == '__main__': + exec(sys.argv[1] + '()') From 6f23e06636e16510bd224177f262caed82dd8b66 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Thu, 18 Aug 2016 15:56:16 +0530 Subject: [PATCH 06/21] Updated base suite for functional test to setup providers - openshift - kubernetes --- tests/system/__init__.py | 0 tests/system/base.py | 341 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 319 insertions(+), 22 deletions(-) create mode 100644 tests/system/__init__.py diff --git a/tests/system/__init__.py b/tests/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/system/base.py b/tests/system/base.py index dcfd3b3b..8af143b7 100644 --- a/tests/system/base.py +++ b/tests/system/base.py @@ -1,5 +1,8 @@ import os +import logging +import logging.config import re +import time import anymarkup import datetime import unittest @@ -7,6 +10,37 @@ from collections import OrderedDict import tempfile +from .providers import kubernetes +from .providers import openshift + +LOGGING_CONF = dict( + version=1, + formatters=dict( + bare={ + "datefmt": "%Y-%m-%d %H:%M:%S", + "format": "[%(asctime)s][%(name)10s %(levelname)7s] %(message)s" + }, + ), + handlers=dict( + console={ + "class": "logging.StreamHandler", + "formatter": "bare", + "level": "DEBUG", + "stream": "ext://sys.stderr", + } + ), + loggers=dict( + test={ + "level": "DEBUG", + "propagate": False, + "handlers": ["console"], + } + ) +) + +logging.config.dictConfig(LOGGING_CONF) +logger = logging.getLogger('test') + class BaseProviderTestSuite(unittest.TestCase): """ @@ -141,25 +175,55 @@ class KubernetesProviderTestSuite(BaseProviderTestSuite): Base test suite for Kubernetes. """ + @classmethod + def setUpClass(cls): + logger.debug('setUpClass...') + logger.debug('Stopping existing kubernetes instance, if any...') + kubernetes.stop() + logger.debug('Starting kubernetes instance...') + kubernetes.start() + time.sleep(10) + cls.answers = anymarkup.parse(kubernetes.answers(), 'ini') + + @classmethod + def tearDownClass(cls): + kubernetes.stop() + def tearDown(self): + logger.debug('Teardown ...') pods = self._get_pods() services = self._get_services() rcs = self._get_rcs() # clean up newly created pods + logger.debug('clean up pods') for pod in pods: if pod not in self._pods: - subprocess.check_output(['/usr/bin/kubectl', 'delete', 'pod', pod]) + subprocess.check_output('kubectl delete pod ' + pod, shell=True) # clean up newly created services for service in services: if service not in self._services: - subprocess.check_output(['/usr/bin/kubectl', 'delete', 'service', service]) + subprocess.check_output('kubectl delete service ' + service, shell=True) # clean up newly created rcs for rc in rcs: if rc not in self._rcs: - subprocess.check_output(['/usr/bin/kubectl', 'delete', 'rc', rc]) + subprocess.check_output('kubectl delete rc ' + rc, shell=True) + + for pod in pods: + if pod not in self._pods: + self.assertPod(pod, exists=False, timeout=360) + + for service in services: + if service not in self._services: + self.assertService(service, exists=False, timeout=360) + + for rc in rcs: + if rc not in self._rcs: + self.assertRc(rc, exists=False, timeout=360) + + time.sleep(10) def deploy(self, app_spec, answers): """ @@ -177,7 +241,8 @@ def deploy(self, app_spec, answers): cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, '--provider=kubernetes', '--destination=%s' % destination, app_spec] - subprocess.check_output(cmd) + output = subprocess.check_output(' '.join(cmd), shell=True) + print output return destination def undeploy(self, workdir): @@ -187,8 +252,7 @@ def undeploy(self, workdir): Args: workdir (str): Path to deployed application dir """ - cmd = ['atomicapp', 'stop', workdir] - subprocess.check_output(cmd) + subprocess.check_output('atomicapp stop %s' % workdir, shell=True) def assertPod(self, name, exists=True, status=None, timeout=1): """ @@ -198,16 +262,18 @@ def assertPod(self, name, exists=True, status=None, timeout=1): get to the desired state. """ start = datetime.datetime.now() - cmd = ['/usr/bin/kubectl', 'get', 'pod', name] + cmd = 'kubectl get pod ' + name while (datetime.datetime.now() - start).total_seconds() <= timeout: try: - output = subprocess.check_output(cmd) + output = subprocess.check_output(cmd, shell=True) except subprocess.CalledProcessError: if exists is False: return True continue for line in output.splitlines()[1:]: pod = self._get_pod_details(line) + if exists is False: + continue result = True if status is not None: if pod['status'] == status: @@ -216,9 +282,6 @@ def assertPod(self, name, exists=True, status=None, timeout=1): result = result and False if result: return True - else: - if exists is False: - return True if exists: message = "Pod: %s does not exist" % name if status is not None: @@ -234,20 +297,19 @@ def assertService(self, name, exists=True, timeout=1): We can also set a timeout to wait for the service to get to the desired state. """ - cmd = ['/usr/bin/kubectl', 'get', 'service', name] + cmd = 'kubectl get service ' + name start = datetime.datetime.now() while (datetime.datetime.now() - start).total_seconds() <= timeout: try: - output = subprocess.check_output(cmd) + output = subprocess.check_output(cmd, shell=True) except subprocess.CalledProcessError: if exists is False: return True continue for line in output.splitlines()[1:]: - return True - else: if exists is False: - return True + continue + return True if exists: message = "Service: %s does not exist" % name else: @@ -261,20 +323,255 @@ def assertRc(self, name, exists=True, timeout=1): We can also set a timeout to wait for the rc to get to the desired state. """ - cmd = ['/usr/bin/kubectl', 'get', 'rc', name] + cmd = 'kubectl get rc ' + name start = datetime.datetime.now() while (datetime.datetime.now() - start).total_seconds() <= timeout: try: - output = subprocess.check_output(cmd) + output = subprocess.check_output(cmd, shell=True) except subprocess.CalledProcessError: if exists is False: return True continue for line in output.splitlines()[1:]: + if exists is False: + continue return True - else: + if exists: + message = "RC: %s does not exist" % name + else: + message = "RC: %s exists." % name + raise AssertionError(message) + + def get_initial_state(self): + """Save initial state of the provider""" + self._services = self._get_services() + self._pods = self._get_pods() + self._rcs = self._get_rcs() + + def _get_services(self): + output = subprocess.check_output('kubectl get services', shell=True) + services = OrderedDict() + for line in output.splitlines()[1:]: + service = self._get_service_details(line) + services[service['name']] = service + return services + + def _get_service_details(self, line): + name, labels, selector, ips, ports = line.split() + service = { + 'name': name, + 'labels': labels, + 'selector': selector, + 'ips': ips, + 'ports': ports + } + return service + + def _get_pods(self): + output = subprocess.check_output('kubectl get pods', shell=True) + pods = OrderedDict() + for line in output.splitlines()[1:]: + pod = self._get_pod_details(line) + pods[pod['name']] = pod + return pods + + def _get_pod_details(self, line): + name, ready, status, restarts, age = line.split() + pod = { + 'name': name, + 'ready': ready, + 'status': status, + 'restarts': restarts, + 'age': age + } + return pod + + def _get_rcs(self): + output = subprocess.check_output('kubectl get rc', shell=True) + rcs = OrderedDict() + for line in output.splitlines()[1:]: + rc = self._get_rc_details(line) + rcs[rc['controller']] = rc + return rcs + + def _get_rc_details(self, line): + controller, container, image, selector, replicas = line.split() + rc = { + 'controller': controller, + 'container': container, + 'image': image, + 'selector': selector, + 'replicas': replicas + } + return rc + + +class OpenshiftProviderTestSuite(BaseProviderTestSuite): + """ + Base test suite for Openshift. + """ + + @classmethod + def setUpClass(cls): + openshift.stop() + openshift.start() + openshift.wait() + time.sleep(10) + cls.answers = anymarkup.parse(openshift.answers(), 'ini') + + @classmethod + def tearDownClass(cls): + openshift.stop() + + def setUp(self): + self.os_exec('oc project %s' % self.answers['general']['namespace']) + + def os_exec(self, cmd): + output = subprocess.check_output('docker exec -ti origin %s' % cmd, shell=True) + return output + + def tearDown(self): + pods = self._get_pods() + services = self._get_services() + rcs = self._get_rcs() + + # clean up newly created pods + for pod in pods: + if pod not in self._pods: + self.os_exec('oc delete pod %s' % pod) + + # clean up newly created services + for service in services: + if service not in self._services: + self.os_exec('oc delete service %s' % service) + + # clean up newly created rcs + for rc in rcs: + if rc not in self._rcs: + self.os_exec('oc delete rc %s' % rc) + + for pod in pods: + if pod not in self._pods: + self.assertPod(pod, exists=False, timeout=360) + + for service in services: + if service not in self._services: + self.assertService(service, exists=False, timeout=360) + + for rc in rcs: + if rc not in self._rcs: + self.assertRc(rc, exists=False, timeout=360) + + time.sleep(10) + + def deploy(self, app_spec, answers): + """ + Deploy app to kuberntes + + Args: + app_spec (str): image name or path to application + answers (dict): Answers data + + Returns: + Path of the deployed dir. + """ + destination = tempfile.mkdtemp() + answers_path = self.get_tmp_answers_file(answers) + cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, + '--provider=openshift', + '--destination=%s' % destination, app_spec] + subprocess.check_output(' '.join(cmd), shell=True) + return destination + + def undeploy(self, workdir): + """ + Undeploy app from kubernetes. + + Args: + workdir (str): Path to deployed application dir + """ + subprocess.check_output('atomicapp stop %s' % workdir, shell=True) + + def assertPod(self, name, exists=True, status=None, timeout=1): + """ + Assert a kubernetes pod, if it exists, what's its status. + + We can also set a timeout to wait for the pod to + get to the desired state. + """ + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = self.os_exec('oc get pod %s' % name) + except subprocess.CalledProcessError: + if exists is False: + return True + continue + for line in output.splitlines()[1:]: + if exists is False: + continue + pod = self._get_pod_details(line) + result = True + if status is not None: + if pod['status'] == status: + result = result and True + else: + result = result and False + if result: + return True + if exists: + message = "Pod: %s does not exist" % name + if status is not None: + message += ' with status: %s' % status + else: + message = "Pod: %s exists." % name + raise AssertionError(message) + + def assertService(self, name, exists=True, timeout=1): + """ + Assert a kubernetes service, if it exists. + + We can also set a timeout to wait for the service to + get to the desired state. + """ + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = self.os_exec('oc get service %s' % name) + except subprocess.CalledProcessError: if exists is False: return True + continue + for line in output.splitlines()[1:]: + if exists is False: + continue + return True + + if exists: + message = "Service: %s does not exist" % name + else: + message = "Service: %s exists." % name + raise AssertionError(message) + + def assertRc(self, name, exists=True, timeout=1): + """ + Assert a kubernetes rc, if it exists. + + We can also set a timeout to wait for the rc to + get to the desired state. + """ + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + try: + output = self.os_exec('oc get rc %s' % name) + except subprocess.CalledProcessError: + if exists is False: + return True + continue + for line in output.splitlines()[1:]: + if exists is False: + continue + return True if exists: message = "RC: %s does not exist" % name else: @@ -288,7 +585,7 @@ def get_initial_state(self): self._rcs = self._get_rcs() def _get_services(self): - output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'services']) + output = self.os_exec('oc get services') services = OrderedDict() for line in output.splitlines()[1:]: service = self._get_service_details(line) @@ -307,7 +604,7 @@ def _get_service_details(self, line): return service def _get_pods(self): - output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'pods']) + output = self.os_exec('oc get pods') pods = OrderedDict() for line in output.splitlines()[1:]: pod = self._get_pod_details(line) @@ -326,7 +623,7 @@ def _get_pod_details(self, line): return pod def _get_rcs(self): - output = subprocess.check_output(['/usr/bin/kubectl', 'get', 'rc']) + output = self.os_exec('oc get rc') rcs = OrderedDict() for line in output.splitlines()[1:]: rc = self._get_rc_details(line) From 172b8f9a93e8a63d18bc6be2df120dc0602e9d83 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Thu, 18 Aug 2016 15:59:01 +0530 Subject: [PATCH 07/21] Updated/added functional tests for kuberenetes/openshift. --- tests/system/test_kubernetes_provider.py | 54 +++++++++++----------- tests/system/test_openshift_provider.py | 57 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 tests/system/test_openshift_provider.py diff --git a/tests/system/test_kubernetes_provider.py b/tests/system/test_kubernetes_provider.py index f520fc74..7aa88ed5 100644 --- a/tests/system/test_kubernetes_provider.py +++ b/tests/system/test_kubernetes_provider.py @@ -1,27 +1,29 @@ +from __future__ import absolute_import + import os -from base import KubernetesProviderTestSuite +from .base import KubernetesProviderTestSuite class TestWordpress(KubernetesProviderTestSuite): """ Test Wordpress Atomic App on Kubernetes Provider """ - answers = { - 'general': { - 'namespace': 'default' - }, - 'mariadb-atomicapp': { - 'db_user': 'foo', - 'db_pass': 'foo', - 'db_name': 'foo' - }, - 'wordpress': { - 'db_user': 'foo', - 'db_pass': 'foo', - 'db_name': 'foo' - } - } + + def setUp(self): + super(TestWordpress, self).setUp() + self.answers.update({ + 'mariadb-atomicapp': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + }, + 'wordpress': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + } + }) def _run(self): app_spec = os.path.join( @@ -31,22 +33,22 @@ def _run(self): def test_wordpress_run(self): self._run() - self.assertPod('wordpress', status='Running', timeout=10) - self.assertPod('mariadb', status='Running', timeout=10) + self.assertPod('wordpress', status='Running', timeout=360) + self.assertPod('mariadb', status='Running', timeout=360) - self.assertService('wordpress') - self.assertService('mariadb') + self.assertService('wordpress', timeout=360) + self.assertService('mariadb', timeout=360) def test_wordpress_stop(self): workdir = self._run() - self.assertPod('wordpress', timeout=10) - self.assertPod('mariadb', timeout=10) + self.assertPod('wordpress', timeout=360) + self.assertPod('mariadb', timeout=360) - self.assertService('wordpress') - self.assertService('mariadb') + self.assertService('wordpress', timeout=360) + self.assertService('mariadb', timeout=360) self.undeploy(workdir) - self.assertPod('wordpress', exists=False) - self.assertPod('mariadb', exists=False) + self.assertPod('wordpress', exists=False, timeout=360) + self.assertPod('mariadb', exists=False, timeout=360) diff --git a/tests/system/test_openshift_provider.py b/tests/system/test_openshift_provider.py new file mode 100644 index 00000000..9935b560 --- /dev/null +++ b/tests/system/test_openshift_provider.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import + +import logging +import os + +from .base import OpenshiftProviderTestSuite + +logger = logging.getLogger() + + +class TestWordpress(OpenshiftProviderTestSuite): + """ + Test Wordpress Atomic App on Kubernetes Provider + """ + + def setUp(self): + super(TestWordpress, self).setUp() + self.answers.update({ + 'mariadb-atomicapp': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + }, + 'wordpress': { + 'db_user': 'foo', + 'db_pass': 'foo', + 'db_name': 'foo' + } + }) + + def _run(self): + app_spec = os.path.join( + self.nulecule_lib, 'wordpress-centos7-atomicapp') + return self.deploy(app_spec, self.answers) + + def test_wordpress_run(self): + self._run() + + self.assertPod('wordpress', status='Running', timeout=360) + self.assertPod('mariadb', status='Running', timeout=360) + + self.assertService('wordpress', timeout=120) + self.assertService('mariadb', timeout=120) + + def test_wordpress_stop(self): + workdir = self._run() + + self.assertPod('wordpress', timeout=360) + self.assertPod('mariadb', timeout=360) + + self.assertService('wordpress', timeout=360) + self.assertService('mariadb', timeout=360) + + self.undeploy(workdir) + + self.assertPod('wordpress', exists=False, timeout=360) + self.assertPod('mariadb', exists=False, timeout=360) From 5857fadb35d63ffe102ae294397c38880d81e403 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Thu, 18 Aug 2016 16:01:46 +0530 Subject: [PATCH 08/21] Updated README for functional tests. --- tests/system/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/system/README.md b/tests/system/README.md index 31592499..3c88df11 100644 --- a/tests/system/README.md +++ b/tests/system/README.md @@ -6,3 +6,9 @@ From root directory of ``atomicapp`` repo, do: ``` sudo NULECULE_LIB= py.test tests/system ``` + +To test individual provider, you can do the following: + +``` +sudo NULECULE_LIB= py.test tests/system/test__provider.py +``` From 4a6772c6d9baef94a140a4064ffa47015a1752cc Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Wed, 24 Aug 2016 13:10:55 +0530 Subject: [PATCH 09/21] Assert if wordpress pod goes till ContainerCreating for openshift tests. --- tests/system/test_openshift_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/test_openshift_provider.py b/tests/system/test_openshift_provider.py index 9935b560..50e7f1e9 100644 --- a/tests/system/test_openshift_provider.py +++ b/tests/system/test_openshift_provider.py @@ -36,7 +36,7 @@ def _run(self): def test_wordpress_run(self): self._run() - self.assertPod('wordpress', status='Running', timeout=360) + self.assertPod('wordpress', status='ContainerCreating', timeout=360) self.assertPod('mariadb', status='Running', timeout=360) self.assertService('wordpress', timeout=120) @@ -45,7 +45,7 @@ def test_wordpress_run(self): def test_wordpress_stop(self): workdir = self._run() - self.assertPod('wordpress', timeout=360) + self.assertPod('wordpress', status='ContainerCreating', timeout=360) self.assertPod('mariadb', timeout=360) self.assertService('wordpress', timeout=360) From 7b714b05bc5c196ffcf983d588dbc6248eef2d42 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Mon, 29 Aug 2016 23:06:35 +0530 Subject: [PATCH 10/21] Fixed functional tests for openshift. --- tests/system/base.py | 8 +++++--- tests/system/providers/openshift.py | 16 ++++++++-------- tests/system/test_openshift_provider.py | 23 +++++++---------------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/tests/system/base.py b/tests/system/base.py index 8af143b7..89f18f55 100644 --- a/tests/system/base.py +++ b/tests/system/base.py @@ -415,19 +415,19 @@ class OpenshiftProviderTestSuite(BaseProviderTestSuite): def setUpClass(cls): openshift.stop() openshift.start() - openshift.wait() - time.sleep(10) cls.answers = anymarkup.parse(openshift.answers(), 'ini') + openshift.wait() @classmethod def tearDownClass(cls): openshift.stop() def setUp(self): + super(OpenshiftProviderTestSuite, self).setUp() self.os_exec('oc project %s' % self.answers['general']['namespace']) def os_exec(self, cmd): - output = subprocess.check_output('docker exec -ti origin %s' % cmd, shell=True) + output = subprocess.check_output('docker exec -i origin %s' % cmd, shell=True) return output def tearDown(self): @@ -462,6 +462,8 @@ def tearDown(self): if rc not in self._rcs: self.assertRc(rc, exists=False, timeout=360) + openshift.wait() + time.sleep(10) def deploy(self, app_spec, answers): diff --git a/tests/system/providers/openshift.py b/tests/system/providers/openshift.py index d4911e3c..346bfb17 100644 --- a/tests/system/providers/openshift.py +++ b/tests/system/providers/openshift.py @@ -39,23 +39,23 @@ def answers(): req.add_header('Authorization', 'Basic %s' % base64string) f = urllib2.urlopen(req) api_key = f.geturl().split('access_token=')[1].split('&')[0] - subprocess.check_call('docker exec -ti origin oc config set-credentials openshift --token={api_key}'.format(api_key=api_key), shell=True) + subprocess.check_call('docker exec -i origin oc config set-credentials openshift --token={api_key}'.format(api_key=api_key), shell=True) subprocess.check_call( - 'docker exec -ti origin oc config set-cluster openshift1 ' + 'docker exec -i origin oc config set-cluster openshift1 ' '--server=https://localhost:8443 --insecure-skip-tls-verify=true', shell=True) subprocess.check_call( - 'docker exec -ti origin oc config set-context openshift ' + 'docker exec -i origin oc config set-context openshift ' '--cluster=openshift1 --user=openshift', shell=True) subprocess.check_call( - 'docker exec -ti origin oc config use-context openshift', shell=True) + 'docker exec -i origin oc config use-context openshift', shell=True) subprocess.check_call( - 'docker exec -ti origin oc config set contexts.openshift.namespace foo', + 'docker exec -i origin oc config set contexts.openshift.namespace foo', shell=True) time.sleep(3) subprocess.check_call( - 'docker exec -ti origin oc new-project foo', shell=True) + 'docker exec -i origin oc new-project foo', shell=True) time.sleep(3) answers = """ @@ -92,11 +92,11 @@ def wait_for_os(): def wait(): cmd = """ echo "Waiting for oc po/svc/rc to finish terminating..." - docker exec -it origin oc get po,svc,rc + docker exec -i origin oc get po,svc,rc sleep 3 # give kubectl chance to catch up to api call while [ 1 ] do - oc=`docker exec -it origin oc get po,svc,rc | grep Terminating` + oc=`docker exec -i origin oc get po,svc,rc | grep Terminating` if [[ $oc == "" ]] then echo "oc po/svc/rc terminated!" diff --git a/tests/system/test_openshift_provider.py b/tests/system/test_openshift_provider.py index 50e7f1e9..3ef4270d 100644 --- a/tests/system/test_openshift_provider.py +++ b/tests/system/test_openshift_provider.py @@ -33,25 +33,16 @@ def _run(self): self.nulecule_lib, 'wordpress-centos7-atomicapp') return self.deploy(app_spec, self.answers) - def test_wordpress_run(self): - self._run() - - self.assertPod('wordpress', status='ContainerCreating', timeout=360) - self.assertPod('mariadb', status='Running', timeout=360) - - self.assertService('wordpress', timeout=120) - self.assertService('mariadb', timeout=120) - - def test_wordpress_stop(self): + def test_wordpress_lifecycle(self): workdir = self._run() - self.assertPod('wordpress', status='ContainerCreating', timeout=360) - self.assertPod('mariadb', timeout=360) + self.assertPod('wordpress', status='ContainerCreating', timeout=120) + self.assertPod('mariadb', status='Running', timeout=120) - self.assertService('wordpress', timeout=360) - self.assertService('mariadb', timeout=360) + self.assertService('wpfrontend', timeout=120) + self.assertService('mariadb', timeout=120) self.undeploy(workdir) - self.assertPod('wordpress', exists=False, timeout=360) - self.assertPod('mariadb', exists=False, timeout=360) + self.assertPod('wordpress', exists=False, timeout=120) + self.assertPod('mariadb', exists=False, timeout=120) From 61512c8c83bb58792d9427fd7b7227232661b145 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Mon, 29 Aug 2016 23:07:41 +0530 Subject: [PATCH 11/21] Singe functional test for entire app lifecycle on k8s. --- tests/system/test_kubernetes_provider.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/system/test_kubernetes_provider.py b/tests/system/test_kubernetes_provider.py index 7aa88ed5..23ec9b06 100644 --- a/tests/system/test_kubernetes_provider.py +++ b/tests/system/test_kubernetes_provider.py @@ -30,8 +30,8 @@ def _run(self): self.nulecule_lib, 'wordpress-centos7-atomicapp') return self.deploy(app_spec, self.answers) - def test_wordpress_run(self): - self._run() + def test_wordpress_lifecycle(self): + workdir = self._run() self.assertPod('wordpress', status='Running', timeout=360) self.assertPod('mariadb', status='Running', timeout=360) @@ -39,15 +39,6 @@ def test_wordpress_run(self): self.assertService('wordpress', timeout=360) self.assertService('mariadb', timeout=360) - def test_wordpress_stop(self): - workdir = self._run() - - self.assertPod('wordpress', timeout=360) - self.assertPod('mariadb', timeout=360) - - self.assertService('wordpress', timeout=360) - self.assertService('mariadb', timeout=360) - self.undeploy(workdir) self.assertPod('wordpress', exists=False, timeout=360) From b8e26d29add10fd3256c4b949ef57913dd6244f0 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Mon, 29 Aug 2016 23:27:21 +0530 Subject: [PATCH 12/21] Added default value for nulecule library path for functional tests. The default value is assumed to be a directory named 'nulecule-library' adjacent to the 'atomicapp' directory. --- tests/system/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/system/base.py b/tests/system/base.py index 89f18f55..263760bb 100644 --- a/tests/system/base.py +++ b/tests/system/base.py @@ -79,7 +79,8 @@ def get_tmp_answers_file(self, answers): @property def nulecule_lib(self): - return os.environ['NULECULE_LIB'] + return os.environ.get('NULECULE_LIB') or \ + os.path.join(os.path.dirname(__file__), '../../../nulecule-library') class DockerProviderTestSuite(BaseProviderTestSuite): From 017ab62b5d0969e84605e97c77dc43720b1c4b11 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 30 Aug 2016 16:15:45 +0530 Subject: [PATCH 13/21] Download kubectl binary if missing during functional tests. --- tests/system/providers/kubernetes.py | 8 ++++++-- tests/system/providers/openshift.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/system/providers/kubernetes.py b/tests/system/providers/kubernetes.py index bb7f8551..5f2da503 100644 --- a/tests/system/providers/kubernetes.py +++ b/tests/system/providers/kubernetes.py @@ -8,8 +8,12 @@ def start(): if not (os.path.exists('/usr/bin/kubectl') or os.path.exists('/usr/local/bin/kubectl')): - print "No kubectl bin exists? Please install." - return + print "No kubectl bin exists? Pulling..." + subprocess.check_call( + 'curl http://storage.googleapis.com/kubernetes-release/release/' + 'v1.3.5/bin/linux/amd64/kubectl > /usr/local/bin/kubectl', + shell=True) + subprocess.check_call('chmod +x /usr/local/bin/kubectl', shell=True) K8S_VERSION = '1.3.4' diff --git a/tests/system/providers/openshift.py b/tests/system/providers/openshift.py index 346bfb17..1fca0c44 100644 --- a/tests/system/providers/openshift.py +++ b/tests/system/providers/openshift.py @@ -14,8 +14,12 @@ def start(): pass if not (os.path.exists('/usr/bin/kubectl') or os.path.exists('/usr/local/bin/kubectl')): - print "No kubectl bin exists? Please install." - sys.exit(1) + print "No kubectl bin exists? Pulling..." + subprocess.check_call( + 'curl http://storage.googleapis.com/kubernetes-release/release/' + 'v1.3.5/bin/linux/amd64/kubectl > /usr/local/bin/kubectl', + shell=True) + subprocess.check_call('chmod +x /usr/local/bin/kubectl', shell=True) cmd = """ docker run -d --name "origin" \ From e4123439a23160dc3bf4a4e9fa47dfdfbd6d8f80 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 30 Aug 2016 19:04:29 +0530 Subject: [PATCH 14/21] Fix SSL error when accessing openshift API during functional tests. --- tests/system/providers/openshift.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/system/providers/openshift.py b/tests/system/providers/openshift.py index 1fca0c44..2cffef81 100644 --- a/tests/system/providers/openshift.py +++ b/tests/system/providers/openshift.py @@ -4,6 +4,12 @@ import sys import time import urllib2 +import ssl + + +ssl_ctx = ssl.create_default_context() +ssl_ctx.check_hostname = False +ssl_ctx.verify_mode = ssl.CERT_NONE def start(): @@ -41,7 +47,7 @@ def answers(): base64string = base64.encodestring('openshift:openshift').replace( '\n', '') req.add_header('Authorization', 'Basic %s' % base64string) - f = urllib2.urlopen(req) + f = urllib2.urlopen(req, context=ssl_ctx) api_key = f.geturl().split('access_token=')[1].split('&')[0] subprocess.check_call('docker exec -i origin oc config set-credentials openshift --token={api_key}'.format(api_key=api_key), shell=True) subprocess.check_call( @@ -83,7 +89,7 @@ def stop(): def wait_for_os(): while True: try: - resp = urllib2.urlopen('https://127.0.0.1:8443') + resp = urllib2.urlopen('https://127.0.0.1:8443', context=ssl_ctx) if resp.getcode() == 200: break except IOError: From 2c9f418a4128a80b3ab772323811dd463dd0237c Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Wed, 31 Aug 2016 20:22:23 +0530 Subject: [PATCH 15/21] Move functional tests to tests/functional dir. --- tests/{system => functional}/README.md | 0 tests/{system => functional}/__init__.py | 0 tests/{system => functional}/base.py | 0 tests/{system => functional}/providers/__init__.py | 0 tests/{system => functional}/providers/kubernetes.py | 0 tests/{system => functional}/providers/openshift.py | 0 tests/{system => functional}/test_docker_provider.py | 0 tests/{system => functional}/test_kubernetes_provider.py | 0 tests/{system => functional}/test_openshift_provider.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename tests/{system => functional}/README.md (100%) rename tests/{system => functional}/__init__.py (100%) rename tests/{system => functional}/base.py (100%) rename tests/{system => functional}/providers/__init__.py (100%) rename tests/{system => functional}/providers/kubernetes.py (100%) rename tests/{system => functional}/providers/openshift.py (100%) rename tests/{system => functional}/test_docker_provider.py (100%) rename tests/{system => functional}/test_kubernetes_provider.py (100%) rename tests/{system => functional}/test_openshift_provider.py (100%) diff --git a/tests/system/README.md b/tests/functional/README.md similarity index 100% rename from tests/system/README.md rename to tests/functional/README.md diff --git a/tests/system/__init__.py b/tests/functional/__init__.py similarity index 100% rename from tests/system/__init__.py rename to tests/functional/__init__.py diff --git a/tests/system/base.py b/tests/functional/base.py similarity index 100% rename from tests/system/base.py rename to tests/functional/base.py diff --git a/tests/system/providers/__init__.py b/tests/functional/providers/__init__.py similarity index 100% rename from tests/system/providers/__init__.py rename to tests/functional/providers/__init__.py diff --git a/tests/system/providers/kubernetes.py b/tests/functional/providers/kubernetes.py similarity index 100% rename from tests/system/providers/kubernetes.py rename to tests/functional/providers/kubernetes.py diff --git a/tests/system/providers/openshift.py b/tests/functional/providers/openshift.py similarity index 100% rename from tests/system/providers/openshift.py rename to tests/functional/providers/openshift.py diff --git a/tests/system/test_docker_provider.py b/tests/functional/test_docker_provider.py similarity index 100% rename from tests/system/test_docker_provider.py rename to tests/functional/test_docker_provider.py diff --git a/tests/system/test_kubernetes_provider.py b/tests/functional/test_kubernetes_provider.py similarity index 100% rename from tests/system/test_kubernetes_provider.py rename to tests/functional/test_kubernetes_provider.py diff --git a/tests/system/test_openshift_provider.py b/tests/functional/test_openshift_provider.py similarity index 100% rename from tests/system/test_openshift_provider.py rename to tests/functional/test_openshift_provider.py From 9a58a13ea1397dacc64327aa4803fdfc42205a2c Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Wed, 31 Aug 2016 20:42:38 +0530 Subject: [PATCH 16/21] Allow running functional tests from make file. --- Makefile | 6 +++ atomicapp/constants.py | 2 + tests/functional/base.py | 44 +++++++++++++++++++- tests/functional/providers/kubernetes.py | 13 +++--- tests/functional/providers/openshift.py | 10 ++--- tests/functional/test_docker_provider.py | 8 ++-- tests/functional/test_kubernetes_provider.py | 3 +- tests/functional/test_openshift_provider.py | 3 +- 8 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index bd4f223f..5aab9a18 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,12 @@ test: pip install -qr test-requirements.txt $(PYTHON) -m pytest tests/units/ -vv --cov atomicapp +.PHONY: functional-test +functional-test: + pip install -qr requirements.txt + pip install -qr test-requirements.txt + $(PYTHON) -m pytest tests/functional/ -vv --cov atomicapp + .PHONY: image image: $(DOCKER) build -t $(tag) . diff --git a/atomicapp/constants.py b/atomicapp/constants.py index 4139f93f..b37cc5b0 100644 --- a/atomicapp/constants.py +++ b/atomicapp/constants.py @@ -43,6 +43,8 @@ NAMESPACE_SEPARATOR = ":" REQUIREMENTS_KEY = "requirements" +K8S_VERSION = '1.3.5' + # Nulecule spec terminology vs the function within /providers REQUIREMENT_FUNCTIONS = { "persistentVolume": "persistent_storage" diff --git a/tests/functional/base.py b/tests/functional/base.py index 263760bb..cb06e735 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -9,6 +9,7 @@ import subprocess from collections import OrderedDict import tempfile +import uuid from .providers import kubernetes from .providers import openshift @@ -47,6 +48,19 @@ class BaseProviderTestSuite(unittest.TestCase): Base test suite for a provider: docker, kubernetes, etc. """ + NULECULE_LIB_REPO = 'https://github.com/projectatomic/nulecule-library' + NULECULE_LIB_PATH = os.path.join(os.path.dirname(__file__), + 'nulecule-library') + + @classmethod + def setUpClass(cls): + cls.fetch_nulecule_lib() + cls.build_image() + + @classmethod + def tearDownClass(cls): + cls.remove_image() + def setUp(self): self.get_initial_state() @@ -79,8 +93,34 @@ def get_tmp_answers_file(self, answers): @property def nulecule_lib(self): - return os.environ.get('NULECULE_LIB') or \ - os.path.join(os.path.dirname(__file__), '../../../nulecule-library') + return self.NULECULE_LIB_PATH + + @classmethod + def fetch_nulecule_lib(cls): + if not os.path.exists(cls.NULECULE_LIB_PATH): + subprocess.check_call( + 'git clone {repo} {path}'.format( + repo=cls.NULECULE_LIB_REPO, path=cls.NULECULE_LIB_PATH), + shell=True) + else: + subprocess.check_call( + 'cd nulecule-library; git checkout master; ' + 'git pull origin master', shell=True) + + @classmethod + def build_image(cls): + app_dir = os.path.join(cls.NULECULE_LIB_PATH, cls.APP_DIR_NAME) + cls.image_name = '{}-{}'.format( + cls.APP_DIR_NAME, uuid.uuid1().hex[:8]) + subprocess.check_call( + 'cd {app_dir}; docker build -t {image_name} .'.format( + app_dir=app_dir, image_name=cls.image_name), + shell=True) + + @classmethod + def remove_image(cls): + subprocess.check_call('docker rmi {}'.format(cls.image_name), + shell=True) class DockerProviderTestSuite(BaseProviderTestSuite): diff --git a/tests/functional/providers/kubernetes.py b/tests/functional/providers/kubernetes.py index 5f2da503..202c252b 100644 --- a/tests/functional/providers/kubernetes.py +++ b/tests/functional/providers/kubernetes.py @@ -4,18 +4,17 @@ import time import urllib +from atomicapp.constants import K8S_VERSION + def start(): if not (os.path.exists('/usr/bin/kubectl') or os.path.exists('/usr/local/bin/kubectl')): - print "No kubectl bin exists? Pulling..." - subprocess.check_call( + print "No kubectl bin exists? You can download it from %s" % ( 'curl http://storage.googleapis.com/kubernetes-release/release/' - 'v1.3.5/bin/linux/amd64/kubectl > /usr/local/bin/kubectl', - shell=True) - subprocess.check_call('chmod +x /usr/local/bin/kubectl', shell=True) - - K8S_VERSION = '1.3.4' + 'v{k8s_version}/bin/linux/amd64/kubectl'.format( + k8s_version=K8S_VERSION)) + sys.exit(1) cmd = ( "docker run " diff --git a/tests/functional/providers/openshift.py b/tests/functional/providers/openshift.py index 2cffef81..b9e59b8e 100644 --- a/tests/functional/providers/openshift.py +++ b/tests/functional/providers/openshift.py @@ -6,6 +6,8 @@ import urllib2 import ssl +from atomicapp.constants import K8S_VERSION + ssl_ctx = ssl.create_default_context() ssl_ctx.check_hostname = False @@ -20,12 +22,10 @@ def start(): pass if not (os.path.exists('/usr/bin/kubectl') or os.path.exists('/usr/local/bin/kubectl')): - print "No kubectl bin exists? Pulling..." - subprocess.check_call( + print "No kubectl bin exists? You can download it from %s" % ( 'curl http://storage.googleapis.com/kubernetes-release/release/' - 'v1.3.5/bin/linux/amd64/kubectl > /usr/local/bin/kubectl', - shell=True) - subprocess.check_call('chmod +x /usr/local/bin/kubectl', shell=True) + 'v{k8s_version}/bin/linux/amd64/kubectl'.format(k8s_version=K8S_VERSION)) + sys.exit(1) cmd = """ docker run -d --name "origin" \ diff --git a/tests/functional/test_docker_provider.py b/tests/functional/test_docker_provider.py index e242503c..faec2ee2 100644 --- a/tests/functional/test_docker_provider.py +++ b/tests/functional/test_docker_provider.py @@ -1,5 +1,3 @@ -import os - from base import DockerProviderTestSuite @@ -7,11 +5,12 @@ class TestWordpress(DockerProviderTestSuite): """ Test Wordpress Atomic App on Kubernetes Provider """ + APP_DIR_NAME = 'wordpress-centos7-atomicapp' answers = { 'general': { 'namespace': 'default' }, - 'mariadb-atomicapp': { + 'mariadb-centos7-atomicapp:mariadb-atomicapp': { 'db_user': 'foo', 'db_pass': 'foo', 'db_name': 'foo' @@ -24,8 +23,7 @@ class TestWordpress(DockerProviderTestSuite): } def _run(self): - app_spec = os.path.join( - self.nulecule_lib, 'wordpress-centos7-atomicapp') + app_spec = self.image_name return self.deploy(app_spec, self.answers) def test_wordpress_run(self): diff --git a/tests/functional/test_kubernetes_provider.py b/tests/functional/test_kubernetes_provider.py index 23ec9b06..2c2eb89f 100644 --- a/tests/functional/test_kubernetes_provider.py +++ b/tests/functional/test_kubernetes_provider.py @@ -9,11 +9,12 @@ class TestWordpress(KubernetesProviderTestSuite): """ Test Wordpress Atomic App on Kubernetes Provider """ + APP_DIR_NAME = 'wordpress-centos7-atomicapp' def setUp(self): super(TestWordpress, self).setUp() self.answers.update({ - 'mariadb-atomicapp': { + 'mariadb-centos7-atomicapp:mariadb-atomicapp': { 'db_user': 'foo', 'db_pass': 'foo', 'db_name': 'foo' diff --git a/tests/functional/test_openshift_provider.py b/tests/functional/test_openshift_provider.py index 3ef4270d..ed413af5 100644 --- a/tests/functional/test_openshift_provider.py +++ b/tests/functional/test_openshift_provider.py @@ -12,11 +12,12 @@ class TestWordpress(OpenshiftProviderTestSuite): """ Test Wordpress Atomic App on Kubernetes Provider """ + APP_DIR_NAME = 'wordpress-centos7-atomicapp' def setUp(self): super(TestWordpress, self).setUp() self.answers.update({ - 'mariadb-atomicapp': { + 'mariadb-centos7-atomicapp:mariadb-atomicapp': { 'db_user': 'foo', 'db_pass': 'foo', 'db_name': 'foo' From 70f61170458df57551302264886628854eb7fb5d Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Wed, 7 Sep 2016 21:08:43 +0530 Subject: [PATCH 17/21] Simplify k8s cleanup and wait calls for k8s functional tests. --- tests/functional/base.py | 50 ++---------------------- tests/functional/providers/kubernetes.py | 29 ++++++++++++++ 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/tests/functional/base.py b/tests/functional/base.py index cb06e735..c48484de 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -176,7 +176,7 @@ def assertContainerRunning(self, name): def assertContainerNotRunning(self, name): containers = self._get_containers() for _id, container in containers.items(): - if container['name'] == name: + if container.get('name') == name: raise AssertionError('Container: %s is running' % name) return True @@ -231,40 +231,8 @@ def tearDownClass(cls): kubernetes.stop() def tearDown(self): - logger.debug('Teardown ...') - pods = self._get_pods() - services = self._get_services() - rcs = self._get_rcs() - - # clean up newly created pods - logger.debug('clean up pods') - for pod in pods: - if pod not in self._pods: - subprocess.check_output('kubectl delete pod ' + pod, shell=True) - - # clean up newly created services - for service in services: - if service not in self._services: - subprocess.check_output('kubectl delete service ' + service, shell=True) - - # clean up newly created rcs - for rc in rcs: - if rc not in self._rcs: - subprocess.check_output('kubectl delete rc ' + rc, shell=True) - - for pod in pods: - if pod not in self._pods: - self.assertPod(pod, exists=False, timeout=360) - - for service in services: - if service not in self._services: - self.assertService(service, exists=False, timeout=360) - - for rc in rcs: - if rc not in self._rcs: - self.assertRc(rc, exists=False, timeout=360) - - time.sleep(10) + kubernetes.clean() + kubernetes.wait() def deploy(self, app_spec, answers): """ @@ -491,18 +459,6 @@ def tearDown(self): if rc not in self._rcs: self.os_exec('oc delete rc %s' % rc) - for pod in pods: - if pod not in self._pods: - self.assertPod(pod, exists=False, timeout=360) - - for service in services: - if service not in self._services: - self.assertService(service, exists=False, timeout=360) - - for rc in rcs: - if rc not in self._rcs: - self.assertRc(rc, exists=False, timeout=360) - openshift.wait() time.sleep(10) diff --git a/tests/functional/providers/kubernetes.py b/tests/functional/providers/kubernetes.py index 202c252b..4c548c49 100644 --- a/tests/functional/providers/kubernetes.py +++ b/tests/functional/providers/kubernetes.py @@ -56,6 +56,15 @@ def stop(): print output +def clean(): + cmd = """ +# Delete all hanging containers +echo "\n-----Cleaning / removing all pods and containers from default namespace-----\n" +kubectl get pvc,pv,svc,rc,po | grep -v 'k8s-\|NAME\|CONTROLLER\|kubernetes' | awk '{print $1}' | xargs --no-run-if-empty kubectl delete pvc,pv,svc,rc,po --grace-period=1 2>/dev/null""" + output = subprocess.check_output(cmd, shell=True) + print output + + def answers(): return """ [general] @@ -64,6 +73,26 @@ def answers(): """ +def wait(): + cmd = """ +echo "Waiting for k8s po/svc/rc to finish terminating..." +kubectl get po,svc,rc +sleep 3 # give kubectl chance to catch up to api call +while [ 1 ] +do + k8s=`kubectl get po,svc,rc | grep Terminating` + if [[ $k8s == "" ]] + then + echo "k8s po/svc/rc terminated!" + break + else + echo "..." + fi + sleep 1 +done""" + subprocess.check_call(cmd, shell=True) + + def wait_until_k8s_is_up(): while True: try: From 04137f60535f051dd5b4c188c992a9b85300f242 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Wed, 7 Sep 2016 21:09:38 +0530 Subject: [PATCH 18/21] Added helper scripts to setup env for running functional tests. --- tests/functional/scripts/atomic.sh | 61 +++++++++++++++++++++++++++ tests/functional/scripts/atomicapp.sh | 45 ++++++++++++++++++++ tests/functional/scripts/prepare.sh | 33 +++++++++++++++ 3 files changed, 139 insertions(+) create mode 100755 tests/functional/scripts/atomic.sh create mode 100755 tests/functional/scripts/atomicapp.sh create mode 100755 tests/functional/scripts/prepare.sh diff --git a/tests/functional/scripts/atomic.sh b/tests/functional/scripts/atomic.sh new file mode 100755 index 00000000..d82157ac --- /dev/null +++ b/tests/functional/scripts/atomic.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# NOTE, atomic 1.8 requires Docker 1.9 +RELEASE="1.8" +LINK="https://github.com/projectatomic/atomic/archive/v1.8.tar.gz" +SOURCE=$2 + +# Install according to github docs +# https://github.com/projectatomic/atomic/tree/master/docs/install +install_atomic() { + echo " + ########## + INSTALLING ATOMIC CLI + ########## + " + if [ "$SOURCE" == "rpm" ]; then + yum install -y atomic + return + elif [ "$SOURCE" == "master" ]; then + #clean this up later + echo "Downloading master" + git clone https://github.com/projectatomic/atomic atomic + cd atomic + else + wget $LINK -O atomic.tar.gz + tar --no-same-owner -xvf atomic.tar.gz + cd atomic-$RELEASE + fi + + # Use rhel yum, if not assume it's ubuntu / debian + if [ -f /etc/redhat-release ]; then + # Remove all previous builds of atomic (issue of upgrading 1.5 to 1.8) + rm -rf /usr/lib/python2.7/site-packages/Atomic/ /usr/lib/python2.7/site-packages/atomic-* + yum install -y epel-release python-pip pylint go-md2man + else + rm -rf /usr/local/lib/python2.7/site-packages/Atomic/ /usr/local/lib/python2.7/site-packages/atomic-* + rm -rf /usr/local/lib/python2.7/dist-packages/Atomic/ /usr/local/lib/python2.7/dist-packages/atomic-* + apt-get install -y make git python-selinux go-md2man python-pip python-dbus python-rpm pylint + ln /usr/local/bin/pylint /usr/bin/pylint + fi + + # Install all the requirements (usually done by make all) + pip install -r requirements.txt + + # Ignore any PYLINT errors and make sure you're installing via Python 2 + PYLINT=true PYTHON=/usr/bin/python2 make install + + # Remove once completed + cd .. + rm -rf atomic-$RELEASE atomic.tar.gz +} + +case "$1" in + install) + install_atomic + ;; + *) + echo $"Usage: atomic.sh {install}" + exit 1 + +esac diff --git a/tests/functional/scripts/atomicapp.sh b/tests/functional/scripts/atomicapp.sh new file mode 100755 index 00000000..30b69eed --- /dev/null +++ b/tests/functional/scripts/atomicapp.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -ex + +LINK="https://github.com/projectatomic/atomicapp" +UPSTREAM="projectatomic/atomicapp" + +install_atomicapp() { + echo " + ########## + INSTALLING ATOMICAPP CLI + + This will install atomicapp to /bin + as well as build an atomicapp container named + atomicapp:build + ########## + " + git clone $LINK + cd atomicapp + + if [ "$1" ] + then + echo "USING PR: $1" + git fetch origin pull/$1/head:PR_$1 + git checkout PR_$1 + fi + + # Install + make install + + # Build docker container + docker pull centos:7 + docker build -t atomicapp:build . + + cd .. + rm -rf atomicapp +} + +case "$1" in + install) + install_atomicapp $2 + ;; + *) + echo $"Usage: atomicpp.sh {install}" + exit 1 +esac diff --git a/tests/functional/scripts/prepare.sh b/tests/functional/scripts/prepare.sh new file mode 100755 index 00000000..315bcfe3 --- /dev/null +++ b/tests/functional/scripts/prepare.sh @@ -0,0 +1,33 @@ +#!/bin/bash + + +do_redhat() { + echo "Prepare for Red Hat" + # hyperkube causes avc denials + setenforce 0 + # make sure wget is installed + yum install -y wget + # Make sure docker is installed/started + yum install -y docker + systemctl start docker + # Make sure kubernetes client is installed/started + yum install -y kubernetes-client +} + +do_debian() { + echo "Prepare for Debian" + +} + +case "$1" in + install) + if [ -f /etc/redhat-release ]; then + do_redhat + else + do_debian + fi + ;; + *) + echo $"Usage: prepare.sh {install}" + exit 1 +esac From 9d9e8a68a51b0f1c8e8eddc6b78d17cb08d46502 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Thu, 22 Sep 2016 15:09:38 +0530 Subject: [PATCH 19/21] Use atomic CLI for functional tests. --- tests/functional/base.py | 125 +++++-------------- tests/functional/test_docker_provider.py | 13 +- tests/functional/test_kubernetes_provider.py | 9 +- 3 files changed, 43 insertions(+), 104 deletions(-) diff --git a/tests/functional/base.py b/tests/functional/base.py index c48484de..f0012bc2 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -51,6 +51,7 @@ class BaseProviderTestSuite(unittest.TestCase): NULECULE_LIB_REPO = 'https://github.com/projectatomic/nulecule-library' NULECULE_LIB_PATH = os.path.join(os.path.dirname(__file__), 'nulecule-library') + PROVIDER = None @classmethod def setUpClass(cls): @@ -75,15 +76,38 @@ def restore_initial_state(self): def deploy(self, app_spec, answers): """ - Deploy to provider + Deploy app to Docker + + Args: + app_spec (str): image name or path to application + answers (dict): Answers data + + Returns: + Path of the deployed dir. """ - raise NotImplementedError + destination = tempfile.mkdtemp() + answers_path = self.get_tmp_answers_file(answers) + cmd = ( + 'atomic run {app_spec} -a {answers} --provider={provider} ' + '--destination={dest}').format( + app_spec=app_spec, + answers=answers_path, + provider=self.PROVIDER, + dest=destination) + subprocess.check_call(cmd, stdin=False, stderr=False, shell=True) + return destination - def undeploy(self, app_spec, answers): + def undeploy(self, app_spec, workdir): """ - Undeploy from provider + Undeploy app from Docker. + + Args: + app_spec (str): image name or path to application + workdir (str): Path to deployed application dir """ - raise NotImplementedError + cmd = 'atomic stop {app_spec} {workdir}'.format( + app_spec=app_spec, workdir=workdir) + subprocess.check_output(cmd, stdin=False, stderr=False, shell=True) def get_tmp_answers_file(self, answers): f = tempfile.NamedTemporaryFile(delete=False, suffix='.conf') @@ -115,11 +139,13 @@ def build_image(cls): subprocess.check_call( 'cd {app_dir}; docker build -t {image_name} .'.format( app_dir=app_dir, image_name=cls.image_name), + stdin=False, stderr=False, shell=True) @classmethod def remove_image(cls): subprocess.check_call('docker rmi {}'.format(cls.image_name), + stdin=False, stderr=False, shell=True) @@ -127,6 +153,7 @@ class DockerProviderTestSuite(BaseProviderTestSuite): """ Base test suite for Docker. """ + PROVIDER = 'docker' def tearDown(self): _containers = self._get_containers(all=True) @@ -137,35 +164,6 @@ def tearDown(self): print cmd subprocess.check_output(cmd) - def deploy(self, app_spec, answers): - """ - Deploy app to Docker - - Args: - app_spec (str): image name or path to application - answers (dict): Answers data - - Returns: - Path of the deployed dir. - """ - destination = tempfile.mkdtemp() - answers_path = self.get_tmp_answers_file(answers) - cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, - '--provider=docker', - '--destination=%s' % destination, app_spec] - subprocess.check_output(cmd) - return destination - - def undeploy(self, workdir): - """ - Undeploy app from Docker. - - Args: - workdir (str): Path to deployed application dir - """ - cmd = ['atomicapp', 'stop', workdir] - subprocess.check_output(cmd) - def assertContainerRunning(self, name): containers = self._get_containers() for _id, container in containers.items(): @@ -215,6 +213,7 @@ class KubernetesProviderTestSuite(BaseProviderTestSuite): """ Base test suite for Kubernetes. """ + PROVIDER = 'kubernetes' @classmethod def setUpClass(cls): @@ -234,35 +233,6 @@ def tearDown(self): kubernetes.clean() kubernetes.wait() - def deploy(self, app_spec, answers): - """ - Deploy app to kuberntes - - Args: - app_spec (str): image name or path to application - answers (dict): Answers data - - Returns: - Path of the deployed dir. - """ - destination = tempfile.mkdtemp() - answers_path = self.get_tmp_answers_file(answers) - cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, - '--provider=kubernetes', - '--destination=%s' % destination, app_spec] - output = subprocess.check_output(' '.join(cmd), shell=True) - print output - return destination - - def undeploy(self, workdir): - """ - Undeploy app from kubernetes. - - Args: - workdir (str): Path to deployed application dir - """ - subprocess.check_output('atomicapp stop %s' % workdir, shell=True) - def assertPod(self, name, exists=True, status=None, timeout=1): """ Assert a kubernetes pod, if it exists, what's its status. @@ -419,6 +389,7 @@ class OpenshiftProviderTestSuite(BaseProviderTestSuite): """ Base test suite for Openshift. """ + PROVIDER = 'openshift' @classmethod def setUpClass(cls): @@ -463,34 +434,6 @@ def tearDown(self): time.sleep(10) - def deploy(self, app_spec, answers): - """ - Deploy app to kuberntes - - Args: - app_spec (str): image name or path to application - answers (dict): Answers data - - Returns: - Path of the deployed dir. - """ - destination = tempfile.mkdtemp() - answers_path = self.get_tmp_answers_file(answers) - cmd = ['atomicapp', 'run', '--answers=%s' % answers_path, - '--provider=openshift', - '--destination=%s' % destination, app_spec] - subprocess.check_output(' '.join(cmd), shell=True) - return destination - - def undeploy(self, workdir): - """ - Undeploy app from kubernetes. - - Args: - workdir (str): Path to deployed application dir - """ - subprocess.check_output('atomicapp stop %s' % workdir, shell=True) - def assertPod(self, name, exists=True, status=None, timeout=1): """ Assert a kubernetes pod, if it exists, what's its status. diff --git a/tests/functional/test_docker_provider.py b/tests/functional/test_docker_provider.py index faec2ee2..72de2118 100644 --- a/tests/functional/test_docker_provider.py +++ b/tests/functional/test_docker_provider.py @@ -10,7 +10,7 @@ class TestWordpress(DockerProviderTestSuite): 'general': { 'namespace': 'default' }, - 'mariadb-centos7-atomicapp:mariadb-atomicapp': { + 'mariadb-atomicapp': { 'db_user': 'foo', 'db_pass': 'foo', 'db_name': 'foo' @@ -22,19 +22,14 @@ class TestWordpress(DockerProviderTestSuite): } } - def _run(self): + def test_wordpress_lifecycle(self): app_spec = self.image_name - return self.deploy(app_spec, self.answers) + workdir = self.deploy(app_spec, self.answers) - def test_wordpress_run(self): - self._run() self.assertContainerRunning('wordpress-atomicapp') self.assertContainerRunning('mariadb-atomicapp-app') - def test_wordpress_stop(self): - workdir = self._run() - - self.undeploy(workdir) + self.undeploy(self.image_name, workdir) self.assertContainerNotRunning('wordpress-atomicapp') self.assertContainerNotRunning('mariadb-atomicapp-app') diff --git a/tests/functional/test_kubernetes_provider.py b/tests/functional/test_kubernetes_provider.py index 2c2eb89f..ecd4ac62 100644 --- a/tests/functional/test_kubernetes_provider.py +++ b/tests/functional/test_kubernetes_provider.py @@ -27,12 +27,13 @@ def setUp(self): }) def _run(self): - app_spec = os.path.join( - self.nulecule_lib, 'wordpress-centos7-atomicapp') + app_spec = self.image_name return self.deploy(app_spec, self.answers) def test_wordpress_lifecycle(self): - workdir = self._run() + app_spec = os.path.join( + self.nulecule_lib, 'wordpress-centos7-atomicapp') + workdir = self.deploy(app_spec, self.answers) self.assertPod('wordpress', status='Running', timeout=360) self.assertPod('mariadb', status='Running', timeout=360) @@ -40,7 +41,7 @@ def test_wordpress_lifecycle(self): self.assertService('wordpress', timeout=360) self.assertService('mariadb', timeout=360) - self.undeploy(workdir) + self.undeploy(app_spec, workdir) self.assertPod('wordpress', exists=False, timeout=360) self.assertPod('mariadb', exists=False, timeout=360) From 5974a240c4a71b7ba9d1f46614f30e87089af2af Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 27 Sep 2016 14:10:11 +0530 Subject: [PATCH 20/21] Build atomic app base and app image to run functional tests on. --- Makefile | 3 + tests/functional/base.py | 64 ++++++++++++++------ tests/functional/test_docker_provider.py | 10 +-- tests/functional/test_kubernetes_provider.py | 9 +-- tests/functional/test_openshift_provider.py | 11 +--- 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index 5aab9a18..06fed434 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,9 @@ test: functional-test: pip install -qr requirements.txt pip install -qr test-requirements.txt + ./tests/functional/scripts/atomic.sh install + ./tests/functional/scripts/prepare.sh install + $(DOCKER) build -t atomicapp:build . $(PYTHON) -m pytest tests/functional/ -vv --cov atomicapp .PHONY: image diff --git a/tests/functional/base.py b/tests/functional/base.py index f0012bc2..ff6442e8 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -1,7 +1,8 @@ +import distutils.dir_util import os +import re import logging import logging.config -import re import time import anymarkup import datetime @@ -51,12 +52,13 @@ class BaseProviderTestSuite(unittest.TestCase): NULECULE_LIB_REPO = 'https://github.com/projectatomic/nulecule-library' NULECULE_LIB_PATH = os.path.join(os.path.dirname(__file__), 'nulecule-library') + BUILD_DIR = os.path.join(os.path.dirname(__file__), 'build') PROVIDER = None @classmethod def setUpClass(cls): cls.fetch_nulecule_lib() - cls.build_image() + cls.build() @classmethod def tearDownClass(cls): @@ -85,8 +87,11 @@ def deploy(self, app_spec, answers): Returns: Path of the deployed dir. """ - destination = tempfile.mkdtemp() - answers_path = self.get_tmp_answers_file(answers) + destination = self.BUILD_DIR + answers_path = os.path.join(self.BUILD_DIR, 'answers.conf') + anymarkup.serialize_file(answers, + answers_path, + format='ini') cmd = ( 'atomic run {app_spec} -a {answers} --provider={provider} ' '--destination={dest}').format( @@ -132,13 +137,29 @@ def fetch_nulecule_lib(cls): 'git pull origin master', shell=True) @classmethod - def build_image(cls): + def build(cls): app_dir = os.path.join(cls.NULECULE_LIB_PATH, cls.APP_DIR_NAME) + + build_dir = cls.BUILD_DIR + + try: + os.rmdir(build_dir) + except: + pass + + distutils.dir_util.copy_tree(app_dir, build_dir) + + with open(os.path.join(build_dir, 'Dockerfile')) as f: + s = f.read() + + with open(os.path.join(build_dir, 'Dockerfile'), 'w') as f: + f.write(re.sub('FROM.*', 'FROM atomicapp:build', s)) + cls.image_name = '{}-{}'.format( cls.APP_DIR_NAME, uuid.uuid1().hex[:8]) subprocess.check_call( - 'cd {app_dir}; docker build -t {image_name} .'.format( - app_dir=app_dir, image_name=cls.image_name), + 'docker build -t {image_name} {path}'.format( + image_name=cls.image_name, path=build_dir), stdin=False, stderr=False, shell=True) @@ -164,18 +185,25 @@ def tearDown(self): print cmd subprocess.check_output(cmd) - def assertContainerRunning(self, name): - containers = self._get_containers() - for _id, container in containers.items(): - if container['names'] == name: - return True + def assertContainerRunning(self, name, timeout=1): + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + containers = self._get_containers() + for _id, container in containers.items(): + if container['names'] == name: + return True raise AssertionError('Container: %s not running.' % name) - def assertContainerNotRunning(self, name): - containers = self._get_containers() - for _id, container in containers.items(): - if container.get('name') == name: - raise AssertionError('Container: %s is running' % name) + def assertContainerNotRunning(self, name, timeout=1): + start = datetime.datetime.now() + while (datetime.datetime.now() - start).total_seconds() <= timeout: + container_running = False + containers = self._get_containers() + for _id, container in containers.items(): + if container.get('name') == name: + container_running = True + if container_running: + raise AssertionError('Container: %s is running' % name) return True def get_initial_state(self): @@ -217,6 +245,7 @@ class KubernetesProviderTestSuite(BaseProviderTestSuite): @classmethod def setUpClass(cls): + super(KubernetesProviderTestSuite, cls).setUpClass() logger.debug('setUpClass...') logger.debug('Stopping existing kubernetes instance, if any...') kubernetes.stop() @@ -393,6 +422,7 @@ class OpenshiftProviderTestSuite(BaseProviderTestSuite): @classmethod def setUpClass(cls): + super(OpenshiftProviderTestSuite, cls).setUpClass() openshift.stop() openshift.start() cls.answers = anymarkup.parse(openshift.answers(), 'ini') diff --git a/tests/functional/test_docker_provider.py b/tests/functional/test_docker_provider.py index 72de2118..4fbbda22 100644 --- a/tests/functional/test_docker_provider.py +++ b/tests/functional/test_docker_provider.py @@ -10,7 +10,7 @@ class TestWordpress(DockerProviderTestSuite): 'general': { 'namespace': 'default' }, - 'mariadb-atomicapp': { + 'mariadb-centos7-atomicapp:mariadb-atomicapp': { 'db_user': 'foo', 'db_pass': 'foo', 'db_name': 'foo' @@ -26,10 +26,10 @@ def test_wordpress_lifecycle(self): app_spec = self.image_name workdir = self.deploy(app_spec, self.answers) - self.assertContainerRunning('wordpress-atomicapp') - self.assertContainerRunning('mariadb-atomicapp-app') + self.assertContainerRunning('wordpress-atomicapp', timeout=10) + self.assertContainerRunning('mariadb-atomicapp-app', timeout=10) self.undeploy(self.image_name, workdir) - self.assertContainerNotRunning('wordpress-atomicapp') - self.assertContainerNotRunning('mariadb-atomicapp-app') + self.assertContainerNotRunning('wordpress-atomicapp', timeout=10) + self.assertContainerNotRunning('mariadb-atomicapp-app', timeout=10) diff --git a/tests/functional/test_kubernetes_provider.py b/tests/functional/test_kubernetes_provider.py index ecd4ac62..8a1c1e03 100644 --- a/tests/functional/test_kubernetes_provider.py +++ b/tests/functional/test_kubernetes_provider.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -import os - from .base import KubernetesProviderTestSuite @@ -26,13 +24,8 @@ def setUp(self): } }) - def _run(self): - app_spec = self.image_name - return self.deploy(app_spec, self.answers) - def test_wordpress_lifecycle(self): - app_spec = os.path.join( - self.nulecule_lib, 'wordpress-centos7-atomicapp') + app_spec = self.image_name workdir = self.deploy(app_spec, self.answers) self.assertPod('wordpress', status='Running', timeout=360) diff --git a/tests/functional/test_openshift_provider.py b/tests/functional/test_openshift_provider.py index ed413af5..ce912903 100644 --- a/tests/functional/test_openshift_provider.py +++ b/tests/functional/test_openshift_provider.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import logging -import os from .base import OpenshiftProviderTestSuite @@ -29,13 +28,9 @@ def setUp(self): } }) - def _run(self): - app_spec = os.path.join( - self.nulecule_lib, 'wordpress-centos7-atomicapp') - return self.deploy(app_spec, self.answers) - def test_wordpress_lifecycle(self): - workdir = self._run() + app_spec = self.image_name + workdir = self.deploy(app_spec, self.answers) self.assertPod('wordpress', status='ContainerCreating', timeout=120) self.assertPod('mariadb', status='Running', timeout=120) @@ -43,7 +38,7 @@ def test_wordpress_lifecycle(self): self.assertService('wpfrontend', timeout=120) self.assertService('mariadb', timeout=120) - self.undeploy(workdir) + self.undeploy(app_spec, workdir) self.assertPod('wordpress', exists=False, timeout=120) self.assertPod('mariadb', exists=False, timeout=120) From 71ef41e0010029487b4725e68a6d43de8cd7bae4 Mon Sep 17 00:00:00 2001 From: Ratnadeep Debnath Date: Tue, 27 Sep 2016 15:01:16 +0530 Subject: [PATCH 21/21] Separate fetching answers data from openshift and creating project in functional tests. --- tests/functional/base.py | 1 + tests/functional/providers/openshift.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/functional/base.py b/tests/functional/base.py index ff6442e8..37cb8b9c 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -426,6 +426,7 @@ def setUpClass(cls): openshift.stop() openshift.start() cls.answers = anymarkup.parse(openshift.answers(), 'ini') + openshift.create_project('foo') openshift.wait() @classmethod diff --git a/tests/functional/providers/openshift.py b/tests/functional/providers/openshift.py index b9e59b8e..691e296b 100644 --- a/tests/functional/providers/openshift.py +++ b/tests/functional/providers/openshift.py @@ -64,9 +64,6 @@ def answers(): shell=True) time.sleep(3) - subprocess.check_call( - 'docker exec -i origin oc new-project foo', shell=True) - time.sleep(3) answers = """ [general] @@ -79,6 +76,12 @@ def answers(): return answers +def create_project(name): + subprocess.check_call( + 'docker exec -i origin oc new-project {}'.format(name), shell=True) + time.sleep(3) + + def stop(): try: subprocess.check_output('docker rm -f origin', shell=True)