Browse Source

Added oc_serviceaccount_secret to lib_openshift.

Thomas Wiest 8 years ago
parent
commit
d508ec2487

+ 15 - 8
roles/lib_openshift/library/oadm_manage_node.py

@@ -310,6 +310,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -318,15 +329,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_edit.py

@@ -338,6 +338,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -346,15 +357,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_label.py

@@ -314,6 +314,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -322,15 +333,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_obj.py

@@ -317,6 +317,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -325,15 +336,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_route.py

@@ -342,6 +342,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -350,15 +361,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_scale.py

@@ -292,6 +292,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -300,15 +311,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_secret.py

@@ -338,6 +338,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -346,15 +357,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_openshift/library/oc_service.py

@@ -344,6 +344,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -352,15 +363,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 16 - 9
roles/lib_openshift/library/oc_serviceaccount.py

@@ -290,6 +290,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -298,15 +309,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 
@@ -1516,7 +1523,7 @@ class OCServiceAccount(OpenShiftCLI):
 
 def main():
     '''
-    ansible oc module for route
+    ansible oc module for service accounts
     '''
 
     module = AnsibleModule(

File diff suppressed because it is too large
+ 1524 - 0
roles/lib_openshift/library/oc_serviceaccount_secret.py


+ 15 - 8
roles/lib_openshift/library/oc_version.py

@@ -262,6 +262,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -270,15 +281,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 1 - 1
roles/lib_openshift/src/ansible/oc_serviceaccount.py

@@ -3,7 +3,7 @@
 
 def main():
     '''
-    ansible oc module for route
+    ansible oc module for service accounts
     '''
 
     module = AnsibleModule(

+ 29 - 0
roles/lib_openshift/src/ansible/oc_serviceaccount_secret.py

@@ -0,0 +1,29 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+    '''
+    ansible oc module to manage service account 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=None, required=True, type='str'),
+            secret=dict(default=None, type='str'),
+            service_account=dict(required=True, type='str'),
+        ),
+        supports_check_mode=True,
+    )
+
+    rval = OCServiceAccountSecret.run_ansible(module.params, module.check_mode)
+    if 'failed' in rval:
+        module.fail_json(**rval)
+
+    module.exit_json(**rval)
+
+if __name__ == '__main__':
+    main()

+ 138 - 0
roles/lib_openshift/src/class/oc_serviceaccount_secret.py

@@ -0,0 +1,138 @@
+# pylint: skip-file
+# flake8: noqa
+
+class OCServiceAccountSecret(OpenShiftCLI):
+    ''' Class to wrap the oc command line tools '''
+
+    kind = 'sa'
+    def __init__(self, config, verbose=False):
+        ''' Constructor for OpenshiftOC '''
+        super(OCServiceAccountSecret, self).__init__(config.namespace, config.kubeconfig)
+        self.config = config
+        self.verbose = verbose
+        self._service_account = None
+
+    @property
+    def service_account(self):
+        ''' Property for the service account '''
+        if not self._service_account:
+            self.get()
+        return self._service_account
+
+    @service_account.setter
+    def service_account(self, data):
+        ''' setter for the service account '''
+        self._service_account = data
+
+    def exists(self, in_secret):
+        ''' verifies if secret exists in the service account '''
+        result = self.service_account.find_secret(in_secret)
+        if not result:
+            return False
+        return True
+
+    def get(self):
+        ''' get the service account definition from the master '''
+        sao = self._get(OCServiceAccountSecret.kind, self.config.name)
+        if sao['returncode'] == 0:
+            self.service_account = ServiceAccount(content=sao['results'][0])
+            sao['results'] = self.service_account.get('secrets')
+        return sao
+
+    def delete(self):
+        ''' delete secrets '''
+
+        modified = []
+        for rem_secret in self.config.secrets:
+            modified.append(self.service_account.delete_secret(rem_secret))
+
+        if any(modified):
+            return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+        return {'returncode': 0, 'changed': False}
+
+    def put(self):
+        ''' place secrets into sa '''
+        modified = False
+        for add_secret in self.config.secrets:
+            if not self.service_account.find_secret(add_secret):
+                self.service_account.add_secret(add_secret)
+                modified = True
+
+        if modified:
+            return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+        return {'returncode': 0, 'changed': 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 '''
+
+        sconfig = ServiceAccountConfig(params['service_account'],
+                                       params['namespace'],
+                                       params['kubeconfig'],
+                                       [params['secret']],
+                                       None)
+
+        oc_sa_sec = OCServiceAccountSecret(sconfig, verbose=params['debug'])
+
+        state = params['state']
+
+        api_rval = oc_sa_sec.get()
+
+        #####
+        # Get
+        #####
+        if state == 'list':
+            return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+        ########
+        # Delete
+        ########
+        if state == 'absent':
+            if oc_sa_sec.exists(params['secret']):
+
+                if check_mode:
+                    return {'changed': True, 'msg': 'Would have removed the " + \
+                            "secret from the service account.'}
+
+                api_rval = oc_sa_sec.delete()
+
+                return {'changed': True, 'results': api_rval, 'state': "absent"}
+
+            return {'changed': False, 'state': "absent"}
+
+        if state == 'present':
+            ########
+            # Create
+            ########
+            if not oc_sa_sec.exists(params['secret']):
+
+                if check_mode:
+                    return {'changed': True, 'msg': 'Would have added the ' + \
+                            'secret to the service account.'}
+
+                # Create it here
+                api_rval = oc_sa_sec.put()
+                if api_rval['returncode'] != 0:
+                    return {'failed': True, 'msg': api_rval}
+
+                # return the created object
+                api_rval = oc_sa_sec.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'}

+ 68 - 0
roles/lib_openshift/src/doc/serviceaccount_secret

@@ -0,0 +1,68 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_serviceaccount_secret
+short_description: Module to manage openshift service account secrets
+description:
+  - Manage openshift service account secrets programmatically.
+options:
+  state:
+    description:
+    - If present, the service account will be linked with the secret if it is not already. If absent, the service account will be unlinked from the secret if it is already linked. If list, information about the service account secrets 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: []
+  service_account:
+    description:
+    - Name of the service account.
+    required: true
+    default: None
+    aliases: []
+  namespace:
+    description:
+    - Namespace of the service account and secret.
+    required: true
+    default: None
+    aliases: []
+  secret:
+    description:
+    - The secret that should be linked to the service account.
+    required: false
+    default: None
+    aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+  - name: get secrets of a service account
+    oc_serviceaccount_secret:
+      state: list
+      service_account: builder
+      namespace: default
+    register: sasecretout
+
+
+  - name: Link a service account to a specific secret
+    oc_serviceaccount_secret:
+      service_account: builder
+      secret: mynewsecret
+      namespace: default
+    register: sasecretout
+'''

+ 11 - 0
roles/lib_openshift/src/sources.yml

@@ -84,6 +84,17 @@ oc_serviceaccount.py:
 - class/oc_serviceaccount.py
 - ansible/oc_serviceaccount.py
 
+oc_serviceaccount_secret.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/serviceaccount_secret
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/serviceaccount.py
+- class/oc_serviceaccount_secret.py
+- ansible/oc_serviceaccount_secret.py
+
 oc_service.py:
 - doc/generated
 - doc/license

+ 79 - 0
roles/lib_openshift/src/test/integration/oc_serviceaccount_secret.yml

@@ -0,0 +1,79 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+---
+- hosts: "{{ cli_master_test }}"
+  gather_facts: no
+  user: root
+
+  vars:
+    namespace: default
+    service_account_name: someserviceaccountname
+    secret_name: somesecretname
+
+  vars_prompt:
+  - name: cli_master_test
+    prompt: "Master to run against"
+    private: false
+    default: localhost
+
+  post_tasks:
+  - name: create service account to test with - Arrange
+    oc_serviceaccount:
+      namespace: "{{ namespace }}"
+      name: "{{ service_account_name }}"
+
+  - name: create secret to test with - Arrange
+    oc_secret:
+      namespace: "{{ namespace }}"
+      name: "{{ secret_name }}"
+      contents:
+      - path: blah
+        data: blahdeblah
+
+  - name: Ensure the service account and secret are not linked - Arrange
+    oc_serviceaccount_secret:
+      state: absent
+      service_account: "{{ service_account_name }}"
+      secret: "{{ secret_name }}"
+      namespace: "{{ namespace }}"
+
+  - name: get secrets of a service account - Act
+    oc_serviceaccount_secret:
+      state: list
+      service_account: builder
+      namespace: "{{ namespace }}"
+    register: sasecretout
+
+  - name: get secrets of a service account - Assert
+    assert:
+      that:
+      - "sasecretout.changed == False"
+      - "sasecretout.state == 'list'"
+      - "sasecretout.results | length > 0"
+
+  - name: Test linking a service account and secret - Act
+    oc_serviceaccount_secret:
+      service_account: "{{ service_account_name }}"
+      secret: "{{ secret_name }}"
+      namespace: "{{ namespace }}"
+    register: sasecretout
+
+  - name: Test linking a service account and secret - Assert
+    assert:
+      that:
+      - "sasecretout.changed == True"
+      - "sasecretout.state == 'present'"
+      - "sasecretout.results.returncode == 0"
+      - "sasecretout.results.results | length > 0"
+
+  - name: Test linking a service account and secret - idempotency - Act
+    oc_serviceaccount_secret:
+      service_account: "{{ service_account_name }}"
+      secret: "{{ secret_name }}"
+      namespace: "{{ namespace }}"
+    register: sasecretout
+
+  - name: Test linking a service account and secret - idempotency - Assert
+    assert:
+      that:
+      - "sasecretout.changed == False"
+      - "sasecretout.state == 'present'"

+ 3 - 3
roles/lib_openshift/src/test/unit/oc_serviceaccount.py

@@ -100,9 +100,9 @@ class OCServiceAccountTest(unittest.TestCase):
 
         # 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),
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'testserviceaccountname', '-o', 'json'], None),
+            mock.call(['oc', '-n', 'default', 'create', '-f', mock.ANY], None),
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'testserviceaccountname', '-o', 'json'], None),
         ])
 
     def tearDown(self):

+ 257 - 0
roles/lib_openshift/src/test/unit/oc_serviceaccount_secret.py

@@ -0,0 +1,257 @@
+#!/usr/bin/env python2
+'''
+ Unit tests for oc secret add
+'''
+# To run:
+# ./oc_serviceaccount_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_serviceaccount_secret import OCServiceAccountSecret  # noqa: E402
+
+
+class OCServiceAccountSecretTest(unittest.TestCase):
+    '''
+     Test class for OCServiceAccountSecret
+    '''
+
+    def setUp(self):
+        ''' setup method will create a file and set to known configuration '''
+        pass
+
+    @mock.patch('oc_serviceaccount_secret.Yedit._write')
+    @mock.patch('oc_serviceaccount_secret.OCServiceAccountSecret._run')
+    def test_adding_a_secret_to_a_serviceaccount(self, mock_cmd, mock_write):
+        ''' Testing adding a secret to a service account '''
+
+        # Arrange
+
+        # run_ansible input parameters
+        params = {
+            'state': 'present',
+            'namespace': 'default',
+            'secret': 'newsecret',
+            'service_account': 'builder',
+            'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+            'debug': False,
+        }
+
+        oc_get_sa_before = '''{
+            "kind": "ServiceAccount",
+            "apiVersion": "v1",
+            "metadata": {
+                "name": "builder",
+                "namespace": "default",
+                "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+                "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+                "resourceVersion": "302879",
+                "creationTimestamp": "2017-02-05T17:02:00Z"
+            },
+            "secrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                },
+                {
+                    "name": "builder-token-akqxi"
+                }
+
+            ],
+            "imagePullSecrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                }
+            ]
+        }
+        '''
+
+        oc_get_sa_after = '''{
+            "kind": "ServiceAccount",
+            "apiVersion": "v1",
+            "metadata": {
+                "name": "builder",
+                "namespace": "default",
+                "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+                "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+                "resourceVersion": "302879",
+                "creationTimestamp": "2017-02-05T17:02:00Z"
+            },
+            "secrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                },
+                {
+                    "name": "builder-token-akqxi"
+                },
+                {
+                    "name": "newsecret"
+                }
+
+            ],
+            "imagePullSecrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                }
+            ]
+        }
+        '''
+
+        builder_yaml_file = '''\
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+- name: newsecret
+kind: ServiceAccount
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+apiVersion: v1
+metadata:
+  name: builder
+  namespace: default
+  resourceVersion: '302879'
+  creationTimestamp: '2017-02-05T17:02:00Z'
+  selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+  uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+'''
+
+        # Return values of our mocked function call. These get returned once per call.
+        mock_cmd.side_effect = [
+            (0, oc_get_sa_before, ''),  # First call to the mock
+            (0, oc_get_sa_before, ''),  # Second call to the mock
+            (0, 'serviceaccount "builder" replaced', ''),  # Third call to the mock
+            (0, oc_get_sa_after, ''),  # Fourth call to the mock
+        ]
+
+        # Act
+        results = OCServiceAccountSecret.run_ansible(params, False)
+
+        # Assert
+        self.assertTrue(results['changed'])
+        self.assertEqual(results['results']['returncode'], 0)
+        self.assertEqual(results['state'], 'present')
+
+        # Making sure our mocks were called as we expected
+        mock_cmd.assert_has_calls([
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'builder', '-o', 'json'], None),
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'builder', '-o', 'json'], None),
+            mock.call(['oc', '-n', 'default', 'replace', '-f', '/tmp/builder'], None),
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'builder', '-o', 'json'], None)
+        ])
+
+        mock_write.assert_has_calls([
+            mock.call('/tmp/builder', builder_yaml_file)
+        ])
+
+    @mock.patch('oc_serviceaccount_secret.Yedit._write')
+    @mock.patch('oc_serviceaccount_secret.OCServiceAccountSecret._run')
+    def test_removing_a_secret_to_a_serviceaccount(self, mock_cmd, mock_write):
+        ''' Testing adding a secret to a service account '''
+
+        # Arrange
+
+        # run_ansible input parameters
+        params = {
+            'state': 'absent',
+            'namespace': 'default',
+            'secret': 'newsecret',
+            'service_account': 'builder',
+            'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+            'debug': False,
+        }
+
+        oc_get_sa_before = '''{
+            "kind": "ServiceAccount",
+            "apiVersion": "v1",
+            "metadata": {
+                "name": "builder",
+                "namespace": "default",
+                "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+                "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+                "resourceVersion": "302879",
+                "creationTimestamp": "2017-02-05T17:02:00Z"
+            },
+            "secrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                },
+                {
+                    "name": "builder-token-akqxi"
+                },
+                {
+                    "name": "newsecret"
+                }
+
+            ],
+            "imagePullSecrets": [
+                {
+                    "name": "builder-dockercfg-rsrua"
+                }
+            ]
+        }
+        '''
+
+        builder_yaml_file = '''\
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+kind: ServiceAccount
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+apiVersion: v1
+metadata:
+  name: builder
+  namespace: default
+  resourceVersion: '302879'
+  creationTimestamp: '2017-02-05T17:02:00Z'
+  selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+  uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+'''
+
+        # Return values of our mocked function call. These get returned once per call.
+        mock_cmd.side_effect = [
+            (0, oc_get_sa_before, ''),  # First call to the mock
+            (0, oc_get_sa_before, ''),  # Second call to the mock
+            (0, 'serviceaccount "builder" replaced', ''),  # Third call to the mock
+        ]
+
+        # Act
+        results = OCServiceAccountSecret.run_ansible(params, False)
+
+        # Assert
+        self.assertTrue(results['changed'])
+        self.assertEqual(results['results']['returncode'], 0)
+        self.assertEqual(results['state'], 'absent')
+
+        # Making sure our mocks were called as we expected
+        mock_cmd.assert_has_calls([
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'builder', '-o', 'json'], None),
+            mock.call(['oc', '-n', 'default', 'get', 'sa', 'builder', '-o', 'json'], None),
+            mock.call(['oc', '-n', 'default', 'replace', '-f', '/tmp/builder'], None),
+        ])
+
+        mock_write.assert_has_calls([
+            mock.call('/tmp/builder', builder_yaml_file)
+        ])
+
+    def tearDown(self):
+        '''TearDown method'''
+        pass
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 15 - 8
roles/lib_utils/library/yedit.py

@@ -356,6 +356,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -364,15 +375,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)
 

+ 15 - 8
roles/lib_utils/src/class/yedit.py

@@ -175,6 +175,17 @@ class Yedit(object):
 
         return data
 
+    @staticmethod
+    def _write(filename, contents):
+        ''' Actually write the file contents to disk. This helps with mocking. '''
+
+        tmp_filename = filename + '.yedit'
+
+        with open(tmp_filename, 'w') as yfd:
+            yfd.write(contents)
+
+        os.rename(tmp_filename, filename)
+
     def write(self):
         ''' write to file '''
         if not self.filename:
@@ -183,15 +194,11 @@ class Yedit(object):
         if self.backup and self.file_exists():
             shutil.copy(self.filename, self.filename + '.orig')
 
-        tmp_filename = self.filename + '.yedit'
-        with open(tmp_filename, 'w') as yfd:
-            # pylint: disable=no-member
-            if hasattr(self.yaml_dict, 'fa'):
-                self.yaml_dict.fa.set_block_style()
-
-            yfd.write(yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+        # pylint: disable=no-member
+        if hasattr(self.yaml_dict, 'fa'):
+            self.yaml_dict.fa.set_block_style()
 
-        os.rename(tmp_filename, self.filename)
+        Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
 
         return (True, self.yaml_dict)