diff options
author | Kenny Woodson <kwoodson@redhat.com> | 2017-02-09 14:08:20 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-09 14:08:20 -0500 |
commit | 32f8df71cd17280d5731d42080d038bd1ecfacd8 (patch) | |
tree | 2a923d131fb981eb75cd288eabbf162a64bef5ce /roles/lib_openshift/src | |
parent | 8b6f88ea657662fb6724c2d4863b6c954f9580d8 (diff) | |
parent | aaeba50e584426fc8f9853dd7128c861b1efe8de (diff) | |
download | openshift-32f8df71cd17280d5731d42080d038bd1ecfacd8.tar.gz openshift-32f8df71cd17280d5731d42080d038bd1ecfacd8.tar.bz2 openshift-32f8df71cd17280d5731d42080d038bd1ecfacd8.tar.xz openshift-32f8df71cd17280d5731d42080d038bd1ecfacd8.zip |
Merge pull request #3250 from kwoodson/oc_process
Adding oc_process to lib_openshift.
Diffstat (limited to 'roles/lib_openshift/src')
-rw-r--r-- | roles/lib_openshift/src/ansible/oc_process.py | 32 | ||||
-rw-r--r-- | roles/lib_openshift/src/class/oc_process.py | 188 | ||||
-rw-r--r-- | roles/lib_openshift/src/doc/process | 84 | ||||
-rw-r--r-- | roles/lib_openshift/src/sources.yml | 10 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/integration/oc_process.yml | 83 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/unit/oc_process.py | 483 |
6 files changed, 880 insertions, 0 deletions
diff --git a/roles/lib_openshift/src/ansible/oc_process.py b/roles/lib_openshift/src/ansible/oc_process.py new file mode 100644 index 000000000..17cf865b7 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_process.py @@ -0,0 +1,32 @@ +# pylint: skip-file +# flake8: noqa + + +def main(): + ''' + ansible oc module for processing templates + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', choices=['present', 'list']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + template_name=dict(default=None, type='str'), + content=dict(default=None, type='str'), + params=dict(default=None, type='dict'), + create=dict(default=False, type='bool'), + reconcile=dict(default=True, type='bool'), + ), + supports_check_mode=True, + ) + + rval = OCProcess.run_ansible(module.params, module.check_mode) + if 'failed' in rval: + module.fail_json(**rval) + + module.exit_json(**rval) + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/class/oc_process.py b/roles/lib_openshift/src/class/oc_process.py new file mode 100644 index 000000000..80d81448d --- /dev/null +++ b/roles/lib_openshift/src/class/oc_process.py @@ -0,0 +1,188 @@ +# pylint: skip-file +# flake8: noqa + + +# pylint: disable=too-many-instance-attributes +class OCProcess(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + + # pylint allows 5. we need 6 + # pylint: disable=too-many-arguments + def __init__(self, + namespace, + tname=None, + params=None, + create=False, + kubeconfig='/etc/origin/master/admin.kubeconfig', + tdata=None, + verbose=False): + ''' Constructor for OpenshiftOC ''' + super(OCProcess, self).__init__(namespace, kubeconfig) + self.namespace = namespace + self.name = tname + self.data = tdata + self.params = params + self.create = create + self.kubeconfig = kubeconfig + self.verbose = verbose + self._template = None + + @property + def template(self): + '''template property''' + if self._template is None: + results = self._process(self.name, False, self.params, self.data) + if results['returncode'] != 0: + raise OpenShiftCLIError('Error processing template [%s].' % self.name) + self._template = results['results']['items'] + + return self._template + + def get(self): + '''get the template''' + results = self._get('template', self.name) + if results['returncode'] != 0: + # Does the template exist?? + if 'not found' in results['stderr']: + results['returncode'] = 0 + results['exists'] = False + results['results'] = [] + + return results + + def delete(self, obj): + '''delete a resource''' + return self._delete(obj['kind'], obj['metadata']['name']) + + def create_obj(self, obj): + '''create a resource''' + return self._create_from_content(obj['metadata']['name'], obj) + + def process(self, create=None): + '''process a template''' + do_create = False + if create != None: + do_create = create + else: + do_create = self.create + + return self._process(self.name, do_create, self.params, self.data) + + def exists(self): + '''return whether the template exists''' + # Always return true if we're being passed template data + if self.data: + return True + t_results = self._get('template', self.name) + + if t_results['returncode'] != 0: + # Does the template exist?? + if 'not found' in t_results['stderr']: + return False + else: + raise OpenShiftCLIError('Something went wrong. %s' % t_results) + + return True + + def needs_update(self): + '''attempt to process the template and return it for comparison with oc objects''' + obj_results = [] + for obj in self.template: + + # build a list of types to skip + skip = [] + + if obj['kind'] == 'ServiceAccount': + skip.extend(['secrets', 'imagePullSecrets']) + if obj['kind'] == 'BuildConfig': + skip.extend(['lastTriggeredImageID']) + if obj['kind'] == 'ImageStream': + skip.extend(['generation']) + if obj['kind'] == 'DeploymentConfig': + skip.extend(['lastTriggeredImage']) + + # fetch the current object + curr_obj_results = self._get(obj['kind'], obj['metadata']['name']) + if curr_obj_results['returncode'] != 0: + # Does the template exist?? + if 'not found' in curr_obj_results['stderr']: + obj_results.append((obj, True)) + continue + + # check the generated object against the existing object + if not Utils.check_def_equal(obj, curr_obj_results['results'][0], skip_keys=skip): + obj_results.append((obj, True)) + continue + + obj_results.append((obj, False)) + + return obj_results + + # pylint: disable=too-many-return-statements + @staticmethod + def run_ansible(params, check_mode): + '''run the ansible idempotent code''' + + ocprocess = OCProcess(params['namespace'], + params['template_name'], + params['params'], + params['create'], + kubeconfig=params['kubeconfig'], + tdata=params['content'], + verbose=params['debug']) + + state = params['state'] + + api_rval = ocprocess.get() + + if state == 'list': + if api_rval['returncode'] != 0: + return {"failed": True, "msg" : api_rval} + + return {"changed" : False, "results": api_rval, "state": "list"} + + elif state == 'present': + if check_mode and params['create']: + return {"changed": True, 'msg': "CHECK_MODE: Would have processed template."} + + if not ocprocess.exists() or not params['reconcile']: + #FIXME: this code will never get run in a way that succeeds when + # module.params['reconcile'] is true. Because oc_process doesn't + # create the actual template, the check of ocprocess.exists() + # is meaningless. Either it's already here and this code + # won't be run, or this code will fail because there is no + # template available for oc process to use. Have we conflated + # the template's existence with the existence of the objects + # it describes? + + # Create it here + api_rval = ocprocess.process() + if api_rval['returncode'] != 0: + return {"failed": True, "msg": api_rval} + + if params['create']: + return {"changed": True, "results": api_rval, "state": "present"} + + return {"changed": False, "results": api_rval, "state": "present"} + + # verify results + update = False + rval = [] + all_results = ocprocess.needs_update() + for obj, status in all_results: + if status: + ocprocess.delete(obj) + results = ocprocess.create_obj(obj) + results['kind'] = obj['kind'] + rval.append(results) + update = True + + if not update: + return {"changed": update, "results": api_rval, "state": "present"} + + for cmd in rval: + if cmd['returncode'] != 0: + return {"failed": True, "changed": update, "results": rval, "state": "present"} + + return {"changed": update, "results": rval, "state": "present"} + diff --git a/roles/lib_openshift/src/doc/process b/roles/lib_openshift/src/doc/process new file mode 100644 index 000000000..86a854c07 --- /dev/null +++ b/roles/lib_openshift/src/doc/process @@ -0,0 +1,84 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_process +short_description: Module to process openshift templates +description: + - Process openshift templates programmatically. +options: + state: + description: + - State has a few different meanings when it comes to process. + - state: present - This state runs an `oc process <template>`. When used in + - conjunction with 'create: True' the process will be piped to | oc create -f + - state: absent - will remove a template + - state: list - will perform an `oc get template <template_name>` + default: present + choices: ["present", "absent", "list"] + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + template_name: + description: + - Name of the openshift template that is being processed. + required: false + default: None + aliases: [] + namespace: + description: + - The namespace where the template lives. + required: false + default: default + aliases: [] + content: + description: + - Template content that will be processed. + required: false + default: None + aliases: [] + params: + description: + - A list of parameters that will be inserted into the template. + required: false + default: None + aliases: [] + create: + description: + - Whether or not to create the template after being processed. e.g. oc process | oc create -f - + required: False + default: False + aliases: [] + reconcile: + description: + - Whether or not to attempt to determine if there are updates or changes in the incoming template. + default: true + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: process the cloud volume provisioner template with variables + oc_process: + namespace: openshift-infra + template_name: online-volume-provisioner + create: True + params: + PLAT: rhel7 + register: processout + run_once: true +- debug: var=processout +''' diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index aa02ce120..e9056655d 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -39,6 +39,16 @@ oc_obj.py: - class/oc_obj.py - ansible/oc_obj.py +oc_process.py: +- doc/generated +- doc/license +- lib/import.py +- doc/process +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- class/oc_process.py +- ansible/oc_process.py + oc_route.py: - doc/generated - doc/license diff --git a/roles/lib_openshift/src/test/integration/oc_process.yml b/roles/lib_openshift/src/test/integration/oc_process.yml new file mode 100755 index 000000000..7ea4c6b99 --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_process.yml @@ -0,0 +1,83 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/:../../../../lib_utils/library + +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + vars: + template_name: mysql-ephemeral + ns_name: test + + post_tasks: + - name: get the mysql-ephemeral template + oc_obj: + name: mysql-ephemeral + state: list + namespace: openshift + kind: template + register: mysqltempl + + - name: fix namespace + yedit: + src: /tmp/mysql-template + key: metadata.namespace + value: test + backup: false + content: "{{ mysqltempl.results.results[0] | to_yaml }}" + + - name: create the test namespace + oc_obj: + name: test + state: present + namespace: test + kind: namespace + content: + path: /tmp/ns_test + data: + apiVersion: v1 + kind: Namespace + metadata: + name: test + spec: + finalizers: + - openshift.io/origin + - kubernetes + register: mysqltempl + + - name: create the mysql-ephemeral template + oc_obj: + name: mysql-ephemeral + state: present + namespace: test + kind: template + files: + - /tmp/mysql-template + delete_after: True + register: mysqltempl + + - name: process mysql-ephemeral + oc_process: + template_name: mysql-ephemeral + namespace: test + params: + NAMESPACE: test + DATABASE_SERVICE_NAME: testdb + create: False + reconcile: false + register: procout + + - assert: + that: + - not procout.changed + - procout.results.results['items'][0]['metadata']['name'] == 'testdb' + - procout.results.results['items'][0]['kind'] == 'Service' + - procout.results.results['items'][1]['metadata']['name'] == 'testdb' + - procout.results.results['items'][1]['kind'] == 'DeploymentConfig' + msg: process failed on template + + - name: remove namespace test + oc_obj: + kind: namespace + name: test + namespace: test + state: absent diff --git a/roles/lib_openshift/src/test/unit/oc_process.py b/roles/lib_openshift/src/test/unit/oc_process.py new file mode 100755 index 000000000..450ff7071 --- /dev/null +++ b/roles/lib_openshift/src/test/unit/oc_process.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python2 +''' + Unit tests for oc process +''' +# To run +# python -m unittest version +# +# . +# Ran 1 test in 0.597s +# +# OK + +import os +import sys +import unittest +import mock + +# Removing invalid variable names for tests so that I can +# keep them brief +# pylint: disable=invalid-name,no-name-in-module +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 +sys.path.insert(0, module_path) +from oc_process import OCProcess # noqa: E402 + + +# pylint: disable=too-many-public-methods +class OCProcessTest(unittest.TestCase): + ''' + Test class for OCProcess + ''' + mysql = '''{ + "kind": "Template", + "apiVersion": "v1", + "metadata": { + "name": "mysql-ephemeral", + "namespace": "openshift", + "selfLink": "/oapi/v1/namespaces/openshift/templates/mysql-ephemeral", + "uid": "fb8b5f04-e3d3-11e6-a982-0e84250fc302", + "resourceVersion": "480", + "creationTimestamp": "2017-01-26T14:30:27Z", + "annotations": { + "iconClass": "icon-mysql-database", + "openshift.io/display-name": "MySQL (Ephemeral)", + "tags": "database,mysql" + } + }, + "objects": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "creationTimestamp": null, + "name": "${DATABASE_SERVICE_NAME}" + }, + "spec": { + "ports": [ + { + "name": "mysql", + "nodePort": 0, + "port": 3306, + "protocol": "TCP", + "targetPort": 3306 + } + ], + "selector": { + "name": "${DATABASE_SERVICE_NAME}" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "DeploymentConfig", + "metadata": { + "creationTimestamp": null, + "name": "${DATABASE_SERVICE_NAME}" + }, + "spec": { + "replicas": 1, + "selector": { + "name": "${DATABASE_SERVICE_NAME}" + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "name": "${DATABASE_SERVICE_NAME}" + } + }, + "spec": { + "containers": [ + { + "capabilities": {}, + "env": [ + { + "name": "MYSQL_USER", + "value": "${MYSQL_USER}" + }, + { + "name": "MYSQL_PASSWORD", + "value": "${MYSQL_PASSWORD}" + }, + { + "name": "MYSQL_DATABASE", + "value": "${MYSQL_DATABASE}" + } + ], + "image": " ", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "initialDelaySeconds": 30, + "tcpSocket": { + "port": 3306 + }, + "timeoutSeconds": 1 + }, + "name": "mysql", + "ports": [ + { + "containerPort": 3306, + "protocol": "TCP" + } + ], + "readinessProbe": { + "exec": { + "command": [ + "/bin/sh", + "-i", + "-c", + "MYSQL_PWD=$MYSQL_PASSWORD mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'" + ] + }, + "initialDelaySeconds": 5, + "timeoutSeconds": 1 + }, + "resources": { + "limits": { + "memory": "${MEMORY_LIMIT}" + } + }, + "securityContext": { + "capabilities": {}, + "privileged": false + }, + "terminationMessagePath": "/dev/termination-log", + "volumeMounts": [ + { + "mountPath": "/var/lib/mysql/data", + "name": "${DATABASE_SERVICE_NAME}-data" + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "volumes": [ + { + "emptyDir": { + "medium": "" + }, + "name": "${DATABASE_SERVICE_NAME}-data" + } + ] + } + }, + "triggers": [ + { + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "mysql" + ], + "from": { + "kind": "ImageStreamTag", + "name": "mysql:${MYSQL_VERSION}", + "namespace": "${NAMESPACE}" + }, + "lastTriggeredImage": "" + }, + "type": "ImageChange" + }, + { + "type": "ConfigChange" + } + ] + }, + "status": {} + } + ], + "parameters": [ + { + "name": "MEMORY_LIMIT", + "displayName": "Memory Limit", + "description": "Maximum amount of memory the container can use.", + "value": "512Mi" + }, + { + "name": "NAMESPACE", + "displayName": "Namespace", + "description": "The OpenShift Namespace where the ImageStream resides.", + "value": "openshift" + }, + { + "name": "DATABASE_SERVICE_NAME", + "displayName": "Database Service Name", + "description": "The name of the OpenShift Service exposed for the database.", + "value": "mysql", + "required": true + }, + { + "name": "MYSQL_USER", + "displayName": "MySQL Connection Username", + "description": "Username for MySQL user that will be used for accessing the database.", + "generate": "expression", + "from": "user[A-Z0-9]{3}", + "required": true + }, + { + "name": "MYSQL_PASSWORD", + "displayName": "MySQL Connection Password", + "description": "Password for the MySQL connection user.", + "generate": "expression", + "from": "[a-zA-Z0-9]{16}", + "required": true + }, + { + "name": "MYSQL_DATABASE", + "displayName": "MySQL Database Name", + "description": "Name of the MySQL database accessed.", + "value": "sampledb", + "required": true + }, + { + "name": "MYSQL_VERSION", + "displayName": "Version of MySQL Image", + "description": "Version of MySQL image to be used (5.5, 5.6 or latest).", + "value": "5.6", + "required": true + } + ], + "labels": { + "template": "mysql-ephemeral-template" + } +}''' + + def setUp(self): + ''' setup method will set to known configuration ''' + pass + + @mock.patch('oc_process.Utils.create_tmpfile_copy') + @mock.patch('oc_process.OCProcess._run') + def test_state_list(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a get ''' + params = {'template_name': 'mysql-ephermeral', + 'namespace': 'test', + 'content': None, + 'state': 'list', + 'reconcile': False, + 'create': False, + 'params': {'NAMESPACE': 'test', 'DATABASE_SERVICE_NAME': 'testdb'}, + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'debug': False} + + mock_cmd.side_effect = [ + (0, OCProcessTest.mysql, '') + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mock_kubeconfig', + ] + + results = OCProcess.run_ansible(params, False) + + self.assertFalse(results['changed']) + self.assertEqual(results['results']['results'][0]['metadata']['name'], 'mysql-ephemeral') + + @mock.patch('oc_process.Utils.create_tmpfile_copy') + @mock.patch('oc_process.OCProcess._run') + def test_process_no_create(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a process with no create ''' + params = {'template_name': 'mysql-ephermeral', + 'namespace': 'test', + 'content': None, + 'state': 'present', + 'reconcile': False, + 'create': False, + 'params': {'NAMESPACE': 'test', 'DATABASE_SERVICE_NAME': 'testdb'}, + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'debug': False} + + mysqlproc = '''{ + "kind": "List", + "apiVersion": "v1", + "metadata": {}, + "items": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "creationTimestamp": null, + "labels": { + "template": "mysql-ephemeral-template" + }, + "name": "testdb" + }, + "spec": { + "ports": [ + { + "name": "mysql", + "nodePort": 0, + "port": 3306, + "protocol": "TCP", + "targetPort": 3306 + } + ], + "selector": { + "name": "testdb" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "DeploymentConfig", + "metadata": { + "creationTimestamp": null, + "labels": { + "template": "mysql-ephemeral-template" + }, + "name": "testdb" + }, + "spec": { + "replicas": 1, + "selector": { + "name": "testdb" + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "name": "testdb" + } + }, + "spec": { + "containers": [ + { + "capabilities": {}, + "env": [ + { + "name": "MYSQL_USER", + "value": "userHJJ" + }, + { + "name": "MYSQL_PASSWORD", + "value": "GITOAduAMaV6k688" + }, + { + "name": "MYSQL_DATABASE", + "value": "sampledb" + } + ], + "image": " ", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "initialDelaySeconds": 30, + "tcpSocket": { + "port": 3306 + }, + "timeoutSeconds": 1 + }, + "name": "mysql", + "ports": [ + { + "containerPort": 3306, + "protocol": "TCP" + } + ], + "readinessProbe": { + "exec": { + "command": [ + "/bin/sh", + "-i", + "-c", + "MYSQL_PWD=$MYSQL_PASSWORD mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'" + ] + }, + "initialDelaySeconds": 5, + "timeoutSeconds": 1 + }, + "resources": { + "limits": { + "memory": "512Mi" + } + }, + "securityContext": { + "capabilities": {}, + "privileged": false + }, + "terminationMessagePath": "/dev/termination-log", + "volumeMounts": [ + { + "mountPath": "/var/lib/mysql/data", + "name": "testdb-data" + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "volumes": [ + { + "emptyDir": { + "medium": "" + }, + "name": "testdb-data" + } + ] + } + }, + "triggers": [ + { + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "mysql" + ], + "from": { + "kind": "ImageStreamTag", + "name": "mysql:5.6", + "namespace": "test" + }, + "lastTriggeredImage": "" + }, + "type": "ImageChange" + }, + { + "type": "ConfigChange" + } + ] + } + } + ] +}''' + + mock_cmd.side_effect = [ + (0, OCProcessTest.mysql, ''), + (0, OCProcessTest.mysql, ''), + (0, mysqlproc, ''), + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mock_kubeconfig', + ] + + results = OCProcess.run_ansible(params, False) + + self.assertFalse(results['changed']) + self.assertEqual(results['results']['results']['items'][0]['metadata']['name'], 'testdb') + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() |