diff --git a/README.md b/README.md index eba181a3b..7abd08d66 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,17 @@ The custom configuration options for the Celery workers are listed below: * `iib_ocp_opm_mapping` - the dictionary mapping of OCP version to OPM version indicating the OPM version to be used for the corresponding OCP version like `{"v4.15": "opm-v1.28.0"}` +* `iib_konflux_cluster_url` - the URL of the Konflux OpenShift cluster to access for Tekton PipelineRuns + (e.g. `https://api.konflux.example.com:6443`). This is required for cross-cluster access to Konflux. +* `iib_konflux_cluster_token` - the authentication token for accessing the Konflux OpenShift cluster. + This should be a service account token with appropriate permissions to access Tekton PipelineRuns. +* `iib_konflux_cluster_ca_cert` - the CA certificate for the Konflux OpenShift cluster. This can be + either a file path to the certificate or the certificate content as a string. This is required + for secure cross-cluster access. +* `iib_konflux_namespace` - the namespace in the Konflux cluster where Tekton PipelineRuns are located. + This is required when using Konflux configuration. +* `iib_konflux_pipeline_timeout` - the timeout in seconds for monitoring Konflux PipelineRuns. + This defaults to `1800` seconds (30 minutes). If you wish to configure AWS S3 bucket for storing artifact files, the following **environment variables** diff --git a/iib/workers/config.py b/iib/workers/config.py index 55dbf1d73..7e56f62ff 100644 --- a/iib/workers/config.py +++ b/iib/workers/config.py @@ -127,6 +127,13 @@ class Config(object): # The minimal version of OPM which requires setting the --migrate-level flag for migrate iib_opm_new_migrate_version = "v1.46.0" + # Konflux configuration for cross-cluster access + iib_konflux_cluster_url: Optional[str] = None + iib_konflux_cluster_token: Optional[str] = None + iib_konflux_cluster_ca_cert: Optional[str] = None + iib_konflux_namespace: Optional[str] = None + iib_konflux_pipeline_timeout: int = 1800 + class ProductionConfig(Config): """The production IIB Celery configuration.""" @@ -326,6 +333,7 @@ def validate_celery_config(conf: app.utils.Settings, **kwargs) -> None: _validate_multiple_opm_mapping(conf['iib_ocp_opm_mapping']) _validate_iib_org_customizations(conf['iib_organization_customizations']) + _validate_konflux_config(conf) if conf.get('iib_aws_s3_bucket_name'): if not isinstance(conf['iib_aws_s3_bucket_name'], str): @@ -481,6 +489,63 @@ def _validate_iib_org_customizations( ) +def _validate_konflux_config(conf: app.utils.Settings) -> None: + """ + Validate Konflux configuration variables. + + :param celery.app.utils.Settings conf: the Celery application configuration to validate + :raises iib.exceptions.ConfigError: if the configuration is invalid + """ + konflux_url = conf.get('iib_konflux_cluster_url') + konflux_token = conf.get('iib_konflux_cluster_token') + konflux_ca_cert = conf.get('iib_konflux_cluster_ca_cert') + konflux_namespace = conf.get('iib_konflux_namespace') + + if any([konflux_url, konflux_token, konflux_ca_cert, konflux_namespace]): + _validate_konflux_fields(konflux_url, konflux_token, konflux_ca_cert, konflux_namespace) + + +def _validate_konflux_fields( + konflux_url: Optional[str], + konflux_token: Optional[str], + konflux_ca_cert: Optional[str], + konflux_namespace: Optional[str], +) -> None: + """ + Validate Konflux configuration fields for presence, types, and formats. + + :param str konflux_url: The Kubernetes cluster API URL + :param str konflux_token: The authentication token for the cluster + :param str konflux_ca_cert: The CA certificate for SSL verification + :param str konflux_namespace: The namespace for Konflux operations + :raises iib.exceptions.ConfigError: if the configuration is invalid + """ + if ( + not konflux_url + or not isinstance(konflux_url, str) + or not konflux_url.startswith('https://') + ): + raise ConfigError( + 'iib_konflux_cluster_url must be a valid HTTPS URL when using Konflux configuration' + ) + if not konflux_token or not isinstance(konflux_token, str): + raise ConfigError( + 'iib_konflux_cluster_token must be a string when using Konflux configuration' + ) + if not konflux_ca_cert or not isinstance(konflux_ca_cert, str): + raise ConfigError( + 'iib_konflux_cluster_ca_cert must be a string when using Konflux configuration' + ) + if ( + not konflux_namespace + or not isinstance(konflux_namespace, str) + or not konflux_namespace.strip() + ): + raise ConfigError( + 'iib_konflux_namespace must be a non-empty string when using Konflux configuration' + ) + + def get_worker_config() -> app.utils.Settings: """Return the Celery configuration.""" # Import this here to avoid a circular import diff --git a/iib/workers/tasks/konflux_utils.py b/iib/workers/tasks/konflux_utils.py new file mode 100644 index 000000000..65f053f0b --- /dev/null +++ b/iib/workers/tasks/konflux_utils.py @@ -0,0 +1,336 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +import logging +import os +import time +from typing import List, Dict, Any, Optional + +from kubernetes import client +from kubernetes.client.rest import ApiException + +from iib.exceptions import IIBError +from iib.workers.config import get_worker_config + +__all__ = ['find_pipelinerun', 'wait_for_pipeline_completion'] + +log = logging.getLogger(__name__) + +# Global variables for Kubernetes client and configuration +_v1_client: Optional[client.CustomObjectsApi] = None + + +def _get_kubernetes_client() -> client.CustomObjectsApi: + """ + Get or create a Kubernetes CustomObjectsApi client for cross-cluster access. + + :return: Configured Kubernetes CustomObjectsApi client + :rtype: client.CustomObjectsApi + :raises IIBError: If unable to create Kubernetes client or configuration is missing + """ + global _v1_client + + if _v1_client is not None: + return _v1_client + + try: + _v1_client = _create_kubernetes_client() + return _v1_client + except IIBError: + # Re-raise IIBError as-is (like CA certificate requirement) + raise + except Exception as e: + # Log error without exposing sensitive information + error_msg = f"Failed to initialize Kubernetes client: {type(e).__name__}" + log.error(error_msg) + raise IIBError(error_msg) + + +def _create_kubernetes_client() -> client.CustomObjectsApi: + """ + Create a new Kubernetes client with cross-cluster configuration. + + :return: Configured Kubernetes CustomObjectsApi client + :rtype: client.CustomObjectsApi + :raises IIBError: If Konflux configuration is missing or invalid + """ + worker_config = get_worker_config() + + # Get cross-cluster configuration (validation is done in config.py) + target_cluster_url = getattr(worker_config, 'iib_konflux_cluster_url', None) + target_cluster_token = getattr(worker_config, 'iib_konflux_cluster_token', None) + target_cluster_ca_cert = getattr(worker_config, 'iib_konflux_cluster_ca_cert', None) + + # If no Konflux configuration is provided, raise an error + if not target_cluster_url or not target_cluster_token or not target_cluster_ca_cert: + raise IIBError( + "Konflux configuration is required. Please set " + "iib_konflux_cluster_url, iib_konflux_cluster_token, and " + "iib_konflux_cluster_ca_cert in IIB worker configuration." + ) + + log.info("Configuring Kubernetes client for cross-cluster access to %s", target_cluster_url) + + configuration = _create_kubernetes_configuration( + target_cluster_url, target_cluster_token, target_cluster_ca_cert + ) + + return client.CustomObjectsApi(client.ApiClient(configuration)) + + +def _create_kubernetes_configuration(url: str, token: str, ca_cert: str) -> client.Configuration: + """ + Create Kubernetes configuration with authentication and SSL settings. + + :param str url: The Kubernetes cluster API URL + :param str token: The authentication token for the cluster + :param str ca_cert: The CA certificate for SSL verification (file path or content) + :return: Configured Kubernetes Configuration object + :rtype: client.Configuration + """ + configuration = client.Configuration() + configuration.host = url + configuration.api_key_prefix['authorization'] = 'Bearer' + configuration.api_key['authorization'] = token + + # If CA cert is provided as a string, write it to a temp file + if not os.path.isfile(ca_cert): + import tempfile + + # TODO: Clean up the temp file once the request is completed + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.crt') as f: + f.write(ca_cert) + ca_cert = f.name + + configuration.ssl_ca_cert = ca_cert + return configuration + + +def find_pipelinerun(commit_sha: str) -> List[Dict[str, Any]]: + """ + Find the Konflux pipelinerun triggered by the git commit. + + :param str commit_sha: The git commit SHA to search for + :return: List of pipelinerun objects matching the commit SHA + :rtype: List[Dict[str, Any]] + :raises IIBError: If there's an error fetching pipelineruns + """ + try: + log.info("Searching for pipelineruns with commit SHA: %s", commit_sha) + + v1_client = _get_kubernetes_client() + worker_config = get_worker_config() + namespace = worker_config.iib_konflux_namespace + + runs = v1_client.list_namespaced_custom_object( + group="tekton.dev", + version="v1", + namespace=namespace, + plural="pipelineruns", + label_selector=f"pipelinesascode.tekton.dev/sha={commit_sha}", + ) + + items = runs.get("items", []) + log.info("Found %s pipelinerun(s) for commit %s", len(items), commit_sha) + + return items + + except ApiException as e: + error_msg = f"Failed to fetch pipelineruns for commit {commit_sha}: API error {e.status}" + log.error("Kubernetes API error while fetching pipelineruns: %s - %s", e.status, e.reason) + raise IIBError(error_msg) + except Exception as e: + error_msg = ( + f"Unexpected error while fetching pipelineruns for commit {commit_sha}: " + f"{type(e).__name__}" + ) + log.error("Unexpected error while fetching pipelineruns: %s", type(e).__name__) + raise IIBError(error_msg) + + +def wait_for_pipeline_completion(pipelinerun_name: str, timeout: Optional[int] = None) -> None: + """ + Poll the status of a tekton Pipelinerun and wait for completion. + + Handles all Tekton PipelineRun status reasons: + - Success: Succeeded, Completed + - Failure: Failed, PipelineRunTimeout, CreateRunFailed, status=False + - Cancellation: Cancelled + + :param str pipelinerun_name: Name of the pipelinerun to monitor + :param int timeout: Maximum time to wait in seconds (default: from config) + :raises IIBError: If the pipelinerun fails, is cancelled, or times out + """ + if timeout is None: + worker_config = get_worker_config() + timeout = getattr(worker_config, 'iib_konflux_pipeline_timeout', 1800) + + log.info("Starting to monitor pipelinerun: %s", pipelinerun_name) + start_time = time.time() + + while True: + try: + _check_timeout(pipelinerun_name, start_time, timeout) + run = _fetch_pipelinerun_status(pipelinerun_name) + + if _handle_pipelinerun_completion(pipelinerun_name, run): + return + + time.sleep(30) + + except ApiException as e: + error_msg = f"Failed to monitor pipelinerun {pipelinerun_name}: API error {e.status}" + log.error( + "Kubernetes API error while monitoring pipelinerun %s: %s - %s", + pipelinerun_name, + e.status, + e.reason, + ) + raise IIBError(error_msg) + except IIBError as e: + log.error("IIBError while monitoring pipelinerun %s: %s", pipelinerun_name, e) + # Re-raise IIBError as-is + raise + except Exception as e: + error_msg = ( + f"Unexpected error while monitoring pipelinerun {pipelinerun_name}: " + f"{type(e).__name__}" + ) + log.error( + "Unexpected error while monitoring pipelinerun %s: %s", + pipelinerun_name, + type(e).__name__, + ) + raise IIBError(error_msg) + + +def _check_timeout(pipelinerun_name: str, start_time: float, timeout: int) -> None: + """ + Check if the timeout has been exceeded for pipelinerun monitoring. + + :param str pipelinerun_name: Name of the pipelinerun being monitored + :param float start_time: The start time of monitoring (from time.time()) + :param int timeout: Maximum time to wait in seconds + :raises IIBError: If the timeout has been exceeded + """ + elapsed_time = time.time() - start_time + if elapsed_time > timeout: + raise IIBError( + f"Timeout waiting for pipelinerun {pipelinerun_name} to complete " + f"after {timeout} seconds" + ) + + +def _fetch_pipelinerun_status(pipelinerun_name: str) -> Dict[str, Any]: + """ + Fetch the current status of the pipelinerun from Kubernetes. + + :param str pipelinerun_name: Name of the pipelinerun to fetch + :return: Dictionary containing the pipelinerun status information + :rtype: Dict[str, Any] + :raises ApiException: If there's an error accessing the Kubernetes API + """ + v1_client = _get_kubernetes_client() + worker_config = get_worker_config() + namespace = worker_config.iib_konflux_namespace + + return v1_client.get_namespaced_custom_object( + group="tekton.dev", + version="v1", + namespace=namespace, + plural="pipelineruns", + name=pipelinerun_name, + ) + + +def _handle_pipelinerun_completion(pipelinerun_name: str, run: Dict[str, Any]) -> bool: + """ + Handle pipelinerun completion status and return True if completed. + + :return: True if pipelinerun completed (success or failure), False if still running + :rtype: bool + :raises IIBError: If the pipelinerun failed or was cancelled + """ + status = run.get("status", {}) + conditions = status.get("conditions", []) + + if not conditions: + log.info("Pipelinerun %s is still initializing...", pipelinerun_name) + return False + + condition = conditions[0] if conditions else {} + reason = condition.get("reason", "Unknown") + condition_type = condition.get("type", "Unknown") + status_value = condition.get("status", "Unknown") + message = condition.get("message", "") + + log.info( + "Pipelinerun %s status: reason=%s, type=%s, status=%s", + pipelinerun_name, + reason, + condition_type, + status_value, + ) + if message: + log.info("Pipelinerun %s message: %s", pipelinerun_name, message) + + if _is_pipelinerun_successful(reason): + log.info("Pipelinerun %s completed successfully", pipelinerun_name) + return True + + _is_pipelinerun_cancelled(reason, pipelinerun_name) + _is_pipelinerun_failed(reason, status_value, message, pipelinerun_name) + + # Still running + log.info("Pipelinerun %s is still running... (reason: %s)", pipelinerun_name, reason) + return False + + +def _is_pipelinerun_successful(reason: str) -> bool: + """ + Check if pipelinerun completed successfully. + + :param str reason: The reason from the pipelinerun condition + :return: True if the pipelinerun completed successfully, False otherwise + :rtype: bool + """ + return reason in ("Succeeded", "Completed") + + +def _is_pipelinerun_failed( + reason: str, status_value: str, message: str, pipelinerun_name: str +) -> None: + """ + Check if pipelinerun failed and raise appropriate error. + + :param str reason: The reason from the pipelinerun condition + :param str status_value: The status value from the pipelinerun condition + :param str message: The message from the pipelinerun condition + :param str pipelinerun_name: Name of the pipelinerun + :raises IIBError: If the pipelinerun failed with appropriate error message + """ + if reason in ("Failed", "PipelineRunTimeout", "CreateRunFailed"): + error_msg = f"Pipelinerun {pipelinerun_name} failed" + if reason == "PipelineRunTimeout": + error_msg += " due to timeout" + elif reason == "CreateRunFailed": + error_msg += " due to resource creation failure" + elif message: + error_msg += f": {message}" + raise IIBError(error_msg) + + if status_value == "False": + error_msg = f"Pipelinerun {pipelinerun_name} failed" + if message: + error_msg += f": {message}" + raise IIBError(error_msg) + + +def _is_pipelinerun_cancelled(reason: str, pipelinerun_name: str) -> None: + """ + Check if pipelinerun was cancelled and raise appropriate error. + + :param str reason: The reason from the pipelinerun condition + :param str pipelinerun_name: Name of the pipelinerun + :raises IIBError: If the pipelinerun was cancelled + """ + if reason == "Cancelled": + raise IIBError(f"Pipelinerun {pipelinerun_name} was cancelled") diff --git a/requirements-test.txt b/requirements-test.txt index 6745ded50..503cb7990 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -39,6 +39,12 @@ botocore==1.40.44 \ # -r requirements.txt # boto3 # s3transfer +cachetools==6.2.0 \ + --hash=sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6 \ + --hash=sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32 + # via + # -r requirements.txt + # google-auth celery==5.5.3 \ --hash=sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525 \ --hash=sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5 @@ -48,6 +54,7 @@ certifi==2025.8.3 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 # via # -r requirements.txt + # kubernetes # requests cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ @@ -426,7 +433,13 @@ dogpile-cache==1.4.1 \ --hash=sha256:99130ce990800c8d89c26a5a8d9923cbe1b78c8a9972c2aaa0abf3d2ef2984ad \ --hash=sha256:e25c60e677a5e28ff86124765fbf18c53257bcd7830749cd5ba350ace2a12989 # via -r requirements.txt -Flask==3.1.2 \ +durationpy==0.10 \ + --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ + --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 + # via + # -r requirements.txt + # kubernetes +flask==3.1.2 \ --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \ --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c # via @@ -448,6 +461,12 @@ flask-sqlalchemy==3.1.1 \ # via # -r requirements.txt # flask-migrate +google-auth==2.41.1 \ + --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ + --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 + # via + # -r requirements.txt + # kubernetes googleapis-common-protos==1.70.0 \ --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 @@ -669,13 +688,19 @@ krb5==0.8.0 \ # via # -r requirements.txt # pyspnego +kubernetes==34.1.0 \ + --hash=sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912 \ + --hash=sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a + # via + # -r requirements-test.in + # -r requirements.txt mako==1.3.10 \ --hash=sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28 \ --hash=sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59 # via # -r requirements.txt # alembic -MarkupSafe==3.0.3 \ +markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ @@ -771,6 +796,12 @@ MarkupSafe==3.0.3 \ # jinja2 # mako # werkzeug +oauthlib==3.3.1 \ + --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ + --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 + # via + # -r requirements.txt + # requests-oauthlib opentelemetry-api==1.37.0 \ --hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \ --hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47 @@ -1005,13 +1036,26 @@ psycopg2-binary==2.9.10 \ --hash=sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567 \ --hash=sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863 # via -r requirements.txt +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 + # via + # -r requirements.txt + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 \ + --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ + --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 + # via + # -r requirements.txt + # google-auth pycparser==2.23 \ --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 # via # -r requirements.txt # cffi -Pygments==2.19.2 \ +pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b # via pytest @@ -1038,6 +1082,7 @@ python-dateutil==2.9.0.post0 \ # -r requirements.txt # botocore # celery + # kubernetes python-memcached==1.62 \ --hash=sha256:0285470599b7f593fbf3bec084daa1f483221e68c1db2cf1d846a9f7c2655103 \ --hash=sha256:1bdd8d2393ff53e80cd5e9442d750e658e0b35c3eebb3211af137303e3b729d1 @@ -1047,17 +1092,108 @@ python-qpid-proton==0.40.0 \ --hash=sha256:a19d8c71c908700ceb38f6cbc1eb4a039428570f96bfc2caeeafdfec804fb94f \ --hash=sha256:fe56211c6dcc7ea1fb9d78a017208a4c08043cd901780b6602a74ff70f38bf1f # via -r requirements.txt +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via + # -r requirements.txt + # kubernetes requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via # -r requirements.txt + # kubernetes # opentelemetry-exporter-otlp-proto-http # requests-kerberos + # requests-oauthlib requests-kerberos==0.15.0 \ --hash=sha256:437512e424413d8113181d696e56694ffa4259eb9a5fc4e803926963864eaf4e \ --hash=sha256:ba9b0980b8489c93bfb13854fd118834e576d6700bfea3745cb2e62278cd16a6 # via -r requirements.txt +requests-oauthlib==2.0.0 \ + --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \ + --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9 + # via + # -r requirements.txt + # kubernetes +rsa==4.9.1 \ + --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ + --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 + # via + # -r requirements.txt + # google-auth ruamel-yaml==0.18.15 \ --hash=sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701 \ --hash=sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700 @@ -1138,8 +1274,9 @@ six==1.17.0 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via # -r requirements.txt + # kubernetes # python-dateutil -SQLAlchemy==2.0.43 \ +sqlalchemy==2.0.43 \ --hash=sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019 \ --hash=sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7 \ --hash=sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c \ @@ -1230,12 +1367,13 @@ tzdata==2025.2 \ # via # -r requirements.txt # kombu -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via # -r requirements.txt # botocore + # kubernetes # requests vine==5.1.0 \ --hash=sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc \ @@ -1251,6 +1389,12 @@ wcwidth==0.2.14 \ # via # -r requirements.txt # prompt-toolkit +websocket-client==1.8.0 \ + --hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \ + --hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da + # via + # -r requirements.txt + # kubernetes werkzeug==3.1.3 \ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 diff --git a/requirements.txt b/requirements.txt index 582331aad..4dd9ab01d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,10 @@ botocore==1.40.44 \ # via # boto3 # s3transfer +cachetools==6.2.0 \ + --hash=sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6 \ + --hash=sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32 + # via google-auth celery==5.5.3 \ --hash=sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525 \ --hash=sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5 @@ -37,7 +41,9 @@ celery==5.5.3 \ certifi==2025.8.3 \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 - # via requests + # via + # kubernetes + # requests cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ @@ -294,7 +300,11 @@ dogpile-cache==1.4.1 \ --hash=sha256:99130ce990800c8d89c26a5a8d9923cbe1b78c8a9972c2aaa0abf3d2ef2984ad \ --hash=sha256:e25c60e677a5e28ff86124765fbf18c53257bcd7830749cd5ba350ace2a12989 # via iib (setup.py) -Flask==3.1.2 \ +durationpy==0.10 \ + --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ + --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 + # via kubernetes +flask==3.1.2 \ --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \ --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c # via @@ -316,6 +326,10 @@ flask-sqlalchemy==3.1.1 \ # via # flask-migrate # iib (setup.py) +google-auth==2.41.1 \ + --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ + --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 + # via kubernetes googleapis-common-protos==1.70.0 \ --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 @@ -513,11 +527,15 @@ krb5==0.8.0 \ --hash=sha256:daaf580cf563a2435cc889d4a0692e02c5788e1eb91f0246d56114cf4f08ba1c \ --hash=sha256:e372a77e4eed6a7c17ce0fff9f52f160c036c89b76d5b8cf2754af1464d4eb34 # via pyspnego +kubernetes==34.1.0 \ + --hash=sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912 \ + --hash=sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a + # via iib (setup.py) mako==1.3.10 \ --hash=sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28 \ --hash=sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59 # via alembic -MarkupSafe==3.0.3 \ +markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ @@ -612,6 +630,10 @@ MarkupSafe==3.0.3 \ # jinja2 # mako # werkzeug +oauthlib==3.3.1 \ + --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ + --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 + # via requests-oauthlib opentelemetry-api==1.37.0 \ --hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \ --hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47 @@ -826,6 +848,16 @@ psycopg2-binary==2.9.10 \ --hash=sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567 \ --hash=sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863 # via iib (setup.py) +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 \ + --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ + --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 + # via google-auth pycparser==2.23 \ --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 @@ -840,6 +872,7 @@ python-dateutil==2.9.0.post0 \ # via # botocore # celery + # kubernetes python-memcached==1.62 \ --hash=sha256:0285470599b7f593fbf3bec084daa1f483221e68c1db2cf1d846a9f7c2655103 \ --hash=sha256:1bdd8d2393ff53e80cd5e9442d750e658e0b35c3eebb3211af137303e3b729d1 @@ -848,18 +881,103 @@ python-qpid-proton==0.40.0 \ --hash=sha256:7680d607cf6e9684f97bf5b2ba16cda7d8512aab9e4ff78f98d44a4644fc819a \ --hash=sha256:a19d8c71c908700ceb38f6cbc1eb4a039428570f96bfc2caeeafdfec804fb94f \ --hash=sha256:fe56211c6dcc7ea1fb9d78a017208a4c08043cd901780b6602a74ff70f38bf1f - # via -r requirements.txt + # via iib (setup.py) +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via kubernetes requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via # iib (setup.py) + # kubernetes # opentelemetry-exporter-otlp-proto-http # requests-kerberos + # requests-oauthlib requests-kerberos==0.15.0 \ --hash=sha256:437512e424413d8113181d696e56694ffa4259eb9a5fc4e803926963864eaf4e \ --hash=sha256:ba9b0980b8489c93bfb13854fd118834e576d6700bfea3745cb2e62278cd16a6 # via iib (setup.py) +requests-oauthlib==2.0.0 \ + --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \ + --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9 + # via kubernetes +rsa==4.9.1 \ + --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ + --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 + # via google-auth ruamel-yaml==0.18.15 \ --hash=sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701 \ --hash=sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700 @@ -936,8 +1054,10 @@ semver==3.0.4 \ six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via python-dateutil -SQLAlchemy==2.0.43 \ + # via + # kubernetes + # python-dateutil +sqlalchemy==2.0.43 \ --hash=sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019 \ --hash=sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7 \ --hash=sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c \ @@ -1023,11 +1143,12 @@ tzdata==2025.2 \ --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 # via kombu -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via # botocore + # kubernetes # requests vine==5.1.0 \ --hash=sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc \ @@ -1040,6 +1161,10 @@ wcwidth==0.2.14 \ --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 # via prompt-toolkit +websocket-client==1.8.0 \ + --hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \ + --hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da + # via kubernetes werkzeug==3.1.3 \ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 diff --git a/setup.py b/setup.py index e545be076..24109e4f3 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ 'flask-migrate', 'flask-sqlalchemy', 'importlib-resources', + 'kubernetes', 'operator-manifest==0.0.5', 'psycopg2-binary', 'python-memcached ', diff --git a/tests/test_workers/test_config.py b/tests/test_workers/test_config.py index 88bc6493c..f48933577 100644 --- a/tests/test_workers/test_config.py +++ b/tests/test_workers/test_config.py @@ -9,7 +9,7 @@ import pytest from iib.exceptions import ConfigError -from iib.workers.config import configure_celery, validate_celery_config +from iib.workers.config import configure_celery, validate_celery_config, _validate_konflux_config @patch('os.path.isfile', return_value=False) @@ -401,3 +401,122 @@ def test_validate_celery_config_iib_opm_ocp_mapping_opm_not_exist(mock_pe, tmpdi } with pytest.raises(ConfigError, match='opm-not-exist is not installed'): validate_celery_config(worker_config) + + +def test_validate_konflux_config_no_config(): + """Test validation when no Konflux config is provided.""" + conf = mock.Mock() + conf.get.return_value = None + + # Should not raise any error when no config is provided + _validate_konflux_config(conf) + + +@pytest.mark.parametrize( + 'url,token,ca_cert,namespace,expected_error', + [ + # Partial config scenarios + ( + None, + 'test-token', + '/path/to/ca.crt', + 'iib-tenant', + 'iib_konflux_cluster_url must be a valid HTTPS URL when using Konflux configuration', + ), + ( + 'https://api.example.com:6443', + None, + '/path/to/ca.crt', + 'iib-tenant', + 'iib_konflux_cluster_token must be a string when using Konflux configuration', + ), + ( + 'https://api.example.com:6443', + 'test-token', + None, + 'iib-tenant', + 'iib_konflux_cluster_ca_cert must be a string when using Konflux configuration', + ), + ( + 'https://api.example.com:6443', + 'test-token', + '/path/to/ca.crt', + None, + 'iib_konflux_namespace must be a non-empty string when using Konflux configuration', + ), + # Invalid URL + ( + 'http://api.example.com:6443', + 'test-token', + '/path/to/ca.crt', + 'iib-tenant', + 'iib_konflux_cluster_url must be a valid HTTPS URL when using Konflux configuration', + ), + # Invalid token type + ( + 'https://api.example.com:6443', + 123, + '/path/to/ca.crt', + 'iib-tenant', + 'iib_konflux_cluster_token must be a string when using Konflux configuration', + ), + # Invalid CA cert type + ( + 'https://api.example.com:6443', + 'test-token', + 123, + 'iib-tenant', + 'iib_konflux_cluster_ca_cert must be a string when using Konflux configuration', + ), + # Invalid namespace type + ( + 'https://api.example.com:6443', + 'test-token', + '/path/to/ca.crt', + 123, + 'iib_konflux_namespace must be a non-empty string when using Konflux configuration', + ), + # Empty namespace + ( + 'https://api.example.com:6443', + 'test-token', + '/path/to/ca.crt', + '', + 'iib_konflux_namespace must be a non-empty string when using Konflux configuration', + ), + # Whitespace-only namespace + ( + 'https://api.example.com:6443', + 'test-token', + '/path/to/ca.crt', + ' ', + 'iib_konflux_namespace must be a non-empty string when using Konflux configuration', + ), + ], +) +def test_validate_konflux_config_failure_scenarios(url, token, ca_cert, namespace, expected_error): + """Test Konflux configuration validation failure scenarios.""" + conf = mock.Mock() + conf.get.side_effect = lambda key: { + 'iib_konflux_cluster_url': url, + 'iib_konflux_cluster_token': token, + 'iib_konflux_cluster_ca_cert': ca_cert, + 'iib_konflux_namespace': namespace, + }.get(key) + + with pytest.raises(ConfigError, match=expected_error): + _validate_konflux_config(conf) + + +def test_validate_konflux_config_valid_config(): + """Test validation with valid configuration.""" + conf = mock.Mock() + conf.get.side_effect = lambda key: { + 'iib_konflux_cluster_url': 'https://api.example.com:6443', + 'iib_konflux_cluster_token': 'test-token', + 'iib_konflux_cluster_ca_cert': '/path/to/ca.crt', + 'iib_konflux_namespace': 'iib-tenant', + }.get(key) + + # Should not raise any error with valid config + _validate_konflux_config(conf) diff --git a/tests/test_workers/test_tasks/test_konflux_utils.py b/tests/test_workers/test_tasks/test_konflux_utils.py new file mode 100644 index 000000000..aa9e1fe88 --- /dev/null +++ b/tests/test_workers/test_tasks/test_konflux_utils.py @@ -0,0 +1,596 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +import pytest +import tempfile +import os +from unittest.mock import Mock, patch +from kubernetes.client.rest import ApiException +from kubernetes import client + +from iib.exceptions import IIBError +from iib.workers.tasks.konflux_utils import ( + find_pipelinerun, + wait_for_pipeline_completion, + _get_kubernetes_client, + _create_kubernetes_client, + _create_kubernetes_configuration, +) + + +def setup_function(): + """Reset the global client before each test.""" + import iib.workers.tasks.konflux_utils + + iib.workers.tasks.konflux_utils._v1_client = None + + +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_find_pipelinerun_success(mock_get_worker_config, mock_get_client): + """Test successful pipelinerun search.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + expected_runs = { + "items": [{"metadata": {"name": "pipelinerun-1"}}, {"metadata": {"name": "pipelinerun-2"}}] + } + mock_client.list_namespaced_custom_object.return_value = expected_runs + + # Test + result = find_pipelinerun("abc123") + + # Verify + assert result == expected_runs["items"] + mock_client.list_namespaced_custom_object.assert_called_once_with( + group="tekton.dev", + version="v1", + namespace="iib-tenant", + plural="pipelineruns", + label_selector="pipelinesascode.tekton.dev/sha=abc123", + ) + + +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_find_pipelinerun_empty_result(mock_get_worker_config, mock_get_client): + """Test pipelinerun search with empty results.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + mock_client.list_namespaced_custom_object.return_value = {"items": []} + + # Test + result = find_pipelinerun("abc123") + + # Verify + assert result == [] + + +@pytest.mark.parametrize( + "exception,expected_error", + [ + ( + ApiException(status=401, reason="Unauthorized"), + "Failed to fetch pipelineruns for commit abc123: API error 401", + ), + ( + Exception("Network error"), + "Unexpected error while fetching pipelineruns for commit abc123: Exception", + ), + ], +) +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_find_pipelinerun_exceptions( + mock_get_worker_config, mock_get_client, exception, expected_error +): + """Test pipelinerun search with various exceptions.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + mock_client.list_namespaced_custom_object.side_effect = exception + + # Test & Verify + with pytest.raises(IIBError, match=expected_error): + find_pipelinerun("abc123") + + +@pytest.mark.parametrize( + "reason,condition_type,status,should_succeed", + [ + ("Succeeded", "Succeeded", "True", True), + ("Completed", "Succeeded", "True", True), + ], +) +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_wait_for_pipeline_completion_success_cases( + mock_get_worker_config, mock_get_client, reason, condition_type, status, should_succeed +): + """Test waiting for pipelinerun completion with success scenarios.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + run_status = { + "status": { + "conditions": [ + { + "reason": reason, + "type": condition_type, + "status": status, + "message": f"Tasks completed with {reason}", + } + ] + } + } + mock_client.get_namespaced_custom_object.return_value = run_status + + # Test + wait_for_pipeline_completion("test-pipelinerun") + + # Verify + mock_client.get_namespaced_custom_object.assert_called_once_with( + group="tekton.dev", + version="v1", + namespace="iib-tenant", + plural="pipelineruns", + name="test-pipelinerun", + ) + + +@pytest.mark.parametrize( + "reason,expected_error_msg", + [ + ("Failed", "Pipelinerun test-pipelinerun failed"), + ("PipelineRunTimeout", "Pipelinerun test-pipelinerun failed due to timeout"), + ("CreateRunFailed", "Pipelinerun test-pipelinerun failed due to resource creation failure"), + ("Cancelled", "Pipelinerun test-pipelinerun was cancelled"), + ], +) +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_wait_for_pipeline_completion_failure_cases( + mock_get_worker_config, mock_get_client, reason, expected_error_msg +): + """Test waiting for pipelinerun completion with failure scenarios.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + run_status = { + "status": { + "conditions": [ + { + "reason": reason, + "type": "Succeeded", + "status": "False" if reason != "Cancelled" else "False", + "message": f"PipelineRun {reason.lower()}", + } + ] + } + } + mock_client.get_namespaced_custom_object.return_value = run_status + + # Test & Verify + with pytest.raises(IIBError, match=expected_error_msg): + wait_for_pipeline_completion("test-pipelinerun") + + +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_wait_for_pipeline_completion_status_false(mock_get_worker_config, mock_get_client): + """Test waiting for pipelinerun completion with status=False.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + run_status = { + "status": { + "conditions": [ + { + "reason": "SomeError", + "type": "Succeeded", + "status": "False", + "message": "Some error occurred", + } + ] + } + } + mock_client.get_namespaced_custom_object.return_value = run_status + + # Test & Verify + with pytest.raises(IIBError, match="Pipelinerun test-pipelinerun failed: Some error occurred"): + wait_for_pipeline_completion("test-pipelinerun") + + +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +@patch('iib.workers.tasks.konflux_utils.time.sleep') +def test_wait_for_pipeline_completion_still_running( + mock_sleep, mock_get_worker_config, mock_get_client +): + """Test waiting for pipelinerun completion when still running.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + # First call: still running, second call: succeeded + run_status_running = { + "status": { + "conditions": [ + { + "reason": "Running", + "type": "Succeeded", + "status": "Unknown", + "message": "PipelineRun is running", + } + ] + } + } + run_status_succeeded = { + "status": { + "conditions": [ + { + "reason": "Succeeded", + "type": "Succeeded", + "status": "True", + "message": "Tasks completed successfully", + } + ] + } + } + mock_client.get_namespaced_custom_object.side_effect = [ + run_status_running, + run_status_succeeded, + ] + + # Test + wait_for_pipeline_completion("test-pipelinerun") + + # Verify + assert mock_client.get_namespaced_custom_object.call_count == 2 + mock_sleep.assert_called_once_with(30) + + +@pytest.mark.parametrize( + "timeout_scenario,time_values,expected_error", + [ + ( + "timeout_exceeded", + [0, 1801], + "Timeout waiting for pipelinerun test-pipelinerun to complete after 1800 seconds", + ), + ( + "no_conditions", + [0, 30, 1801], + "Timeout waiting for pipelinerun test-pipelinerun to complete after 1800 seconds", + ), + ], +) +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +@patch('iib.workers.tasks.konflux_utils.time.sleep') +@patch('iib.workers.tasks.konflux_utils.time.time') +@patch('iib.workers.tasks.konflux_utils.log') +def test_wait_for_pipeline_completion_timeout_scenarios( + mock_log, + mock_time, + mock_sleep, + mock_get_worker_config, + mock_get_client, + timeout_scenario, + time_values, + expected_error, +): + """Test waiting for pipelinerun completion with timeout scenarios.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + # Mock time to simulate timeout + mock_time.side_effect = time_values + + if timeout_scenario == "timeout_exceeded": + run_status = { + "status": { + "conditions": [ + { + "reason": "Running", + "type": "Succeeded", + "status": "Unknown", + "message": "PipelineRun is running", + } + ] + } + } + else: # no_conditions + run_status = {"status": {"conditions": []}} + + mock_client.get_namespaced_custom_object.return_value = run_status + + # Test & Verify + with pytest.raises(IIBError, match=expected_error): + wait_for_pipeline_completion("test-pipelinerun", timeout=1800) + + +@pytest.mark.parametrize( + "exception,expected_error", + [ + ( + ApiException(status=404, reason="Not Found"), + "Failed to monitor pipelinerun test-pipelinerun: API error 404", + ), + ( + Exception("Network error"), + "Unexpected error while monitoring pipelinerun test-pipelinerun: Exception", + ), + ], +) +@patch('iib.workers.tasks.konflux_utils._get_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_wait_for_pipeline_completion_exceptions( + mock_get_worker_config, mock_get_client, exception, expected_error +): + """Test waiting for pipelinerun completion with various exceptions.""" + # Setup + mock_client = Mock() + mock_get_client.return_value = mock_client + + mock_config = Mock() + mock_config.iib_konflux_namespace = 'iib-tenant' + mock_config.iib_konflux_pipeline_timeout = 1800 + mock_get_worker_config.return_value = mock_config + + mock_client.get_namespaced_custom_object.side_effect = exception + + # Test & Verify + with pytest.raises(IIBError, match=expected_error): + wait_for_pipeline_completion("test-pipelinerun") + + +@patch('iib.workers.tasks.konflux_utils._create_kubernetes_client') +def test_get_kubernetes_client_caching(mock_create_client): + """Test that _get_kubernetes_client caches the client.""" + # Setup + mock_client = Mock() + mock_create_client.return_value = mock_client + + # Reset global client + import iib.workers.tasks.konflux_utils + + iib.workers.tasks.konflux_utils._v1_client = None + + # Test - first call should create client + result1 = _get_kubernetes_client() + assert result1 == mock_client + assert mock_create_client.call_count == 1 + + # Test - second call should return cached client + result2 = _get_kubernetes_client() + assert result2 == mock_client + assert mock_create_client.call_count == 1 # Should not be called again + + +@pytest.mark.parametrize( + "exception,expected_error,should_log", + [ + (IIBError("Original IIBError message"), "Original IIBError message", False), + ( + ValueError("Some unexpected error"), + "Failed to initialize Kubernetes client: ValueError", + True, + ), + ], +) +@patch('iib.workers.tasks.konflux_utils._create_kubernetes_client') +@patch('iib.workers.tasks.konflux_utils.log') +def test_get_kubernetes_client_exception_handling( + mock_log, mock_create_client, exception, expected_error, should_log +): + """Test that _get_kubernetes_client handles different types of exceptions.""" + # Setup + mock_create_client.side_effect = exception + + # Reset global client + import iib.workers.tasks.konflux_utils + + iib.workers.tasks.konflux_utils._v1_client = None + + # Test & Verify + with pytest.raises(IIBError, match=expected_error) as exc_info: + _get_kubernetes_client() + + # For IIBError, ensure it's the same exception object (re-raised, not wrapped) + if isinstance(exception, IIBError): + assert exc_info.value is exception + + # Verify logging only for general exceptions + if should_log: + mock_log.error.assert_called_once_with("Failed to initialize Kubernetes client: ValueError") + else: + mock_log.error.assert_not_called() + + +@pytest.mark.parametrize( + "url,token,ca_cert,description", + [ + (None, 'test-token', '/path/to/ca.crt', 'missing URL'), + ('https://api.example.com:6443', None, '/path/to/ca.crt', 'missing token'), + ('https://api.example.com:6443', 'test-token', None, 'missing CA cert'), + ], +) +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +def test_create_kubernetes_client_missing_config( + mock_get_worker_config, url, token, ca_cert, description +): + """Test _create_kubernetes_client with missing configuration.""" + # Setup + mock_config = Mock() + mock_config.iib_konflux_cluster_url = url + mock_config.iib_konflux_cluster_token = token + mock_config.iib_konflux_cluster_ca_cert = ca_cert + mock_get_worker_config.return_value = mock_config + + # Test & Verify + with pytest.raises(IIBError, match="Konflux configuration is required"): + _create_kubernetes_client() + + +@patch('iib.workers.tasks.konflux_utils.client.CustomObjectsApi') +@patch('iib.workers.tasks.konflux_utils._create_kubernetes_configuration') +@patch('iib.workers.tasks.konflux_utils.get_worker_config') +@patch('iib.workers.tasks.konflux_utils.log') +def test_create_kubernetes_client_success( + mock_log, mock_get_worker_config, mock_create_config, mock_custom_objects_api +): + """Test successful _create_kubernetes_client.""" + # Setup + mock_config = Mock() + mock_config.iib_konflux_cluster_url = 'https://api.example.com:6443' + mock_config.iib_konflux_cluster_token = 'test-token' + mock_config.iib_konflux_cluster_ca_cert = '/path/to/ca.crt' + mock_get_worker_config.return_value = mock_config + + # Create a real configuration object to avoid mock issues + from kubernetes import client + + real_config = client.Configuration() + real_config.host = 'https://api.example.com:6443' + real_config.api_key_prefix['authorization'] = 'Bearer' + real_config.api_key['authorization'] = 'test-token' + real_config.ssl_ca_cert = '/path/to/ca.crt' + mock_create_config.return_value = real_config + + mock_client = Mock() + mock_custom_objects_api.return_value = mock_client + + # Test + result = _create_kubernetes_client() + + # Verify + assert result == mock_client + mock_create_config.assert_called_once_with( + 'https://api.example.com:6443', 'test-token', '/path/to/ca.crt' + ) + mock_custom_objects_api.assert_called_once() + mock_log.info.assert_called_once_with( + "Configuring Kubernetes client for cross-cluster access to %s", + 'https://api.example.com:6443', + ) + + +def test_create_kubernetes_configuration_with_file_path(): + """Test _create_kubernetes_configuration with existing file path.""" + # Setup + with tempfile.NamedTemporaryFile(mode='w', suffix='.crt', delete=False) as f: + f.write('test-cert-content') + ca_cert_path = f.name + + try: + # Test + config = _create_kubernetes_configuration( + 'https://api.example.com:6443', 'test-token', ca_cert_path + ) + + # Verify + assert isinstance(config, client.Configuration) + assert config.host == 'https://api.example.com:6443' + assert config.api_key_prefix['authorization'] == 'Bearer' + assert config.api_key['authorization'] == 'test-token' + assert config.ssl_ca_cert == ca_cert_path + + finally: + # Cleanup + os.unlink(ca_cert_path) + + +@pytest.mark.parametrize( + "ca_cert_input,expected_ssl_ca_cert,should_create_temp_file,description", + [ + ('test-cert-content-as-string', '/tmp/temp_cert_123.crt', True, 'string content'), + ('/existing/path/ca.crt', '/existing/path/ca.crt', False, 'existing file path'), + ], +) +@patch('iib.workers.tasks.konflux_utils.os.path.isfile') +@patch('tempfile.NamedTemporaryFile') +def test_create_kubernetes_configuration_ca_cert_handling( + mock_tempfile, + mock_isfile, + ca_cert_input, + expected_ssl_ca_cert, + should_create_temp_file, + description, +): + """Test _create_kubernetes_configuration with different CA cert scenarios.""" + # Setup + mock_isfile.return_value = not should_create_temp_file + + if should_create_temp_file: + mock_temp_file = Mock() + mock_temp_file.name = '/tmp/temp_cert_123.crt' + mock_tempfile.return_value.__enter__.return_value = mock_temp_file + + # Test + config = _create_kubernetes_configuration( + 'https://api.example.com:6443', 'test-token', ca_cert_input + ) + + # Verify + assert isinstance(config, client.Configuration) + assert config.host == 'https://api.example.com:6443' + assert config.api_key_prefix['authorization'] == 'Bearer' + assert config.api_key['authorization'] == 'test-token' + assert config.ssl_ca_cert == expected_ssl_ca_cert + + # Verify file existence check + mock_isfile.assert_called_once_with(ca_cert_input) + + # Verify temp file creation if needed + if should_create_temp_file: + mock_tempfile.assert_called_once_with(mode='w', delete=False, suffix='.crt') + mock_temp_file.write.assert_called_once_with(ca_cert_input) + else: + mock_tempfile.assert_not_called()