diff options
Diffstat (limited to 'roles/lib_openshift/src')
-rw-r--r-- | roles/lib_openshift/src/ansible/oc_secret.py | 37 | ||||
-rw-r--r-- | roles/lib_openshift/src/class/oc_secret.py | 202 | ||||
-rw-r--r-- | roles/lib_openshift/src/doc/secret | 116 | ||||
-rwxr-xr-x | roles/lib_openshift/src/generate.py | 37 | ||||
-rw-r--r-- | roles/lib_openshift/src/lib/secret.py | 99 | ||||
-rw-r--r-- | roles/lib_openshift/src/sources.yml | 10 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/integration/oc_secret.yml | 125 | ||||
-rwxr-xr-x | roles/lib_openshift/src/test/unit/oc_secret.py | 94 |
8 files changed, 719 insertions, 1 deletions
diff --git a/roles/lib_openshift/src/ansible/oc_secret.py b/roles/lib_openshift/src/ansible/oc_secret.py new file mode 100644 index 000000000..1337cbbe5 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_secret.py @@ -0,0 +1,37 @@ +# pylint: skip-file +# flake8: noqa + + +def main(): + ''' + ansible oc module for managing OpenShift Secrets + ''' + + 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'), + namespace=dict(default='default', type='str'), + name=dict(default=None, type='str'), + files=dict(default=None, type='list'), + delete_after=dict(default=False, type='bool'), + contents=dict(default=None, type='list'), + force=dict(default=False, type='bool'), + decode=dict(default=False, type='bool'), + ), + mutually_exclusive=[["contents", "files"]], + + supports_check_mode=True, + ) + + + rval = OCSecret.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_secret.py b/roles/lib_openshift/src/class/oc_secret.py new file mode 100644 index 000000000..40b2aa4d1 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_secret.py @@ -0,0 +1,202 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: skip-file + +# pylint: disable=wrong-import-position,wrong-import-order +import base64 + +# pylint: disable=too-many-arguments +class OCSecret(OpenShiftCLI): + ''' Class to wrap the oc command line tools + ''' + def __init__(self, + namespace, + secret_name=None, + decode=False, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for OpenshiftOC ''' + super(OCSecret, self).__init__(namespace, kubeconfig) + self.namespace = namespace + self.name = secret_name + self.kubeconfig = kubeconfig + self.decode = decode + self.verbose = verbose + + def get(self): + '''return a secret by name ''' + results = self._get('secrets', self.name) + results['decoded'] = {} + results['exists'] = False + if results['returncode'] == 0 and results['results'][0]: + results['exists'] = True + if self.decode: + if results['results'][0].has_key('data'): + for sname, value in results['results'][0]['data'].items(): + results['decoded'][sname] = base64.b64decode(value) + + if results['returncode'] != 0 and '"%s" not found' % self.name in results['stderr']: + results['returncode'] = 0 + + return results + + def delete(self): + '''delete a secret by name''' + return self._delete('secrets', self.name) + + def create(self, files=None, contents=None): + '''Create a secret ''' + if not files: + files = Utils.create_files_from_contents(contents) + + secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] + cmd = ['secrets', 'new', self.name] + cmd.extend(secrets) + + results = self.openshift_cmd(cmd) + + return results + + def update(self, files, force=False): + '''run update secret + + This receives a list of file names and converts it into a secret. + The secret is then written to disk and passed into the `oc replace` command. + ''' + secret = self.prep_secret(files) + if secret['returncode'] != 0: + return secret + + sfile_path = '/tmp/%s' % self.name + with open(sfile_path, 'w') as sfd: + sfd.write(json.dumps(secret['results'])) + + atexit.register(Utils.cleanup, [sfile_path]) + + return self._replace(sfile_path, force=force) + + def prep_secret(self, files=None, contents=None): + ''' return what the secret would look like if created + This is accomplished by passing -ojson. This will most likely change in the future + ''' + if not files: + files = Utils.create_files_from_contents(contents) + + secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] + cmd = ['-ojson', 'secrets', 'new', self.name] + cmd.extend(secrets) + + return self.openshift_cmd(cmd, output=True) + + @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''' + + ocsecret = OCSecret(params['namespace'], + params['name'], + params['decode'], + kubeconfig=params['kubeconfig'], + verbose=params['debug']) + + state = params['state'] + + api_rval = ocsecret.get() + + ##### + # Get + ##### + if state == 'list': + return {'changed': False, 'results': api_rval, state: 'list'} + + if not params['name']: + return {'failed': True, + 'msg': 'Please specify a name when state is absent|present.'} + + ######## + # Delete + ######## + if state == 'absent': + if not Utils.exists(api_rval['results'], params['name']): + return {'changed': False, 'state': 'absent'} + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a delete.'} + + api_rval = ocsecret.delete() + return {'changed': True, 'results': api_rval, 'state': 'absent'} + + if state == 'present': + if params['files']: + files = params['files'] + elif params['contents']: + files = Utils.create_files_from_contents(params['contents']) + else: + return {'failed': True, + 'msg': 'Either specify files or contents.'} + + ######## + # Create + ######## + if not Utils.exists(api_rval['results'], params['name']): + + if check_mode: + return {'changed': True, + 'msg': 'Would have performed a create.'} + + api_rval = ocsecret.create(params['files'], params['contents']) + + # Remove files + if files and params['delete_after']: + Utils.cleanup([ftmp['path'] for ftmp in files]) + + if api_rval['returncode'] != 0: + return {'failed': True, + 'msg': api_rval} + + return {'changed': True, + 'results': api_rval, + 'state': 'present'} + + ######## + # Update + ######## + secret = ocsecret.prep_secret(params['files'], params['contents']) + + if secret['returncode'] != 0: + return {'failed': True, 'msg': secret} + + if Utils.check_def_equal(secret['results'], api_rval['results'][0]): + + # Remove files + if files and params['delete_after']: + Utils.cleanup([ftmp['path'] for ftmp in files]) + + return {'changed': False, + 'results': secret['results'], + 'state': 'present'} + + if check_mode: + return {'changed': True, + 'msg': 'Would have performed an update.'} + + api_rval = ocsecret.update(files, force=params['force']) + + # Remove files + if secret and params['delete_after']: + Utils.cleanup([ftmp['path'] for ftmp in files]) + + if api_rval['returncode'] != 0: + return {'failed': True, + 'msg': api_rval} + + return {'changed': True, + '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/secret b/roles/lib_openshift/src/doc/secret new file mode 100644 index 000000000..5c2bd9bc0 --- /dev/null +++ b/roles/lib_openshift/src/doc/secret @@ -0,0 +1,116 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_secret +short_description: Module to manage openshift secrets +description: + - Manage openshift secrets programmatically. +options: + state: + description: + - If present, the secret will be created if it doesn't exist or updated if different. If absent, the secret will be removed if present. If list, information about the secret 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 object that is being queried. + required: false + default: None + aliases: [] + namespace: + description: + - The namespace where the object lives. + required: false + default: default + aliases: [] + files: + description: + - A list of files provided for secrets + required: false + default: None + aliases: [] + delete_after: + description: + - Whether or not to delete the files after processing them. + required: false + default: false + aliases: [] + contents: + description: + - Content of the secrets + required: false + default: None + aliases: [] + force: + description: + - Whether or not to force the operation + required: false + default: false + aliases: [] + decode: + description: + - base64 decode the object + required: false + default: false + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: create secret + oc_secret: + state: present + namespace: openshift-infra + name: metrics-deployer + files: + - name: nothing + path: /dev/null + register: secretout + run_once: true + +- name: get ca from hawkular + oc_secret: + state: list + namespace: openshift-infra + name: hawkular-metrics-certificate + decode: True + register: hawkout + run_once: true + +- name: Create secrets + oc_secret: + namespace: mynamespace + name: mysecrets + contents: + - path: data.yml + data: "{{ data_content }}" + - path: auth-keys + data: "{{ auth_keys_content }}" + - path: configdata.yml + data: "{{ configdata_content }}" + - path: cert.crt + data: "{{ cert_content }}" + - path: key.pem + data: "{{ osso_site_key_content }}" + - path: ca.cert.pem + data: "{{ ca_cert_content }}" + register: secretout +''' diff --git a/roles/lib_openshift/src/generate.py b/roles/lib_openshift/src/generate.py index 6daade108..3f23455b5 100755 --- a/roles/lib_openshift/src/generate.py +++ b/roles/lib_openshift/src/generate.py @@ -27,19 +27,54 @@ def parse_args(): return parser.parse_args() +def fragment_banner(fragment_path, side, data): + """Generate a banner to wrap around file fragments + +:param string fragment_path: A path to a module fragment +:param string side: ONE OF: "header", "footer" +:param StringIO data: A StringIO object to write the banner to +""" + side_msg = { + "header": "Begin included fragment: {}", + "footer": "End included fragment: {}" + } + annotation = side_msg[side].format(fragment_path) + + banner = """ +# -*- -*- -*- {} -*- -*- -*- +""".format(annotation) + + # Why skip? + # + # * 'generated' - This is the head of the script, we don't want to + # put comments before the #!shebang + # + # * 'license' - Wrapping this just seemed like gratuitous extra + if ("generated" not in fragment_path) and ("license" not in fragment_path): + data.write(banner) + + # Make it self-contained testable + return banner + + def generate(parts): - '''generate the source code for the ansible modules''' + '''generate the source code for the ansible modules + +:param Array parts: An array of paths (strings) to module fragments + ''' data = six.StringIO() for fpart in parts: # first line is pylint disable so skip it with open(os.path.join(OPENSHIFT_ANSIBLE_PATH, fpart)) as pfd: + fragment_banner(fpart, "header", data) for idx, line in enumerate(pfd): if idx in [0, 1] and 'flake8: noqa' in line or 'pylint: skip-file' in line: # noqa: E501 continue data.write(line) + fragment_banner(fpart, "footer", data) return data diff --git a/roles/lib_openshift/src/lib/secret.py b/roles/lib_openshift/src/lib/secret.py new file mode 100644 index 000000000..39bf3c33a --- /dev/null +++ b/roles/lib_openshift/src/lib/secret.py @@ -0,0 +1,99 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: disable=too-many-instance-attributes +class SecretConfig(object): + ''' Handle secret options ''' + # pylint: disable=too-many-arguments + def __init__(self, + sname, + namespace, + kubeconfig, + secrets=None): + ''' constructor for handling secret options ''' + self.kubeconfig = kubeconfig + self.name = sname + self.namespace = namespace + self.secrets = secrets + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' return a secret as a dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'Secret' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + self.data['data'] = {} + if self.secrets: + for key, value in self.secrets.items(): + self.data['data'][key] = value + +# pylint: disable=too-many-instance-attributes +class Secret(Yedit): + ''' Class to wrap the oc command line tools ''' + secret_path = "data" + kind = 'secret' + + def __init__(self, content): + '''secret constructor''' + super(Secret, self).__init__(content=content) + self._secrets = None + + @property + def secrets(self): + '''secret property getter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + @secrets.setter + def secrets(self): + '''secret property setter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + def get_secrets(self): + ''' returns all of the defined secrets ''' + return self.get(Secret.secret_path) or {} + + def add_secret(self, key, value): + ''' add a secret ''' + if self.secrets: + self.secrets[key] = value + else: + self.put(Secret.secret_path, {key: value}) + + return True + + def delete_secret(self, key): + ''' delete secret''' + try: + del self.secrets[key] + except KeyError as _: + return False + + return True + + def find_secret(self, key): + ''' find secret''' + rval = None + try: + rval = self.secrets[key] + except KeyError as _: + return None + + return {'key': key, 'value': rval} + + def update_secret(self, key, value): + ''' update a secret''' + # pylint: disable=no-member + if self.secrets.has_key(key): + self.secrets[key] = value + else: + self.add_secret(key, value) + + return True diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index f1fd558d3..3c20b614d 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -36,3 +36,13 @@ oc_version.py: - lib/base.py - class/oc_version.py - ansible/oc_version.py +oc_secret.py: +- doc/generated +- doc/license +- lib/import.py +- doc/secret +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/secret.py +- class/oc_secret.py +- ansible/oc_secret.py diff --git a/roles/lib_openshift/src/test/integration/oc_secret.yml b/roles/lib_openshift/src/test/integration/oc_secret.yml new file mode 100755 index 000000000..e0456bd6e --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_secret.yml @@ -0,0 +1,125 @@ +#!/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: + secret_name: secret-int-test + ns_name: default + config_path: "/tmp/{{ secret_name }}--config.yml" + passwords_path: "/tmp/{{ secret_name }}--passwords.yml" + + post_tasks: + + - name: Setup our files to test with + copy: + dest: "{{ item.name }}" + content: "{{ item.content }}" + with_items: + - name: "{{ config_path }}" + content: | + value: True + - name: "{{ passwords_path }}" + content: | + test1 + test2 + test3 + test4 + + + - name: Make sure we're starting with a clean slate + oc_secret: + state: absent + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + register: secret_out + + - name: Test adding a secret - Act + oc_secret: + state: present + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + files: + - name: config.yml + path: "{{ config_path }}" + - name: passwords.yml + path: "{{ passwords_path }}" + register: secret_out + + - name: Test adding a secret - Assert + assert: + that: + - "secret_out.results.returncode == 0" + - "secret_out.changed == True" + + - name: Test secret present idempotentcy - Act + oc_secret: + state: present + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + files: + - name: config.yml + path: "{{ config_path }}" + - name: passwords.yml + path: "{{ passwords_path }}" + register: secret_out + + - name: Test secret present idempotentcy - Assert + assert: + that: + - "secret_out.changed == false" + + - name: Test list secrets - Act + oc_secret: + state: list + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + register: secret_out + + - name: Test list secrets - Assert + assert: + that: + - "secret_out.changed == false" + - "secret_out.results.exists == true" + + + - name: Test secret absent - Act + oc_secret: + state: absent + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + register: secret_out + + - name: Test secret absent - Assert + assert: + that: + - "secret_out.changed == true" + - "secret_out.results.returncode == 0" + + - name: Test secret absent idempotentcy - Act + oc_secret: + state: absent + namespace: "{{ ns_name }}" + name: "{{ secret_name }}" + register: secret_out + + - name: Test secret idempotentcy - Assert + assert: + that: + - "secret_out.changed == false" + + + - name: Clean up the files we created + file: + state: absent + path: "{{ item }}" + with_items: + - "/tmp/{{ secret_name }}--config.yml" + - "/tmp/{{ secret_name }}--passwords.yml" diff --git a/roles/lib_openshift/src/test/unit/oc_secret.py b/roles/lib_openshift/src/test/unit/oc_secret.py new file mode 100755 index 000000000..221f00ed6 --- /dev/null +++ b/roles/lib_openshift/src/test/unit/oc_secret.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python2 +''' + Unit tests for oc secret +''' +# To run: +# ./oc_secret.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_secret import OCSecret # noqa: E402 + + +class OCSecretTest(unittest.TestCase): + ''' + Test class for OCSecret + ''' + + def setUp(self): + ''' setup method will create a file and set to known configuration ''' + pass + + @mock.patch('oc_secret.OCSecret.openshift_cmd') + def test_adding_a_secret(self, mock_openshift_cmd): + ''' Testing adding a secret ''' + + # Arrange + + # run_ansible input parameters + params = { + 'state': 'present', + 'namespace': 'default', + 'name': 'secretname', + 'contents': [{ + 'path': "/tmp/somesecret.json", + 'data': "{'one': 1, 'two': 2, 'three', 3}", + }], + 'decode': False, + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'debug': False, + 'files': None, + 'delete_after': True, + } + + # Return values of our mocked function call. These get returned once per call. + mock_openshift_cmd.side_effect = [ + { + "cmd": "/usr/bin/oc get secrets -o json secretname", + "results": "", + "returncode": 0, + }, # oc output for first call to openshift_cmd (oc secrets get) + { + "cmd": "/usr/bin/oc secrets new secretname somesecret.json=/tmp/somesecret.json", + "results": "", + "returncode": 0, + }, # oc output for second call to openshift_cmd (oc secrets new) + ] + + # Act + results = OCSecret.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_openshift_cmd.assert_has_calls([ + mock.call(['get', 'secrets', '-o', 'json', 'secretname'], output=True), + mock.call(['secrets', 'new', 'secretname', 'somesecret.json=/tmp/somesecret.json']), + ]) + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() |