From 4b57f0580b8c2802fed078a86a6ed4622e0cf015 Mon Sep 17 00:00:00 2001 From: Daniela Plascencia Date: Thu, 14 Sep 2023 22:08:55 +0200 Subject: [PATCH] refactor: charmed-kubeflow-chisme's base charm to re-write arg-controller (#127) * refactor: charmed-kubeflow-chisme's based charm to re-write arg-controller * refactor(tests): match unit tests to the new charm code * build: add charms.observability_libs.v1.kubernetes_service_patch library * build: update requirement input files to match new charm requirements --- .gitignore | 1 + charms/argo-controller/.coverage | Bin 53248 -> 0 bytes charms/argo-controller/charmcraft.yaml | 2 - .../argoproj.io_clusterworkflowtemplates.yaml | 35 - .../files/crds/argoproj.io_cronworkflows.yaml | 38 - .../argoproj.io_workfloweventbindings.yaml | 34 - .../files/crds/argoproj.io_workflows.yaml | 48 -- .../crds/argoproj.io_workflowtaskresults.yaml | 425 ----------- .../crds/argoproj.io_workflowtasksets.yaml | 41 -- .../crds/argoproj.io_workflowtemplates.yaml | 34 - .../v1/kubernetes_service_patch.py | 341 +++++++++ charms/argo-controller/metadata.yaml | 10 +- charms/argo-controller/requirements-unit.in | 18 +- charms/argo-controller/requirements-unit.txt | 133 +++- charms/argo-controller/requirements.in | 7 +- charms/argo-controller/requirements.txt | 55 +- charms/argo-controller/src/charm.py | 391 ++++------ .../src/components/pebble_component.py | 71 ++ .../src/templates/auth_manifests.yaml.j2 | 348 +++++++++ .../argo-controller/src/templates/crds.yaml | 670 ++++++++++++++++++ .../src/templates/minio_configmap.yaml.j2 | 45 ++ .../mlpipeline_minio_artifact_secret.yaml.j2 | 11 + .../tests/integration/test_charm.py | 1 + .../argo-controller/tests/unit/test_charm.py | 303 ++++---- 24 files changed, 1946 insertions(+), 1116 deletions(-) delete mode 100644 charms/argo-controller/.coverage delete mode 100644 charms/argo-controller/files/crds/argoproj.io_clusterworkflowtemplates.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_cronworkflows.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_workfloweventbindings.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_workflows.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_workflowtaskresults.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_workflowtasksets.yaml delete mode 100644 charms/argo-controller/files/crds/argoproj.io_workflowtemplates.yaml create mode 100644 charms/argo-controller/lib/charms/observability_libs/v1/kubernetes_service_patch.py create mode 100644 charms/argo-controller/src/components/pebble_component.py create mode 100644 charms/argo-controller/src/templates/auth_manifests.yaml.j2 create mode 100644 charms/argo-controller/src/templates/crds.yaml create mode 100644 charms/argo-controller/src/templates/minio_configmap.yaml.j2 create mode 100644 charms/argo-controller/src/templates/mlpipeline_minio_artifact_secret.yaml.j2 diff --git a/.gitignore b/.gitignore index db0d2cd..d25fe35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.coverage *.charm build/ __pycache__ diff --git a/charms/argo-controller/.coverage b/charms/argo-controller/.coverage deleted file mode 100644 index c06c84fc9262f48f9a50a8ec3473dc93136d79a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 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 I|GN(SKTX~XD*ylh diff --git a/charms/argo-controller/charmcraft.yaml b/charms/argo-controller/charmcraft.yaml index e34ea4c..9325e88 100644 --- a/charms/argo-controller/charmcraft.yaml +++ b/charms/argo-controller/charmcraft.yaml @@ -11,6 +11,4 @@ bases: channel: "20.04" parts: charm: - prime: - - ./files/crds/argoproj.io_*.yaml charm-python-packages: [setuptools, pip] # Fixes install of some packages diff --git a/charms/argo-controller/files/crds/argoproj.io_clusterworkflowtemplates.yaml b/charms/argo-controller/files/crds/argoproj.io_clusterworkflowtemplates.yaml deleted file mode 100644 index fa7da83..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_clusterworkflowtemplates.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: clusterworkflowtemplates.argoproj.io -spec: - group: argoproj.io - names: - kind: ClusterWorkflowTemplate - listKind: ClusterWorkflowTemplateList - plural: clusterworkflowtemplates - shortNames: - - clusterwftmpl - - cwft - singular: clusterworkflowtemplate - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true diff --git a/charms/argo-controller/files/crds/argoproj.io_cronworkflows.yaml b/charms/argo-controller/files/crds/argoproj.io_cronworkflows.yaml deleted file mode 100644 index 2878fe9..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_cronworkflows.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: cronworkflows.argoproj.io -spec: - group: argoproj.io - names: - kind: CronWorkflow - listKind: CronWorkflowList - plural: cronworkflows - shortNames: - - cwf - - cronwf - singular: cronworkflow - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - status: - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true diff --git a/charms/argo-controller/files/crds/argoproj.io_workfloweventbindings.yaml b/charms/argo-controller/files/crds/argoproj.io_workfloweventbindings.yaml deleted file mode 100644 index 9585686..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_workfloweventbindings.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: workfloweventbindings.argoproj.io -spec: - group: argoproj.io - names: - kind: WorkflowEventBinding - listKind: WorkflowEventBindingList - plural: workfloweventbindings - shortNames: - - wfeb - singular: workfloweventbinding - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true diff --git a/charms/argo-controller/files/crds/argoproj.io_workflows.yaml b/charms/argo-controller/files/crds/argoproj.io_workflows.yaml deleted file mode 100644 index f3751e1..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_workflows.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: workflows.argoproj.io -spec: - group: argoproj.io - names: - kind: Workflow - listKind: WorkflowList - plural: workflows - shortNames: - - wf - singular: workflow - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: Status of the workflow - jsonPath: .status.phase - name: Status - type: string - - description: When the workflow was started - format: date-time - jsonPath: .status.startedAt - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - status: - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true - subresources: {} diff --git a/charms/argo-controller/files/crds/argoproj.io_workflowtaskresults.yaml b/charms/argo-controller/files/crds/argoproj.io_workflowtaskresults.yaml deleted file mode 100644 index 9c173ef..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_workflowtaskresults.yaml +++ /dev/null @@ -1,425 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: workflowtaskresults.argoproj.io -spec: - group: argoproj.io - names: - kind: WorkflowTaskResult - listKind: WorkflowTaskResultList - plural: workflowtaskresults - singular: workflowtaskresult - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - message: - type: string - metadata: - type: object - outputs: - properties: - artifacts: - items: - properties: - archive: - properties: - none: - type: object - tar: - properties: - compressionLevel: - format: int32 - type: integer - type: object - zip: - type: object - type: object - archiveLogs: - type: boolean - artifactory: - properties: - passwordSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - url: - type: string - usernameSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - required: - - url - type: object - from: - type: string - fromExpression: - type: string - gcs: - properties: - bucket: - type: string - key: - type: string - serviceAccountKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - required: - - key - type: object - git: - properties: - depth: - format: int64 - type: integer - disableSubmodules: - type: boolean - fetch: - items: - type: string - type: array - insecureIgnoreHostKey: - type: boolean - passwordSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - repo: - type: string - revision: - type: string - sshPrivateKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - usernameSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - required: - - repo - type: object - globalName: - type: string - hdfs: - properties: - addresses: - items: - type: string - type: array - force: - type: boolean - hdfsUser: - type: string - krbCCacheSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - krbConfigConfigMap: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - krbKeytabSecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - krbRealm: - type: string - krbServicePrincipalName: - type: string - krbUsername: - type: string - path: - type: string - required: - - path - type: object - http: - properties: - headers: - items: - properties: - name: - type: string - value: - type: string - required: - - name - - value - type: object - type: array - url: - type: string - required: - - url - type: object - mode: - format: int32 - type: integer - name: - type: string - optional: - type: boolean - oss: - properties: - accessKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - bucket: - type: string - createBucketIfNotPresent: - type: boolean - endpoint: - type: string - key: - type: string - lifecycleRule: - properties: - markDeletionAfterDays: - format: int32 - type: integer - markInfrequentAccessAfterDays: - format: int32 - type: integer - type: object - secretKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - securityToken: - type: string - required: - - key - type: object - path: - type: string - raw: - properties: - data: - type: string - required: - - data - type: object - recurseMode: - type: boolean - s3: - properties: - accessKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - bucket: - type: string - createBucketIfNotPresent: - properties: - objectLocking: - type: boolean - type: object - encryptionOptions: - properties: - enableEncryption: - type: boolean - kmsEncryptionContext: - type: string - kmsKeyId: - type: string - serverSideCustomerKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - type: object - endpoint: - type: string - insecure: - type: boolean - key: - type: string - region: - type: string - roleARN: - type: string - secretKeySecret: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - useSDKCreds: - type: boolean - type: object - subPath: - type: string - required: - - name - type: object - type: array - exitCode: - type: string - parameters: - items: - properties: - default: - type: string - description: - type: string - enum: - items: - type: string - type: array - globalName: - type: string - name: - type: string - value: - type: string - valueFrom: - properties: - configMapKeyRef: - properties: - key: - type: string - name: - type: string - optional: - type: boolean - required: - - key - type: object - default: - type: string - event: - type: string - expression: - type: string - jqFilter: - type: string - jsonPath: - type: string - parameter: - type: string - path: - type: string - supplied: - type: object - type: object - required: - - name - type: object - type: array - result: - type: string - type: object - phase: - type: string - progress: - type: string - required: - - metadata - type: object - served: true - storage: true diff --git a/charms/argo-controller/files/crds/argoproj.io_workflowtasksets.yaml b/charms/argo-controller/files/crds/argoproj.io_workflowtasksets.yaml deleted file mode 100644 index 2f72ccd..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_workflowtasksets.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: workflowtasksets.argoproj.io -spec: - group: argoproj.io - names: - kind: WorkflowTaskSet - listKind: WorkflowTaskSetList - plural: workflowtasksets - shortNames: - - wfts - singular: workflowtaskset - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - status: - type: object - x-kubernetes-map-type: atomic - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charms/argo-controller/files/crds/argoproj.io_workflowtemplates.yaml b/charms/argo-controller/files/crds/argoproj.io_workflowtemplates.yaml deleted file mode 100644 index f6fa080..0000000 --- a/charms/argo-controller/files/crds/argoproj.io_workflowtemplates.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: workflowtemplates.argoproj.io -spec: - group: argoproj.io - names: - kind: WorkflowTemplate - listKind: WorkflowTemplateList - plural: workflowtemplates - shortNames: - - wftmpl - singular: workflowtemplate - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - metadata - - spec - type: object - served: true - storage: true diff --git a/charms/argo-controller/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/charms/argo-controller/lib/charms/observability_libs/v1/kubernetes_service_patch.py new file mode 100644 index 0000000..64dd13c --- /dev/null +++ b/charms/argo-controller/lib/charms/observability_libs/v1/kubernetes_service_patch.py @@ -0,0 +1,341 @@ +# Copyright 2021 Canonical Ltd. +# See LICENSE file for licensing details. + +"""# KubernetesServicePatch Library. + +This library is designed to enable developers to more simply patch the Kubernetes Service created +by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a +service named after the application in the namespace (named after the Juju model). This service by +default contains a "placeholder" port, which is 65536/TCP. + +When modifying the default set of resources managed by Juju, one must consider the lifecycle of the +charm. In this case, any modifications to the default service (created during deployment), will be +overwritten during a charm upgrade. + +When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` +events which applies the patch to the cluster. This should ensure that the service ports are +correct throughout the charm's life. + +The constructor simply takes a reference to the parent charm, and a list of +[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the +service. For information regarding the `lightkube` `ServicePort` model, please visit the +`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport). + +Optionally, a name of the service (in case service name needs to be patched as well), labels, +selectors, and annotations can be provided as keyword arguments. + +## Getting Started + +To get started using the library, you just need to fetch the library using `charmcraft`. **Note +that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** + +```shell +cd some-charm +charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch +cat << EOF >> requirements.txt +lightkube +lightkube-models +EOF +``` + +Then, to initialise the library: + +For `ClusterIP` services: + +```python +# ... +from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch +from lightkube.models.core_v1 import ServicePort + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + port = ServicePort(443, name=f"{self.app.name}") + self.service_patcher = KubernetesServicePatch(self, [port]) + # ... +``` + +For `LoadBalancer`/`NodePort` services: + +```python +# ... +from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch +from lightkube.models.core_v1 import ServicePort + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666) + self.service_patcher = KubernetesServicePatch( + self, [port], "LoadBalancer" + ) + # ... +``` + +Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"` + +```python +# ... +from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch +from lightkube.models.core_v1 import ServicePort + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP") + udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP") + sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP") + self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp]) + # ... +``` + +Bound with custom events by providing `refresh_event` argument: +For example, you would like to have a configurable port in your charm and want to apply +service patch every time charm config is changed. + +```python +from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch +from lightkube.models.core_v1 import ServicePort + +class SomeCharm(CharmBase): + def __init__(self, *args): + # ... + port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") + self.service_patcher = KubernetesServicePatch( + self, + [port], + refresh_event=self.on.config_changed + ) + # ... +``` + +Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library +does not try to make any API calls, or open any files during testing that are unlikely to be +present, and could break your tests. The easiest way to do this is during your test `setUp`: + +```python +# ... + +@patch("charm.KubernetesServicePatch", lambda x, y: None) +def setUp(self, *unused): + self.harness = Harness(SomeCharm) + # ... +``` +""" + +import logging +from types import MethodType +from typing import List, Literal, Optional, Union + +from lightkube import ApiError, Client +from lightkube.core import exceptions +from lightkube.models.core_v1 import ServicePort, ServiceSpec +from lightkube.models.meta_v1 import ObjectMeta +from lightkube.resources.core_v1 import Service +from lightkube.types import PatchType +from ops.charm import CharmBase +from ops.framework import BoundEvent, Object + +logger = logging.getLogger(__name__) + +# The unique Charmhub library identifier, never change it +LIBID = "0042f86d0a874435adef581806cddbbb" + +# Increment this major API version when introducing breaking changes +LIBAPI = 1 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 7 + +ServiceType = Literal["ClusterIP", "LoadBalancer"] + + +class KubernetesServicePatch(Object): + """A utility for patching the Kubernetes service set up by Juju.""" + + def __init__( + self, + charm: CharmBase, + ports: List[ServicePort], + service_name: Optional[str] = None, + service_type: ServiceType = "ClusterIP", + additional_labels: Optional[dict] = None, + additional_selectors: Optional[dict] = None, + additional_annotations: Optional[dict] = None, + *, + refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, + ): + """Constructor for KubernetesServicePatch. + + Args: + charm: the charm that is instantiating the library. + ports: a list of ServicePorts + service_name: allows setting custom name to the patched service. If none given, + application name will be used. + service_type: desired type of K8s service. Default value is in line with ServiceSpec's + default value. + additional_labels: Labels to be added to the kubernetes service (by default only + "app.kubernetes.io/name" is set to the service name) + additional_selectors: Selectors to be added to the kubernetes service (by default only + "app.kubernetes.io/name" is set to the service name) + additional_annotations: Annotations to be added to the kubernetes service. + refresh_event: an optional bound event or list of bound events which + will be observed to re-apply the patch (e.g. on port change). + The `install` and `upgrade-charm` events would be observed regardless. + """ + super().__init__(charm, "kubernetes-service-patch") + self.charm = charm + self.service_name = service_name if service_name else self._app + self.service = self._service_object( + ports, + service_name, + service_type, + additional_labels, + additional_selectors, + additional_annotations, + ) + + # Make mypy type checking happy that self._patch is a method + assert isinstance(self._patch, MethodType) + # Ensure this patch is applied during the 'install' and 'upgrade-charm' events + self.framework.observe(charm.on.install, self._patch) + self.framework.observe(charm.on.upgrade_charm, self._patch) + self.framework.observe(charm.on.update_status, self._patch) + + # apply user defined events + if refresh_event: + if not isinstance(refresh_event, list): + refresh_event = [refresh_event] + + for evt in refresh_event: + self.framework.observe(evt, self._patch) + + def _service_object( + self, + ports: List[ServicePort], + service_name: Optional[str] = None, + service_type: ServiceType = "ClusterIP", + additional_labels: Optional[dict] = None, + additional_selectors: Optional[dict] = None, + additional_annotations: Optional[dict] = None, + ) -> Service: + """Creates a valid Service representation. + + Args: + ports: a list of ServicePorts + service_name: allows setting custom name to the patched service. If none given, + application name will be used. + service_type: desired type of K8s service. Default value is in line with ServiceSpec's + default value. + additional_labels: Labels to be added to the kubernetes service (by default only + "app.kubernetes.io/name" is set to the service name) + additional_selectors: Selectors to be added to the kubernetes service (by default only + "app.kubernetes.io/name" is set to the service name) + additional_annotations: Annotations to be added to the kubernetes service. + + Returns: + Service: A valid representation of a Kubernetes Service with the correct ports. + """ + if not service_name: + service_name = self._app + labels = {"app.kubernetes.io/name": self._app} + if additional_labels: + labels.update(additional_labels) + selector = {"app.kubernetes.io/name": self._app} + if additional_selectors: + selector.update(additional_selectors) + return Service( + apiVersion="v1", + kind="Service", + metadata=ObjectMeta( + namespace=self._namespace, + name=service_name, + labels=labels, + annotations=additional_annotations, # type: ignore[arg-type] + ), + spec=ServiceSpec( + selector=selector, + ports=ports, + type=service_type, + ), + ) + + def _patch(self, _) -> None: + """Patch the Kubernetes service created by Juju to map the correct port. + + Raises: + PatchFailed: if patching fails due to lack of permissions, or otherwise. + """ + try: + client = Client() + except exceptions.ConfigError as e: + logger.warning("Error creating k8s client: %s", e) + return + + try: + if self._is_patched(client): + return + if self.service_name != self._app: + self._delete_and_create_service(client) + client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) + except ApiError as e: + if e.status.code == 403: + logger.error("Kubernetes service patch failed: `juju trust` this application.") + else: + logger.error("Kubernetes service patch failed: %s", str(e)) + else: + logger.info("Kubernetes service '%s' patched successfully", self._app) + + def _delete_and_create_service(self, client: Client): + service = client.get(Service, self._app, namespace=self._namespace) + service.metadata.name = self.service_name # type: ignore[attr-defined] + service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 + client.delete(Service, self._app, namespace=self._namespace) + client.create(service) + + def is_patched(self) -> bool: + """Reports if the service patch has been applied. + + Returns: + bool: A boolean indicating if the service patch has been applied. + """ + client = Client() + return self._is_patched(client) + + def _is_patched(self, client: Client) -> bool: + # Get the relevant service from the cluster + try: + service = client.get(Service, name=self.service_name, namespace=self._namespace) + except ApiError as e: + if e.status.code == 404 and self.service_name != self._app: + return False + logger.error("Kubernetes service get failed: %s", str(e)) + raise + + # Construct a list of expected ports, should the patch be applied + expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] + # Construct a list in the same manner, using the fetched service + fetched_ports = [ + (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] + ] # noqa: E501 + return expected_ports == fetched_ports + + @property + def _app(self) -> str: + """Name of the current Juju application. + + Returns: + str: A string containing the name of the current Juju application. + """ + return self.charm.app.name + + @property + def _namespace(self) -> str: + """The Kubernetes namespace we're running in. + + Returns: + str: A string containing the name of the current Kubernetes namespace. + """ + with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: + return f.read().strip() diff --git a/charms/argo-controller/metadata.yaml b/charms/argo-controller/metadata.yaml index 4350b5c..15b74d7 100755 --- a/charms/argo-controller/metadata.yaml +++ b/charms/argo-controller/metadata.yaml @@ -7,14 +7,15 @@ website: https://charmhub.io/argo-controller source: https://github.com/canonical/argo-operators/argo-controller issues: https://github.com/canonical/argo-operators/issues docs: https://discourse.charmhub.io/t/argo-controller/8212 -min-juju-version: "2.9.0" -series: [kubernetes] resources: oci-image: type: oci-image description: 'Backing OCI image' auto-fetch: true - upstream-source: argoproj/workflow-controller:v3.3.8 + upstream-source: argoproj/workflow-controller:v3.3.10 +containers: + argo-controller: + resource: oci-image requires: object-storage: interface: object-storage @@ -45,9 +46,6 @@ requires: - service versions: [v1] __schema_source: https://raw.githubusercontent.com/canonical/operator-schemas/master/object-storage.yaml -deployment: - type: stateless - service: omit provides: metrics-endpoint: interface: prometheus_scrape diff --git a/charms/argo-controller/requirements-unit.in b/charms/argo-controller/requirements-unit.in index 421af66..52191de 100644 --- a/charms/argo-controller/requirements-unit.in +++ b/charms/argo-controller/requirements-unit.in @@ -1,22 +1,6 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -# Please note this file introduces dependencies from the charm's requirements.in, -# special attention must be taken when updating this or the other .in file to try -# to avoid incompatibilities. -# Rules for editing this file: -# * Removing a dependency that is no longer used in the unit test file(s) -# is allowed, and should not represent any risk. -# * Adding a dependency in this file means the dependency is directly used -# in the unit test files(s). -# * ALL python packages/libs used directly in the unit test file(s) must be -# listed here even if requirements.in is already adding them. This will -# add clarity to the dependency list. -# * Pinning a version of a python package/lib shared with requirements.in -# must not introduce any incompatibilities. coverage -ops pytest pytest-mock -pytest-lazy-fixture -pyyaml --r requirements.in +-r requirements.txt diff --git a/charms/argo-controller/requirements-unit.txt b/charms/argo-controller/requirements-unit.txt index 79e7f74..713ee1a 100644 --- a/charms/argo-controller/requirements-unit.txt +++ b/charms/argo-controller/requirements-unit.txt @@ -4,62 +4,151 @@ # # pip-compile requirements-unit.in # +anyio==4.0.0 + # via + # -r requirements.txt + # httpcore attrs==23.1.0 - # via jsonschema + # via + # -r requirements.txt + # jsonschema certifi==2023.7.22 - # via requests + # via + # -r requirements.txt + # httpcore + # httpx + # requests +charmed-kubeflow-chisme==0.2.0 + # via -r requirements.txt charset-normalizer==3.2.0 - # via requests + # via + # -r requirements.txt + # requests coverage==7.3.0 # via -r requirements-unit.in +deepdiff==6.2.1 + # via + # -r requirements.txt + # charmed-kubeflow-chisme exceptiongroup==1.1.3 - # via pytest + # via + # -r requirements.txt + # anyio + # pytest +h11==0.14.0 + # via + # -r requirements.txt + # httpcore +httpcore==0.17.3 + # via + # -r requirements.txt + # httpx +httpx==0.24.1 + # via + # -r requirements.txt + # lightkube idna==3.4 - # via requests + # via + # -r requirements.txt + # anyio + # httpx + # requests importlib-resources==6.0.1 - # via jsonschema + # via + # -r requirements.txt + # jsonschema iniconfig==2.0.0 # via pytest +jinja2==3.1.2 + # via + # -r requirements.txt + # charmed-kubeflow-chisme jsonschema==4.17.3 - # via serialized-data-interface -oci-image==1.0.0 - # via -r requirements.in + # via + # -r requirements.txt + # serialized-data-interface +lightkube==0.14.0 + # via + # -r requirements.txt + # charmed-kubeflow-chisme +lightkube-models==1.28.1.4 + # via + # -r requirements.txt + # lightkube +markupsafe==2.1.3 + # via + # -r requirements.txt + # jinja2 ops==2.5.1 # via - # -r requirements-unit.in - # -r requirements.in + # -r requirements.txt + # charmed-kubeflow-chisme # serialized-data-interface +ordered-set==4.1.0 + # via + # -r requirements.txt + # deepdiff packaging==23.1 # via pytest pkgutil-resolve-name==1.3.10 - # via jsonschema + # via + # -r requirements.txt + # jsonschema pluggy==1.2.0 # via pytest pyrsistent==0.19.3 - # via jsonschema + # via + # -r requirements.txt + # jsonschema pytest==7.4.0 # via # -r requirements-unit.in - # pytest-lazy-fixture # pytest-mock -pytest-lazy-fixture==0.6.3 - # via -r requirements-unit.in pytest-mock==3.11.1 # via -r requirements-unit.in pyyaml==6.0.1 # via - # -r requirements-unit.in + # -r requirements.txt + # lightkube # ops # serialized-data-interface requests==2.31.0 - # via serialized-data-interface + # via + # -r requirements.txt + # serialized-data-interface +ruamel-yaml==0.17.32 + # via + # -r requirements.txt + # charmed-kubeflow-chisme +ruamel-yaml-clib==0.2.7 + # via + # -r requirements.txt + # ruamel-yaml serialized-data-interface==0.7.0 - # via -r requirements.in + # via + # -r requirements.txt + # charmed-kubeflow-chisme +sniffio==1.3.0 + # via + # -r requirements.txt + # anyio + # httpcore + # httpx +tenacity==8.2.3 + # via + # -r requirements.txt + # charmed-kubeflow-chisme tomli==2.0.1 # via pytest urllib3==2.0.4 - # via requests + # via + # -r requirements.txt + # requests websocket-client==1.6.1 - # via ops + # via + # -r requirements.txt + # ops zipp==3.16.2 - # via importlib-resources + # via + # -r requirements.txt + # importlib-resources diff --git a/charms/argo-controller/requirements.in b/charms/argo-controller/requirements.in index dbd7fca..57e7b54 100644 --- a/charms/argo-controller/requirements.in +++ b/charms/argo-controller/requirements.in @@ -1,6 +1,9 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. - -oci-image +# Pinning charmed-kubeflow-chisme will avoid pip-compile to resolve +# conflicts by using an older version of this package. +# Version >=0.2.0 contains the base charm code that's needed. +charmed-kubeflow-chisme>=0.2.0 +lightkube ops serialized-data-interface diff --git a/charms/argo-controller/requirements.txt b/charms/argo-controller/requirements.txt index 51b25b4..b51257a 100644 --- a/charms/argo-controller/requirements.txt +++ b/charms/argo-controller/requirements.txt @@ -4,36 +4,81 @@ # # pip-compile requirements.in # +anyio==4.0.0 + # via httpcore attrs==23.1.0 # via jsonschema certifi==2023.7.22 - # via requests + # via + # httpcore + # httpx + # requests +charmed-kubeflow-chisme==0.2.0 + # via -r requirements.in charset-normalizer==3.2.0 # via requests +deepdiff==6.2.1 + # via charmed-kubeflow-chisme +exceptiongroup==1.1.3 + # via anyio +h11==0.14.0 + # via httpcore +httpcore==0.17.3 + # via httpx +httpx==0.24.1 + # via lightkube idna==3.4 - # via requests + # via + # anyio + # httpx + # requests importlib-resources==6.0.1 # via jsonschema +jinja2==3.1.2 + # via charmed-kubeflow-chisme jsonschema==4.17.3 # via serialized-data-interface -oci-image==1.0.0 - # via -r requirements.in +lightkube==0.14.0 + # via + # -r requirements.in + # charmed-kubeflow-chisme +lightkube-models==1.28.1.4 + # via lightkube +markupsafe==2.1.3 + # via jinja2 ops==2.5.1 # via # -r requirements.in + # charmed-kubeflow-chisme # serialized-data-interface +ordered-set==4.1.0 + # via deepdiff pkgutil-resolve-name==1.3.10 # via jsonschema pyrsistent==0.19.3 # via jsonschema pyyaml==6.0.1 # via + # lightkube # ops # serialized-data-interface requests==2.31.0 # via serialized-data-interface +ruamel-yaml==0.17.32 + # via charmed-kubeflow-chisme +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml serialized-data-interface==0.7.0 - # via -r requirements.in + # via + # -r requirements.in + # charmed-kubeflow-chisme +sniffio==1.3.0 + # via + # anyio + # httpcore + # httpx +tenacity==8.2.3 + # via charmed-kubeflow-chisme urllib3==2.0.4 # via requests websocket-client==1.6.1 diff --git a/charms/argo-controller/src/charm.py b/charms/argo-controller/src/charm.py index 23f4422..4fedbaf 100755 --- a/charms/argo-controller/src/charm.py +++ b/charms/argo-controller/src/charm.py @@ -2,42 +2,68 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. +"""Charm for the Argo Workflow Controller. + +https://github.com/canonical/argo-operators +""" + import logging from base64 import b64encode -from glob import glob -from pathlib import Path -import yaml +import lightkube +from charmed_kubeflow_chisme.components import SdiRelationDataReceiverComponent +from charmed_kubeflow_chisme.components.charm_reconciler import CharmReconciler +from charmed_kubeflow_chisme.components.kubernetes_component import KubernetesComponent +from charmed_kubeflow_chisme.components.leadership_gate_component import LeadershipGateComponent +from charmed_kubeflow_chisme.kubernetes import create_charm_default_labels from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider +from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider -from oci_image import OCIImageResource, OCIImageResourceError +from lightkube.models.core_v1 import ServicePort +from lightkube.resources.apiextensions_v1 import CustomResourceDefinition +from lightkube.resources.core_v1 import ConfigMap, Secret, ServiceAccount +from lightkube.resources.rbac_authorization_v1 import ( + ClusterRole, + ClusterRoleBinding, + Role, + RoleBinding, +) from ops.charm import CharmBase from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus -from serialized_data_interface import NoCompatibleVersions, NoVersionsListed, get_interfaces -METRICS_PATH = "/metrics" -METRICS_PORT = "9090" +from components.pebble_component import ( + ARGO_CONTROLLER_CONFIGMAP, + METRICS_PORT, + ArgoControllerPebbleService, +) +logger = logging.getLogger(__name__) -class CheckFailed(Exception): - """Raise this exception if one of the checks in main fails.""" +K8S_RESOURCE_FILES = [ + "src/templates/auth_manifests.yaml.j2", + "src/templates/crds.yaml", + "src/templates/minio_configmap.yaml.j2", + "src/templates/mlpipeline_minio_artifact_secret.yaml.j2", +] +METRICS_PATH = "/metrics" - def __init__(self, msg: str, status_type=None): - super().__init__() - self.msg = str(msg) - self.status_type = status_type - self.status = status_type(self.msg) +class ArgoControllerOperator(CharmBase): + """Charm for the Argo Workflows controller. + https://github.com/canonical/argo-operators + """ -class ArgoControllerCharm(CharmBase): def __init__(self, *args): super().__init__(*args) - self.log = logging.getLogger(__name__) - - self.image = OCIImageResource(self, "oci-image") + # patch service ports + metrics_port = ServicePort(int(METRICS_PORT), name="metrics-port") + self.service_patcher = KubernetesServicePatch( + self, + [metrics_port], + service_name=self.app.name, + ) self.prometheus_provider = MetricsEndpointProvider( charm=self, @@ -54,260 +80,93 @@ def __init__(self, *args): # by user M4t3o self.dashboard_provider = GrafanaDashboardProvider(self) - for event in [ - self.on.install, - self.on.leader_elected, - self.on.upgrade_charm, - self.on.config_changed, - self.on["object-storage"].relation_changed, - ]: - self.framework.observe(event, self.main) - - def main(self, event): - try: - self._check_leader() + self.charm_reconciler = CharmReconciler(self) - interfaces = self._get_interfaces() - - image_details = self._check_image_details() - - os = self._check_object_storage(interfaces) - - except CheckFailed as check_failed: - self.model.unit.status = check_failed.status - return - - self.model.unit.status = MaintenanceStatus("Setting pod spec") - - # Sync the argoproj/argoexec image to the same version - executor_image = self.model.config["executor-image"] + self.leadership_gate = self.charm_reconciler.add( + component=LeadershipGateComponent( + charm=self, + name="leadership-gate", + ), + depends_on=[], + ) - config_map = { - "containerRuntimeExecutor": self.model.config["executor"], - "kubeletInsecure": self.model.config["kubelet-insecure"], - "artifactRepository": { - "s3": { - "bucket": self.model.config["bucket"], - "keyFormat": self.model.config["key-format"], - "endpoint": f"{os['service']}.{os['namespace']}:{os['port']}", - "insecure": not os["secure"], - "accessKeySecret": { - "name": "mlpipeline-minio-artifact", - "key": "accesskey", - }, - "secretKeySecret": { - "name": "mlpipeline-minio-artifact", - "key": "secretkey", - }, - } - }, - } + self.object_storage_relation = self.charm_reconciler.add( + component=SdiRelationDataReceiverComponent( + charm=self, + name="relation:object_storage", + relation_name="object-storage", + ), + depends_on=[self.leadership_gate], + ) - crd_root = "files/crds" - crds = [yaml.safe_load(Path(f).read_text()) for f in glob(f"{crd_root}/*.yaml")] - self.model.pod.set_spec( - { - "version": 3, - "serviceAccount": { - "roles": [ - { - "global": True, - "rules": [ - { - "apiGroups": [""], - "resources": ["pods", "pods/exec"], - "verbs": [ - "create", - "get", - "list", - "watch", - "update", - "patch", - "delete", - ], - }, - { - "apiGroups": [""], - "resources": ["configmaps"], - "verbs": ["get", "watch", "list"], - }, - { - "apiGroups": [""], - "resources": [ - "persistentvolumeclaims", - "persistentvolumeclaims/finalizers", - ], - "verbs": ["create", "delete", "get", "update"], - }, - { - "apiGroups": ["argoproj.io"], - "resources": [ - "workflows", - "workflows/finalizers", - "workflowtasksets", - "workflowtasksets/finalizers", - ], - "verbs": [ - "get", - "list", - "watch", - "update", - "patch", - "delete", - "create", - ], - }, - { - "apiGroups": ["argoproj.io"], - "resources": [ - "workflowtemplates", - "workflowtemplates/finalizers", - "clusterworkflowtemplates", - "clusterworkflowtemplates/finalizers", - ], - "verbs": [ - "get", - "list", - "watch", - ], - }, - { - "apiGroups": [""], - "resources": ["serviceaccounts"], - "verbs": ["get", "list"], - }, - { - "apiGroups": ["argoproj.io"], - "resources": [ - "cronworkflows", - "cronworkflows/finalizers", - ], - "verbs": [ - "get", - "list", - "watch", - "update", - "patch", - "delete", - ], - }, - { - "apiGroups": ["argoproj.io"], - "resources": [ - "workflowtaskresults", - ], - "verbs": [ - "list", - "watch", - "delete", - ], - }, - { - "apiGroups": [""], - "resources": ["events"], - "verbs": ["create", "patch"], - }, - { - "apiGroups": ["policy"], - "resources": ["poddisruptionbudgets"], - "verbs": ["create", "get", "delete"], - }, - { - "apiGroups": ["coordination.k8s.io"], - "resources": ["leases"], - "verbs": ["create", "get", "update"], - }, - { - "apiGroups": [""], - "resources": ["secrets"], - "verbs": ["get"], - }, - ], - } - ], - }, - "service": { - "updateStrategy": { - "type": "RollingUpdate", - "rollingUpdate": {"maxUnavailable": 1}, - }, + self.kubernetes_resources = self.charm_reconciler.add( + component=KubernetesComponent( + charm=self, + name="kubernetes:auth-crds-cm-and-secrets", + resource_templates=K8S_RESOURCE_FILES, + krh_resource_types={ + ClusterRole, + ClusterRoleBinding, + ConfigMap, + CustomResourceDefinition, + Role, + RoleBinding, + Secret, + ServiceAccount, }, - "containers": [ - { - "name": self.model.app.name, - "imageDetails": image_details, - "imagePullPolicy": "Always", - "args": [ - "--configmap", - "argo-controller-configmap-config", - "--executor-image", - executor_image, - ], - "envConfig": { - "ARGO_NAMESPACE": self.model.name, - "LEADER_ELECTION_IDENTITY": self.model.app.name, - }, - "volumeConfig": [ - { - "name": "configmap", - "mountPath": "/config-map.yaml", - "files": [ - { - "path": "config", - "content": yaml.dump(config_map), - } - ], - } - ], - }, - ], - "kubernetesResources": { - "customResourceDefinitions": [ - {"name": crd["metadata"]["name"], "spec": crd["spec"]} for crd in crds - ], - "secrets": [ - { - "name": "mlpipeline-minio-artifact", - "type": "Opaque", - "data": { - "accesskey": b64encode(os["access-key"].encode("utf-8")), - "secretkey": b64encode(os["secret-key"].encode("utf-8")), - }, - } - ], - }, - } + krh_labels=create_charm_default_labels( + self.app.name, + self.model.name, + scope="auth-crds-cm-and-secrets", + ), + context_callable=self._context_callable, + lightkube_client=lightkube.Client(), + ), + depends_on=[ + self.leadership_gate, + self.object_storage_relation, + ], ) - self.model.unit.status = ActiveStatus() - - def _check_leader(self): - if not self.unit.is_leader(): - # We can't do anything useful when not the leader, so do nothing. - raise CheckFailed("Waiting for leadership", WaitingStatus) - - def _get_interfaces(self): - try: - interfaces = get_interfaces(self) - except NoVersionsListed as err: - raise CheckFailed(err, WaitingStatus) - except NoCompatibleVersions as err: - raise CheckFailed(err, BlockedStatus) - return interfaces - - def _check_object_storage(self, interfaces): - if not ((os := interfaces["object-storage"]) and os.get_data()): - raise CheckFailed("Waiting for object-storage relation data", BlockedStatus) - - return list(os.get_data().values())[0] + self.argo_controller_container = self.charm_reconciler.add( + component=ArgoControllerPebbleService( + charm=self, + name="container:argo-controller", + container_name="argo-controller", + service_name="argo-controller", + ), + depends_on=[ + self.leadership_gate, + self.kubernetes_resources, + self.object_storage_relation, + ], + ) - def _check_image_details(self): - try: - image_details = self.image.fetch() - except OCIImageResourceError as e: - raise CheckFailed(f"{e.status.message}", e.status_type) - return image_details + self.charm_reconciler.install_default_event_handlers() + + @property + def _context_callable(self): + return lambda: { + "app_name": self.app.name, + "namespace": self.model.name, + "access_key": b64encode( + self.object_storage_relation.component.get_data()["access-key"].encode("utf-8") + ).decode("utf-8"), + "secret_key": b64encode( + self.object_storage_relation.component.get_data()["secret-key"].encode("utf-8") + ).decode("utf-8"), + "mlpipeline_minio_artifact_secret": "mlpipeline-minio-artifact-secret", + "argo_controller_configmap": ARGO_CONTROLLER_CONFIGMAP, + "s3_bucket": self.model.config["bucket"], + "s3_minio_endpoint": ( + f"{self.object_storage_relation.component.get_data()['service']}." + f"{self.object_storage_relation.component.get_data()['namespace']}:" + f"{self.object_storage_relation.component.get_data()['port']}" + ), + "kubelet_insecure": self.model.config["kubelet-insecure"], + "runtime_executor": self.model.config["executor"], + } if __name__ == "__main__": - main(ArgoControllerCharm) + main(ArgoControllerOperator) diff --git a/charms/argo-controller/src/components/pebble_component.py b/charms/argo-controller/src/components/pebble_component.py new file mode 100644 index 0000000..35688d6 --- /dev/null +++ b/charms/argo-controller/src/components/pebble_component.py @@ -0,0 +1,71 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +import logging + +from charmed_kubeflow_chisme.components.pebble_component import PebbleServiceComponent +from ops.pebble import Layer + +logger = logging.getLogger(__name__) + +ARGO_CONTROLLER_CONFIGMAP = "argo-workflow-controller-configmap" +EXECUTOR_IMAGE_CONFIG_NAME = "executor-image" +LIVENESS_PROBE_PORT = "6060" +METRICS_PORT = "9090" +LIVENESS_PROBE_PATH = "/healthz" +LIVENESS_PROBE_NAME = "argo-controller-up" + + +class ArgoControllerPebbleService(PebbleServiceComponent): + """Pebble service container component to configure Pebble layer.""" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.environment = { + "ARGO_NAMESPACE": self.model.name, + "LEADER_ELECTION_IDENTITY": self.model.app.name, + } + + def get_layer(self) -> Layer: + """Defines and returns Pebble layer configuration + + This method is required for subclassing PebbleServiceContainer + """ + logger.info("PebbleServiceComponent.get_layer executing") + return Layer( + { + "summary": "argo-controller layer", + "description": "Pebble config layer for argo-controller", + "services": { + self.service_name: { + "override": "replace", + "summary": "Entry point for kfp-viewer image", + "command": ( + "workflow-controller " + "--configmap " + f"{ARGO_CONTROLLER_CONFIGMAP} " + "--executor-image " + f"{self.model.config[EXECUTOR_IMAGE_CONFIG_NAME]} " + "--namespaced" + ), + "startup": "enabled", + "environment": self.environment, + "on-check-failure": {LIVENESS_PROBE_NAME: "restart"}, + } + }, + "checks": { + LIVENESS_PROBE_NAME: { + "override": "replace", + "period": "30s", + "timeout": "20s", + "threshold": 3, + "http": { + "url": f"http://localhost:{LIVENESS_PROBE_PORT}{LIVENESS_PROBE_PATH}" + }, + } + }, + } + ) diff --git a/charms/argo-controller/src/templates/auth_manifests.yaml.j2 b/charms/argo-controller/src/templates/auth_manifests.yaml.j2 new file mode 100644 index 0000000..c5f8830 --- /dev/null +++ b/charms/argo-controller/src/templates/auth_manifests.yaml.j2 @@ -0,0 +1,348 @@ +# From argo-workflows/manifests/quick-start-minimal.yaml +# Only RBAC resources and ServiceAccounts +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ app_name }}-argo + namespace: {{ namespace }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ app_name }}-github.com + namespace: {{ namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + workflows.argoproj.io/description: | + This is the minimum recommended permissions needed if you want to use the agent, e.g. for HTTP or plugin templates. + + If <= v3.2 you must replace `workflowtasksets/status` with `patch workflowtasksets`. + name: {{ app_name }}-agent + namespace: {{ namespace }} +rules: +- apiGroups: + - argoproj.io + resources: + - workflowtasksets + verbs: + - list + - watch +- apiGroups: + - argoproj.io + resources: + - workflowtasksets/status + verbs: + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ app_name }}-argo-role + namespace: {{ namespace }} +rules: +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update +- apiGroups: + - "" + resources: + - pods + - pods/exec + verbs: + - create + - get + - list + - watch + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - watch + - list +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - persistentvolumeclaims/finalizers + verbs: + - create + - update + - delete + - get +- apiGroups: + - argoproj.io + resources: + - workflows + - workflows/finalizers + - workflowtasksets + - workflowtasksets/finalizers + verbs: + - get + - list + - watch + - update + - patch + - delete + - create +- apiGroups: + - argoproj.io + resources: + - workflowtemplates + - workflowtemplates/finalizers + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list +- apiGroups: + - argoproj.io + resources: + - workflowtaskresults + verbs: + - list + - watch + - deletecollection +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list +- apiGroups: + - "" + resources: + - secrets + verbs: + - get +- apiGroups: + - argoproj.io + resources: + - cronworkflows + - cronworkflows/finalizers + verbs: + - get + - list + - watch + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - get + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + workflows.argoproj.io/description: | + Recommended minimum permissions for the `emissary` executor. + name: {{ app_name }}-executor + namespace: {{ namespace }} +rules: +- apiGroups: + - argoproj.io + resources: + - workflowtaskresults + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + workflows.argoproj.io/description: | + This is an example of the permissions you would need if you wanted to use a resource template to create and manage + other pods. The same pattern would be suitable for other resources, e.g. a service + name: {{ app_name }}-pod-manager + namespace: {{ namespace }} +rules: +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - get + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ app_name }}-submit-workflow-template + namespace: {{ namespace }} +rules: +- apiGroups: + - argoproj.io + resources: + - workfloweventbindings + verbs: + - list +- apiGroups: + - argoproj.io + resources: + - workflowtemplates + verbs: + - get +- apiGroups: + - argoproj.io + resources: + - workflows + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + workflows.argoproj.io/description: | + This is an example of the permissions you would need if you wanted to use a resource template to create and manage + other workflows. The same pattern would be suitable for other resources, e.g. a service + name: {{ app_name }}-workflow-manager + namespace: {{ namespace }} +rules: +- apiGroups: + - argoproj.io + resources: + - workflows + verbs: + - create + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ app_name }}-argo-clusterworkflowtemplate-role +rules: +- apiGroups: + - argoproj.io + resources: + - clusterworkflowtemplates + - clusterworkflowtemplates/finalizers + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-agent-default + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-agent +subjects: +- kind: ServiceAccount + name: {{ app_name }}-default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-argo-binding + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-argo-role +subjects: +- kind: ServiceAccount + name: {{ app_name }}-argo +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-executor-default + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-executor +subjects: +- kind: ServiceAccount + name: {{ app_name }}-default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-github.com + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-submit-workflow-template +subjects: +- kind: ServiceAccount + name: {{ app_name }}-github.com +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-pod-manager-default + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-pod-manager +subjects: +- kind: ServiceAccount + name: {{ app_name }}-default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ app_name }}-workflow-manager-default + namespace: {{ namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ app_name }}-workflow-manager +subjects: +- kind: ServiceAccount + name: {{ app_name }}-default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ app_name }}-argo-clusterworkflowtemplate-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ app_name }}-argo-clusterworkflowtemplate-role +subjects: +- kind: ServiceAccount + name: {{ app_name }}-argo + namespace: {{ namespace }} diff --git a/charms/argo-controller/src/templates/crds.yaml b/charms/argo-controller/src/templates/crds.yaml new file mode 100644 index 0000000..9f1e9ad --- /dev/null +++ b/charms/argo-controller/src/templates/crds.yaml @@ -0,0 +1,670 @@ +# From argo-workflows/manifests/quick-start-minimal.yaml +# Only CRDs +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterworkflowtemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: ClusterWorkflowTemplate + listKind: ClusterWorkflowTemplateList + plural: clusterworkflowtemplates + shortNames: + - clusterwftmpl + - cwft + singular: clusterworkflowtemplate + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: cronworkflows.argoproj.io +spec: + group: argoproj.io + names: + kind: CronWorkflow + listKind: CronWorkflowList + plural: cronworkflows + shortNames: + - cwf + - cronwf + singular: cronworkflow + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + status: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workfloweventbindings.argoproj.io +spec: + group: argoproj.io + names: + kind: WorkflowEventBinding + listKind: WorkflowEventBindingList + plural: workfloweventbindings + shortNames: + - wfeb + singular: workfloweventbinding + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflows.argoproj.io +spec: + group: argoproj.io + names: + kind: Workflow + listKind: WorkflowList + plural: workflows + shortNames: + - wf + singular: workflow + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status of the workflow + jsonPath: .status.phase + name: Status + type: string + - description: When the workflow was started + format: date-time + jsonPath: .status.startedAt + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + status: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflowtaskresults.argoproj.io +spec: + group: argoproj.io + names: + kind: WorkflowTaskResult + listKind: WorkflowTaskResultList + plural: workflowtaskresults + singular: workflowtaskresult + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + message: + type: string + metadata: + type: object + outputs: + properties: + artifacts: + items: + properties: + archive: + properties: + none: + type: object + tar: + properties: + compressionLevel: + format: int32 + type: integer + type: object + zip: + type: object + type: object + archiveLogs: + type: boolean + artifactory: + properties: + passwordSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + url: + type: string + usernameSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + required: + - url + type: object + from: + type: string + fromExpression: + type: string + gcs: + properties: + bucket: + type: string + key: + type: string + serviceAccountKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + required: + - key + type: object + git: + properties: + depth: + format: int64 + type: integer + disableSubmodules: + type: boolean + fetch: + items: + type: string + type: array + insecureIgnoreHostKey: + type: boolean + passwordSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + repo: + type: string + revision: + type: string + sshPrivateKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + usernameSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + required: + - repo + type: object + globalName: + type: string + hdfs: + properties: + addresses: + items: + type: string + type: array + force: + type: boolean + hdfsUser: + type: string + krbCCacheSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + krbConfigConfigMap: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + krbKeytabSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + krbRealm: + type: string + krbServicePrincipalName: + type: string + krbUsername: + type: string + path: + type: string + required: + - path + type: object + http: + properties: + headers: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + url: + type: string + required: + - url + type: object + mode: + format: int32 + type: integer + name: + type: string + optional: + type: boolean + oss: + properties: + accessKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + bucket: + type: string + createBucketIfNotPresent: + type: boolean + endpoint: + type: string + key: + type: string + lifecycleRule: + properties: + markDeletionAfterDays: + format: int32 + type: integer + markInfrequentAccessAfterDays: + format: int32 + type: integer + type: object + secretKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + securityToken: + type: string + required: + - key + type: object + path: + type: string + raw: + properties: + data: + type: string + required: + - data + type: object + recurseMode: + type: boolean + s3: + properties: + accessKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + bucket: + type: string + createBucketIfNotPresent: + properties: + objectLocking: + type: boolean + type: object + encryptionOptions: + properties: + enableEncryption: + type: boolean + kmsEncryptionContext: + type: string + kmsKeyId: + type: string + serverSideCustomerKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + type: object + endpoint: + type: string + insecure: + type: boolean + key: + type: string + region: + type: string + roleARN: + type: string + secretKeySecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + useSDKCreds: + type: boolean + type: object + subPath: + type: string + required: + - name + type: object + type: array + exitCode: + type: string + parameters: + items: + properties: + default: + type: string + description: + type: string + enum: + items: + type: string + type: array + globalName: + type: string + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + default: + type: string + event: + type: string + expression: + type: string + jqFilter: + type: string + jsonPath: + type: string + parameter: + type: string + path: + type: string + supplied: + type: object + type: object + required: + - name + type: object + type: array + result: + type: string + type: object + phase: + type: string + progress: + type: string + required: + - metadata + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflowtasksets.argoproj.io +spec: + group: argoproj.io + names: + kind: WorkflowTaskSet + listKind: WorkflowTaskSetList + plural: workflowtasksets + shortNames: + - wfts + singular: workflowtaskset + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + status: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: workflowtemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: WorkflowTemplate + listKind: WorkflowTemplateList + plural: workflowtemplates + shortNames: + - wftmpl + singular: workflowtemplate + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-map-type: atomic + x-kubernetes-preserve-unknown-fields: true + required: + - metadata + - spec + type: object + served: true + storage: true diff --git a/charms/argo-controller/src/templates/minio_configmap.yaml.j2 b/charms/argo-controller/src/templates/minio_configmap.yaml.j2 new file mode 100644 index 0000000..adbcc8c --- /dev/null +++ b/charms/argo-controller/src/templates/minio_configmap.yaml.j2 @@ -0,0 +1,45 @@ +apiVersion: v1 +data: + artifactRepository: | + s3: + bucket: {{ s3_bucket }} + endpoint: {{ s3_minio_endpoint }} + insecure: {{ kubelet_insecure }} + accessKeySecret: + name: {{ mlpipeline_minio_artifact_secret }} + key: accesskey + secretKeySecret: + name: {{ mlpipeline_minio_artifact_secret }} + key: secretkey + containerRuntimeExecutors: | + - name: {{ runtime_executor }} + selector: + matchLabels: + workflows.argoproj.io/container-runtime-executor: {{ runtime_executor }} + executor: | + resources: + requests: + cpu: 10m + memory: 64Mi + images: | + argoproj/argosay:v1: + command: [cowsay] + argoproj/argosay:v2: + command: [/argosay] + docker/whalesay:latest: + command: [cowsay] + python:alpine3.6: + command: [python3] + metricsConfig: | + enabled: true + path: /metrics + port: 9090 + namespaceParallelism: "10" + retentionPolicy: | + completed: 10 + failed: 3 + errored: 3 +kind: ConfigMap +metadata: + name: {{ argo_controller_configmap }} + namespace: {{ namespace }} diff --git a/charms/argo-controller/src/templates/mlpipeline_minio_artifact_secret.yaml.j2 b/charms/argo-controller/src/templates/mlpipeline_minio_artifact_secret.yaml.j2 new file mode 100644 index 0000000..5170944 --- /dev/null +++ b/charms/argo-controller/src/templates/mlpipeline_minio_artifact_secret.yaml.j2 @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + labels: + app: minio + name: {{ mlpipeline_minio_artifact_secret }} + namespace: {{ namespace }} +stringData: + accesskey: {{ access_key }} + secretkey: {{ secret_key }} +type: Opaque diff --git a/charms/argo-controller/tests/integration/test_charm.py b/charms/argo-controller/tests/integration/test_charm.py index 604e4a3..bc147b9 100644 --- a/charms/argo-controller/tests/integration/test_charm.py +++ b/charms/argo-controller/tests/integration/test_charm.py @@ -35,6 +35,7 @@ async def test_build_and_deploy_with_relations(ops_test: OpsTest): entity_url=built_charm_path, application_name=APP_NAME, resources=resources, + trust=True, ) # Deploy required relations diff --git a/charms/argo-controller/tests/unit/test_charm.py b/charms/argo-controller/tests/unit/test_charm.py index 08dc656..1ade3f5 100644 --- a/charms/argo-controller/tests/unit/test_charm.py +++ b/charms/argo-controller/tests/unit/test_charm.py @@ -1,154 +1,175 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -import json +from unittest.mock import MagicMock import pytest -import yaml -from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from charmed_kubeflow_chisme.testing import add_sdi_relation_to_harness +from ops.model import ActiveStatus, BlockedStatus from ops.testing import Harness -from charm import ArgoControllerCharm +from charm import ArgoControllerOperator +MOCK_OBJECT_STORAGE_DATA = { + "access-key": "access-key", + "secret-key": "secret-key", + "service": "service", + "namespace": "namespace", + "port": 1234, + "secure": True, +} + +EXPECTED_ENVIRONMENT = { + "ARGO_NAMESPACE": "namespace", + "LEADER_ELECTION_IDENTITY": "argo-controller", +} -@pytest.fixture(scope="function") -def harness() -> Harness: - """Create and return Harness for testing.""" - harness = Harness(ArgoControllerCharm) +@pytest.fixture +def harness() -> Harness: + harness = Harness(ArgoControllerOperator) return harness -class TestCharm: - """Test class for Argo Workflows Controller.""" - - def test_not_leader(self, harness): - """Test not a leader scenario.""" - harness.begin_with_initial_hooks() - assert isinstance(harness.charm.model.unit.status, WaitingStatus) - - def test_missing_image(self, harness): - """Test missing image scenario.""" - harness.set_leader(True) - harness.begin_with_initial_hooks() - assert isinstance(harness.charm.model.unit.status, BlockedStatus) - - def test_main_no_relation(self, harness): - """Test no relation scenario.""" - harness.set_leader(True) - harness.add_oci_resource( - "oci-image", - { - "registrypath": "argoproj/workflow-controller:v3.0.1", - "username": "", - "password": "", - }, - ) - harness.begin_with_initial_hooks() - pod_spec = harness.get_pod_spec() - - # confirm that we can serialize the pod spec - yaml.safe_dump(pod_spec) - - assert isinstance(harness.charm.model.unit.status, BlockedStatus) - - def test_main_with_relation(self, harness): - """Test relation scenario.""" - harness.set_leader(True) - harness.set_model_name("test_model") - harness.add_oci_resource( - "oci-image", - { - "registrypath": "argoproj/workflow-controller:v3.0.1", - "username": "", - "password": "", - }, - ) - rel_id = harness.add_relation("object-storage", "minio") - harness.add_relation_unit(rel_id, "minio/0") - data = { - "service": "my-service", - "port": 4242, - "access-key": "my-access-key", - "secret-key": "my-secret-key", - "secure": True, - "namespace": "my-namespace", - } - harness.update_relation_data( - rel_id, - "minio", - { - "data": yaml.dump(data), - "_supported_versions": yaml.dump(["v1"]), - }, - ) - harness.begin_with_initial_hooks() - assert isinstance(harness.charm.model.unit.status, ActiveStatus) - - def test_prometheus_data_set(self, harness: Harness, mocker): - """Test Prometheus data setting.""" - harness.set_leader(True) - harness.set_model_name("test_kubeflow") - harness.begin() - - mock_net_get = mocker.patch("ops.testing._TestingModelBackend.network_get") - - bind_address = "1.1.1.1" - fake_network = { - "bind-addresses": [ - { - "interface-name": "eth0", - "addresses": [{"hostname": "cassandra-tester-0", "value": bind_address}], - } - ] - } - mock_net_get.return_value = fake_network - rel_id = harness.add_relation("metrics-endpoint", "otherapp") - harness.add_relation_unit(rel_id, "otherapp/0") - harness.update_relation_data(rel_id, "otherapp", {}) - - # basic data - assert json.loads( - harness.get_relation_data(rel_id, harness.model.app.name)["scrape_jobs"] - )[0]["static_configs"][0]["targets"] == ["*:9090"] - - # load alert rules from rules files - # currently there is single alert per file - test_alerts = [] - with open("src/prometheus_alert_rules/loglines_error.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - with open("src/prometheus_alert_rules/loglines_warning.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - with open("src/prometheus_alert_rules/unit_unavailable.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - with open("src/prometheus_alert_rules/workflows_erroring.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - with open("src/prometheus_alert_rules/workflows_failing.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - with open("src/prometheus_alert_rules/workflows_pending.rule") as f: - file_alert = yaml.safe_load(f.read()) - test_alerts.append(file_alert["alert"]) - - # alert rules - alert_rules = json.loads( - harness.get_relation_data(rel_id, harness.model.app.name)["alert_rules"] - ) - assert alert_rules is not None - assert alert_rules["groups"] is not None - - # there are 6 groups with single alert - rules = [] - for group in alert_rules["groups"]: - rules.append(group["rules"][0]) - - # verify number of alerts is the same in relation and in the rules file - assert len(rules) == len(test_alerts) - - # verify alerts in relation match alerts in the rules file - for rule in rules: - assert rule["alert"] in test_alerts +@pytest.fixture() +def mocked_lightkube_client(mocker): + """Mocks the Lightkube Client in charm.py, returning a mock instead.""" + mocked_lightkube_client = MagicMock() + mocker.patch("charm.lightkube.Client", return_value=mocked_lightkube_client) + yield mocked_lightkube_client + + +@pytest.fixture() +def mocked_kubernetes_service_patch(mocker): + """Mocks the KubernetesServicePatch for the charm.""" + mocked_kubernetes_service_patch = mocker.patch( + "charm.KubernetesServicePatch", lambda x, y, service_name: None + ) + yield mocked_kubernetes_service_patch + + +def test_not_leader(harness, mocked_lightkube_client, mocked_kubernetes_service_patch): + """Test when we are not the leader.""" + harness.begin_with_initial_hooks() + # Assert that we are not Active, and that the leadership-gate is the cause. + assert not isinstance(harness.charm.model.unit.status, ActiveStatus) + assert harness.charm.model.unit.status.message.startswith("[leadership-gate]") + + +def test_object_storage_relation_with_data( + harness, mocked_lightkube_client, mocked_kubernetes_service_patch +): + """Test that if Leadership is Active, the object storage relation operates as expected. + + Note: See test_relation_components.py for an alternative way of unit testing Components without + mocking the regular charm. + """ + # Arrange + harness.begin() + + # Mock: + # * leadership_gate to be active and executed + harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) + + # Add relation with data. This should trigger a charm reconciliation due to relation-changed. + add_sdi_relation_to_harness(harness, "object-storage", data=MOCK_OBJECT_STORAGE_DATA) + + # Assert + assert isinstance(harness.charm.object_storage_relation.status, ActiveStatus) + + +def test_object_storage_relation_without_data( + harness, mocked_lightkube_client, mocked_kubernetes_service_patch +): + """Test that the object storage relation goes Blocked if no data is available.""" + # Arrange + harness.begin() + + # Mock: + # * leadership_gate to be active and executed + harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) + + # Add relation with data. This should trigger a charm reconciliation due to relation-changed. + add_sdi_relation_to_harness(harness, "object-storage", data={}) + + # Assert + assert isinstance(harness.charm.object_storage_relation.status, BlockedStatus) + + +def test_object_storage_relation_without_relation( + harness, mocked_lightkube_client, mocked_kubernetes_service_patch +): + """Test that the object storage relation goes Blocked if no relation is established.""" + # Arrange + harness.begin() + + # Mock: + # * leadership_gate to be active and executed + harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) + + # Act + harness.charm.on.install.emit() + + # Assert + assert isinstance(harness.charm.object_storage_relation.status, BlockedStatus) + + +def test_kubernetes_created_method( + harness, mocked_lightkube_client, mocked_kubernetes_service_patch +): + """Test whether we try to create Kubernetes resources when we have leadership.""" + # Arrange + # Needed because kubernetes component will only apply to k8s if we are the leader + harness.set_leader(True) + harness.begin() + + # Need to mock the leadership-gate to be active, and the kubernetes auth component so that it + # sees the expected resources when calling _get_missing_kubernetes_resources + + harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) + + # Add relation with data. This should trigger a charm reconciliation due to relation-changed. + add_sdi_relation_to_harness(harness, "object-storage", data=MOCK_OBJECT_STORAGE_DATA) + + harness.charm.kubernetes_resources.component._get_missing_kubernetes_resources = MagicMock( + return_value=[] + ) + + # Act + harness.charm.on.install.emit() + + # Assert + # FIXME: why is it counting 50? + assert mocked_lightkube_client.apply.call_count == 50 + assert isinstance(harness.charm.kubernetes_resources.status, ActiveStatus) + + +def test_pebble_services_running( + harness, mocked_lightkube_client, mocked_kubernetes_service_patch +): + """Test that if the Kubernetes Component is Active, the pebble services successfully start.""" + # Arrange + harness.set_model_name(EXPECTED_ENVIRONMENT["ARGO_NAMESPACE"]) + harness.begin() + harness.set_can_connect("argo-controller", True) + + # Mock: + # * leadership_gate to have get_status=>Active + # * object_storage_relation to return mock data, making the item go active + # * kubernetes_resources to have get_status=>Active + harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) + harness.charm.object_storage_relation.component.get_data = MagicMock( + return_value=MOCK_OBJECT_STORAGE_DATA + ) + harness.charm.kubernetes_resources.get_status = MagicMock(return_value=ActiveStatus()) + + # Act + harness.charm.on.install.emit() + + # Assert + container = harness.charm.unit.get_container("argo-controller") + service = container.get_service("argo-controller") + assert service.is_running() + # Assert the environment variables that are set from inputs are correctly applied + environment = container.get_plan().services["argo-controller"].environment + assert environment == EXPECTED_ENVIRONMENT