mirror of
https://github.com/openshift/community.okd.git
synced 2026-03-27 03:13:08 +00:00
* Upgrade Ansible and OKD versions for CI * Use ubi9 and fix sanity * Use correct pip install * Try using quotes * Ensure python3.9 * Upgrade ansible and molecule versions * Remove DeploymentConfig DeploymentConfigs are deprecated and seem to now be causing idempotence problems. Replacing them with Deployments fixes it. * Attempt to fix ldap integration tests Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Move sanity and unit tests to GH actions Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Firt round of sanity fixes Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add kubernetes.core collection as sanity requirement Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add ignore-2.16.txt Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Attempt to fix units Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add ignore-2.17 Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Attempt to fix unit tests Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add pytest-ansible to test-requirements.txt Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add changelog fragment Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add workflow for ansible-lint Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Apply black Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Fix linters Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Add # fmt: skip Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Yet another round of linting Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Yet another round of linting Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Remove setup.cfg Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Revert #fmt Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Use ansible-core 2.14 Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Cleanup ansible-lint ignores Signed-off-by: Alina Buzachis <abuzachis@redhat.com> * Try using service instead of pod IP * Fix typo * Actually use the correct port * See if NetworkPolicy is preventing connection * using Pod internal IP * fix adm prune auth roles syntax * adding some retry steps * fix: openshift_builds target * add flag --force-with-deps when building downstream collection * Remove yamllint from tox linters, bump minimum python supported version to 3.9, Remove support for ansible-core < 2.14 --------- Signed-off-by: Alina Buzachis <abuzachis@redhat.com> Co-authored-by: Mike Graves <mgraves@redhat.com> Co-authored-by: Alina Buzachis <abuzachis@redhat.com>
322 lines
12 KiB
Python
322 lines
12 KiB
Python
#!/usr/bin/env python
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
import re
|
|
import operator
|
|
from functools import reduce
|
|
from ansible_collections.community.okd.plugins.module_utils.openshift_common import (
|
|
AnsibleOpenshiftModule,
|
|
)
|
|
|
|
try:
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
|
create_definitions,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
|
CoreException,
|
|
)
|
|
except ImportError:
|
|
pass
|
|
|
|
from ansible.module_utils._text import to_native
|
|
|
|
try:
|
|
from kubernetes.dynamic.exceptions import (
|
|
DynamicApiError,
|
|
NotFoundError,
|
|
ForbiddenError,
|
|
)
|
|
except ImportError as e:
|
|
pass
|
|
|
|
|
|
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(AnsibleOpenshiftModule):
|
|
def __init__(self, **kwargs):
|
|
super(OKDRawModule, self).__init__(**kwargs)
|
|
|
|
@property
|
|
def module(self):
|
|
return self._module
|
|
|
|
def execute_module(self):
|
|
results = []
|
|
changed = False
|
|
|
|
try:
|
|
definitions = create_definitions(self.params)
|
|
except Exception as e:
|
|
msg = "Failed to load resource definition: {0}".format(e)
|
|
raise CoreException(msg) from e
|
|
|
|
for definition in definitions:
|
|
result = {"changed": False, "result": {}}
|
|
warnings = []
|
|
|
|
if self.params.get("state") != "absent":
|
|
existing = None
|
|
name = definition.get("metadata", {}).get("name")
|
|
namespace = definition.get("metadata", {}).get("namespace")
|
|
if definition.get("kind") in ["Project", "ProjectRequest"]:
|
|
try:
|
|
resource = self.svc.find_resource(
|
|
kind=definition.get("kind"),
|
|
api_version=definition.get("apiVersion", "v1"),
|
|
)
|
|
existing = resource.get(
|
|
name=name, namespace=namespace
|
|
).to_dict()
|
|
except (NotFoundError, ForbiddenError):
|
|
result = self.create_project_request(definition)
|
|
changed |= result["changed"]
|
|
results.append(result)
|
|
continue
|
|
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,
|
|
)
|
|
|
|
if definition.get("kind") not in ["Project", "ProjectRequest"]:
|
|
try:
|
|
resource = self.svc.find_resource(
|
|
kind=definition.get("kind"),
|
|
api_version=definition.get("apiVersion", "v1"),
|
|
)
|
|
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
|
|
)
|
|
|
|
if self.params.get("validate") is not None:
|
|
warnings = self.validate(definition)
|
|
|
|
try:
|
|
result = self.perform_action(definition, self.params)
|
|
except Exception as e:
|
|
try:
|
|
error = e.result
|
|
except AttributeError:
|
|
error = {}
|
|
try:
|
|
error["reason"] = e.__cause__.reason
|
|
except AttributeError:
|
|
pass
|
|
error["msg"] = to_native(e)
|
|
if warnings:
|
|
error.setdefault("warnings", []).extend(warnings)
|
|
|
|
if self.params.get("continue_on_error"):
|
|
result["error"] = error
|
|
else:
|
|
self.fail_json(**error)
|
|
|
|
if warnings:
|
|
result.setdefault("warnings", []).extend(warnings)
|
|
changed |= result["changed"]
|
|
results.append(result)
|
|
|
|
if len(results) == 1:
|
|
self.exit_json(**results[0])
|
|
|
|
self.exit_json(**{"changed": changed, "result": {"results": results}})
|
|
|
|
@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.svc.find_resource(
|
|
kind="ProjectRequest", api_version=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
|