mirror of
https://github.com/openshift/community.okd.git
synced 2026-03-27 19:33:09 +00:00
- delay the loading of external modules when possible - delay the loading of OKDRawModule and OpenShiftProcess classes - k8s reuse the design of the kubernetes.core modules We've got a chicken/egg problem that prevent us from properly reporting if kubernetes.core is missing. We need args_common to create the module object. And we need the module object to report the missing dependency. The dependency is declared in the galaxy.yml file anyway, the problem should not happen.
187 lines
8.8 KiB
Python
187 lines
8.8 KiB
Python
#!/usr/bin/env python
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import re
|
|
import operator
|
|
from functools import reduce
|
|
import traceback
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
|
|
K8sAnsibleMixin,
|
|
get_api_client,
|
|
)
|
|
from ansible.module_utils._text import to_native
|
|
|
|
|
|
try:
|
|
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError, ForbiddenError
|
|
HAS_KUBERNETES_COLLECTION = True
|
|
except ImportError as e:
|
|
HAS_KUBERNETES_COLLECTION = False
|
|
k8s_collection_import_exception = e
|
|
K8S_COLLECTION_ERROR = traceback.format_exc()
|
|
|
|
|
|
TRIGGER_ANNOTATION = 'image.openshift.io/triggers'
|
|
TRIGGER_CONTAINER = re.compile(r"(?P<path>.*)\[((?P<index>[0-9]+)|\?\(@\.name==[\"'\\]*(?P<name>[a-z0-9]([-a-z0-9]*[a-z0-9])?))")
|
|
|
|
|
|
class OKDRawModule(K8sAnsibleMixin):
|
|
|
|
def __init__(self, module, k8s_kind=None, *args, **kwargs):
|
|
self.module = module
|
|
self.client = get_api_client(module=module)
|
|
self.check_mode = self.module.check_mode
|
|
self.params = self.module.params
|
|
self.fail_json = self.module.fail_json
|
|
self.fail = self.module.fail_json
|
|
self.exit_json = self.module.exit_json
|
|
|
|
super(OKDRawModule, self).__init__(module, *args, **kwargs)
|
|
|
|
self.warnings = []
|
|
|
|
self.kind = k8s_kind or self.params.get('kind')
|
|
self.api_version = self.params.get('api_version')
|
|
self.name = self.params.get('name')
|
|
self.namespace = self.params.get('namespace')
|
|
|
|
self.check_library_version()
|
|
self.set_resource_definitions(module)
|
|
|
|
def perform_action(self, resource, definition):
|
|
state = self.params.get('state', None)
|
|
name = definition['metadata'].get('name')
|
|
namespace = definition['metadata'].get('namespace')
|
|
|
|
if state != 'absent':
|
|
|
|
if resource.kind in ['Project', 'ProjectRequest']:
|
|
try:
|
|
resource.get(name, namespace)
|
|
except (NotFoundError, ForbiddenError):
|
|
return self.create_project_request(definition)
|
|
except DynamicApiError as exc:
|
|
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
|
|
error=exc.status, status=exc.status, reason=exc.reason)
|
|
|
|
try:
|
|
existing = resource.get(name=name, namespace=namespace).to_dict()
|
|
except Exception:
|
|
existing = None
|
|
|
|
if existing:
|
|
if resource.kind == 'DeploymentConfig':
|
|
if definition.get('spec', {}).get('triggers'):
|
|
definition = self.resolve_imagestream_triggers(existing, definition)
|
|
elif existing['metadata'].get('annotations', {}).get(TRIGGER_ANNOTATION):
|
|
definition = self.resolve_imagestream_trigger_annotation(existing, definition)
|
|
|
|
return super(OKDRawModule, self).perform_action(resource, definition)
|
|
|
|
@staticmethod
|
|
def get_index(desired, objects, keys):
|
|
""" Iterates over keys, returns the first object from objects where the value of the key
|
|
matches the value in desired
|
|
"""
|
|
# pylint: disable=use-a-generator
|
|
# Use a generator instead 'all(desired.get(key, True) == item.get(key, False) for key in keys)'
|
|
for i, item in enumerate(objects):
|
|
if item and all([desired.get(key, True) == item.get(key, False) for key in keys]):
|
|
return i
|
|
|
|
def resolve_imagestream_trigger_annotation(self, existing, definition):
|
|
import yaml
|
|
|
|
def get_from_fields(d, fields):
|
|
try:
|
|
return reduce(operator.getitem, fields, d)
|
|
except Exception:
|
|
return None
|
|
|
|
def set_from_fields(d, fields, value):
|
|
get_from_fields(d, fields[:-1])[fields[-1]] = value
|
|
|
|
if TRIGGER_ANNOTATION in definition['metadata'].get('annotations', {}).keys():
|
|
triggers = yaml.safe_load(definition['metadata']['annotations'][TRIGGER_ANNOTATION] or '[]')
|
|
else:
|
|
triggers = yaml.safe_load(existing['metadata'].get('annotations', '{}').get(TRIGGER_ANNOTATION, '[]'))
|
|
|
|
if not isinstance(triggers, list):
|
|
return definition
|
|
|
|
for trigger in triggers:
|
|
if trigger.get('fieldPath'):
|
|
parsed = self.parse_trigger_fieldpath(trigger['fieldPath'])
|
|
path = parsed.get('path', '').split('.')
|
|
if path:
|
|
existing_containers = get_from_fields(existing, path)
|
|
new_containers = get_from_fields(definition, path)
|
|
if parsed.get('name'):
|
|
existing_index = self.get_index({'name': parsed['name']}, existing_containers, ['name'])
|
|
new_index = self.get_index({'name': parsed['name']}, new_containers, ['name'])
|
|
elif parsed.get('index') is not None:
|
|
existing_index = new_index = int(parsed['index'])
|
|
else:
|
|
existing_index = new_index = None
|
|
if existing_index is not None and new_index is not None:
|
|
if existing_index < len(existing_containers) and new_index < len(new_containers):
|
|
set_from_fields(definition, path + [new_index, 'image'], get_from_fields(existing, path + [existing_index, 'image']))
|
|
return definition
|
|
|
|
def resolve_imagestream_triggers(self, existing, definition):
|
|
|
|
existing_triggers = existing.get('spec', {}).get('triggers')
|
|
new_triggers = definition['spec']['triggers']
|
|
existing_containers = existing.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
|
|
new_containers = definition.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
|
|
for i, trigger in enumerate(new_triggers):
|
|
if trigger.get('type') == 'ImageChange' and trigger.get('imageChangeParams'):
|
|
names = trigger['imageChangeParams'].get('containerNames', [])
|
|
for name in names:
|
|
old_container_index = self.get_index({'name': name}, existing_containers, ['name'])
|
|
new_container_index = self.get_index({'name': name}, new_containers, ['name'])
|
|
if old_container_index is not None and new_container_index is not None:
|
|
image = existing['spec']['template']['spec']['containers'][old_container_index]['image']
|
|
definition['spec']['template']['spec']['containers'][new_container_index]['image'] = image
|
|
|
|
existing_index = self.get_index(trigger['imageChangeParams'],
|
|
[x.get('imageChangeParams') for x in existing_triggers],
|
|
['containerNames'])
|
|
if existing_index is not None:
|
|
existing_image = existing_triggers[existing_index].get('imageChangeParams', {}).get('lastTriggeredImage')
|
|
if existing_image:
|
|
definition['spec']['triggers'][i]['imageChangeParams']['lastTriggeredImage'] = existing_image
|
|
existing_from = existing_triggers[existing_index].get('imageChangeParams', {}).get('from', {})
|
|
new_from = trigger['imageChangeParams'].get('from', {})
|
|
existing_namespace = existing_from.get('namespace')
|
|
existing_name = existing_from.get('name', False)
|
|
new_name = new_from.get('name', True)
|
|
add_namespace = existing_namespace and 'namespace' not in new_from.keys() and existing_name == new_name
|
|
if add_namespace:
|
|
definition['spec']['triggers'][i]['imageChangeParams']['from']['namespace'] = existing_from['namespace']
|
|
|
|
return definition
|
|
|
|
def parse_trigger_fieldpath(self, expression):
|
|
parsed = TRIGGER_CONTAINER.search(expression).groupdict()
|
|
if parsed.get('index'):
|
|
parsed['index'] = int(parsed['index'])
|
|
return parsed
|
|
|
|
def create_project_request(self, definition):
|
|
definition['kind'] = 'ProjectRequest'
|
|
result = {'changed': False, 'result': {}}
|
|
resource = self.find_resource('ProjectRequest', definition['apiVersion'], fail=True)
|
|
if not self.check_mode:
|
|
try:
|
|
k8s_obj = resource.create(definition)
|
|
result['result'] = k8s_obj.to_dict()
|
|
except DynamicApiError as exc:
|
|
self.fail_json(msg="Failed to create object: {0}".format(exc.body),
|
|
error=exc.status, status=exc.status, reason=exc.reason)
|
|
result['changed'] = True
|
|
result['method'] = 'create'
|
|
return result
|