1
0
mirror of https://github.com/appuio/ansible-module-openshift.git synced 2026-02-05 15:45:23 +01:00
Files
ansible-module-openshift/library/openshift_resource.py
Daniel Tschan 4247dc8733 openshift_resource: Avoid resource normalization.
Switch from oc new-app to oc process to avoid resource normalization, e.g. removal of booleans which have default values.
2017-09-18 19:22:57 +02:00

298 lines
9.4 KiB
Python

#!/usr/bin/python
import json
import tempfile
import re
import traceback
DOCUMENTATION = '''
---
module: openshift_resource
short_description: Creates and patches OpenShift resources.
description:
- Creates and patches OpenShift resources idempotently
- based on template or strategic merge patch.
options:
namespace:
description:
- The namespace in which to configure resources
default: None
required: true
aliases: []
template:
description:
- Path to template of resources to configure
- Mutually exclusive with I(patch)
required: false
default: None
aliases: []
app_name:
description:
- Name of application resources when instantiating the template,
- corresponds to the C(--name) option of C(oc new-app).
- Only relevant when I(template) parameter is given.
required: false
default: None
aliases: []
arguments:
description:
- Arguments to use when instantiating the template.
- Only relevant when I(template) parameter is given.
required: false
default: None
aliases: []
patch:
description:
- Strategic merge patch to apply
- Mutually exclusive with I(template)
required: false
default: None
aliases: []
author:
- "Daniel Tschan <tschan@puzzle.ch>"
extends_documentation_fragment: []
'''
EXAMPLES = '''
# TODO
'''
class ResourceModule:
def __init__(self, module):
self.module = module
self.changed = False
self.msg = []
self.log = []
self.arguments = []
for key in module.params:
setattr(self, key, module.params[key])
def debug(self, msg, *args):
if self.module._verbosity >= 3:
self.log.append(msg % args)
def trace(self, msg, *args):
if self.module._verbosity >= 4:
self.log.append(msg % args)
def run_command(self, args, **kwargs):
if self.module._verbosity < 3 or not kwargs['check_rc']: # Not running in debug mode, call module run_command which filters passwords
return self.module.run_command(args, **kwargs)
kwargs['check_rc'] = False
(rc, stdout, stderr) = self.module.run_command(args, **kwargs)
if rc != 0:
self.module.fail_json(cmd=args, rc=rc, stdout=stdout, stderr=stderr, msg=stderr, debug=self.log)
return (rc, stdout, stderr)
def remove_omitted_keys(self, object, parent = None, object_key = None):
if isinstance(object, dict):
for k, v in object.items():
self.remove_omitted_keys(v, object, k)
elif isinstance(object, list):
for i, v in enumerate(object[:]):
self.remove_omitted_keys(v, object, i)
elif isinstance(object, basestring):
if isinstance(object, basestring) and object.startswith('__omit_place_holder__'):
del parent[object_key]
def exemption(self, kind, current, patch, path):
if patch is None or isinstance(patch, (dict, list)) and not patch:
return True
elif re.match('\.status\..*', path):
return True
elif kind == 'DeploymentConfig' and re.match('.spec.template.spec.containers\[[0-9]+\].image', path):
return "@" in current
return False
def patch_applied(self, kind, name, current, patch, path = ""):
self.trace("patch_applied %s", path)
if current is None:
if not patch is None and not patch is False and not self.exemption(kind, current, patch, path):
self.msg.append(self.namespace + "::" + kind + "/" + name + "{" + path + "}(" + str(patch) + " != " + str(current) + ")")
return False
elif isinstance(patch, dict):
for key, val in patch.iteritems():
if not self.patch_applied(kind, name, current.get(key), val, path + "." + key):
return False
elif isinstance(patch, list):
if not self.strategic_list_compare(kind, name, current, patch, path):
return False
else:
if current != patch and not self.exemption(kind, current, patch, path):
self.msg.append(self.namespace + "::" + kind + "/" + name + "{" + path + "}(" + str(patch) + " != " + str(current) + ")")
return False
return True
def equalList(self, kind, resource, current, patch, path):
"""Compare two lists recursively."""
if len(current) != len(patch):
self.msg.append(self.namespace + "::" + kind + "/" + resource + "{" + path + "}(length mismatch)")
return False
for i, val in enumerate(patch):
if not self.patch_applied(kind, resource, current[i], val, path + "[" + str(i) + "]"):
return False
return True
def strategic_list_compare(self, kind, name, current, patch, path):
if not current and not patch:
return True
elif not current:
self.msg.append(self.namespace + "::" + kind + "/" + name + "{" + path + "}(new)")
return False
elif isinstance(current[0], dict) and 'name' in current[0]:
for i, patchVal in enumerate(patch):
elementName = patchVal.get('name')
if elementName is None: # Patch contains element without name attribute => fall back to plain list comparison.
self.debug("Patch contains element without name attribute => fall back to plain list comparison.")
return self.equalList(kind, name, current, patch, path)
curVals = [curVal for curVal in current if curVal.get('name') == elementName]
if len(curVals) == 0:
self.msg.append(self.namespace + "::" + kind + "/" + name + "{" + path + '[' + str(len(current)) + ']' + "}(new)")
return False
elif len(curVals) == 1:
if not self.patch_applied(kind, name, curVals[0], patchVal, path + '[' + str(i) + ']'):
return False
else:
self.module.fail_json(msg="Patch contains multiple attributes with name '" + elementName + "' under path: " + path, debug=self.log)
else:
return self.equalList(kind, name, current, patch, path)
return True
def export_resource(self, kind, name = None, label = None):
if label:
name = '-l ' + label
(rc, stdout, stderr) = self.module.run_command(['oc', 'get', '-n', self.namespace, kind + '/' + name, '-o', 'json'])
if rc == 0:
result = json.loads(stdout)
else:
result = {}
return result
def create_resource(self, kind, name, object):
if not self.module.check_mode:
file = tempfile.NamedTemporaryFile(prefix=kind + '_' + name, delete=True)
json.dump(object, file)
file.flush()
(rc, stdout, stderr) = self.run_command(['oc', 'create', '-n', self.namespace, '-f', file.name], check_rc=True)
file.close()
def patch_resource(self, kind, name, patch):
if not self.module.check_mode:
(rc, stdout, stderr) = self.run_command(['oc', 'patch', '-n', self.namespace, kind + '/' + name, '-p', json.dumps(patch)], check_rc=True)
def update_resource(self, object, path = ""):
kind = object.get('kind')
name = object.get('metadata', {}).get('name')
self.debug("update_resource %s %s", kind, name)
if not kind:
self.module.fail_json(msg=path + ".kind is undefined!", debug=self.log)
if not name:
self.module.fail_json(msg=path + ".metadata.name is undefined!", debug=self.log)
self.remove_omitted_keys(object)
current = self.export_resource(kind, name)
if not current:
self.changed = True
self.msg.append(self.namespace + "::" + kind + "/" + name + "(new)")
self.create_resource(kind, name, object)
elif not self.patch_applied(kind, name, current, object):
self.changed = True
self.patch_resource(kind, name, object)
return self.changed
def process_template(self, template_name, arguments):
self.debug("process_template")
if arguments:
args = [_ for arg in arguments.items() for _ in ('-v', "=".join(arg))]
else:
args = []
if "\n" in template_name:
(rc, stdout, stderr) = self.run_command(['oc', 'process', '-o', 'json', '-f', '-'] + args, data=template_name, check_rc=True)
else:
(rc, stdout, stderr) = self.run_command(['oc', 'process', '-o', 'json', '-f', template_name] + args, check_rc=True)
if stderr:
self.module.fail_json(msg=stderr, debug=self.log)
template = json.loads(stdout)
if self.app_name:
for item in template['items']:
item.setdefault('metadata', {}).setdefault('labels', {})['app'] = self.app_name
return template
def apply_template(self, template_name, arguments):
template = self.process_template(template_name, arguments)
self.remove_omitted_keys(template)
for i, object in enumerate(template['items']):
self.update_resource(object, ".items[" + str(i) + "]")
def main():
module = AnsibleModule(
argument_spec=dict(
namespace = dict(type='str'),
template = dict(type='str'),
app_name = dict(type='str'),
arguments = dict(type='dict'),
patch = dict(type='dict'),
),
supports_check_mode=True
)
resource = ResourceModule(module)
try:
if resource.template:
resource.apply_template(resource.template, resource.arguments)
else:
resource.update_resource(resource.patch)
except Exception as e:
module.fail_json(msg=e.message, traceback=traceback.format_exc().split('\n'), debug=resource.log)
if module._verbosity >= 3:
module.exit_json(changed=resource.changed, msg=resource.msg, debug=resource.log)
else:
module.exit_json(changed=resource.changed, msg=resource.msg)
from ansible.module_utils.basic import *
if __name__ == "__main__":
main()