diff options
Diffstat (limited to 'roles/lib_openshift/src')
-rw-r--r-- | roles/lib_openshift/src/ansible/oc_serviceaccount.py | 30 | ||||
-rw-r--r-- | roles/lib_openshift/src/class/oc_serviceaccount.py | 165 | ||||
-rw-r--r-- | roles/lib_openshift/src/doc/serviceaccount | 68 | ||||
-rw-r--r-- | roles/lib_openshift/src/lib/base.py | 23 | ||||
-rw-r--r-- | roles/lib_openshift/src/lib/serviceaccount.py | 129 | ||||
-rw-r--r-- | roles/lib_openshift/src/sources.yml | 10 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/integration/oc_serviceaccount.yml | 101 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/unit/oc_serviceaccount.py | 114 |
8 files changed, 632 insertions, 8 deletions
diff --git a/roles/lib_openshift/src/ansible/oc_serviceaccount.py b/roles/lib_openshift/src/ansible/oc_serviceaccount.py new file mode 100644 index 000000000..ea9bdb455 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_serviceaccount.py @@ -0,0 +1,30 @@ +# pylint: skip-file +# flake8: noqa + +def main(): + ''' + ansible oc module for route + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', + choices=['present', 'absent', 'list']), + debug=dict(default=False, type='bool'), + name=dict(default=None, required=True, type='str'), + namespace=dict(default=None, required=True, type='str'), + secrets=dict(default=None, type='list'), + image_pull_secrets=dict(default=None, type='list'), + ), + supports_check_mode=True, + ) + + rval = OCServiceAccount.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_serviceaccount.py b/roles/lib_openshift/src/class/oc_serviceaccount.py new file mode 100644 index 000000000..47c7b5c94 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_serviceaccount.py @@ -0,0 +1,165 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: disable=too-many-instance-attributes +class OCServiceAccount(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + kind = 'sa' + + # pylint allows 5 + # pylint: disable=too-many-arguments + def __init__(self, + config, + verbose=False): + ''' Constructor for OCVolume ''' + super(OCServiceAccount, self).__init__(config.namespace, config.kubeconfig) + self.config = config + self.namespace = config.namespace + self.service_account = None + + def exists(self): + ''' return whether a volume exists ''' + if self.service_account: + return True + + return False + + def get(self): + '''return volume information ''' + result = self._get(self.kind, self.config.name) + if result['returncode'] == 0: + self.service_account = ServiceAccount(content=result['results'][0]) + elif '\"%s\" not found' % self.config.name in result['stderr']: + result['returncode'] = 0 + result['results'] = [{}] + + return result + + def delete(self): + '''delete the object''' + return self._delete(self.kind, self.config.name) + + def create(self): + '''create the object''' + return self._create_from_content(self.config.name, self.config.data) + + def update(self): + '''update the object''' + # need to update the tls information and the service name + for secret in self.config.secrets: + result = self.service_account.find_secret(secret) + if not result: + self.service_account.add_secret(secret) + + for secret in self.config.image_pull_secrets: + result = self.service_account.find_image_pull_secret(secret) + if not result: + self.service_account.add_image_pull_secret(secret) + + return self._replace_content(self.kind, self.config.name, self.config.data) + + def needs_update(self): + ''' verify an update is needed ''' + # since creating an service account generates secrets and imagepullsecrets + # check_def_equal will not work + # Instead, verify all secrets passed are in the list + for secret in self.config.secrets: + result = self.service_account.find_secret(secret) + if not result: + return True + + for secret in self.config.image_pull_secrets: + result = self.service_account.find_image_pull_secret(secret) + if not result: + return True + + return False + + @staticmethod + # pylint: disable=too-many-return-statements,too-many-branches + # TODO: This function should be refactored into its individual parts. + def run_ansible(params, check_mode): + '''run the ansible idempotent code''' + + rconfig = ServiceAccountConfig(params['name'], + params['namespace'], + params['kubeconfig'], + params['secrets'], + params['image_pull_secrets'], + ) + + oc_sa = OCServiceAccount(rconfig, + verbose=params['debug']) + + state = params['state'] + + api_rval = oc_sa.get() + + ##### + # Get + ##### + if state == 'list': + return {'changed': False, 'results': api_rval['results'], 'state': 'list'} + + ######## + # Delete + ######## + if state == 'absent': + if oc_sa.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a delete.'} + + api_rval = oc_sa.delete() + + return {'changed': True, 'results': api_rval, 'state': 'absent'} + + return {'changed': False, 'state': 'absent'} + + if state == 'present': + ######## + # Create + ######## + if not oc_sa.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a create.'} + + # Create it here + api_rval = oc_sa.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sa.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + ######## + # Update + ######## + if oc_sa.needs_update(): + api_rval = oc_sa.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sa.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + return {'changed': False, 'results': api_rval, 'state': 'present'} + + + return {'failed': True, + 'changed': False, + 'msg': 'Unknown state passed. %s' % state, + 'state': 'unknown'} diff --git a/roles/lib_openshift/src/doc/serviceaccount b/roles/lib_openshift/src/doc/serviceaccount new file mode 100644 index 000000000..b2eafab51 --- /dev/null +++ b/roles/lib_openshift/src/doc/serviceaccount @@ -0,0 +1,68 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_serviceaccount +short_description: Module to manage openshift service accounts +description: + - Manage openshift service accounts programmatically. +options: + state: + description: + - If present, the service account will be created if it doesn't exist or updated if different. If absent, the service account will be removed if present. If list, information about the service account will be gathered and returned as part of the Ansible call results. + required: false + 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: [] + name: + description: + - Name of the service account. + required: true + default: None + aliases: [] + namespace: + description: + - Namespace of the service account. + required: true + default: default + aliases: [] + secrets: + description: + - A list of secrets that are associated with the service account. + required: false + default: None + aliases: [] + image_pull_secrets: + description: + - A list of the image pull secrets that are associated with the service account. + required: false + default: None + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: create registry serviceaccount + oc_serviceaccount: + name: registry + namespace: default + secrets: + - docker-registry-config + - registry-secret + register: sa_out +''' diff --git a/roles/lib_openshift/src/lib/base.py b/roles/lib_openshift/src/lib/base.py index 2656f572e..d0d6c7afc 100644 --- a/roles/lib_openshift/src/lib/base.py +++ b/roles/lib_openshift/src/lib/base.py @@ -205,6 +205,18 @@ class OpenShiftCLI(object): cmd.append('--confirm') return self.openshift_cmd(cmd) + def _run(self, cmds, input_data): + ''' Actually executes the command. This makes mocking easier. ''' + proc = subprocess.Popen(cmds, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={'KUBECONFIG': self.kubeconfig}) + + stdout, stderr = proc.communicate(input_data) + + return proc.returncode, stdout, stderr + # pylint: disable=too-many-arguments,too-many-branches def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None): '''Base command for oc ''' @@ -228,18 +240,13 @@ class OpenShiftCLI(object): if self.verbose: print(' '.join(cmds)) - proc = subprocess.Popen(cmds, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env={'KUBECONFIG': self.kubeconfig}) + returncode, stdout, stderr = self._run(cmds, input_data) - stdout, stderr = proc.communicate(input_data) - rval = {"returncode": proc.returncode, + rval = {"returncode": returncode, "results": results, "cmd": ' '.join(cmds)} - if proc.returncode == 0: + if returncode == 0: if output: if output_type == 'json': try: diff --git a/roles/lib_openshift/src/lib/serviceaccount.py b/roles/lib_openshift/src/lib/serviceaccount.py new file mode 100644 index 000000000..47a55757e --- /dev/null +++ b/roles/lib_openshift/src/lib/serviceaccount.py @@ -0,0 +1,129 @@ +# pylint: skip-file +# flake8: noqa + +class ServiceAccountConfig(object): + '''Service account config class + + This class stores the options and returns a default service account + ''' + + # pylint: disable=too-many-arguments + def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None): + self.name = sname + self.kubeconfig = kubeconfig + self.namespace = namespace + self.secrets = secrets or [] + self.image_pull_secrets = image_pull_secrets or [] + self.data = {} + self.create_dict() + + def create_dict(self): + ''' return a properly structured volume ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'ServiceAccount' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + + self.data['secrets'] = [] + if self.secrets: + for sec in self.secrets: + self.data['secrets'].append({"name": sec}) + + self.data['imagePullSecrets'] = [] + if self.image_pull_secrets: + for sec in self.image_pull_secrets: + self.data['imagePullSecrets'].append({"name": sec}) + +class ServiceAccount(Yedit): + ''' Class to wrap the oc command line tools ''' + image_pull_secrets_path = "imagePullSecrets" + secrets_path = "secrets" + + def __init__(self, content): + '''ServiceAccount constructor''' + super(ServiceAccount, self).__init__(content=content) + self._secrets = None + self._image_pull_secrets = None + + @property + def image_pull_secrets(self): + ''' property for image_pull_secrets ''' + if self._image_pull_secrets is None: + self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or [] + return self._image_pull_secrets + + @image_pull_secrets.setter + def image_pull_secrets(self, secrets): + ''' property for secrets ''' + self._image_pull_secrets = secrets + + @property + def secrets(self): + ''' property for secrets ''' + if not self._secrets: + self._secrets = self.get(ServiceAccount.secrets_path) or [] + return self._secrets + + @secrets.setter + def secrets(self, secrets): + ''' property for secrets ''' + self._secrets = secrets + + def delete_secret(self, inc_secret): + ''' remove a secret ''' + remove_idx = None + for idx, sec in enumerate(self.secrets): + if sec['name'] == inc_secret: + remove_idx = idx + break + + if remove_idx: + del self.secrets[remove_idx] + return True + + return False + + def delete_image_pull_secret(self, inc_secret): + ''' remove a image_pull_secret ''' + remove_idx = None + for idx, sec in enumerate(self.image_pull_secrets): + if sec['name'] == inc_secret: + remove_idx = idx + break + + if remove_idx: + del self.image_pull_secrets[remove_idx] + return True + + return False + + def find_secret(self, inc_secret): + '''find secret''' + for secret in self.secrets: + if secret['name'] == inc_secret: + return secret + + return None + + def find_image_pull_secret(self, inc_secret): + '''find secret''' + for secret in self.image_pull_secrets: + if secret['name'] == inc_secret: + return secret + + return None + + def add_secret(self, inc_secret): + '''add secret''' + if self.secrets: + self.secrets.append({"name": inc_secret}) # pylint: disable=no-member + else: + self.put(ServiceAccount.secrets_path, [{"name": inc_secret}]) + + def add_image_pull_secret(self, inc_secret): + '''add image_pull_secret''' + if self.image_pull_secrets: + self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member + else: + self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}]) diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index b0835784b..8a825a402 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -66,3 +66,13 @@ oc_version.py: - lib/base.py - class/oc_version.py - ansible/oc_version.py +oc_serviceaccount.py: +- doc/generated +- doc/license +- lib/import.py +- doc/serviceaccount +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/serviceaccount.py +- class/oc_serviceaccount.py +- ansible/oc_serviceaccount.py diff --git a/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml b/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml new file mode 100755 index 000000000..46369b8f4 --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml @@ -0,0 +1,101 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ + +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + vars_prompt: + - name: cli_master_test + prompt: "Master to run against" + private: false + default: localhost + + vars: + service_account_name: serviceaccount-int-test + ns_name: default + + post_tasks: + - name: Make sure we start clean - Arrange + oc_serviceaccount: + state: absent + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + + - name: List when account does not exist - Act + oc_serviceaccount: + state: list + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + register: saout + + - name: List when account does not exist - Assert + assert: + that: + - "saout.changed == False" + - "saout.state == 'list'" + - "saout.results == [{}]" + + - name: create serviceaccount - Act + oc_serviceaccount: + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + secrets: + - one + - two + - three + register: saout + + - name: create serviceaccount - Assert + assert: + that: + - "saout.changed == True" + - "saout.state == 'present'" + - "saout.results.returncode == 0" + - "saout.results.results.0.metadata.name == '{{ service_account_name }}'" + - "saout.results.results.0.metadata.namespace == '{{ ns_name }}'" + + - name: create serviceaccount - check idempotency - Act + oc_serviceaccount: + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + secrets: + - one + - two + - three + register: saout + + - name: create serviceaccount - check idempotency - Assert + assert: + that: + - "saout.changed == False" + - "saout.state == 'present'" + - "saout.results.returncode == 0" + - "saout.results.results.0.metadata.name == '{{ service_account_name }}'" + - "saout.results.results.0.metadata.namespace == '{{ ns_name }}'" + + - name: Delete serviceaccount - Act + oc_serviceaccount: + state: absent + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + register: saout + + - name: Delete serviceaccount - Assert + assert: + that: + - "saout.changed == True" + - "saout.state == 'absent'" + - "saout.results.returncode == 0" + + - name: Delete serviceaccount - check idempotency - Act + oc_serviceaccount: + state: absent + name: "{{ service_account_name }}" + namespace: "{{ ns_name }}" + register: saout + + - name: Delete serviceaccount - check idempotency - Assert + assert: + that: + - "saout.changed == False" + - "saout.state == 'absent'" diff --git a/roles/lib_openshift/src/test/unit/oc_serviceaccount.py b/roles/lib_openshift/src/test/unit/oc_serviceaccount.py new file mode 100755 index 000000000..faf0bfeb5 --- /dev/null +++ b/roles/lib_openshift/src/test/unit/oc_serviceaccount.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python2 +''' + Unit tests for oc serviceaccount +''' +# To run: +# ./oc_serviceaccount.py +# +# . +# Ran 1 test in 0.002s +# +# 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,wrong-import-position +# 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_serviceaccount import OCServiceAccount # noqa: E402 + + +class OCServiceAccountTest(unittest.TestCase): + ''' + Test class for OCServiceAccount + ''' + + def setUp(self): + ''' setup method will create a file and set to known configuration ''' + pass + + @mock.patch('oc_serviceaccount.OCServiceAccount._run') + def test_adding_a_serviceaccount(self, mock_cmd): + ''' Testing adding a serviceaccount ''' + + # Arrange + + # run_ansible input parameters + params = { + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'state': 'present', + 'debug': False, + 'name': 'testserviceaccountname', + 'namespace': 'default', + 'secrets': None, + 'image_pull_secrets': None, + } + + valid_result_json = '''{ + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "testserviceaccountname", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/serviceaccounts/testserviceaccountname", + "uid": "4d8320c9-e66f-11e6-8edc-0eece8f2ce22", + "resourceVersion": "328450", + "creationTimestamp": "2017-01-29T22:07:19Z" + }, + "secrets": [ + { + "name": "testserviceaccountname-dockercfg-4lqd0" + }, + { + "name": "testserviceaccountname-token-9h0ej" + } + ], + "imagePullSecrets": [ + { + "name": "testserviceaccountname-dockercfg-4lqd0" + } + ] + }''' + + # Return values of our mocked function call. These get returned once per call. + mock_cmd.side_effect = [ + # First call to mock + (1, '', 'Error from server: serviceaccounts "testserviceaccountname" not found'), + + # Second call to mock + (0, 'serviceaccount "testserviceaccountname" created', ''), + + # Third call to mock + (0, valid_result_json, ''), + ] + + # Act + results = OCServiceAccount.run_ansible(params, False) + + # Assert + self.assertTrue(results['changed']) + self.assertEqual(results['results']['returncode'], 0) + self.assertEqual(results['state'], 'present') + + # Making sure our mock was called as we expected + mock_cmd.assert_has_calls([ + mock.call(['/usr/bin/oc', '-n', 'default', 'get', 'sa', 'testserviceaccountname', '-o', 'json'], None), + mock.call(['/usr/bin/oc', '-n', 'default', 'create', '-f', '/tmp/testserviceaccountname'], None), + mock.call(['/usr/bin/oc', '-n', 'default', 'get', 'sa', 'testserviceaccountname', '-o', 'json'], None), + ]) + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() |