#!/usr/bin/env python from __future__ import (absolute_import, division, print_function) __metaclass__ = type from datetime import datetime, timezone, timedelta import traceback import time from ansible.module_utils._text import to_native try: from ansible_collections.kubernetes.core.plugins.module_utils.common import ( K8sAnsibleMixin, get_api_client, ) HAS_KUBERNETES_COLLECTION = True k8s_collection_import_exception = None K8S_COLLECTION_ERROR = None except ImportError as e: HAS_KUBERNETES_COLLECTION = False k8s_collection_import_exception = e K8S_COLLECTION_ERROR = traceback.format_exc() try: from kubernetes.dynamic.exceptions import DynamicApiError except ImportError as e: pass class OpenShiftBuilds(K8sAnsibleMixin): def __init__(self, module): self.module = module self.fail_json = self.module.fail_json self.exit_json = self.module.exit_json if not HAS_KUBERNETES_COLLECTION: self.module.fail_json( msg="The kubernetes.core collection must be installed", exception=K8S_COLLECTION_ERROR, error=to_native(k8s_collection_import_exception), ) super(OpenShiftBuilds, self).__init__(self.module) self.params = self.module.params self.check_mode = self.module.check_mode self.client = get_api_client(self.module) def get_build_config(self, name, namespace): params = dict( kind="BuildConfig", api_version="build.openshift.io/v1", name=name, namespace=namespace, ) result = self.kubernetes_facts(**params) return result["resources"] def prune(self): # list replicationcontroller candidate for pruning kind = 'Build' api_version = 'build.openshift.io/v1' resource = self.find_resource(kind=kind, api_version=api_version, fail=True) self.max_creation_timestamp = None keep_younger_than = self.params.get("keep_younger_than") if keep_younger_than: now = datetime.now(timezone.utc).replace(tzinfo=None) self.max_creation_timestamp = now - timedelta(minutes=keep_younger_than) def _prunable_build(build): return build["status"]["phase"] in ("Complete", "Failed", "Error", "Cancelled") def _orphan_build(build): if not _prunable_build(build): return False config = build["status"].get("config", None) if not config: return True build_config = self.get_build_config(config["name"], config["namespace"]) return len(build_config) == 0 def _younger_build(build): if not self.max_creation_timestamp: return False creation_timestamp = datetime.strptime(build['metadata']['creationTimestamp'], '%Y-%m-%dT%H:%M:%SZ') return creation_timestamp < self.max_creation_timestamp predicates = [ _prunable_build, ] if self.params.get("orphans"): predicates.append(_orphan_build) if self.max_creation_timestamp: predicates.append(_younger_build) # Get ReplicationController params = dict( kind=kind, api_version=api_version, namespace=self.params.get("namespace"), ) result = self.kubernetes_facts(**params) candidates = result["resources"] for pred in predicates: candidates = list(filter(pred, candidates)) if self.check_mode: changed = len(candidates) > 0 self.exit_json(changed=changed, builds=candidates) changed = False for build in candidates: changed = True try: name = build["metadata"]["name"] namespace = build["metadata"]["namespace"] resource.delete(name=name, namespace=namespace, body={}) except DynamicApiError as exc: msg = "Failed to delete Build %s/%s due to: %s" % (namespace, name, exc.body) self.fail_json(msg=msg, status=exc.status, reason=exc.reason) except Exception as e: msg = "Failed to delete Build %s/%s due to: %s" % (namespace, name, to_native(e)) self.fail_json(msg=msg, error=to_native(e), exception=e) self.exit_json(changed=changed, builds=candidates) def clone_build(self, name, namespace, request): try: result = self.client.request( method="POST", path="/apis/build.openshift.io/v1/namespaces/{namespace}/builds/{name}/clone".format( namespace=namespace, name=name ), body=request, content_type="application/json", ) return result.to_dict() except DynamicApiError as exc: msg = "Failed to clone Build %s/%s due to: %s" % (namespace, name, exc.body) self.fail_json(msg=msg, status=exc.status, reason=exc.reason) except Exception as e: msg = "Failed to clone Build %s/%s due to: %s" % (namespace, name, to_native(e)) self.fail_json(msg=msg, error=to_native(e), exception=e) def instantiate_build_config(self, name, namespace, request): try: result = self.client.request( method="POST", path="/apis/build.openshift.io/v1/namespaces/{namespace}/buildconfigs/{name}/instantiate".format( namespace=namespace, name=name ), body=request, content_type="application/json", ) return result.to_dict() except DynamicApiError as exc: msg = "Failed to instantiate BuildConfig %s/%s due to: %s" % (namespace, name, exc.body) self.fail_json(msg=msg, status=exc.status, reason=exc.reason) except Exception as e: msg = "Failed to instantiate BuildConfig %s/%s due to: %s" % (namespace, name, to_native(e)) self.fail_json(msg=msg, error=to_native(e), exception=e) def start_build(self): result = None name = self.params.get("build_config_name") if not name: name = self.params.get("build_name") build_request = { "kind": "BuildRequest", "apiVersion": "build.openshift.io/v1", "metadata": { "name": name }, "triggeredBy": [ {"message": "Manually triggered"} ], } # Overrides incremental incremental = self.params.get("incremental") if incremental is not None: build_request.update( { "sourceStrategyOptions": { "incremental": incremental } } ) # Environment variable if self.params.get("env_vars"): build_request.update( { "env": self.params.get("env_vars") } ) # Docker strategy option if self.params.get("build_args"): build_request.update( { "dockerStrategyOptions": { "buildArgs": self.params.get("build_args") }, } ) # caching option no_cache = self.params.get("no_cache") if no_cache is not None: build_request.update( { "dockerStrategyOptions": { "noCache": no_cache }, } ) # commit if self.params.get("commit"): build_request.update( { "revision": { "git": { "commit": self.params.get("commit") } } } ) if self.params.get("build_config_name"): # Instantiate Build from Build config result = self.instantiate_build_config( name=self.params.get("build_config_name"), namespace=self.params.get("namespace"), request=build_request ) else: # Re-run Build result = self.clone_build( name=self.params.get("build_name"), namespace=self.params.get("namespace"), request=build_request ) if result and self.params.get("wait"): start = datetime.now() def _total_wait_time(): return (datetime.now() - start).seconds wait_timeout = self.params.get("wait_timeout") wait_sleep = self.params.get("wait_sleep") last_status_phase = None while _total_wait_time() < wait_timeout: params = dict( kind=result["kind"], api_version=result["apiVersion"], name=result["metadata"]["name"], namespace=result["metadata"]["namespace"], ) facts = self.kubernetes_facts(**params) if len(facts["resources"]) > 0: last_status_phase = facts["resources"][0]["status"]["phase"] if last_status_phase == "Complete": result = facts["resources"][0] break elif last_status_phase in ("Cancelled", "Error", "Failed"): self.fail_json( msg="Unexpected status for Build %s/%s: %s" % ( result["metadata"]["name"], result["metadata"]["namespace"], last_status_phase ) ) time.sleep(wait_sleep) if last_status_phase != "Complete": name = result["metadata"]["name"] namespace = result["metadata"]["namespace"] msg = "Build %s/%s has not complete after %d second(s)," \ "current status is %s" % (namespace, name, wait_timeout, last_status_phase) self.fail_json(msg=msg) result = [result] if result else [] self.exit_json(changed=True, builds=result) def cancel_build(self, restart): kind = 'Build' api_version = 'build.openshift.io/v1' namespace = self.params.get("namespace") phases = ["new", "pending", "running"] if len(self.params.get("build_phases")): phases = [p.lower() for p in self.params.get("build_phases")] names = [] if self.params.get("build_name"): names.append(self.params.get("build_name")) else: build_config = self.params.get("build_config_name") # list all builds from namespace params = dict( kind=kind, api_version=api_version, namespace=namespace ) resources = self.kubernetes_facts(**params).get("resources", []) def _filter_builds(build): config = build["metadata"].get("labels", {}).get("openshift.io/build-config.name") return config in build_config for item in list(filter(_filter_builds, resources)): name = item["metadata"]["name"] if name not in names: names.append(name) if len(names) == 0: self.exit_json(changed=False, msg="No Build found from namespace %s" % namespace) warning = [] builds_to_cancel = [] for name in names: params = dict( kind=kind, api_version=api_version, name=name, namespace=namespace ) resource = self.kubernetes_facts(**params).get("resources", []) if len(resource) == 0: warning.append("Build %s/%s not found" % (namespace, name)) continue resource = resource[0] phase = resource["status"].get("phase").lower() # Build status.phase is matching the requested state and is not completed if phase in phases: builds_to_cancel.append(resource) else: warning.append("build %s/%s is not in expected phase, found %s" % (namespace, name, phase)) changed = False result = [] for build in builds_to_cancel: # Set cancelled to true build["status"]["cancelled"] = True name = build["metadata"]["name"] changed = True try: content_type = "application/json" cancelled_build = self.client.request( "PUT", "/apis/build.openshift.io/v1/namespaces/{0}/builds/{1}".format( namespace, name ), body=build, content_type=content_type, ).to_dict() result.append(cancelled_build) except DynamicApiError as exc: self.fail_json( msg="Failed to cancel Build %s/%s due to: %s" % (namespace, name, exc), reason=exc.reason, status=exc.status ) except Exception as e: self.fail_json( msg="Failed to cancel Build %s/%s due to: %s" % (namespace, name, e) ) # Make sure the build phase is really cancelled. def _wait_until_cancelled(build, wait_timeout, wait_sleep): start = datetime.now() last_phase = None name = build["metadata"]["name"] while (datetime.now() - start).seconds < wait_timeout: params = dict( kind=kind, api_version=api_version, name=name, namespace=namespace ) resource = self.kubernetes_facts(**params).get("resources", []) if len(resource) == 0: return None, "Build %s/%s not found" % (namespace, name) resource = resource[0] last_phase = resource["status"]["phase"] if last_phase == "Cancelled": return resource, None time.sleep(wait_sleep) return None, "Build %s/%s is not cancelled as expected, current state is %s" % (namespace, name, last_phase) if result and self.params.get("wait"): wait_timeout = self.params.get("wait_timeout") wait_sleep = self.params.get("wait_sleep") wait_result = [] for build in result: ret, err = _wait_until_cancelled(build, wait_timeout, wait_sleep) if err: self.exit_json(msg=err) wait_result.append(ret) result = wait_result if restart: self.start_build() self.exit_json(builds=result, changed=changed) def execute_module(self): state = self.params.get("state") if state == "started": self.start_build() else: restart = bool(state == "restarted") self.cancel_build(restart=restart)