From 6bbe27a314d42b8d07f98b94c00bc072caebc66d Mon Sep 17 00:00:00 2001 From: Tiago Seabra Date: Fri, 6 Dec 2024 16:44:57 +0000 Subject: [PATCH] feat: release changes for version 0.4.10 --- galaxy/api/__init__.py | 9 +- .../routes.py | 22 +- galaxy/core/mapper.py | 123 ++++++---- .../integrations/flux/.rely/automations.json | 1 + .../integrations/flux/.rely/blueprints.json | 221 ++++++++++++++++++ galaxy/integrations/flux/.rely/mappings.yaml | 82 +++++++ galaxy/integrations/flux/README.md | 7 + galaxy/integrations/flux/client.py | 130 +++++++++++ galaxy/integrations/flux/config.yaml | 13 ++ galaxy/integrations/flux/main.py | 70 ++++++ galaxy/integrations/flux/models.py | 65 ++++++ galaxy/integrations/flux/routes.py | 53 +++++ galaxy/integrations/flux/tests/conftest.py | 0 galaxy/integrations/flux/tests/test_flux.py | 0 galaxy/integrations/gitlab/main.py | 5 +- poetry.lock | 132 +++++++---- pyproject.toml | 1 + 17 files changed, 828 insertions(+), 106 deletions(-) create mode 100644 galaxy/integrations/flux/.rely/automations.json create mode 100644 galaxy/integrations/flux/.rely/blueprints.json create mode 100644 galaxy/integrations/flux/.rely/mappings.yaml create mode 100644 galaxy/integrations/flux/README.md create mode 100644 galaxy/integrations/flux/client.py create mode 100644 galaxy/integrations/flux/config.yaml create mode 100644 galaxy/integrations/flux/main.py create mode 100644 galaxy/integrations/flux/models.py create mode 100644 galaxy/integrations/flux/routes.py create mode 100644 galaxy/integrations/flux/tests/conftest.py create mode 100644 galaxy/integrations/flux/tests/test_flux.py diff --git a/galaxy/api/__init__.py b/galaxy/api/__init__.py index 92e1289..fabd39a 100644 --- a/galaxy/api/__init__.py +++ b/galaxy/api/__init__.py @@ -1,3 +1,4 @@ +import copy import importlib import logging from datetime import datetime @@ -12,7 +13,7 @@ from fastapi import Depends, FastAPI, HTTPException from galaxy.core.galaxy import Integration, run_integration -from galaxy.core.logging import get_log_format +from galaxy.core.logging import get_log_format, get_magneto_logs from galaxy.core.magneto import Magneto from galaxy.core.mapper import Mapper from galaxy.core.models import SchedulerJobStates @@ -56,8 +57,12 @@ def job_listener(event): scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_SUBMITTED | EVENT_JOB_REMOVED) async def _run_integration(): + if (log_handler := get_magneto_logs(logger)) is not None: + log_handler.logs.clear() + + fresh_instance = copy.deepcopy(instance) async with Magneto(instance.config.rely.url, instance.config.rely.token, logger=logger) as magneto_client: - success = await run_integration(instance, magneto_client=magneto_client, logger=app.state.logger) + success = await run_integration(fresh_instance, magneto_client=magneto_client, logger=logger) if success: logger.info("Integration %r run completed successfully: %r", instance.type_, instance.id_) else: diff --git a/galaxy/cli/cookiecutter/{{cookiecutter.integration_name}}/routes.py b/galaxy/cli/cookiecutter/{{cookiecutter.integration_name}}/routes.py index bc6a270..85252c8 100644 --- a/galaxy/cli/cookiecutter/{{cookiecutter.integration_name}}/routes.py +++ b/galaxy/cli/cookiecutter/{{cookiecutter.integration_name}}/routes.py @@ -1,11 +1,11 @@ +from logging import Logger + from fastapi import APIRouter, Depends from galaxy.core.magneto import Magneto from galaxy.core.mapper import Mapper from galaxy.core.utils import get_mapper, get_logger, get_magneto_client -import logging - router = APIRouter(prefix="/{{cookiecutter.integration_name}}", tags=["{{cookiecutter.integration_name}}"]) @@ -13,9 +13,17 @@ async def {{cookiecutter.integration_name}}_webhook( event: dict, mapper: Mapper = Depends(get_mapper), - logger: logging = Depends(get_logger), + logger: Logger = Depends(get_logger), magneto_client: Magneto = Depends(get_magneto_client), -) -> dict: - entity = await mapper.process("entities", [event]) - logger.info(f"Received entity: {entity}") - return {"message": "received gitlab webhook"} +) -> None: + try: + entity, *_ = await mapper.process("entities", [event]) + logger.info("Received entity: %s", entity) + except Exception: + ... + return + + try: + await magneto_client.upsert_entity(entity) + except Exception: + ... diff --git a/galaxy/core/mapper.py b/galaxy/core/mapper.py index aef3cdc..79ecddd 100644 --- a/galaxy/core/mapper.py +++ b/galaxy/core/mapper.py @@ -1,4 +1,3 @@ -import asyncio import re from typing import Any @@ -6,50 +5,62 @@ import yaml from galaxy.core.resources import load_integration_resource +from galaxy.utils.concurrency import run_in_thread -__all__ = ["Mapper"] +__all__ = ["Mapper", "MapperError", "MapperNotFoundError", "MapperCompilationError"] class Mapper: + MAPPINGS_FILE_PATH: str = ".rely/mappings.yaml" + def __init__(self, integration_name: str): self.integration_name = integration_name self.id_allowed_chars = "[^a-zA-Z0-9-]" - async def _load_mapping(self, mapping_kind: str) -> list[dict]: - mappings = yaml.safe_load(load_integration_resource(self.integration_name, ".rely/mappings.yaml")) - return [mapping for mapping in mappings.get("resources") if mapping["kind"] == mapping_kind] - - def _compile_mappings(self, mapping: dict) -> dict: - compiled_mapping = {} - for key, value in mapping.items(): - if isinstance(value, dict): - compiled_mapping[key] = self._compile_mappings(value) - elif isinstance(value, list): - compiled_mapping[key] = [ - self._compile_mappings(item) if isinstance(item, dict) else item for item in value - ] - else: - try: - compiled_mapping[key] = jq.compile(value) if isinstance(value, str) else value - except Exception as e: - raise Exception(f"Error compiling maps for key {key} with expression {value}: {e}") + self._mappings: dict[str, dict[str, Any]] | None = None + self._compiled_mappings: dict[str, dict[str, Any]] = {} + + @property + def mappings(self) -> dict[str, dict[str, Any]]: + if self._mappings is None: + mappings = yaml.safe_load(load_integration_resource(self.integration_name, self.MAPPINGS_FILE_PATH)) + self._mappings = {mapping["kind"]: mapping["mappings"] for mapping in mappings.get("resources") or []} + return self._mappings + + def get_compiled_mappings(self, mapping_kind: str) -> list[Any]: + if mapping_kind not in self._compiled_mappings: + try: + self._compiled_mappings[mapping_kind] = self._compile_mappings(self.mappings.get(mapping_kind) or {}) + except Exception as e: + raise MapperCompilationError(mapping_kind) from e + return self._compiled_mappings[mapping_kind] + + def _compile_mappings(self, item: Any) -> Any: + if isinstance(item, dict): + return {key: self._compile_mappings(value) for key, value in item.items()} + if isinstance(item, list | tuple | set): + return [self._compile_mappings(value) for value in item] + if isinstance(item, str): + try: + return jq.compile(item) + except Exception as e: + raise Exception(f"Error compiling maps with expression {item}: {e}") from e + return item + + def _map_data(self, compiled_mapping: Any, context: dict[str, Any]) -> Any: + if isinstance(compiled_mapping, dict): + return {key: self._map_data(value, context) for key, value in compiled_mapping.items()} + if isinstance(compiled_mapping, list): + return [self._map_data(item, context) for item in compiled_mapping] + if isinstance(compiled_mapping, jq._Program): + try: + return compiled_mapping.input(context).first() + except Exception as e: + raise Exception(f"Error mapping with expression {compiled_mapping} and payload {compiled_mapping}: {e}") return compiled_mapping - def _map_entity(self, compiled_mapping: dict, json_data: dict) -> dict: - entity = {} - - for key, value in compiled_mapping.items(): - if isinstance(value, dict): - entity[key] = self._map_entity(value, json_data) - elif isinstance(value, list): - entity[key] = [self._map_entity(item, json_data) if isinstance(item, dict) else item for item in value] - else: - try: - entity[key] = value.input(json_data).first() if isinstance(value, jq._Program) else value - except Exception as e: - raise Exception(f"Error mapping key {key} with expression {value} and payload {json_data}: {e}") - - return self._sanitize(entity) + def _map_entity(self, compiled_mapping: dict, json_data: dict[str, Any]) -> dict: + return self._sanitize(self._map_data(compiled_mapping, json_data)) def _replace_non_matching_characters(self, input_string: str, regex_pattern: str) -> str: res = re.sub(regex_pattern, ".", input_string) @@ -77,21 +88,31 @@ def _sanitize(self, entity: dict) -> dict: return entity - async def process(self, mapping_kind: str, json_data: list[dict], context=None) -> tuple[Any]: - try: - mappings = await self._load_mapping(mapping_kind) - if not mappings: - raise Exception(f"Unknown Mapper {mapping_kind}") - compiled_mappings = self._compile_mappings(mappings[0]["mappings"]) + def process_sync(self, mapping_kind: str, json_data: list[dict], context: Any | None = None) -> list[Any]: + mappings = self.get_compiled_mappings(mapping_kind) + if not mappings: + raise MapperNotFoundError(mapping_kind) + return [self._map_entity(mappings, {**each, "context": context}) for each in json_data] - loop = asyncio.get_running_loop() + async def process(self, mapping_kind: str, json_data: list[dict], context: Any | None = None) -> tuple[Any]: + # There is no advantage in using async here as all the work is done in a thread. + # Keeping it as async for now to avoid breaking existing code that calls this as `await mapper.process(...)`. + return await run_in_thread(self.process_sync, mapping_kind, json_data, context) - entities = await asyncio.gather( - *[ - loop.run_in_executor(None, self._map_entity, compiled_mappings, {**each, "context": context}) - for each in json_data - ] - ) - return entities - except Exception as e: - raise e + +class MapperError(Exception): + """Base class for Mapper errors.""" + + +class MapperNotFoundError(MapperError): + """Mapper not found error.""" + + def __init__(self, mapping_kind: str): + super().__init__(f"Unknown Mapper {mapping_kind}") + + +class MapperCompilationError(MapperError): + """Mapper compilation error.""" + + def __init__(self, mapping_kind: str): + super().__init__(f"Error compiling mappings for kind {mapping_kind}") diff --git a/galaxy/integrations/flux/.rely/automations.json b/galaxy/integrations/flux/.rely/automations.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/galaxy/integrations/flux/.rely/automations.json @@ -0,0 +1 @@ +[] diff --git a/galaxy/integrations/flux/.rely/blueprints.json b/galaxy/integrations/flux/.rely/blueprints.json new file mode 100644 index 0000000..98c06f0 --- /dev/null +++ b/galaxy/integrations/flux/.rely/blueprints.json @@ -0,0 +1,221 @@ +[ + { + "id": "flux.v1.kubernetes_cluster", + "title": "Kubernetes Cluster", + "description": "Blueprint defining a Kubernetes Cluster", + "icon": "kubernetes", + "schemaProperties": { + "title": "blueprint properties", + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + } + }, + "isHideable": false + }, + { + "id": "flux.v1.kubernetes_namespace", + "title": "Kubernetes Namespace", + "description": "Blueprint defining a Kubernetes Namespace", + "icon": "kubernetes", + "schemaProperties": { + "title": "blueprint properties", + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "labels": { + "type": "object", + "title": "Labels", + "additionalProperties": true + } + } + }, + "relations": { + "cluster": { + "value": "flux.v1.kubernetes_cluster", + "title": "Kubernetes cluster", + "description": "The cluster this namespace is associated to" + } + }, + "isHideable": false + }, + { + "id": "flux.v1.source", + "title": "Flux Source", + "description": "Blueprint defining a Flux Source", + "icon": "flux", + "schemaProperties": { + "title": "blueprint properties", + "type": "object", + "properties": { + "sourceType": { + "type": "string", + "title": "Source Type", + "enum": [ + "Bucket", + "GitRepository", + "HelmChart", + "HelmRepository", + "OCIRepository" + ] + }, + "sourceUrl": { + "type": "string", + "title": "Source URL" + }, + "interval": { + "type": "string", + "title": "Interval" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "branch": { + "type": "string", + "title": "Branch" + }, + "tag": { + "type": "string", + "title": "Tag" + } + } + }, + "relations": { + "namespace": { + "value": "flux.v1.kubernetes_namespace", + "title": "Kubernetes namespace", + "description": "The namespace this flux source is associated to" + } + }, + "isHideable": false + }, + { + "id": "flux.v1.application", + "title": "Flux Application", + "description": "Blueprint defining a Flux Application", + "icon": "flux", + "schemaProperties": { + "title": "blueprint properties", + "type": "object", + "properties": { + "applicationType": { + "type": "string", + "title": "Application type", + "enum": [ + "Kustomization", + "HelmRelease" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "title": "Created at" + }, + "lastTransitionTime": { + "type": "string", + "format": "date-time", + "title": "Last transition time" + }, + "ready": { + "type": "string", + "title": "Ready status", + "enum": [ + "True", + "False", + "Unknown" + ] + }, + "healthy": { + "type": "string", + "title": "Health status", + "enum": [ + "True", + "False", + "Unknown" + ] + }, + "interval": { + "type": "string", + "title": "Interval" + }, + "lastAppliedRevision": { + "type": "string", + "title": "Last applied revision" + } + } + }, + "relations": { + "namespace": { + "value": "flux.v1.kubernetes_namespace", + "title": "Namespace", + "description": "The namespace this flux application is associated to" + }, + "targetNamespace": { + "value": "flux.v1.kubernetes_namespace", + "title": "Target Namespace", + "description": "The namespace this application is applied to" + }, + "source": { + "value": "flux.v1.source", + "title": "Source", + "description": "The Flux Source this application refers to" + } + }, + "isHideable": false + }, + { + "id": "flux.v1.pipeline", + "title": "Flux Pipeline", + "description": "Blueprint defining a Flux Pipeline", + "icon": "flux", + "schemaProperties": { + "title": "blueprint properties", + "type": "object", + "properties": { + "kind": { + "type": "string", + "title": "Kind", + "enum": [ + "Kustomization", + "HelmRelease" + ] + }, + "status": { + "type": "string", + "title": "Status" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "message": { + "type": "string", + "title": "Message" + }, + "revision": { + "type": "string", + "title": "Revision" + } + } + }, + "relations": { + "application": { + "value": "flux.v1.application", + "title": "Flux Application", + "description": "The application this flux pipeline is associated to" + } + }, + "isHideable": false + } +] diff --git a/galaxy/integrations/flux/.rely/mappings.yaml b/galaxy/integrations/flux/.rely/mappings.yaml new file mode 100644 index 0000000..810b278 --- /dev/null +++ b/galaxy/integrations/flux/.rely/mappings.yaml @@ -0,0 +1,82 @@ +resources: + - kind: kubernetes_cluster + mappings: + id: .metadata.uid + title: '"Kubernetes cluster " + .metadata.uid' + blueprintId: '"flux.v1.kubernetes_cluster"' + description: '""' + properties: + createdAt: .metadata.creation_timestamp + + - kind: kubernetes_namespace + mappings: + id: .metadata.uid + title: .metadata.name + blueprintId: '"flux.v1.kubernetes_namespace"' + description: '""' + properties: + createdAt: .metadata.creation_timestamp + labels: .metadata.labels + relations: + cluster: + value: .context.cluster_id + + - kind: source + mappings: + id: .metadata.uid + title: .metadata.name + blueprintId: '"flux.v1.source"' + description: '""' + properties: + sourceType: .kind + sourceUrl: .spec.url + interval: .spec.interval + createdAt: .metadata.creationTimestamp + branch: .spec.ref.branch + tag: .spec.ref.tag + relations: + namespace: + value: .context.namespaces[.metadata.namespace] + + - kind: application + mappings: + id: .metadata.uid + title: .metadata.name + blueprintId: '"flux.v1.application"' + description: '""' + properties: + applicationType: .kind + createdAt: .metadata.creationTimestamp + lastTransitionTime: '(.status.conditions[] | select(.type == "Ready")) // {} | .lastTransitionTime' + ready: '(.status.conditions[] | select(.type == "Ready")) // {} | .status' + healthy: '(.status.conditions[] | select(.type == "Healthy")) // {} | .status' + lastAppliedRevision: .status.lastAppliedRevision + interval: .spec.interval + relations: + namespace: + value: .context.namespaces[.metadata.namespace] + targetNamespace: + value: .context.namespaces[(.spec.targetNamespace // .metadata.namespace)] + source: + value: | + if .kind == "HelmRelease" then + try .context.sources[(.spec.chart.spec.sourceRef.namespace // .metadata.namespace) + "-" + .spec.chart.spec.sourceRef.kind + "-" + .spec.chart.spec.sourceRef.name] catch null + else + try .context.sources[(.spec.sourceRef.namespace // .metadata.namespace) + "-" + .spec.sourceRef.kind + "-" + .spec.sourceRef.name] catch null + end + + - kind: pipeline + mappings: + id: '.involved_object.uid + "-" + .metadata.revision' + title: '.involved_object.uid + "-" + .metadata.revision' + blueprintId: '"flux.v1.pipeline"' + description: '""' + properties: + kind: .involved_object.kind + status: .context.status + timestamp: .timestamp + message: .message + revision: .metadata.revision + relations: + application: + value: .involved_object.uid diff --git a/galaxy/integrations/flux/README.md b/galaxy/integrations/flux/README.md new file mode 100644 index 0000000..d8c09dd --- /dev/null +++ b/galaxy/integrations/flux/README.md @@ -0,0 +1,7 @@ +# Galaxy Framework - Integration: Flux + +This integration is responsible for retrieving Flux-related data. + +### Configuration + +TODO: Proper k8s API auth configuration diff --git a/galaxy/integrations/flux/client.py b/galaxy/integrations/flux/client.py new file mode 100644 index 0000000..2a5eaa3 --- /dev/null +++ b/galaxy/integrations/flux/client.py @@ -0,0 +1,130 @@ +import datetime +import functools +import itertools +from types import TracebackType +from typing import Any + +from kubernetes import client as k8s_client, config as k8s_config +from kubernetes.client import ApiClient +from kubernetes.config.config_exception import ConfigException + +__all__ = ["FluxClient"] + + +def encode_dt(obj): + """Workaround while mapper can't handle datetime values.""" + if isinstance(obj, datetime.datetime): + return obj.isoformat() + elif isinstance(obj, dict): + return {k: encode_dt(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [encode_dt(v) for v in obj] + else: + return obj + + +class FluxClient: + PAGE_SIZE: int = 100 + + def __init__(self, config, logger): + self.config = config + self.logger = logger + + self._client: ApiClient | None = None + self._configure_client() + + async def __aenter__(self) -> "FluxClient": + self._client = ApiClient() + return self + + async def __aexit__(self, exc_type: type, exc: Exception, tb: TracebackType) -> None: + await self.close() + + async def close(self) -> None: + if self._client is not None: + self._client.close() + + @property + def client(self) -> ApiClient: + if self._client is None: + raise ValueError("Kubernetes client has not been created") + + return self._client + + def _configure_client(self) -> None: + attempts = [(k8s_config.load_incluster_config, "in-cluster"), (k8s_config.load_kube_config, "local kube")] + for load_config_func, name in attempts: + try: + load_config_func() + self.logger.debug("Successfully loaded %s configuration", name) + return + except ConfigException: + continue + + raise RuntimeError("Unable to load kubernetes client configuration") + + async def _fetch_list_data( + self, api_instance: k8s_client.CoreV1Api | k8s_client.CustomObjectsApi, method: str, *args: Any, **kwargs: Any + ) -> list[dict]: + kwargs = {"limit": self.PAGE_SIZE} | kwargs + func = functools.partial(getattr(api_instance, method), *args, **kwargs) + + items, _continue = [], None + while True: + response = func() if not _continue else func(_continue=_continue) + + if isinstance(response, dict): + _continue = response["metadata"]["continue"] + else: + _continue = response._metadata._continue + response = encode_dt(response.to_dict()) + + items.extend(response["items"]) + + if not _continue: + break + + return items + + async def get_cluster(self) -> dict: + api_instance = k8s_client.CoreV1Api(self.client) + response = api_instance.read_namespace("kube-system") + return encode_dt(response.to_dict()) + + async def get_namespaces(self, exclude_system: bool) -> list[dict]: + api_instance = k8s_client.CoreV1Api(self.client) + items = await self._fetch_list_data(api_instance, "list_namespace") + + if not exclude_system: + return items + + to_exclude = {"kube-node-lease", "kube-public", "kube-system"} + filtered_namespaces = [ns for ns in items if ns["metadata"]["name"] not in to_exclude] + return filtered_namespaces + + async def get_sources(self) -> list[dict]: + api_instance = k8s_client.CustomObjectsApi(self.client) + crds = [ + ("source.toolkit.fluxcd.io", "v1beta2", "buckets"), + ("source.toolkit.fluxcd.io", "v1beta2", "gitrepositories"), + ("source.toolkit.fluxcd.io", "v1beta2", "helmcharts"), + ("source.toolkit.fluxcd.io", "v1beta2", "helmrepositories"), + ("source.toolkit.fluxcd.io", "v1beta2", "ocirepositories"), + ] + + items = itertools.chain.from_iterable( + [await self._fetch_list_data(api_instance, "list_cluster_custom_object", *crd) for crd in crds] + ) + return list(items) + + async def get_applications(self) -> list[dict]: + api_instance = k8s_client.CustomObjectsApi(self.client) + crds = [ + ("kustomize.toolkit.fluxcd.io", "v1beta2", "kustomizations"), + ("helm.toolkit.fluxcd.io", "v2beta2", "helmreleases"), + ] + + items = itertools.chain.from_iterable( + [await self._fetch_list_data(api_instance, "list_cluster_custom_object", *crd) for crd in crds] + ) + return list(items) diff --git a/galaxy/integrations/flux/config.yaml b/galaxy/integrations/flux/config.yaml new file mode 100644 index 0000000..1a71832 --- /dev/null +++ b/galaxy/integrations/flux/config.yaml @@ -0,0 +1,13 @@ +rely: + token: "{{ env('RELY_API_TOKEN') }}" + url: "{{ env('RELY_API_URL') }}" + +integration: + id: "{{ env('RELY_INTEGRATION_ID') }}" + type: "flux" + executionType: "{{ env('RELY_INTEGRATION_EXECUTION_TYPE') | default('cronjob', true) }}" + + scheduledInterval: "{{ env('RELY_INTEGRATION_DAEMON_INTERVAL') | default(60, true) | int }}" + defaultModelMappings: {} + properties: + excludeSystemNamespaces: "{{ env('RELY_INTEGRATION_FLUX_EXCLUDE_SYSTEM_NAMESPACES') | default('true', true) }}" diff --git a/galaxy/integrations/flux/main.py b/galaxy/integrations/flux/main.py new file mode 100644 index 0000000..70c8b38 --- /dev/null +++ b/galaxy/integrations/flux/main.py @@ -0,0 +1,70 @@ +from types import TracebackType +from typing import Any + +from galaxy.core.galaxy import register, Integration +from galaxy.core.models import Config +from galaxy.integrations.flux.client import FluxClient +from galaxy.utils.parsers import to_bool + + +class Flux(Integration): + _methods = [] + + def __init__(self, config: Config): + super().__init__(config) + self.client = FluxClient(self.config, self.logger) + + self.exclude_system_namespaces = to_bool(config.integration.properties["excludeSystemNamespaces"]) + + self._cluster_id = None + self._namespace_ids = {} + self._source_ids = {} + + async def __aenter__(self) -> "Flux": + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type: type, exc: Exception, tb: TracebackType) -> None: + await self.client.__aexit__(exc_type, exc, tb) + + @register(_methods, group=1) + async def kubernetes_cluster(self) -> tuple[Any]: + raw_cluster = await self.client.get_cluster() + + self._cluster_id = raw_cluster["metadata"]["uid"] + + mapped_cluster = await self.mapper.process("kubernetes_cluster", [raw_cluster]) + return mapped_cluster + + @register(_methods, group=2) + async def kubernetes_namespaces(self) -> tuple[Any]: + raw_namespaces = await self.client.get_namespaces(exclude_system=self.exclude_system_namespaces) + + for ns in raw_namespaces: + id_, name = ns["metadata"]["uid"], ns["metadata"]["name"] + self._namespace_ids[name] = id_ + + mapped_namespaces = await self.mapper.process( + "kubernetes_namespace", raw_namespaces, context={"cluster_id": self._cluster_id} + ) + return mapped_namespaces + + @register(_methods, group=3) + async def sources(self) -> tuple[Any]: + raw_sources = await self.client.get_sources() + + for source in raw_sources: + id_ = source["metadata"]["uid"] + namespace, kind, name = source["metadata"]["namespace"], source["kind"], source["metadata"]["name"] + self._source_ids[f"{namespace}-{kind}-{name}"] = id_ + + mapped_sources = await self.mapper.process("source", raw_sources, context={"namespaces": self._namespace_ids}) + return mapped_sources + + @register(_methods, group=4) + async def applications(self) -> tuple[Any]: + raw_applications = await self.client.get_applications() + mapped_applications = await self.mapper.process( + "application", raw_applications, context={"namespaces": self._namespace_ids, "sources": self._source_ids} + ) + return mapped_applications diff --git a/galaxy/integrations/flux/models.py b/galaxy/integrations/flux/models.py new file mode 100644 index 0000000..d4f6d1e --- /dev/null +++ b/galaxy/integrations/flux/models.py @@ -0,0 +1,65 @@ +from datetime import datetime +from enum import Enum +from typing import Literal + +from pydantic import BaseModel, Field +from pydantic.functional_serializers import field_serializer + + +class Kind(str, Enum): + KUSTOMIZATION = "Kustomization" + HELM_RELEASE = "HelmRelease" + + +class Reason(str, Enum): + PROGRESSING = "Progressing" + PROGRESSING_WITH_RETRY = "ProgressingWithRetry" + + # Kustomization success + RECONCILIATION_SUCCEEDED = "ReconciliationSucceeded" + + # HelmRelease success + INSTALL_SUCCEEDED = "InstallSucceeded" + UPGRADE_SUCCEEDED = "UpgradeSucceeded" + TEST_SUCCEEDED = "TestSucceeded" + + # Kustomization failure + PRUNE_FAILED = "PruneFailed" + ARTIFACT_FAILED = "ArtifactFailed" + BUILD_FAILED = "BuildFailed" + HEALTH_CHECK_FAILED = "HealthCheckFailed" + DEPENDENCY_NOT_READY = "DependencyNotReady" + RECONCILIATION_FAILED = "ReconciliationFailed" + + # HelmRelease failure + INSTALL_FAILED = "InstallFailed" + UPGRADE_FAILED = "UpgradeFailed" + TEST_FAILED = "TestFailed" + ROLLBACK_SUCCEEDED = "RollbackSucceeded" + UNINSTALL_SUCCEEDED = "UninstallSucceeded" + ROLLBACK_FAILED = "RollbackFailed" + UNINSTALL_FAILED = "UninstallFailed" + + +class InvolvedObject(BaseModel): + kind: Kind + namespace: str + name: str + uid: str + api_version: str = Field(alias="apiVersion") + resource_version: str | None = Field(alias="resourceVersion") + + +class Event(BaseModel): + involved_object: InvolvedObject = Field(alias="involvedObject") + severity: Literal["trace", "info", "error"] + timestamp: datetime + message: str + reason: Reason + metadata: dict[str, str] | None + reporting_controller: str = Field(alias="reportingController") + reporting_instance: str | None = Field(default=None, alias="reportingInstance") + + @field_serializer("timestamp") + def serialize_timestamp(self, timestamp: datetime, _info): + return timestamp.isoformat() diff --git a/galaxy/integrations/flux/routes.py b/galaxy/integrations/flux/routes.py new file mode 100644 index 0000000..9ee7b64 --- /dev/null +++ b/galaxy/integrations/flux/routes.py @@ -0,0 +1,53 @@ +from logging import Logger + +from fastapi import APIRouter, Depends + +from galaxy.core.magneto import Magneto +from galaxy.core.mapper import Mapper +from galaxy.core.utils import get_logger, get_mapper, get_magneto_client +from galaxy.integrations.flux.models import Event, Reason + +router = APIRouter(prefix="/flux", tags=["flux"]) + + +@router.post("/webhook") +async def flux_webhook( + event: Event, + mapper: Mapper = Depends(get_mapper), + logger: Logger = Depends(get_logger), + magneto_client: Magneto = Depends(get_magneto_client), +) -> None: + match event.reason: + case Reason.PROGRESSING | Reason.PROGRESSING_WITH_RETRY: + return + case ( + Reason.RECONCILIATION_SUCCEEDED + | Reason.INSTALL_SUCCEEDED + | Reason.UPGRADE_SUCCEEDED + | Reason.TEST_SUCCEEDED + ): + status = "success" + case _: + status = "failure" + + revision = event.metadata["revision"] if event.metadata else "?" + try: + entity, *_ = await mapper.process("pipeline", [event.model_dump()], context={"status": status}) + except Exception: + logger.error( + "Failed to process event for %s/%s, revision: %s", + event.involved_object.kind, + event.involved_object.name, + revision, + ) + return + + try: + await magneto_client.upsert_entity(entity) + except Exception: + logger.error( + "Failed to push entity for %s/%s, revision: %s", + event.involved_object.kind, + event.involved_object.name, + revision, + ) diff --git a/galaxy/integrations/flux/tests/conftest.py b/galaxy/integrations/flux/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/galaxy/integrations/flux/tests/test_flux.py b/galaxy/integrations/flux/tests/test_flux.py new file mode 100644 index 0000000..e69de29 diff --git a/galaxy/integrations/gitlab/main.py b/galaxy/integrations/gitlab/main.py index 3028a34..72cff33 100644 --- a/galaxy/integrations/gitlab/main.py +++ b/galaxy/integrations/gitlab/main.py @@ -117,9 +117,8 @@ async def get_repo_files(self, repo_full_path: str) -> dict: for file_check in self.repo_files_to_check: file = await self.client.get_file(repo_full_path, file_check["path"]) if file: - repo_file_checks[file_check["destination"]] = not file_check["regex"] or bool( - re.search(file_check["regex"], file["content"]) - ) + result = re.search(file_check["regex"], file["content"]) if file_check["regex"] else None + repo_file_checks[file_check["destination"]] = result else: repo_file_checks[file_check["destination"]] = False diff --git a/poetry.lock b/poetry.lock index 8b76ae6..563cfd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -346,10 +346,6 @@ files = [ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, - {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, @@ -362,14 +358,8 @@ files = [ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, - {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, - {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, @@ -380,24 +370,8 @@ files = [ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, - {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, - {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, - {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, - {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, - {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, - {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, - {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, - {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, - {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, - {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, - {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, - {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, - {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, @@ -407,10 +381,6 @@ files = [ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, - {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, @@ -422,10 +392,6 @@ files = [ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, - {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, @@ -438,10 +404,6 @@ files = [ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, - {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, @@ -454,10 +416,6 @@ files = [ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, - {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, @@ -968,6 +926,17 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "durationpy" +version = "0.9" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +files = [ + {file = "durationpy-0.9-py3-none-any.whl", hash = "sha256:e65359a7af5cedad07fb77a2dd3f390f8eb0b74cb845589fa6c057086834dd38"}, + {file = "durationpy-0.9.tar.gz", hash = "sha256:fd3feb0a69a0057d582ef643c355c40d2fa1c942191f914d12203b1a01ac722a"}, +] + [[package]] name = "email-validator" version = "2.2.0" @@ -1672,6 +1641,33 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "kubernetes" +version = "31.0.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +files = [ + {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, + {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +durationpy = ">=0.7" +google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] + [[package]] name = "magneto-api-client" version = "1.0.0" @@ -1963,6 +1959,22 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "packaging" version = "24.1" @@ -2738,6 +2750,24 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rich" version = "13.8.0" @@ -3373,6 +3403,22 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "websockets" version = "13.0.1" @@ -3646,4 +3692,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = ">=3.11,<4.0" -content-hash = "943421eb5d777ac940669b0f22f650dc1726f7ad84fb3a67a5dc58ae315f3298" +content-hash = "f1b7ebb84f763f2b0aa401ba55f93ba1ead059764822f02875101d40e786dbfc" diff --git a/pyproject.toml b/pyproject.toml index 5484826..2c6908a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ google-cloud-asset = "^3.26.4" jinja2 = "^3.1.4" jq = "^1.7.0" jsonschema = "^4.23.0" +kubernetes = "^31.0.0" msgspec = "^0.18.6" pdpyras = "^5.2.0" pygithub = "^2.3.0"