├── charmcraft.yaml ├── CODEOWNERS ├── .gitignore ├── pyproject.toml ├── renovate.json ├── charms └── argo-controller │ ├── terraform │ ├── versions.tf │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── README.md │ ├── tests │ ├── unit │ │ ├── __init__.py │ │ └── test_charm.py │ ├── integration │ │ ├── charms_dependencies.py │ │ └── test_charm.py │ └── data │ │ └── simple_artifact.yaml │ ├── src │ ├── templates │ │ ├── mlpipeline_minio_artifact_secret.yaml.j2 │ │ └── minio_configmap.yaml.j2 │ ├── prometheus_alert_rules │ │ ├── too_many_requests.rule │ │ ├── operation_panic_error.rule │ │ ├── missing_pods.rule │ │ └── KubeflowArgoControllerServices.rules │ ├── components │ │ └── pebble_component.py │ ├── charm.py │ └── grafana_dashboards │ │ └── basic.json.tmpl │ ├── README.md │ ├── config.yaml │ ├── metadata.yaml │ ├── CONTRIBUTING.md │ ├── tox.ini │ ├── pyproject.toml │ ├── charmcraft.yaml │ ├── LICENSE │ ├── lib │ └── charms │ │ └── observability_libs │ │ └── v1 │ │ └── kubernetes_service_patch.py │ └── icon.svg ├── poetry.lock ├── README.md ├── bundle.yaml ├── tools └── get-images.sh ├── .github ├── workflows │ ├── on_pull_request.yaml │ ├── on_push.yaml │ ├── get-charm-paths.sh │ ├── release.yaml │ ├── tiobe_scan.yaml │ ├── integrate.yaml │ └── publish.yaml ├── .jira_sync_config.yaml └── ISSUE_TEMPLATE │ ├── task.yaml │ └── bug.yaml ├── concierge.yaml ├── tox.ini ├── CONTRIBUTING.md └── LICENSE /charmcraft.yaml: -------------------------------------------------------------------------------- 1 | type: bundle 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @canonical/kubeflow 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.charm 3 | build/ 4 | __pycache__ 5 | operators/argo-controller/build 6 | .tox 7 | .idea 8 | venv/ 9 | .terraform* 10 | *.tfstate* 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "argo-operators" 3 | requires-python = ">=3.12,<4.0" 4 | 5 | [tool.poetry] 6 | package-mode = false 7 | 8 | [tool.poetry.group] 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>canonical/charmed-kubeflow-workflows" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /charms/argo-controller/terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6" 3 | required_providers { 4 | juju = { 5 | source = "juju/juju" 6 | version = ">= 0.14.0, < 1.0.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /charms/argo-controller/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Initialize unit tests 3 | # 4 | 5 | """Setup test environment for unit tests.""" 6 | 7 | import ops.testing 8 | 9 | # enable simulation of container networking 10 | ops.testing.SIMULATE_CAN_CONNECT = True 11 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. 2 | package = [] 3 | 4 | [metadata] 5 | lock-version = "2.1" 6 | python-versions = ">=3.12,<4.0" 7 | content-hash = "0b97d492cd09af7a8c7f687a8215368ab3a85b526b74425a795d3c7e32a8b006" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Argo Operators 2 | ============= 3 | 4 | # Overview 5 | This Repository includes the operators for [Argo](https://argoproj.github.io) 6 | 7 | # Install 8 | 9 | To install the argo-operatorsm run: 10 | 11 | `juju deploy ./bundle.yaml` 12 | 13 | For more information, see https://juju.is/docs 14 | -------------------------------------------------------------------------------- /charms/argo-controller/src/templates/mlpipeline_minio_artifact_secret.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app: minio 6 | name: {{ mlpipeline_minio_artifact_secret }} 7 | namespace: {{ namespace }} 8 | data: 9 | accesskey: {{ access_key }} 10 | secretkey: {{ secret_key }} 11 | type: Opaque 12 | -------------------------------------------------------------------------------- /charms/argo-controller/README.md: -------------------------------------------------------------------------------- 1 | Charmed Argo 2 | ============ 3 | 4 | This charm is part of the Kubeflow bundle. For instructions on how to deploy it, 5 | see https://jaas.ai/kubeflow. 6 | 7 | Upstream documentation can be found at https://argoproj.github.io/argo/ 8 | 9 | The provided dashboard template is based on https://grafana.com/grafana/dashboards/13927 by user M4t3o 10 | -------------------------------------------------------------------------------- /charms/argo-controller/tests/integration/charms_dependencies.py: -------------------------------------------------------------------------------- 1 | """Charms dependencies for tests.""" 2 | 3 | from charmed_kubeflow_chisme.testing import CharmSpec 4 | 5 | MINIO = CharmSpec( 6 | charm="minio", 7 | channel="latest/edge", 8 | trust=False, 9 | config={ 10 | "access-key": "minio", 11 | "secret-key": "minio-secret-key", 12 | }, 13 | ) 14 | -------------------------------------------------------------------------------- /charms/argo-controller/src/prometheus_alert_rules/too_many_requests.rule: -------------------------------------------------------------------------------- 1 | alert: ArgoTooManyRequests 2 | expr: sum by(kind, verb) (rate(argo_workflows_k8s_request_total{status_code="429"}[5m])) > 0 3 | for: 1m 4 | labels: 5 | severity: warning 6 | annotations: 7 | summary: "Argo is receiving too many requests (429)" 8 | description: "Argo controller is receiving 429 (Too Many Requests) responses." 9 | -------------------------------------------------------------------------------- /charms/argo-controller/src/prometheus_alert_rules/operation_panic_error.rule: -------------------------------------------------------------------------------- 1 | alert: ArgoWorkflowOperationPanic 2 | expr: increase(argo_workflows_error_count{cause="OperationPanic"}[5m]) > 0 3 | for: 2m 4 | labels: 5 | severity: warning 6 | annotations: 7 | summary: "Argo Workflow Operation Panic Detected" 8 | description: | 9 | One or more workflow operations have panicked in the last 5 minutes. 10 | -------------------------------------------------------------------------------- /charms/argo-controller/terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "juju_application" "argo_controller" { 2 | charm { 3 | name = "argo-controller" 4 | base = var.base 5 | channel = var.channel 6 | revision = var.revision 7 | } 8 | config = var.config 9 | model = var.model_name 10 | name = var.app_name 11 | resources = var.resources 12 | trust = true 13 | units = 1 14 | } 15 | -------------------------------------------------------------------------------- /charms/argo-controller/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_name" { 2 | value = juju_application.argo_controller.name 3 | } 4 | 5 | output "provides" { 6 | value = { 7 | metrics_endpoint = "metrics-endpoint", 8 | grafana_dashboard = "grafana-dashboard" 9 | } 10 | } 11 | 12 | output "requires" { 13 | value = { 14 | object_storage = "object-storage", 15 | logging = "logging" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bundle.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | bundle: kubernetes 5 | name: argo-workflows 6 | applications: 7 | argo-controller: 8 | charm: ch:argo-controller 9 | channel: latest/edge 10 | source: ./operators/argo-controller 11 | scale: 1 12 | minio: 13 | charm: ch:minio 14 | channel: latest/edge 15 | scale: 1 16 | relations: 17 | - [argo-controller, minio] 18 | -------------------------------------------------------------------------------- /charms/argo-controller/src/prometheus_alert_rules/missing_pods.rule: -------------------------------------------------------------------------------- 1 | alert: ArgoWorkflowPodsMissing 2 | expr: max_over_time(argo_pod_missing[5m]) > 0 3 | for: 5m 4 | labels: 5 | severity: critical 6 | annotations: 7 | summary: "Missing workflow pods detected" 8 | description: > 9 | Detected missing workflow pods in the last 5 minutes. 10 | Missing pods are expected pods that never appeared or were deleted. 11 | See https://argo-workflows.readthedocs.io/en/release-3.5/metrics/#argo_pod_missing for details. 12 | -------------------------------------------------------------------------------- /tools/get-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script returns list of container images that are managed by this charm and/or its workload 4 | # 5 | IMAGE_LIST=() 6 | IMAGE_LIST+=($(find -type f -name metadata.yaml -exec yq '.resources | to_entries | .[] | .value | ."upstream-source"' {} \;)) 7 | VERSION=$(grep upstream-source charms/argo-controller/metadata.yaml | awk -F':' '{print $3}') 8 | IMAGE_LIST+=($(yq '.options.executor-image.default' ./charms/argo-controller/config.yaml)) 9 | printf "%s\n" "${IMAGE_LIST[@]}" 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | # On pull_request, we: 4 | # * always publish to charmhub at latest/edge/branchname 5 | # * always run tests 6 | 7 | on: 8 | pull_request: 9 | 10 | jobs: 11 | 12 | tests: 13 | name: Run Tests 14 | uses: ./.github/workflows/integrate.yaml 15 | secrets: inherit 16 | 17 | # publish runs in parallel with tests, as we always publish in this situation 18 | publish-charm: 19 | name: Publish Charm 20 | uses: ./.github/workflows/publish.yaml 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /concierge.yaml: -------------------------------------------------------------------------------- 1 | juju: 2 | channel: 3.6/stable 3 | model-defaults: 4 | logging-config: =INFO; unit=DEBUG 5 | 6 | providers: 7 | k8s: 8 | enable: true 9 | bootstrap: true 10 | channel: 1.32-classic/stable 11 | features: 12 | local-storage: 13 | load-balancer: 14 | enabled: true 15 | l2-mode: true 16 | cidrs: 10.64.140.43/32 17 | bootstrap-constraints: 18 | root-disk: 2G 19 | 20 | lxd: 21 | enable: true 22 | bootstrap: false 23 | channel: latest/stable 24 | 25 | host: 26 | snaps: 27 | charmcraft: 28 | channel: 3.x/stable 29 | -------------------------------------------------------------------------------- /.github/workflows/on_push.yaml: -------------------------------------------------------------------------------- 1 | name: On Push 2 | 3 | # On push to a "special" branch, we: 4 | # * always publish to charmhub at latest/edge/branchname 5 | # * always run tests 6 | # where a "special" branch is one of main or track/**, as 7 | # by convention these branches are the source for a corresponding 8 | # charmhub edge channel. 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | - track/** 15 | 16 | jobs: 17 | 18 | tests: 19 | name: Run Tests 20 | uses: ./.github/workflows/integrate.yaml 21 | secrets: inherit 22 | 23 | # publish runs in series with tests, and only publishes if tests passes 24 | publish-charm: 25 | name: Publish Charm 26 | needs: tests 27 | uses: ./.github/workflows/publish.yaml 28 | secrets: inherit 29 | -------------------------------------------------------------------------------- /charms/argo-controller/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | options: 5 | bucket: 6 | type: string 7 | default: mlpipeline 8 | description: S3 bucket name 9 | key-format: 10 | type: string 11 | default: "artifacts/{{workflow.name}}/{{workflow.creationTimestamp.Y}}/{{workflow.creationTimestamp.m}}/{{workflow.creationTimestamp.d}}/{{pod.name}}" 12 | description: S3 key prefix 13 | executor-image: 14 | type: string 15 | default: docker.io/charmedkubeflow/argoexec:3.5.14-9761df4 16 | description: | 17 | Image to use for runtime executor. Should be updated alongside updating the rest of the charm's images. 18 | kubelet-insecure: 19 | type: boolean 20 | default: true 21 | description: | 22 | If true, Argo will skip checking kubelet's TLS certificate. Has no effect 23 | with other executors. 24 | -------------------------------------------------------------------------------- /charms/argo-controller/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "app_name" { 2 | description = "Application name" 3 | type = string 4 | default = "argo-controller" 5 | } 6 | 7 | variable "base" { 8 | description = "Charm base" 9 | type = string 10 | default = "ubuntu@24.04" 11 | } 12 | 13 | variable "channel" { 14 | description = "Charm channel" 15 | type = string 16 | default = "latest/edge" 17 | } 18 | 19 | variable "config" { 20 | description = "Map of charm configuration options" 21 | type = map(string) 22 | default = {} 23 | } 24 | 25 | variable "model_name" { 26 | description = "Model name" 27 | type = string 28 | } 29 | 30 | variable "resources" { 31 | description = "Map of resources" 32 | type = map(string) 33 | default = null 34 | } 35 | 36 | variable "revision" { 37 | description = "Charm revision" 38 | type = number 39 | default = null 40 | } 41 | -------------------------------------------------------------------------------- /charms/argo-controller/src/templates/minio_configmap.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | artifactRepository: | 4 | archiveLogs: true 5 | s3: 6 | endpoint: {{ s3_minio_endpoint }} 7 | bucket: {{ s3_bucket }} 8 | # keyFormat defines how artifacts will be organized in a bucket. 9 | keyFormat: {{ key_format }} 10 | # insecure will disable TLS. Primarily used for minio installs not configured with TLS 11 | insecure: {{ kubelet_insecure }} 12 | accessKeySecret: 13 | name: {{ mlpipeline_minio_artifact_secret }} 14 | key: accesskey 15 | secretKeySecret: 16 | name: {{ mlpipeline_minio_artifact_secret }} 17 | key: secretkey 18 | executor: | 19 | imagePullPolicy: IfNotPresent 20 | kind: ConfigMap 21 | metadata: 22 | annotations: 23 | internal.kpt.dev/upstream-identifier: '|ConfigMap|default|workflow-controller-configmap' 24 | labels: 25 | application-crd-id: kubeflow-pipelines 26 | name: {{ argo_controller_configmap }} 27 | namespace: {{ namespace }} 28 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | [flake8] 5 | max-line-length = 100 6 | 7 | [tox] 8 | skipsdist = True 9 | envlist = argo-controller-{lint,unit,integration} 10 | 11 | [testenv] 12 | basepython = python3 13 | allowlist_externals = tox 14 | passenv = HOME 15 | ignore_errors = true 16 | setenv = 17 | argo-controller: CHARM = argo-controller 18 | lint: TYPE = lint 19 | unit: TYPE = unit 20 | integration: TYPE = integration 21 | update-requirements: TYPE = update-requirements 22 | commands = 23 | tox -c {toxinidir}/charms/{env:CHARM} -vve {env:TYPE} 24 | deps = 25 | poetry>=2.1.3 26 | 27 | [testenv:update-requirements] 28 | commands = 29 | # updating all groups' locked dependencies: 30 | poetry lock --regenerate 31 | # updating all groups' locked dependencies for every charm subfolder: 32 | find charms/ -maxdepth 1 -mindepth 1 -type d -exec bash -c "cd {} && poetry lock --regenerate" \; 33 | description = Update requirements, including those in charm subfolders 34 | allowlist_externals = find 35 | skip_install = true 36 | -------------------------------------------------------------------------------- /charms/argo-controller/src/prometheus_alert_rules/KubeflowArgoControllerServices.rules: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: KubeflowArgoControllerServices 3 | rules: 4 | - alert: KubeflowServiceDown 5 | expr: up{} < 1 6 | for: 5m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "{{ $labels.juju_charm }} service is Down ({{ $labels.juju_model }}/{{ $labels.juju_unit }})" 11 | description: | 12 | One or more targets of {{ $labels.juju_charm }} charm are down on unit {{ $labels.juju_model }}/{{ $labels.juju_unit }}. 13 | LABELS = {{ $labels }} 14 | 15 | - alert: KubeflowServiceIsNotStable 16 | expr: avg_over_time(up{}[10m]) < 0.5 17 | for: 0m 18 | labels: 19 | severity: warning 20 | annotations: 21 | summary: "{{ $labels.juju_charm }} service is not stable ({{ $labels.juju_model }}/{{ $labels.juju_unit }})" 22 | description: | 23 | {{ $labels.juju_charm }} unit {{ $labels.juju_model }}/{{ $labels.juju_unit }} has been unreachable at least 50% of the time over the last 10 minutes. 24 | LABELS = {{ $labels }} 25 | -------------------------------------------------------------------------------- /.github/workflows/get-charm-paths.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | # Finds the charms in this repo, outputting them as JSON 4 | # Will return one of: 5 | # * the relative paths of the directories listed in `./charms`, if that directory exists 6 | # * "./", if the root directory has a "metadata.yaml" file 7 | # * otherwise, error 8 | # 9 | # Modified from: https://stackoverflow.com/questions/63517732/github-actions-build-matrix-for-lambda-functions/63736071#63736071 10 | CHARMS_DIR="./charms" 11 | if [ -d "$CHARMS_DIR" ]; 12 | then 13 | CHARM_PATHS=$(find $CHARMS_DIR -maxdepth 1 -type d -not -path '*/\.*' -not -path "$CHARMS_DIR") 14 | else 15 | if [ -f "./metadata.yaml" ] 16 | then 17 | CHARM_PATHS="./" 18 | else 19 | echo "Cannot find valid charm directories - aborting" 20 | exit 1 21 | fi 22 | fi 23 | 24 | # Convert output to JSON string format 25 | # { charm_paths: [...] } 26 | CHARM_PATHS_LIST=$(echo "$CHARM_PATHS" | jq -c --slurp --raw-input 'split("\n")[:-1]') 27 | 28 | echo "Found CHARM_PATHS_LIST: $CHARM_PATHS_LIST" 29 | 30 | echo "::set-output name=CHARM_PATHS_LIST::$CHARM_PATHS_LIST" 31 | -------------------------------------------------------------------------------- /.github/.jira_sync_config.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | # Jira project key to create the issue in 3 | jira_project_key: "KF" 4 | 5 | # Dictionary mapping GitHub issue status to Jira issue status 6 | status_mapping: 7 | opened: Untriaged 8 | closed: done 9 | 10 | # (Optional) GitHub labels. Only issues with one of those labels will be synchronized. 11 | # If not specified, all issues will be synchronized 12 | labels: 13 | - bug 14 | - enhancement 15 | 16 | # (Optional) (Default: false) Add a new comment in GitHub with a link to Jira created issue 17 | add_gh_comment: true 18 | 19 | # (Optional) (Default: true) Synchronize issue description from GitHub to Jira 20 | sync_description: true 21 | 22 | # (Optional) (Default: true) Synchronize comments from GitHub to Jira 23 | sync_comments: false 24 | 25 | # (Optional) (Default: None) Parent Epic key to link the issue to 26 | epic_key: "KF-4805" 27 | 28 | # (Optional) Dictionary mapping GitHub issue labels to Jira issue types. 29 | # If label on the issue is not in specified list, this issue will be created as a Bug 30 | label_mapping: 31 | enhancement: Story 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow triggered manually 2 | name: Release charm to other tracks and channels 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | destination-channel: 8 | description: 'Destination Channel' 9 | required: true 10 | origin-channel: 11 | description: 'Origin Channel' 12 | required: true 13 | charm-name: 14 | description: 'Charm subdirectory name' 15 | required: true 16 | 17 | jobs: 18 | promote-charm: 19 | name: Promote charm 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Release charm to channel 24 | uses: canonical/charming-actions/release-charm@2.6.2 25 | with: 26 | credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | destination-channel: ${{ github.event.inputs.destination-channel }} 29 | origin-channel: ${{ github.event.inputs.origin-channel }} 30 | base-channel: '24.04' 31 | tag-prefix: ${{ github.event.inputs.charm-name }} 32 | charm-path: charms/${{ github.event.inputs.charm-name}} 33 | -------------------------------------------------------------------------------- /.github/workflows/tiobe_scan.yaml: -------------------------------------------------------------------------------- 1 | name: TICS run self-hosted test (github-action) 2 | 3 | on: 4 | workflow_dispatch: # Allows manual triggering 5 | schedule: 6 | - cron: "0 2 * * 6" # Every Saturday 2:00 AM UTC 7 | 8 | jobs: 9 | build: 10 | runs-on: [self-hosted, linux, amd64, tiobe, jammy] 11 | 12 | steps: 13 | - name: Checkout the project 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 3.12 17 | uses: actions/setup-python@v5.6.0 18 | with: 19 | python-version: "3.12" 20 | 21 | - name: Install dependencies 22 | run: | 23 | pip install tox 24 | pip install pylint flake8 25 | 26 | - name: Run tox tests to create coverage.xml 27 | run: | 28 | tox run -e argo-controller-unit 29 | 30 | 31 | - name: move results to necessary folder for TICS 32 | run: | 33 | mkdir cover 34 | cp charms/argo-controller/coverage.xml cover/argo-controller.xml 35 | 36 | 37 | - name: Run TICS analysis with github-action 38 | uses: tiobe/tics-github-action@v3 39 | with: 40 | mode: qserver 41 | project: argo-operators 42 | branchdir: . 43 | viewerUrl: https://canonical.tiobe.com/tiobeweb/TICS/api/cfg?name=default 44 | ticsAuthToken: ${{ secrets.TICSAUTHTOKEN }} 45 | installTics: true 46 | -------------------------------------------------------------------------------- /charms/argo-controller/tests/data/simple_artifact.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Workflow 3 | metadata: 4 | generateName: artifact-passing- 5 | spec: 6 | entrypoint: artifact-example 7 | templates: 8 | - name: artifact-example 9 | steps: 10 | - - name: generate-artifact 11 | template: whalesay 12 | - - name: consume-artifact 13 | template: print-message 14 | arguments: 15 | artifacts: 16 | # bind message to the hello-art artifact 17 | # generated by the generate-artifact step 18 | - name: message 19 | from: "{{steps.generate-artifact.outputs.artifacts.hello-art}}" 20 | 21 | - name: whalesay 22 | container: 23 | image: docker/whalesay:latest 24 | command: [sh, -c] 25 | args: ["cowsay hello world | tee /tmp/hello_world.txt; echo sleeping before exit; sleep 10"] 26 | outputs: 27 | artifacts: 28 | # generate hello-art artifact from /tmp/hello_world.txt 29 | # artifacts can be directories as well as files 30 | - name: hello-art 31 | path: /tmp/hello_world.txt 32 | 33 | - name: print-message 34 | inputs: 35 | artifacts: 36 | # unpack the message input artifact 37 | # and put it at /tmp/message 38 | - name: message 39 | path: /tmp/message 40 | container: 41 | image: alpine:latest 42 | command: [sh, -c] 43 | args: ["cat /tmp/message"] 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.yaml: -------------------------------------------------------------------------------- 1 | name: Task 2 | description: File an enhancement proposal 3 | labels: "enhancement" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this enhancement 9 | proposal! Before submitting your issue, please make sure there 10 | isn't already a prior issue concerning this. If there is, 11 | please join that discussion instead. 12 | - type: textarea 13 | id: enhancement-proposal-context 14 | attributes: 15 | label: Context 16 | description: > 17 | Describe why we should work on this task/enhancement, as well as 18 | existing context we should be aware of 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: enhancement-proposal-what 23 | attributes: 24 | label: What needs to get done 25 | description: > 26 | Describe what needs to get done 27 | placeholder: | 28 | 1. Look into X 29 | 2. Implement Y 30 | 3. Create file Z 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: enhancement-proposal-dod 35 | attributes: 36 | label: Definition of Done 37 | description: > 38 | What are the requirements for the task to be considered done 39 | placeholder: | 40 | 1. We know how X works (spike) 41 | 2. Code is doing Y 42 | 3. Charm has functionality Z 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /charms/argo-controller/metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | name: argo-controller 4 | summary: Container-Native Workflow Engine for Kubernetes 5 | description: Container-Native Workflow Engine for Kubernetes 6 | website: https://charmhub.io/argo-controller 7 | source: https://github.com/canonical/argo-operators/argo-controller 8 | issues: https://github.com/canonical/argo-operators/issues 9 | docs: https://discourse.charmhub.io/t/8212 10 | resources: 11 | oci-image: 12 | type: oci-image 13 | description: 'Backing OCI image' 14 | auto-fetch: true 15 | upstream-source: docker.io/charmedkubeflow/workflow-controller:3.5.14-ee1fda3 16 | containers: 17 | argo-controller: 18 | resource: oci-image 19 | uid: 584792 20 | gid: 584792 21 | requires: 22 | object-storage: 23 | interface: object-storage 24 | schema: 25 | v1: 26 | provides: 27 | type: object 28 | properties: 29 | access-key: 30 | type: string 31 | namespace: 32 | type: 33 | - string 34 | - 'null' 35 | port: 36 | type: number 37 | secret-key: 38 | type: string 39 | secure: 40 | type: boolean 41 | service: 42 | type: string 43 | required: 44 | - access-key 45 | - port 46 | - secret-key 47 | - secure 48 | - service 49 | versions: [v1] 50 | __schema_source: https://raw.githubusercontent.com/canonical/operator-schemas/master/object-storage.yaml 51 | logging: 52 | interface: loki_push_api 53 | optional: true 54 | provides: 55 | metrics-endpoint: 56 | interface: prometheus_scrape 57 | grafana-dashboard: 58 | interface: grafana_dashboard 59 | charm-user: non-root 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to Manage Python Dependencies and Environments 2 | 3 | 4 | ### Prerequisites 5 | 6 | `tox` is the only tool required locally, as `tox` internally installs and uses `poetry`, be it to manage Python dependencies or to run `tox` environments. To install it: `pipx install tox`. 7 | 8 | Optionally, `poerty` can be additionally installed independently just for the sake of running Python commands locally outside of `tox` during debugging/development. To install it: `pipx install poetry`. 9 | 10 | 11 | ### Updating Dependencies 12 | 13 | To add/update/remove any dependencies and/or to upgrade Python, simply: 14 | 15 | 1. add/update/remove such dependencies to/in/from the desired group(s) below `[tool.poetry.group..dependencies]` in `pyproject.toml`, and/or upgrade Python itself in `requires-python` under `[project]` 16 | 17 | _⚠️ dependencies for the charm itself are also defined as dependencies of a dedicated group called `charm`, specifically below `[tool.poetry.group.charm.dependencies]`, and not as project dependencies below `[project.dependencies]` or `[tool.poetry.dependencies]` ⚠️_ 18 | 19 | 2. run `tox -e update-requirements` to update the lock file 20 | 21 | by this point, `poerty`, through `tox`, will let you know if there are any dependency conflicts to solve. 22 | 23 | 3. optionally, if you also want to update your local environment for running Python commands/scripts yourself and not through tox, see [Running Python Environments](#running-python-environments) below 24 | 25 | 26 | ### Running `tox` Environments 27 | 28 | To run `tox` environments, either locally for development or in CI workflows for testing, ensure to have `tox` installed first and then simply run your `tox` environments natively (e.g.: `tox -e lint`). `tox` will internally first install `poetry` and then rely on it to install and run its environments. 29 | 30 | 31 | ### Running Python Environments 32 | 33 | To run Python commands locally for debugging/development from any environments built from any combinations of dependency groups without relying on `tox`: 34 | 1. ensure you have `poetry` installed 35 | 2. install any required dependency groups: `poetry install --only ,` (or all groups, if you prefer: `poetry install --all-groups`) 36 | 3. run Python commands via poetry: `poetry run python3 ` 37 | -------------------------------------------------------------------------------- /charms/argo-controller/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Overview 4 | 5 | This document outlines the processes and practices recommended for contributing enhancements to `argo-controller`. 6 | 7 | ## Talk to us First 8 | 9 | Before developing enhancements to this charm, you should [open an issue](/../../issues) explaining your use case. If you would like to chat with us about your use-cases or proposed implementation, you can reach us at [MLOps Mattermost public channel](https://chat.charmhub.io/charmhub/channels/mlops-documentation) or on [Discourse](https://discourse.charmhub.io/). 10 | 11 | ## Pull Requests 12 | 13 | Please help us out in ensuring easy to review branches by rebasing your pull request branch onto the `main` branch. This also avoids merge commits and creates a linear Git commit history. 14 | 15 | All pull requests require review before being merged. Code review typically examines: 16 | - code quality 17 | - test coverage 18 | - user experience for Juju administrators of this charm. 19 | 20 | ## Recommended Knowledge 21 | 22 | Familiarising yourself with the [Charmed Operator Framework](https://juju.is/docs/sdk) library will help you a lot when working on new features or bug fixes. 23 | 24 | ## Build Charm 25 | 26 | To build `argo-controller` run: 27 | 28 | ```shell 29 | charmcraft pack 30 | ``` 31 | 32 | ## Developing 33 | 34 | You can use the environments created by `tox` for development. For example, to load the `unit` environment into your shell, run: 35 | 36 | ```shell 37 | tox --notest -e unit 38 | source .tox/unit/bin/activate 39 | ``` 40 | 41 | ### Testing 42 | 43 | Use tox for testing. For example to test the `integration` environment, run: 44 | 45 | ```shell 46 | tox -e integration 47 | ``` 48 | 49 | See `tox.ini` for all available environments. 50 | 51 | ### Deploy 52 | 53 | ```bash 54 | # Create a model 55 | juju add-model dev 56 | # Enable DEBUG logging 57 | juju model-config logging-config="=INFO;unit=DEBUG" 58 | # Deploy the charm 59 | juju deploy ./argo-controller_ubuntu@24.04-amd64.charm \ 60 | --resource oci-image=$(yq '.resources."oci-image"."upstream-source"' metadata.yaml) 61 | 62 | ## Canonical Contributor Agreement 63 | 64 | Canonical welcomes contributions to this charm. Please check out our [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this bug report! Before submitting your issue, please make 9 | sure you are using the latest version of the charms. If not, please switch to the newest revision prior to 10 | posting your report to make sure it's not already solved. 11 | - type: textarea 12 | id: bug-description 13 | attributes: 14 | label: Bug Description 15 | description: > 16 | If applicable, add screenshots to help explain your problem. If applicable, add screenshots to 17 | help explain the problem you are facing. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: reproduction 22 | attributes: 23 | label: To Reproduce 24 | description: > 25 | Please provide a step-by-step instruction of how to reproduce the behavior. 26 | placeholder: | 27 | 1. `juju deploy ...` 28 | 2. `juju relate ...` 29 | 3. `juju status --relations` 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: environment 34 | attributes: 35 | label: Environment 36 | description: > 37 | We need to know a bit more about the context in which you run the charm. 38 | - Are you running Juju locally, on lxd, in multipass or on some other platform? 39 | - What track and channel you deployed the charm from (ie. `latest/edge` or similar). 40 | - Version of any applicable components, like the juju snap, the model controller, lxd, microk8s, and/or multipass. 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: logs 45 | attributes: 46 | label: Relevant Log Output 47 | description: > 48 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 49 | Fetch the logs using `juju debug-log --replay` and `kubectl logs ...`. Additional details available in the juju docs 50 | at https://juju.is/docs/olm/juju-logs 51 | render: shell 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: additional-context 56 | attributes: 57 | label: Additional Context 58 | -------------------------------------------------------------------------------- /charms/argo-controller/terraform/README.md: -------------------------------------------------------------------------------- 1 | # Terraform module for argo-controller 2 | 3 | This is a Terraform module facilitating the deployment of argo-controller charm, using the [Terraform juju provider](https://github.com/juju/terraform-provider-juju/). For more information, refer to the provider [documentation](https://registry.terraform.io/providers/juju/juju/latest/docs). 4 | 5 | ## Requirements 6 | This module requires a `juju` model to be available. Refer to the [usage section](#usage) below for more details. 7 | 8 | ## API 9 | 10 | ### Inputs 11 | The module offers the following configurable inputs: 12 | 13 | | Name | Type | Description | Required | 14 | | - | - | - | - | 15 | | `app_name`| string | Application name | False | 16 | | `base`| string | Charm base | False | 17 | | `channel`| string | Channel that the charm is deployed from | False | 18 | | `config`| map(string) | Map of the charm configuration options | False | 19 | | `model_name`| string | Name of the model that the charm is deployed on | True | 20 | | `resources`| map(string) | Map of the charm resources | False | 21 | | `revision`| number | Revision number of the charm name | False | 22 | 23 | ### Outputs 24 | Upon applied, the module exports the following outputs: 25 | 26 | | Name | Description | 27 | | - | - | 28 | | `app_name`| Application name | 29 | | `provides`| Map of `provides` endpoints | 30 | | `requires`| Map of `reqruires` endpoints | 31 | 32 | ## Usage 33 | 34 | This module is intended to be used as part of a higher-level module. When defining one, users should ensure that Terraform is aware of the `juju_model` dependency of the charm module. There are two options to do so when creating a high-level module: 35 | 36 | ### Define a `juju_model` resource 37 | Define a `juju_model` resource and pass to the `model_name` input a reference to the `juju_model` resource's name. For example: 38 | 39 | ``` 40 | resource "juju_model" "testing" { 41 | name = kubeflow 42 | } 43 | 44 | module "argo-controller" { 45 | source = "" 46 | model_name = juju_model.testing.name 47 | } 48 | ``` 49 | 50 | ### Define a `data` source 51 | Define a `data` source and pass to the `model_name` input a reference to the `data.juju_model` resource's name. This will enable Terraform to look for a `juju_model` resource with a name attribute equal to the one provided, and apply only if this is present. Otherwise, it will fail before applying anything. 52 | ``` 53 | data "juju_model" "testing" { 54 | name = var.model_name 55 | } 56 | 57 | module "argo-controller" { 58 | source = "" 59 | model_name = data.juju_model.testing.name 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /charms/argo-controller/tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | [flake8] 5 | max-line-length = 100 6 | 7 | [tox] 8 | skipsdist = True 9 | skip_missing_interpreters = True 10 | envlist = fmt, lint, unit, integration 11 | 12 | [vars] 13 | all_path = {[vars]src_path} {[vars]tst_path} 14 | src_path = {toxinidir}/src/ 15 | tst_path = {toxinidir}/tests/ 16 | 17 | [testenv] 18 | passenv = 19 | PYTHONPATH 20 | CHARM_BUILD_DIR 21 | MODEL_SETTINGS 22 | KUBECONFIG 23 | setenv = 24 | PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} 25 | PYTHONBREAKPOINT=ipdb.set_trace 26 | PY_COLORS=1 27 | deps = 28 | poetry>=2.1.3 29 | 30 | [testenv:update-requirements] 31 | commands = 32 | # updating all groups' locked dependencies: 33 | poetry lock --regenerate 34 | description = Update requirements 35 | skip_install = true 36 | 37 | [testenv:fmt] 38 | commands = 39 | isort {[vars]all_path} 40 | black {[vars]all_path} 41 | description = Apply coding style standards to code 42 | commands_pre = 43 | poetry install --only fmt 44 | skip_install = true 45 | 46 | [testenv:lint] 47 | commands = 48 | # uncomment the following line if this charm owns a lib 49 | # codespell {[vars]lib_path} 50 | codespell {toxinidir}/. --skip {toxinidir}/./.git --skip {toxinidir}/./.tox \ 51 | --skip {toxinidir}/./build --skip {toxinidir}/./lib --skip {toxinidir}/./venv \ 52 | --skip {toxinidir}/./.mypy_cache \ 53 | --skip {toxinidir}/./icon.svg --skip *.json.tmpl \ 54 | --skip {toxinidir}/./src/templates/* \ 55 | --skip *.lock 56 | # pflake8 wrapper supports config from pyproject.toml 57 | pflake8 {[vars]all_path} 58 | isort --check-only --diff {[vars]all_path} 59 | black --check --diff {[vars]all_path} 60 | description = Check code against coding style standards 61 | commands_pre = 62 | poetry install --only lint 63 | skip_install = true 64 | 65 | [testenv:tflint] 66 | allowlist_externals = 67 | tflint 68 | commands = 69 | tflint --chdir=terraform --recursive 70 | description = Check Terraform code against coding style standards 71 | 72 | [testenv:unit] 73 | commands = 74 | coverage run --source={[vars]src_path} \ 75 | -m pytest --ignore={[vars]tst_path}integration -vv --tb native {posargs} 76 | coverage report 77 | coverage xml 78 | description = Run unit tests 79 | commands_pre = 80 | poetry install --only unit,charm 81 | skip_install = true 82 | 83 | [testenv:integration] 84 | commands = pytest -v --tb native --asyncio-mode=auto {[vars]tst_path}integration --log-cli-level=INFO -s {posargs} 85 | description = Run integration tests 86 | commands_pre = 87 | poetry install --only integration 88 | skip_install = true 89 | -------------------------------------------------------------------------------- /charms/argo-controller/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # Testing tools configuration 5 | [tool.coverage.run] 6 | branch = true 7 | 8 | [tool.coverage.report] 9 | show_missing = true 10 | 11 | [tool.pytest.ini_options] 12 | minversion = "6.0" 13 | log_cli_level = "INFO" 14 | 15 | # Formatting tools configuration 16 | [tool.black] 17 | line-length = 99 18 | target-version = ["py38"] 19 | 20 | [tool.isort] 21 | line_length = 99 22 | profile = "black" 23 | 24 | # Linting tools configuration 25 | [tool.flake8] 26 | max-line-length = 99 27 | max-doc-length = 99 28 | max-complexity = 10 29 | exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] 30 | select = ["E", "W", "F", "C", "N", "R", "D", "H"] 31 | # Ignore W503, E501 because using black creates errors with this 32 | # Ignore D107 Missing docstring in __init__ 33 | ignore = ["W503", "E501", "D107"] 34 | # D100, D101, D102, D103: Ignore missing docstrings in tests 35 | per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] 36 | docstring-convention = "google" 37 | # Check for properly formatted copyright header in each file 38 | copyright-check = "True" 39 | copyright-author = "Canonical Ltd." 40 | copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" 41 | 42 | [tool.poetry] 43 | package-mode = false 44 | 45 | [tool.poetry.group.charm] 46 | optional = true 47 | 48 | [tool.poetry.group.charm.dependencies] 49 | charmed-kubeflow-chisme = ">=0.4.14" 50 | cosl = "^1.0.0" 51 | lightkube = "^0.17.1" 52 | ops = "^2.17.1" 53 | serialized-data-interface = "^0.7.0" 54 | 55 | [tool.poetry.group.fmt] 56 | optional = true 57 | 58 | [tool.poetry.group.fmt.dependencies] 59 | black = "^24.8.0" 60 | isort = "^5.13.2" 61 | 62 | [tool.poetry.group.lint] 63 | optional = true 64 | 65 | [tool.poetry.group.lint.dependencies] 66 | black = "^24.8.0" 67 | codespell = "^2.3.0" 68 | colorama = "^0.4.6" 69 | flake8 = "^7.0.0" 70 | flake8-builtins = "^2.5.0" 71 | flake8-copyright = "^0.2.4" 72 | isort = "^5.13.2" 73 | pep8-naming = "^0.14.1" 74 | pyproject-flake8 = "^7.0.0" 75 | 76 | [tool.poetry.group.unit] 77 | optional = true 78 | 79 | [tool.poetry.group.unit.dependencies] 80 | coverage = "^7.6.1" 81 | pytest = "^8.3.4" 82 | pytest-mock = "^3.14.0" 83 | 84 | [tool.poetry.group.integration] 85 | optional = true 86 | 87 | [tool.poetry.group.integration.dependencies] 88 | juju = "<4.0" 89 | charmed-kubeflow-chisme = ">=0.4.14" 90 | aiohttp = "^3.10.11" 91 | jinja2 = "^3.1.4" 92 | lightkube = "^0.17.1" 93 | pytest-operator = "^0.38.0" 94 | requests = "^2.32.3" 95 | tenacity = "^9.0.0" 96 | 97 | [project] 98 | name = "argo-operators" 99 | requires-python = ">=3.12,<4.0" 100 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow triggered by other actions 2 | name: CI 3 | 4 | on: 5 | workflow_call: 6 | secrets: 7 | CHARMCRAFT_CREDENTIALS: 8 | required: true 9 | 10 | jobs: 11 | lib-check: 12 | name: Check libraries 13 | uses: canonical/charmed-kubeflow-workflows/.github/workflows/_quality-checks.yaml@main 14 | secrets: inherit 15 | with: 16 | charm-path: ./charms/argo-controller 17 | 18 | lint: 19 | name: Lint Code 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - name: Check out repo 23 | uses: actions/checkout@v4 24 | - run: pipx install tox 25 | - run: tox -vve argo-controller-lint 26 | 27 | unit: 28 | name: Unit Tests 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - name: Check out repo 32 | uses: actions/checkout@v4 33 | - run: pipx install tox 34 | - run: tox -vve argo-controller-unit 35 | 36 | terraform-checks: 37 | name: Terraform 38 | uses: canonical/charmed-kubeflow-workflows/.github/workflows/terraform-checks.yaml@main 39 | with: 40 | charm-path: ./charms/argo-controller 41 | 42 | integration: 43 | name: Integration Test 44 | runs-on: ubuntu-24.04 45 | strategy: 46 | matrix: 47 | charm: [argo-controller] 48 | steps: 49 | - name: Maximise GH runner space 50 | uses: easimon/maximize-build-space@v8 51 | with: 52 | root-reserve-mb: 34816 53 | remove-dotnet: 'true' 54 | remove-haskell: 'true' 55 | remove-android: 'true' 56 | remove-codeql: 'true' 57 | remove-docker-images: 'true' 58 | - uses: actions/checkout@v4 59 | - name: Install dependencies 60 | run: pipx install tox 61 | 62 | - name: Setup environment 63 | run: | 64 | sudo apt-get remove -y docker-ce docker-ce-cli containerd.io 65 | sudo rm -rf /run/containerd 66 | sudo snap install concierge --classic 67 | sudo concierge prepare --trace 68 | 69 | - name: Build and test 70 | working-directory: ./charms/${{ matrix.charm }} 71 | run: | 72 | tox -vve integration -- --model testing 73 | 74 | # On failure, capture debugging resources 75 | - name: Get all 76 | run: kubectl get all -A 77 | if: failure() 78 | 79 | - name: Describe deployments 80 | run: kubectl describe deployments -A 81 | if: failure() 82 | 83 | - name: Describe replicasets 84 | run: kubectl describe replicasets -A 85 | if: failure() 86 | 87 | - name: Get juju status 88 | run: juju status 89 | if: failure() 90 | 91 | - name: Get application logs 92 | run: kubectl logs -n testing --tail 1000 -lapp.kubernetes.io/name=${{ matrix.charm }} 93 | if: failure() 94 | 95 | - name: Get argo-controller operator logs 96 | run: kubectl logs -n testing --tail 1000 -loperator.juju.is/name==${{ matrix.charm }} 97 | if: failure() 98 | -------------------------------------------------------------------------------- /charms/argo-controller/src/components/pebble_component.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | import logging 4 | 5 | from charmed_kubeflow_chisme.components.pebble_component import PebbleServiceComponent 6 | from ops.pebble import Layer 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | ARGO_CONTROLLER_CONFIGMAP = "argo-workflow-controller-configmap" 11 | ARGO_KEYFORMAT = ( 12 | "artifacts/{{workflow.name}}/" 13 | "{{workflow.creationTimestamp.Y}}/" 14 | "{{workflow.creationTimestamp.m}}/" 15 | "{{workflow.creationTimestamp.d}}/" 16 | "{{pod.name}}" 17 | ) 18 | EXECUTOR_IMAGE_CONFIG_NAME = "executor-image" 19 | LIVENESS_PROBE_PORT = "6060" 20 | METRICS_PORT = "9090" 21 | LIVENESS_PROBE_PATH = "/healthz" 22 | LIVENESS_PROBE_NAME = "argo-controller-up" 23 | 24 | 25 | class ArgoControllerPebbleService(PebbleServiceComponent): 26 | """Pebble service container component to configure Pebble layer.""" 27 | 28 | def __init__( 29 | self, 30 | *args, 31 | **kwargs, 32 | ): 33 | super().__init__(*args, **kwargs) 34 | self.environment = { 35 | "ARGO_NAMESPACE": self.model.name, 36 | "LEADER_ELECTION_IDENTITY": self.model.app.name, 37 | } 38 | 39 | def get_layer(self) -> Layer: 40 | """Defines and returns Pebble layer configuration 41 | 42 | This method is required for subclassing PebbleServiceContainer 43 | """ 44 | logger.info("PebbleServiceComponent.get_layer executing") 45 | return Layer( 46 | { 47 | "summary": "argo-controller layer", 48 | "description": "Pebble config layer for argo-controller", 49 | "services": { 50 | self.service_name: { 51 | "override": "replace", 52 | "summary": "Entry point for kfp-viewer image", 53 | "command": ( 54 | "workflow-controller " 55 | "--configmap " 56 | f"{ARGO_CONTROLLER_CONFIGMAP} " 57 | "--executor-image " 58 | f"{self.model.config[EXECUTOR_IMAGE_CONFIG_NAME]}" 59 | ), 60 | "startup": "enabled", 61 | "user": "_daemon_", # This is needed only for rocks 62 | "environment": self.environment, 63 | "on-check-failure": {LIVENESS_PROBE_NAME: "restart"}, 64 | } 65 | }, 66 | "checks": { 67 | LIVENESS_PROBE_NAME: { 68 | "override": "replace", 69 | "period": "30s", 70 | "timeout": "20s", 71 | "threshold": 3, 72 | "http": { 73 | "url": f"http://localhost:{LIVENESS_PROBE_PORT}{LIVENESS_PROBE_PATH}" 74 | }, 75 | } 76 | }, 77 | } 78 | ) 79 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow for publishing all charms in this repo 2 | name: Publish 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | source_branch: 8 | description: Github branch from this repo to publish. If blank, will use the default branch 9 | default: '' 10 | required: false 11 | type: string 12 | secrets: 13 | CHARMCRAFT_CREDENTIALS: 14 | required: true 15 | workflow_dispatch: 16 | inputs: 17 | destination_channel: 18 | description: CharmHub channel to publish to 19 | required: false 20 | default: 'latest/edge' 21 | type: string 22 | source_branch: 23 | description: Github branch from this repo to publish. If blank, will use the default branch 24 | required: false 25 | default: '' 26 | type: string 27 | 28 | jobs: 29 | get-charm-paths: 30 | name: Generate the Charm Matrix 31 | runs-on: ubuntu-24.04 32 | outputs: 33 | charm_paths_list: ${{ steps.get-charm-paths.outputs.CHARM_PATHS_LIST }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | ref: ${{ inputs.source_branch }} 39 | - name: Get paths for all charms in repo 40 | id: get-charm-paths 41 | run: bash .github/workflows/get-charm-paths.sh 42 | 43 | 44 | publish-charm: 45 | name: Publish Charm 46 | runs-on: ubuntu-24.04 47 | needs: get-charm-paths 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | charm-path: ${{ fromJson(needs.get-charm-paths.outputs.charm_paths_list) }} 52 | 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v4 56 | with: 57 | fetch-depth: 0 58 | ref: ${{ inputs.source_branch }} 59 | 60 | - name: Select charmhub channel 61 | uses: canonical/charming-actions/channel@2.6.2 62 | id: select-channel 63 | if: ${{ inputs.destination_channel == '' }} 64 | 65 | # Combine inputs from different sources to a single canonical value so later steps don't 66 | # need logic for picking the right one 67 | - name: Parse and combine inputs 68 | id: parse-inputs 69 | run: | 70 | # destination_channel 71 | destination_channel="${{ inputs.destination_channel || steps.select-channel.outputs.name }}" 72 | echo "setting output of destination_channel=$destination_channel" 73 | echo "::set-output name=destination_channel::$destination_channel" 74 | 75 | # tag_prefix 76 | # if charm_path = ./ --> tag_prefix = '' (null) 77 | # if charm_path != ./some-charm (eg: a charm in a ./charms dir) --> tag_prefix = 'some-charm' 78 | if [ ${{ matrix.charm-path }} == './' ]; then 79 | tag_prefix='' 80 | else 81 | tag_prefix=$(basename ${{ matrix.charm-path }} ) 82 | fi 83 | echo "setting output of tag_prefix=$tag_prefix" 84 | echo "::set-output name=tag_prefix::$tag_prefix" 85 | 86 | # Required to charmcraft pack in non-destructive mode 87 | - name: Setup lxd 88 | uses: canonical/setup-lxd@v0.1.2 89 | with: 90 | channel: latest/stable 91 | 92 | - name: Upload charm to charmhub 93 | uses: canonical/charming-actions/upload-charm@2.6.2 94 | with: 95 | credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} 96 | github-token: ${{ secrets.GITHUB_TOKEN }} 97 | charm-path: ${{ matrix.charm-path }} 98 | channel: ${{ steps.parse-inputs.outputs.destination_channel }} 99 | tag-prefix: ${{ steps.parse-inputs.outputs.tag_prefix }} 100 | charmcraft-channel: 3.x/stable 101 | destructive-mode: false 102 | -------------------------------------------------------------------------------- /charms/argo-controller/charmcraft.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | type: charm 5 | platforms: 6 | ubuntu@24.04:amd64: 7 | 8 | # Files implicitly created by charmcraft without a part: 9 | # - dispatch (https://github.com/canonical/charmcraft/pull/1898) 10 | # - manifest.yaml 11 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L259) 12 | # Files implicitly copied/"staged" by charmcraft without a part: 13 | # - actions.yaml, config.yaml, metadata.yaml 14 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L290-L293 15 | # https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L156-L157) 16 | parts: 17 | # "poetry-deps" part name is a magic constant 18 | # https://github.com/canonical/craft-parts/pull/901 19 | poetry-deps: 20 | plugin: nil 21 | build-packages: 22 | - curl 23 | override-build: | 24 | # Use environment variable instead of `--break-system-packages` to avoid failing on older 25 | # versions of pip that do not recognize `--break-system-packages` 26 | # `--user` needed (in addition to `--break-system-packages`) for Ubuntu >=24.04 27 | PIP_BREAK_SYSTEM_PACKAGES=true python3 -m pip install --user --upgrade pip==24.3.1 # renovate: charmcraft-pip-latest 28 | 29 | # Use uv to install poetry so that a newer version of Python can be installed if needed by poetry 30 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.5.15/uv-installer.sh | sh # renovate: charmcraft-uv-latest 31 | # poetry 2.0.0 requires Python >=3.9 32 | if ! "$HOME/.local/bin/uv" python find '>=3.9' 33 | then 34 | # Use first Python version that is >=3.9 and available in an Ubuntu LTS 35 | # (to reduce the number of Python versions we use) 36 | "$HOME/.local/bin/uv" python install 3.10.12 # renovate: charmcraft-python-ubuntu-22.04 37 | fi 38 | "$HOME/.local/bin/uv" tool install --no-python-downloads --python '>=3.9' poetry==2.0.0 --with poetry-plugin-export==1.8.0 # renovate: charmcraft-poetry-latest 39 | 40 | ln -sf "$HOME/.local/bin/poetry" /usr/local/bin/poetry 41 | # "charm-poetry" part name is arbitrary; use for consistency 42 | # Avoid using "charm" part name since that has special meaning to charmcraft 43 | charm-poetry: 44 | # By default, the `poetry` plugin creates/stages these directories: 45 | # - lib, src 46 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L76-L78) 47 | # - venv 48 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L95 49 | # https://github.com/canonical/craft-parts/blob/afb0d652eb330b6aaad4f40fbd6e5357d358de47/craft_parts/plugins/base.py#L270) 50 | plugin: poetry 51 | source: . 52 | after: 53 | - poetry-deps 54 | poetry-export-extra-args: ['--only', 'charm'] 55 | build-packages: 56 | - libffi-dev # Needed to build Python dependencies with Rust from source 57 | - libssl-dev # Needed to build Python dependencies with Rust from source 58 | - pkg-config # Needed to build Python dependencies with Rust from source 59 | override-build: | 60 | # Workaround for https://github.com/canonical/charmcraft/issues/2068 61 | # rustup used to install rustc and cargo, which are needed to build Python dependencies with Rust from source 62 | if [[ "$CRAFT_PLATFORM" == ubuntu@20.04:* || "$CRAFT_PLATFORM" == ubuntu@22.04:* ]] 63 | then 64 | snap install rustup --classic 65 | else 66 | apt-get install rustup -y 67 | fi 68 | 69 | # If Ubuntu version < 24.04, rustup was installed from snap instead of from the Ubuntu 70 | # archive—which means the rustup version could be updated at any time. Print rustup version 71 | # to build log to make changes to the snap's rustup version easier to track 72 | rustup --version 73 | 74 | # rpds-py (Python package) >=0.19.0 requires rustc >=1.76, which is not available in the 75 | # Ubuntu 22.04 archive. Install rustc and cargo using rustup instead of the Ubuntu archive 76 | rustup set profile minimal 77 | rustup default 1.83.0 # renovate: charmcraft-rust-latest 78 | 79 | craftctl default 80 | # Include requirements.txt in *.charm artifact for easier debugging 81 | cp requirements.txt "$CRAFT_PART_INSTALL/requirements.txt" 82 | # "files" part name is arbitrary; use for consistency 83 | files: 84 | plugin: dump 85 | source: . 86 | stage: 87 | - LICENSE 88 | -------------------------------------------------------------------------------- /charms/argo-controller/src/charm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2023 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | 5 | """Charm for the Argo Workflow Controller. 6 | 7 | https://github.com/canonical/argo-operators 8 | """ 9 | 10 | import logging 11 | from base64 import b64encode 12 | 13 | import lightkube 14 | from charmed_kubeflow_chisme.components import SdiRelationDataReceiverComponent 15 | from charmed_kubeflow_chisme.components.charm_reconciler import CharmReconciler 16 | from charmed_kubeflow_chisme.components.kubernetes_component import KubernetesComponent 17 | from charmed_kubeflow_chisme.components.leadership_gate_component import LeadershipGateComponent 18 | from charmed_kubeflow_chisme.kubernetes import create_charm_default_labels 19 | from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider 20 | from charms.loki_k8s.v1.loki_push_api import LogForwarder 21 | from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch 22 | from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider 23 | from lightkube.models.core_v1 import ServicePort 24 | from lightkube.resources.apiextensions_v1 import CustomResourceDefinition 25 | from lightkube.resources.core_v1 import ConfigMap, Secret 26 | from ops import main 27 | from ops.charm import CharmBase 28 | 29 | from components.pebble_component import ( 30 | ARGO_CONTROLLER_CONFIGMAP, 31 | ARGO_KEYFORMAT, 32 | METRICS_PORT, 33 | ArgoControllerPebbleService, 34 | ) 35 | 36 | logger = logging.getLogger(__name__) 37 | 38 | K8S_RESOURCE_FILES = [ 39 | "src/templates/crds.yaml", 40 | "src/templates/minio_configmap.yaml.j2", 41 | "src/templates/mlpipeline_minio_artifact_secret.yaml.j2", 42 | ] 43 | METRICS_PATH = "/metrics" 44 | 45 | 46 | class ArgoControllerOperator(CharmBase): 47 | """Charm for the Argo Workflows controller. 48 | 49 | https://github.com/canonical/argo-operators 50 | """ 51 | 52 | def __init__(self, *args): 53 | super().__init__(*args) 54 | 55 | # patch service ports 56 | metrics_port = ServicePort(int(METRICS_PORT), name="metrics-port") 57 | self.service_patcher = KubernetesServicePatch( 58 | self, 59 | [metrics_port], 60 | service_name=self.app.name, 61 | ) 62 | 63 | self.prometheus_provider = MetricsEndpointProvider( 64 | charm=self, 65 | relation_name="metrics-endpoint", 66 | jobs=[ 67 | { 68 | "metrics_path": METRICS_PATH, 69 | "static_configs": [{"targets": ["*:{}".format(METRICS_PORT)]}], 70 | } 71 | ], 72 | ) 73 | 74 | # The provided dashboard template is based on https://grafana.com/grafana/dashboards/13927 75 | # by user M4t3o 76 | self.dashboard_provider = GrafanaDashboardProvider(self) 77 | 78 | self.charm_reconciler = CharmReconciler(self) 79 | 80 | self.leadership_gate = self.charm_reconciler.add( 81 | component=LeadershipGateComponent( 82 | charm=self, 83 | name="leadership-gate", 84 | ), 85 | depends_on=[], 86 | ) 87 | 88 | self.object_storage_relation = self.charm_reconciler.add( 89 | component=SdiRelationDataReceiverComponent( 90 | charm=self, 91 | name="relation:object_storage", 92 | relation_name="object-storage", 93 | ), 94 | depends_on=[self.leadership_gate], 95 | ) 96 | 97 | self.kubernetes_resources = self.charm_reconciler.add( 98 | component=KubernetesComponent( 99 | charm=self, 100 | name="kubernetes:crds-cm-and-secrets", 101 | resource_templates=K8S_RESOURCE_FILES, 102 | krh_resource_types={ 103 | ConfigMap, 104 | CustomResourceDefinition, 105 | Secret, 106 | }, 107 | krh_labels=create_charm_default_labels( 108 | self.app.name, 109 | self.model.name, 110 | scope="crds-cm-and-secrets", 111 | ), 112 | context_callable=self._context_callable, 113 | lightkube_client=lightkube.Client(), 114 | ), 115 | depends_on=[ 116 | self.leadership_gate, 117 | self.object_storage_relation, 118 | ], 119 | ) 120 | 121 | self.argo_controller_container = self.charm_reconciler.add( 122 | component=ArgoControllerPebbleService( 123 | charm=self, 124 | name="container:argo-controller", 125 | container_name="argo-controller", 126 | service_name="argo-controller", 127 | ), 128 | depends_on=[ 129 | self.leadership_gate, 130 | self.kubernetes_resources, 131 | self.object_storage_relation, 132 | ], 133 | ) 134 | 135 | self.charm_reconciler.install_default_event_handlers() 136 | self._logging = LogForwarder(charm=self) 137 | 138 | @property 139 | def _context_callable(self): 140 | return lambda: { 141 | "app_name": self.app.name, 142 | "namespace": self.model.name, 143 | "access_key": b64encode( 144 | self.object_storage_relation.component.get_data()["access-key"].encode("utf-8") 145 | ).decode("utf-8"), 146 | "secret_key": b64encode( 147 | self.object_storage_relation.component.get_data()["secret-key"].encode("utf-8") 148 | ).decode("utf-8"), 149 | "mlpipeline_minio_artifact_secret": "mlpipeline-minio-artifact", 150 | "argo_controller_configmap": ARGO_CONTROLLER_CONFIGMAP, 151 | "s3_bucket": self.model.config["bucket"], 152 | "s3_minio_endpoint": ( 153 | f"{self.object_storage_relation.component.get_data()['service']}." 154 | f"{self.object_storage_relation.component.get_data()['namespace']}:" 155 | f"{self.object_storage_relation.component.get_data()['port']}" 156 | ), 157 | "kubelet_insecure": self.model.config["kubelet-insecure"], 158 | "key_format": ARGO_KEYFORMAT, 159 | } 160 | 161 | 162 | if __name__ == "__main__": 163 | main(ArgoControllerOperator) 164 | -------------------------------------------------------------------------------- /charms/argo-controller/tests/integration/test_charm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | import logging 5 | from pathlib import Path 6 | 7 | import lightkube 8 | import pytest 9 | import yaml 10 | from charmed_kubeflow_chisme.testing import ( 11 | GRAFANA_AGENT_APP, 12 | assert_alert_rules, 13 | assert_logging, 14 | assert_metrics_endpoint, 15 | assert_security_context, 16 | deploy_and_assert_grafana_agent, 17 | generate_container_securitycontext_map, 18 | get_alert_rules, 19 | get_pod_names, 20 | ) 21 | from charms_dependencies import MINIO 22 | from pytest_operator.plugin import OpsTest 23 | 24 | METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) 25 | CONTAINERS_SECURITY_CONTEXT_MAP = generate_container_securitycontext_map(METADATA) 26 | CHARM_ROOT = "." 27 | ARGO_CONTROLLER = METADATA["name"] 28 | ARGO_CONTROLLER_TRUST = True 29 | 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | @pytest.fixture(scope="session") 35 | def lightkube_client() -> lightkube.Client: 36 | """Returns lightkube Kubernetes client""" 37 | client = lightkube.Client(field_manager=f"{ARGO_CONTROLLER}") 38 | return client 39 | 40 | 41 | @pytest.mark.abort_on_fail 42 | async def test_build_and_deploy_with_relations(ops_test: OpsTest): 43 | built_charm_path = await ops_test.build_charm(CHARM_ROOT) 44 | log.info(f"Built charm {built_charm_path}") 45 | 46 | image_path = METADATA["resources"]["oci-image"]["upstream-source"] 47 | resources = {"oci-image": image_path} 48 | 49 | await ops_test.model.deploy( 50 | entity_url=built_charm_path, 51 | application_name=ARGO_CONTROLLER, 52 | resources=resources, 53 | trust=ARGO_CONTROLLER_TRUST, 54 | ) 55 | 56 | # Deploy required relations 57 | await ops_test.model.deploy( 58 | entity_url=MINIO.charm, config=MINIO.config, channel=MINIO.channel, trust=MINIO.trust 59 | ) 60 | await ops_test.model.integrate( 61 | f"{ARGO_CONTROLLER}:object-storage", f"{MINIO.charm}:object-storage" 62 | ) 63 | 64 | await ops_test.model.wait_for_idle(timeout=60 * 10) 65 | # TODO: This does not handle blocked status right. Sometimes it passes when argo-controller 66 | # is still setting up 67 | 68 | # The unit should be active before creating/testing resources 69 | await ops_test.model.wait_for_idle(apps=[ARGO_CONTROLLER], status="active", timeout=1000) 70 | 71 | # Deploying grafana-agent-k8s and add all relations 72 | await deploy_and_assert_grafana_agent( 73 | ops_test.model, ARGO_CONTROLLER, metrics=True, dashboard=True, logging=True 74 | ) 75 | 76 | 77 | async def create_artifact_bucket(ops_test: OpsTest): 78 | # Ensure bucket is available 79 | model_name = ops_test.model_name 80 | # TODO Get minio name and port dynamically 81 | port = "9000" 82 | url = f"http://minio.{model_name}.svc.cluster.local:{port}" 83 | alias = "storage" 84 | bucket = "mlpipeline" 85 | 86 | minio_cmd = ( 87 | f"mc alias set {alias} {url} {MINIO.config['access-key']} {MINIO.config['secret-key']}" # noqa 88 | f"&& mc mb {alias}/{bucket} -p" 89 | ) 90 | kubectl_cmd = ( 91 | "kubectl", 92 | "run", 93 | "--rm", 94 | "-i", 95 | "--restart=Never", 96 | "--command", 97 | f"--namespace={ops_test.model_name}", 98 | "minio-deployment-test", 99 | "--image=minio/mc", 100 | "--", 101 | "sh", 102 | "-c", 103 | minio_cmd, 104 | ) 105 | 106 | ret_code, stdout, stderr = await ops_test.run(*kubectl_cmd) 107 | assert ret_code == 0, ( 108 | f"kubectl command to create argo bucket returned code {ret_code} with " 109 | f"stdout:\n{stdout}\nstderr:\n{stderr}" 110 | ) 111 | 112 | 113 | async def submit_workflow_using_artifact(ops_test: OpsTest): 114 | kubectl_cmd = ( 115 | "kubectl", 116 | f"--namespace={ops_test.model_name}", 117 | "create", 118 | "-f", 119 | "tests/data/simple_artifact.yaml", 120 | ) 121 | ret_code, stdout, stderr = await ops_test.run(*kubectl_cmd) 122 | assert ret_code == 0, ( 123 | f"kubectl command to submit argo workflow returned code {ret_code} with " 124 | f"stdout:\n{stdout}\nstderr:\n{stderr}" 125 | ) 126 | 127 | workflow_name = stdout.split()[0] 128 | log.info(f"Found workflow_name={workflow_name}") 129 | log.info(f"Waiting on {workflow_name} to finish") 130 | 131 | kubectl_wait_cmd = ( 132 | "kubectl", 133 | f"--namespace={ops_test.model_name}", 134 | "wait", 135 | workflow_name, 136 | "--for=condition=Completed", 137 | "--timeout=120s", 138 | ) 139 | ret_code, stdout, stderr = await ops_test.run(*kubectl_wait_cmd) 140 | assert ret_code == 0, ( 141 | f"kubectl command to wait on argo workflow completion returned code {ret_code} with" 142 | f" stdout:\n{stdout}\nstderr:\n{stderr}" 143 | ) 144 | 145 | 146 | async def test_workflow_using_artifacts(ops_test: OpsTest): 147 | # Argo will fail if the artifact bucket it uses does not exist 148 | await create_artifact_bucket(ops_test) 149 | 150 | # Submit argo workflow using artifacts and wait for it to finish 151 | await submit_workflow_using_artifact(ops_test) 152 | 153 | 154 | async def test_alert_rules(ops_test): 155 | """Test check charm alert rules and rules defined in relation data bag.""" 156 | app = ops_test.model.applications[ARGO_CONTROLLER] 157 | alert_rules = get_alert_rules() 158 | log.info("found alert_rules: %s", alert_rules) 159 | await assert_alert_rules(app, alert_rules) 160 | 161 | 162 | async def test_metrics_enpoint(ops_test): 163 | """Test metrics_endpoints are defined in relation data bag and their accessibility. 164 | 165 | This function gets all the metrics_endpoints from the relation data bag, checks if 166 | they are available from the grafana-agent-k8s charm and finally compares them with the 167 | ones provided to the function. 168 | """ 169 | app = ops_test.model.applications[ARGO_CONTROLLER] 170 | await assert_metrics_endpoint(app, metrics_port=9090, metrics_path="/metrics") 171 | 172 | 173 | async def test_logging(ops_test): 174 | """Test logging is defined in relation data bag.""" 175 | app = ops_test.model.applications[GRAFANA_AGENT_APP] 176 | await assert_logging(app) 177 | 178 | 179 | @pytest.mark.parametrize("container_name", list(CONTAINERS_SECURITY_CONTEXT_MAP.keys())) 180 | async def test_container_security_context( 181 | ops_test: OpsTest, 182 | lightkube_client: lightkube.Client, 183 | container_name: str, 184 | ): 185 | """Test container security context is correctly set. 186 | 187 | Verify that container spec defines the security context with correct 188 | user ID and group ID. 189 | """ 190 | pod_name = get_pod_names(ops_test.model.name, ARGO_CONTROLLER)[0] 191 | assert_security_context( 192 | lightkube_client, 193 | pod_name, 194 | container_name, 195 | CONTAINERS_SECURITY_CONTEXT_MAP, 196 | ops_test.model.name, 197 | ) 198 | -------------------------------------------------------------------------------- /charms/argo-controller/tests/unit/test_charm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | from charmed_kubeflow_chisme.testing import add_sdi_relation_to_harness 8 | from ops.model import ActiveStatus, BlockedStatus 9 | from ops.testing import Harness 10 | 11 | from charm import ArgoControllerOperator 12 | 13 | MOCK_OBJECT_STORAGE_DATA = { 14 | "access-key": "access-key", 15 | "secret-key": "secret-key", 16 | "service": "service", 17 | "namespace": "namespace", 18 | "port": 1234, 19 | "secure": True, 20 | } 21 | 22 | EXPECTED_ENVIRONMENT = { 23 | "ARGO_NAMESPACE": "namespace", 24 | "LEADER_ELECTION_IDENTITY": "argo-controller", 25 | } 26 | 27 | 28 | @pytest.fixture 29 | def harness() -> Harness: 30 | harness = Harness(ArgoControllerOperator) 31 | return harness 32 | 33 | 34 | @pytest.fixture() 35 | def mocked_lightkube_client(mocker): 36 | """Mocks the Lightkube Client in charm.py, returning a mock instead.""" 37 | mocked_lightkube_client = MagicMock() 38 | mocker.patch("charm.lightkube.Client", return_value=mocked_lightkube_client) 39 | yield mocked_lightkube_client 40 | 41 | 42 | @pytest.fixture() 43 | def mocked_kubernetes_service_patch(mocker): 44 | """Mocks the KubernetesServicePatch for the charm.""" 45 | mocked_kubernetes_service_patch = mocker.patch( 46 | "charm.KubernetesServicePatch", lambda x, y, service_name: None 47 | ) 48 | yield mocked_kubernetes_service_patch 49 | 50 | 51 | def test_log_forwarding( 52 | harness: Harness, mocked_lightkube_client, mocked_kubernetes_service_patch 53 | ): 54 | with patch("charm.LogForwarder") as mock_logging: 55 | harness.begin() 56 | mock_logging.assert_called_once_with(charm=harness.charm) 57 | 58 | 59 | def test_not_leader(harness, mocked_lightkube_client, mocked_kubernetes_service_patch): 60 | """Test when we are not the leader.""" 61 | harness.begin_with_initial_hooks() 62 | # Assert that we are not Active, and that the leadership-gate is the cause. 63 | assert not isinstance(harness.charm.model.unit.status, ActiveStatus) 64 | assert harness.charm.model.unit.status.message.startswith("[leadership-gate]") 65 | 66 | 67 | def test_object_storage_relation_with_data( 68 | harness, mocked_lightkube_client, mocked_kubernetes_service_patch 69 | ): 70 | """Test that if Leadership is Active, the object storage relation operates as expected. 71 | 72 | Note: See test_relation_components.py for an alternative way of unit testing Components without 73 | mocking the regular charm. 74 | """ 75 | # Arrange 76 | harness.begin() 77 | 78 | # Mock: 79 | # * leadership_gate to be active and executed 80 | harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) 81 | 82 | # Add relation with data. This should trigger a charm reconciliation due to relation-changed. 83 | add_sdi_relation_to_harness(harness, "object-storage", data=MOCK_OBJECT_STORAGE_DATA) 84 | 85 | # Assert 86 | assert isinstance(harness.charm.object_storage_relation.status, ActiveStatus) 87 | 88 | 89 | def test_object_storage_relation_without_data( 90 | harness, mocked_lightkube_client, mocked_kubernetes_service_patch 91 | ): 92 | """Test that the object storage relation goes Blocked if no data is available.""" 93 | # Arrange 94 | harness.begin() 95 | 96 | # Mock: 97 | # * leadership_gate to be active and executed 98 | harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) 99 | 100 | # Add relation with data. This should trigger a charm reconciliation due to relation-changed. 101 | add_sdi_relation_to_harness(harness, "object-storage", data={}) 102 | 103 | # Assert 104 | assert isinstance(harness.charm.object_storage_relation.status, BlockedStatus) 105 | 106 | 107 | def test_object_storage_relation_without_relation( 108 | harness, mocked_lightkube_client, mocked_kubernetes_service_patch 109 | ): 110 | """Test that the object storage relation goes Blocked if no relation is established.""" 111 | # Arrange 112 | harness.begin() 113 | 114 | # Mock: 115 | # * leadership_gate to be active and executed 116 | harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) 117 | 118 | # Act 119 | harness.charm.on.install.emit() 120 | 121 | # Assert 122 | assert isinstance(harness.charm.object_storage_relation.status, BlockedStatus) 123 | 124 | 125 | def test_kubernetes_created_method( 126 | harness, mocked_lightkube_client, mocked_kubernetes_service_patch 127 | ): 128 | """Test whether we try to create Kubernetes resources when we have leadership.""" 129 | # Arrange 130 | # Needed because kubernetes component will only apply to k8s if we are the leader 131 | harness.set_leader(True) 132 | harness.begin() 133 | 134 | # Need to mock the leadership-gate to be active, and the kubernetes auth component so that it 135 | # sees the expected resources when calling _get_missing_kubernetes_resources 136 | 137 | harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) 138 | 139 | # Add relation with data. This should trigger a charm reconciliation due to relation-changed. 140 | add_sdi_relation_to_harness(harness, "object-storage", data=MOCK_OBJECT_STORAGE_DATA) 141 | 142 | harness.charm.kubernetes_resources.component._get_missing_kubernetes_resources = MagicMock( 143 | return_value=[] 144 | ) 145 | 146 | # Act 147 | harness.charm.on.install.emit() 148 | 149 | # FIXME: This is a hardcoded count of the Kubernetes objects that should be created. 150 | # The `reconcile` function is called twice, once for `object_storage_relation_changed` 151 | # and once for `install`, so we expect 2 apply calls for each resource 152 | assert mocked_lightkube_client.apply.call_count == 26 153 | assert isinstance(harness.charm.kubernetes_resources.status, ActiveStatus) 154 | 155 | 156 | def test_pebble_services_running( 157 | harness, mocked_lightkube_client, mocked_kubernetes_service_patch 158 | ): 159 | """Test that if the Kubernetes Component is Active, the pebble services successfully start.""" 160 | # Arrange 161 | harness.set_model_name(EXPECTED_ENVIRONMENT["ARGO_NAMESPACE"]) 162 | harness.begin() 163 | harness.set_can_connect("argo-controller", True) 164 | 165 | # Mock: 166 | # * leadership_gate to have get_status=>Active 167 | # * object_storage_relation to return mock data, making the item go active 168 | # * kubernetes_resources to have get_status=>Active 169 | harness.charm.leadership_gate.get_status = MagicMock(return_value=ActiveStatus()) 170 | harness.charm.object_storage_relation.component.get_data = MagicMock( 171 | return_value=MOCK_OBJECT_STORAGE_DATA 172 | ) 173 | harness.charm.kubernetes_resources.get_status = MagicMock(return_value=ActiveStatus()) 174 | 175 | # Act 176 | harness.charm.on.install.emit() 177 | 178 | # Assert 179 | container = harness.charm.unit.get_container("argo-controller") 180 | service = container.get_service("argo-controller") 181 | assert service.is_running() 182 | # Assert the environment variables that are set from inputs are correctly applied 183 | environment = container.get_plan().services["argo-controller"].environment 184 | assert environment == EXPECTED_ENVIRONMENT 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /charms/argo-controller/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /charms/argo-controller/lib/charms/observability_libs/v1/kubernetes_service_patch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """# [DEPRECATED!] KubernetesServicePatch Library. 5 | 6 | The `kubernetes_service_patch` library is DEPRECATED and will be removed in October 2025. 7 | 8 | For patching the Kubernetes service created by Juju during the deployment of a charm, 9 | `ops.Unit.set_ports` functionality should be used instead. 10 | 11 | """ 12 | 13 | import logging 14 | from types import MethodType 15 | from typing import Any, List, Literal, Optional, Union 16 | 17 | from lightkube import ApiError, Client # pyright: ignore 18 | from lightkube.core import exceptions 19 | from lightkube.models.core_v1 import ServicePort, ServiceSpec 20 | from lightkube.models.meta_v1 import ObjectMeta 21 | from lightkube.resources.core_v1 import Service 22 | from lightkube.types import PatchType 23 | from ops import UpgradeCharmEvent 24 | from ops.charm import CharmBase 25 | from ops.framework import BoundEvent, Object 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | # The unique Charmhub library identifier, never change it 30 | LIBID = "0042f86d0a874435adef581806cddbbb" 31 | 32 | # Increment this major API version when introducing breaking changes 33 | LIBAPI = 1 34 | 35 | # Increment this PATCH version before using `charmcraft publish-lib` or reset 36 | # to 0 if you are raising the major API version 37 | LIBPATCH = 13 38 | 39 | ServiceType = Literal["ClusterIP", "LoadBalancer"] 40 | 41 | 42 | class KubernetesServicePatch(Object): 43 | """A utility for patching the Kubernetes service set up by Juju.""" 44 | 45 | def __init__( 46 | self, 47 | charm: CharmBase, 48 | ports: List[ServicePort], 49 | service_name: Optional[str] = None, 50 | service_type: ServiceType = "ClusterIP", 51 | additional_labels: Optional[dict] = None, 52 | additional_selectors: Optional[dict] = None, 53 | additional_annotations: Optional[dict] = None, 54 | *, 55 | refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, 56 | ): 57 | """Constructor for KubernetesServicePatch. 58 | 59 | Args: 60 | charm: the charm that is instantiating the library. 61 | ports: a list of ServicePorts 62 | service_name: allows setting custom name to the patched service. If none given, 63 | application name will be used. 64 | service_type: desired type of K8s service. Default value is in line with ServiceSpec's 65 | default value. 66 | additional_labels: Labels to be added to the kubernetes service (by default only 67 | "app.kubernetes.io/name" is set to the service name) 68 | additional_selectors: Selectors to be added to the kubernetes service (by default only 69 | "app.kubernetes.io/name" is set to the service name) 70 | additional_annotations: Annotations to be added to the kubernetes service. 71 | refresh_event: an optional bound event or list of bound events which 72 | will be observed to re-apply the patch (e.g. on port change). 73 | The `install` and `upgrade-charm` events would be observed regardless. 74 | """ 75 | logger.warning( 76 | "The ``kubernetes_service_patch v1`` library is DEPRECATED and will be removed " 77 | "in October 2025. For patching the Kubernetes service created by Juju during " 78 | "the deployment of a charm, ``ops.Unit.set_ports`` functionality should be used instead." 79 | ) 80 | super().__init__(charm, "kubernetes-service-patch") 81 | self.charm = charm 82 | self.service_name = service_name or self._app 83 | # To avoid conflicts with the default Juju service, append "-lb" to the service name. 84 | # The Juju application name is retained for the default service created by Juju. 85 | if self.service_name == self._app and service_type == "LoadBalancer": 86 | self.service_name = f"{self._app}-lb" 87 | self.service_type = service_type 88 | self.service = self._service_object( 89 | ports, 90 | self.service_name, 91 | service_type, 92 | additional_labels, 93 | additional_selectors, 94 | additional_annotations, 95 | ) 96 | 97 | # Make mypy type checking happy that self._patch is a method 98 | assert isinstance(self._patch, MethodType) 99 | # Ensure this patch is applied during the 'install' and 'upgrade-charm' events 100 | self.framework.observe(charm.on.install, self._patch) 101 | self.framework.observe(charm.on.upgrade_charm, self._on_upgrade_charm) 102 | self.framework.observe(charm.on.update_status, self._patch) 103 | # Sometimes Juju doesn't clean-up a manually created LB service, 104 | # so we clean it up ourselves just in case. 105 | self.framework.observe(charm.on.remove, self._remove_service) 106 | 107 | # apply user defined events 108 | if refresh_event: 109 | if not isinstance(refresh_event, list): 110 | refresh_event = [refresh_event] 111 | 112 | for evt in refresh_event: 113 | self.framework.observe(evt, self._patch) 114 | 115 | def _service_object( 116 | self, 117 | ports: List[ServicePort], 118 | service_name: Optional[str] = None, 119 | service_type: ServiceType = "ClusterIP", 120 | additional_labels: Optional[dict] = None, 121 | additional_selectors: Optional[dict] = None, 122 | additional_annotations: Optional[dict] = None, 123 | ) -> Service: 124 | """Creates a valid Service representation. 125 | 126 | Args: 127 | ports: a list of ServicePorts 128 | service_name: allows setting custom name to the patched service. If none given, 129 | application name will be used. 130 | service_type: desired type of K8s service. Default value is in line with ServiceSpec's 131 | default value. 132 | additional_labels: Labels to be added to the kubernetes service (by default only 133 | "app.kubernetes.io/name" is set to the service name) 134 | additional_selectors: Selectors to be added to the kubernetes service (by default only 135 | "app.kubernetes.io/name" is set to the service name) 136 | additional_annotations: Annotations to be added to the kubernetes service. 137 | 138 | Returns: 139 | Service: A valid representation of a Kubernetes Service with the correct ports. 140 | """ 141 | if not service_name: 142 | service_name = self._app 143 | labels = {"app.kubernetes.io/name": self._app} 144 | if additional_labels: 145 | labels.update(additional_labels) 146 | selector = {"app.kubernetes.io/name": self._app} 147 | if additional_selectors: 148 | selector.update(additional_selectors) 149 | return Service( 150 | apiVersion="v1", 151 | kind="Service", 152 | metadata=ObjectMeta( 153 | namespace=self._namespace, 154 | name=service_name, 155 | labels=labels, 156 | annotations=additional_annotations, # type: ignore[arg-type] 157 | ), 158 | spec=ServiceSpec( 159 | selector=selector, 160 | ports=ports, 161 | type=service_type, 162 | ), 163 | ) 164 | 165 | def _patch(self, _) -> None: 166 | """Patch the Kubernetes service created by Juju to map the correct port. 167 | 168 | Raises: 169 | PatchFailed: if patching fails due to lack of permissions, or otherwise. 170 | """ 171 | try: 172 | client = Client() # pyright: ignore 173 | except exceptions.ConfigError as e: 174 | logger.warning("Error creating k8s client: %s", e) 175 | return 176 | 177 | try: 178 | if self._is_patched(client): 179 | return 180 | if self.service_name != self._app: 181 | if not self.service_type == "LoadBalancer": 182 | self._delete_and_create_service(client) 183 | else: 184 | self._create_lb_service(client) 185 | client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) 186 | except ApiError as e: 187 | if e.status.code == 403: 188 | logger.error("Kubernetes service patch failed: `juju trust` this application.") 189 | else: 190 | logger.error("Kubernetes service patch failed: %s", str(e)) 191 | else: 192 | logger.info("Kubernetes service '%s' patched successfully", self._app) 193 | 194 | def _delete_and_create_service(self, client: Client): 195 | service = client.get(Service, self._app, namespace=self._namespace) 196 | service.metadata.name = self.service_name # type: ignore[attr-defined] 197 | service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 198 | client.delete(Service, self._app, namespace=self._namespace) 199 | client.create(service) 200 | 201 | def _create_lb_service(self, client: Client): 202 | try: 203 | client.get(Service, self.service_name, namespace=self._namespace) 204 | except ApiError: 205 | client.create(self.service) 206 | 207 | def is_patched(self) -> bool: 208 | """Reports if the service patch has been applied. 209 | 210 | Returns: 211 | bool: A boolean indicating if the service patch has been applied. 212 | """ 213 | client = Client() # pyright: ignore 214 | return self._is_patched(client) 215 | 216 | def _is_patched(self, client: Client) -> bool: 217 | # Get the relevant service from the cluster 218 | try: 219 | service = client.get(Service, name=self.service_name, namespace=self._namespace) 220 | except ApiError as e: 221 | if e.status.code == 404 and self.service_name != self._app: 222 | return False 223 | logger.error("Kubernetes service get failed: %s", str(e)) 224 | raise 225 | 226 | # Construct a list of expected ports, should the patch be applied 227 | expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] # type: ignore[attr-defined] 228 | # Construct a list in the same manner, using the fetched service 229 | fetched_ports = [ 230 | (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] 231 | ] # noqa: E501 232 | return expected_ports == fetched_ports 233 | 234 | def _on_upgrade_charm(self, event: UpgradeCharmEvent): 235 | """Handle the upgrade charm event.""" 236 | # If a charm author changed the service type from LB to ClusterIP across an upgrade, we need to delete the previous LB. 237 | if self.service_type == "ClusterIP": 238 | 239 | client = Client() # pyright: ignore 240 | 241 | # Define a label selector to find services related to the app 242 | selector: dict[str, Any] = {"app.kubernetes.io/name": self._app} 243 | 244 | # Check if any service of type LoadBalancer exists 245 | services = client.list(Service, namespace=self._namespace, labels=selector) 246 | for service in services: 247 | if ( 248 | not service.metadata 249 | or not service.metadata.name 250 | or not service.spec 251 | or not service.spec.type 252 | ): 253 | logger.warning( 254 | "Service patch: skipping resource with incomplete metadata: %s.", service 255 | ) 256 | continue 257 | if service.spec.type == "LoadBalancer": 258 | client.delete(Service, service.metadata.name, namespace=self._namespace) 259 | logger.info(f"LoadBalancer service {service.metadata.name} deleted.") 260 | 261 | # Continue the upgrade flow normally 262 | self._patch(event) 263 | 264 | def _remove_service(self, _): 265 | """Remove a Kubernetes service associated with this charm. 266 | 267 | Specifically designed to delete the load balancer service created by the charm, since Juju only deletes the 268 | default ClusterIP service and not custom services. 269 | 270 | Returns: 271 | None 272 | 273 | Raises: 274 | ApiError: for deletion errors, excluding when the service is not found (404 Not Found). 275 | """ 276 | client = Client() # pyright: ignore 277 | 278 | try: 279 | client.delete(Service, self.service_name, namespace=self._namespace) 280 | logger.info("The patched k8s service '%s' was deleted.", self.service_name) 281 | except ApiError as e: 282 | if e.status.code == 404: 283 | # Service not found, so no action needed 284 | return 285 | # Re-raise for other statuses 286 | raise 287 | 288 | @property 289 | def _app(self) -> str: 290 | """Name of the current Juju application. 291 | 292 | Returns: 293 | str: A string containing the name of the current Juju application. 294 | """ 295 | return self.charm.app.name 296 | 297 | @property 298 | def _namespace(self) -> str: 299 | """The Kubernetes namespace we're running in. 300 | 301 | Returns: 302 | str: A string containing the name of the current Kubernetes namespace. 303 | """ 304 | with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: 305 | return f.read().strip() 306 | -------------------------------------------------------------------------------- /charms/argo-controller/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Argo logo 27 | 28 | 29 | 30 | 32 | 52 | Argo logo 54 | 56 | 58 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 149 | 154 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /charms/argo-controller/src/grafana_dashboards/basic.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "${prometheusds}", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "target": { 12 | "limit": 100, 13 | "matchAny": false, 14 | "tags": [], 15 | "type": "dashboard" 16 | }, 17 | "type": "dashboard" 18 | } 19 | ] 20 | }, 21 | "description": "Metrics from argoWF compatible with multi prometheus origins like Thanos.", 22 | "editable": true, 23 | "fiscalYearStartMonth": 0, 24 | "gnetId": 13927, 25 | "graphTooltip": 1, 26 | "links": [], 27 | "liveNow": false, 28 | "panels": [ 29 | { 30 | "collapsed": false, 31 | "datasource": "${prometheusds}", 32 | "gridPos": { 33 | "h": 1, 34 | "w": 24, 35 | "x": 0, 36 | "y": 0 37 | }, 38 | "id": 28, 39 | "panels": [], 40 | "targets": [ 41 | { 42 | "datasource": "${prometheusds}", 43 | "refId": "A" 44 | } 45 | ], 46 | "title": "Currently", 47 | "type": "row" 48 | }, 49 | { 50 | "datasource": "${prometheusds}", 51 | "gridPos": { 52 | "h": 5, 53 | "w": 2, 54 | "x": 0, 55 | "y": 1 56 | }, 57 | "id": 5, 58 | "options": { 59 | "code": { 60 | "language": "plaintext", 61 | "showLineNumbers": false, 62 | "showMiniMap": false 63 | }, 64 | "content": "
\n\n
", 65 | "mode": "html" 66 | }, 67 | "pluginVersion": "9.5.3", 68 | "targets": [ 69 | { 70 | "datasource": "${prometheusds}", 71 | "queryType": "randomWalk", 72 | "refId": "A" 73 | } 74 | ], 75 | "type": "text" 76 | }, 77 | { 78 | "datasource": "${prometheusds}", 79 | "fieldConfig": { 80 | "defaults": { 81 | "color": { 82 | "mode": "thresholds" 83 | }, 84 | "mappings": [], 85 | "thresholds": { 86 | "mode": "absolute", 87 | "steps": [ 88 | { 89 | "color": "green", 90 | "value": null 91 | }, 92 | { 93 | "color": "orange", 94 | "value": 1 95 | } 96 | ] 97 | } 98 | }, 99 | "overrides": [] 100 | }, 101 | "gridPos": { 102 | "h": 6, 103 | "w": 3, 104 | "x": 5, 105 | "y": 1 106 | }, 107 | "id": 10, 108 | "options": { 109 | "colorMode": "value", 110 | "graphMode": "area", 111 | "justifyMode": "auto", 112 | "orientation": "auto", 113 | "reduceOptions": { 114 | "calcs": [ 115 | "lastNotNull" 116 | ], 117 | "fields": "", 118 | "values": false 119 | }, 120 | "text": {}, 121 | "textMode": "auto" 122 | }, 123 | "pluginVersion": "9.5.3", 124 | "targets": [ 125 | { 126 | "datasource": "${prometheusds}", 127 | "exemplar": false, 128 | "expr": "argo_workflows_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\",status=\"Pending\"}", 129 | "instant": true, 130 | "interval": "", 131 | "legendFormat": "", 132 | "queryType": "randomWalk", 133 | "refId": "A" 134 | } 135 | ], 136 | "title": "WF Pending", 137 | "transparent": true, 138 | "type": "stat" 139 | }, 140 | { 141 | "datasource": "${prometheusds}", 142 | "fieldConfig": { 143 | "defaults": { 144 | "color": { 145 | "mode": "thresholds" 146 | }, 147 | "mappings": [], 148 | "thresholds": { 149 | "mode": "absolute", 150 | "steps": [ 151 | { 152 | "color": "green", 153 | "value": null 154 | }, 155 | { 156 | "color": "red", 157 | "value": 1 158 | } 159 | ] 160 | } 161 | }, 162 | "overrides": [] 163 | }, 164 | "gridPos": { 165 | "h": 6, 166 | "w": 3, 167 | "x": 8, 168 | "y": 1 169 | }, 170 | "id": 8, 171 | "options": { 172 | "colorMode": "value", 173 | "graphMode": "area", 174 | "justifyMode": "auto", 175 | "orientation": "auto", 176 | "reduceOptions": { 177 | "calcs": [ 178 | "lastNotNull" 179 | ], 180 | "fields": "", 181 | "values": false 182 | }, 183 | "text": {}, 184 | "textMode": "auto" 185 | }, 186 | "pluginVersion": "9.5.3", 187 | "targets": [ 188 | { 189 | "datasource": "${prometheusds}", 190 | "exemplar": false, 191 | "expr": "argo_workflows_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\",status=\"Error\"}", 192 | "instant": true, 193 | "interval": "", 194 | "legendFormat": "", 195 | "queryType": "randomWalk", 196 | "refId": "A" 197 | } 198 | ], 199 | "title": "Workflow Errors", 200 | "transparent": true, 201 | "type": "stat" 202 | }, 203 | { 204 | "datasource": "${prometheusds}", 205 | "fieldConfig": { 206 | "defaults": { 207 | "color": { 208 | "mode": "thresholds" 209 | }, 210 | "mappings": [], 211 | "thresholds": { 212 | "mode": "absolute", 213 | "steps": [ 214 | { 215 | "color": "green", 216 | "value": null 217 | }, 218 | { 219 | "color": "red", 220 | "value": 1 221 | } 222 | ] 223 | } 224 | }, 225 | "overrides": [] 226 | }, 227 | "gridPos": { 228 | "h": 6, 229 | "w": 3, 230 | "x": 11, 231 | "y": 1 232 | }, 233 | "id": 9, 234 | "options": { 235 | "colorMode": "value", 236 | "graphMode": "area", 237 | "justifyMode": "auto", 238 | "orientation": "auto", 239 | "reduceOptions": { 240 | "calcs": [ 241 | "lastNotNull" 242 | ], 243 | "fields": "", 244 | "values": false 245 | }, 246 | "text": {}, 247 | "textMode": "auto" 248 | }, 249 | "pluginVersion": "9.5.3", 250 | "targets": [ 251 | { 252 | "datasource": "${prometheusds}", 253 | "exemplar": false, 254 | "expr": "argo_workflows_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\",status=\"Failed\"}", 255 | "instant": true, 256 | "interval": "", 257 | "legendFormat": "", 258 | "queryType": "randomWalk", 259 | "refId": "A" 260 | } 261 | ], 262 | "title": "Workflows Failed", 263 | "transparent": true, 264 | "type": "stat" 265 | }, 266 | { 267 | "datasource": "${prometheusds}", 268 | "fieldConfig": { 269 | "defaults": { 270 | "color": { 271 | "mode": "thresholds" 272 | }, 273 | "mappings": [], 274 | "noValue": "-", 275 | "thresholds": { 276 | "mode": "absolute", 277 | "steps": [ 278 | { 279 | "color": "green", 280 | "value": null 281 | }, 282 | { 283 | "color": "red", 284 | "value": 80 285 | } 286 | ] 287 | } 288 | }, 289 | "overrides": [] 290 | }, 291 | "gridPos": { 292 | "h": 6, 293 | "w": 3, 294 | "x": 14, 295 | "y": 1 296 | }, 297 | "id": 12, 298 | "options": { 299 | "colorMode": "value", 300 | "graphMode": "area", 301 | "justifyMode": "auto", 302 | "orientation": "auto", 303 | "reduceOptions": { 304 | "calcs": [ 305 | "lastNotNull" 306 | ], 307 | "fields": "", 308 | "values": false 309 | }, 310 | "text": {}, 311 | "textMode": "auto" 312 | }, 313 | "pluginVersion": "9.5.3", 314 | "targets": [ 315 | { 316 | "datasource": "${prometheusds}", 317 | "exemplar": false, 318 | "expr": "argo_workflows_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\",status=\"Skipped\"}", 319 | "instant": true, 320 | "interval": "", 321 | "legendFormat": "", 322 | "queryType": "randomWalk", 323 | "refId": "A" 324 | } 325 | ], 326 | "title": "Workflows Skipped", 327 | "transparent": true, 328 | "type": "stat" 329 | }, 330 | { 331 | "datasource": "${prometheusds}", 332 | "description": "The rate of K8s API requests for each status code", 333 | "fieldConfig": { 334 | "defaults": { 335 | "color": { 336 | "mode": "palette-classic" 337 | }, 338 | "custom": { 339 | "axisCenteredZero": false, 340 | "axisColorMode": "text", 341 | "axisLabel": "", 342 | "axisPlacement": "auto", 343 | "barAlignment": 0, 344 | "drawStyle": "line", 345 | "fillOpacity": 0, 346 | "gradientMode": "none", 347 | "hideFrom": { 348 | "legend": false, 349 | "tooltip": false, 350 | "viz": false 351 | }, 352 | "lineInterpolation": "linear", 353 | "lineWidth": 1, 354 | "pointSize": 5, 355 | "scaleDistribution": { 356 | "type": "linear" 357 | }, 358 | "showPoints": "auto", 359 | "spanNulls": false, 360 | "stacking": { 361 | "group": "A", 362 | "mode": "none" 363 | }, 364 | "thresholdsStyle": { 365 | "mode": "off" 366 | } 367 | }, 368 | "mappings": [], 369 | "thresholds": { 370 | "mode": "absolute", 371 | "steps": [ 372 | { 373 | "color": "green", 374 | "value": null 375 | }, 376 | { 377 | "color": "red", 378 | "value": 80 379 | } 380 | ] 381 | } 382 | }, 383 | "overrides": [] 384 | }, 385 | "gridPos": { 386 | "h": 8, 387 | "w": 12, 388 | "x": 0, 389 | "y": 7 390 | }, 391 | "id": 30, 392 | "options": { 393 | "legend": { 394 | "calcs": [], 395 | "displayMode": "list", 396 | "placement": "bottom", 397 | "showLegend": true 398 | }, 399 | "tooltip": { 400 | "mode": "single", 401 | "sort": "none" 402 | } 403 | }, 404 | "pluginVersion": "9.5.3", 405 | "targets": [ 406 | { 407 | "datasource": "${prometheusds}", 408 | "editorMode": "code", 409 | "expr": "sum by(status_code) (rate(argo_workflows_k8s_request_total{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\"}[5m]))", 410 | "legendFormat": "{{status_code}}", 411 | "range": true, 412 | "refId": "A" 413 | } 414 | ], 415 | "title": "Kubernetes API requests rate", 416 | "transparent": true, 417 | "type": "timeseries" 418 | }, 419 | { 420 | "datasource": "${prometheusds}", 421 | "description": "Pods expected by argo controller that never appeared or were deleted", 422 | "fieldConfig": { 423 | "defaults": { 424 | "color": { 425 | "mode": "thresholds" 426 | }, 427 | "decimals": 0, 428 | "mappings": [], 429 | "thresholds": { 430 | "mode": "absolute", 431 | "steps": [ 432 | { 433 | "color": "green", 434 | "value": null 435 | }, 436 | { 437 | "color": "red", 438 | "value": 80 439 | } 440 | ] 441 | }, 442 | "unit": "none" 443 | }, 444 | "overrides": [] 445 | }, 446 | "gridPos": { 447 | "h": 8, 448 | "w": 12, 449 | "x": 12, 450 | "y": 7 451 | }, 452 | "id": 31, 453 | "options": { 454 | "orientation": "auto", 455 | "reduceOptions": { 456 | "calcs": [ 457 | "lastNotNull" 458 | ], 459 | "fields": "", 460 | "values": false 461 | }, 462 | "showThresholdLabels": false, 463 | "showThresholdMarkers": true, 464 | "text": {} 465 | }, 466 | "pluginVersion": "9.5.3", 467 | "targets": [ 468 | { 469 | "datasource": "${prometheusds}", 470 | "editorMode": "builder", 471 | "exemplar": false, 472 | "expr": "argo_pod_missing{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\"}", 473 | "format": "time_series", 474 | "instant": false, 475 | "interval": "", 476 | "legendFormat": "{{node_phase}}", 477 | "range": true, 478 | "refId": "A" 479 | } 480 | ], 481 | "title": "Workflows missing Pods", 482 | "transparent": true, 483 | "type": "gauge" 484 | }, 485 | { 486 | "aliasColors": {}, 487 | "bars": false, 488 | "dashLength": 10, 489 | "dashes": false, 490 | "datasource": "${prometheusds}", 491 | "decimals": 0, 492 | "description": "The number of workflows monitored by the controller that are in Error or Failed status", 493 | "fill": 0, 494 | "fillGradient": 1, 495 | "gridPos": { 496 | "h": 9, 497 | "w": 12, 498 | "x": 0, 499 | "y": 15 500 | }, 501 | "hiddenSeries": false, 502 | "id": 29, 503 | "legend": { 504 | "alignAsTable": true, 505 | "avg": false, 506 | "current": true, 507 | "hideEmpty": true, 508 | "max": true, 509 | "min": false, 510 | "rightSide": false, 511 | "show": true, 512 | "sort": "current", 513 | "sortDesc": true, 514 | "total": false, 515 | "values": true 516 | }, 517 | "lines": true, 518 | "linewidth": 2, 519 | "nullPointMode": "null", 520 | "options": { 521 | "alertThreshold": true 522 | }, 523 | "percentage": false, 524 | "pluginVersion": "9.5.3", 525 | "pointradius": 2, 526 | "points": false, 527 | "renderer": "flot", 528 | "seriesOverrides": [], 529 | "spaceLength": 10, 530 | "stack": false, 531 | "steppedLine": false, 532 | "targets": [ 533 | { 534 | "datasource": "${prometheusds}", 535 | "exemplar": true, 536 | "expr": "argo_workflows_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\",status=~\"(Error|Failed)\"}", 537 | "interval": "1m", 538 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{status}}", 539 | "queryType": "randomWalk", 540 | "refId": "A" 541 | } 542 | ], 543 | "thresholds": [], 544 | "timeRegions": [], 545 | "title": "Workflows with Error or Failed status", 546 | "tooltip": { 547 | "shared": false, 548 | "sort": 2, 549 | "value_type": "individual" 550 | }, 551 | "transparent": true, 552 | "type": "graph", 553 | "xaxis": { 554 | "mode": "time", 555 | "show": true, 556 | "values": [] 557 | }, 558 | "yaxes": [ 559 | { 560 | "$$hashKey": "object:151", 561 | "decimals": 0, 562 | "format": "short", 563 | "logBase": 1, 564 | "min": "0", 565 | "show": true 566 | }, 567 | { 568 | "$$hashKey": "object:152", 569 | "format": "short", 570 | "logBase": 1, 571 | "show": true 572 | } 573 | ], 574 | "yaxis": { 575 | "align": false 576 | } 577 | }, 578 | { 579 | "datasource": "${prometheusds}", 580 | "description": "The number of new errors encountered by the controller over the past 5 minutes", 581 | "fieldConfig": { 582 | "defaults": { 583 | "color": { 584 | "mode": "palette-classic" 585 | }, 586 | "custom": { 587 | "axisCenteredZero": false, 588 | "axisColorMode": "text", 589 | "axisLabel": "", 590 | "axisPlacement": "auto", 591 | "barAlignment": 0, 592 | "drawStyle": "line", 593 | "fillOpacity": 0, 594 | "gradientMode": "none", 595 | "hideFrom": { 596 | "legend": false, 597 | "tooltip": false, 598 | "viz": false 599 | }, 600 | "lineInterpolation": "linear", 601 | "lineWidth": 1, 602 | "pointSize": 5, 603 | "scaleDistribution": { 604 | "type": "linear" 605 | }, 606 | "showPoints": "auto", 607 | "spanNulls": false, 608 | "stacking": { 609 | "group": "A", 610 | "mode": "none" 611 | }, 612 | "thresholdsStyle": { 613 | "mode": "off" 614 | } 615 | }, 616 | "mappings": [], 617 | "thresholds": { 618 | "mode": "absolute", 619 | "steps": [ 620 | { 621 | "color": "green", 622 | "value": null 623 | }, 624 | { 625 | "color": "red", 626 | "value": 80 627 | } 628 | ] 629 | } 630 | }, 631 | "overrides": [] 632 | }, 633 | "gridPos": { 634 | "h": 8, 635 | "w": 12, 636 | "x": 12, 637 | "y": 15 638 | }, 639 | "id": 32, 640 | "options": { 641 | "legend": { 642 | "calcs": [], 643 | "displayMode": "list", 644 | "placement": "bottom", 645 | "showLegend": true 646 | }, 647 | "tooltip": { 648 | "mode": "single", 649 | "sort": "none" 650 | } 651 | }, 652 | "pluginVersion": "9.5.3", 653 | "targets": [ 654 | { 655 | "datasource": "${prometheusds}", 656 | "editorMode": "code", 657 | "expr": "increase(argo_workflows_error_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\"}[5m])", 658 | "legendFormat": "{{cause}}", 659 | "range": true, 660 | "refId": "errors rate" 661 | } 662 | ], 663 | "title": "New Controller errors in the past 5 minutes", 664 | "transparent": true, 665 | "type": "timeseries" 666 | }, 667 | { 668 | "collapsed": false, 669 | "datasource": "${prometheusds}", 670 | "gridPos": { 671 | "h": 1, 672 | "w": 24, 673 | "x": 0, 674 | "y": 24 675 | }, 676 | "id": 20, 677 | "panels": [], 678 | "targets": [ 679 | { 680 | "datasource": "${prometheusds}", 681 | "refId": "A" 682 | } 683 | ], 684 | "title": "Histogram of durations of operations", 685 | "type": "row" 686 | }, 687 | { 688 | "aliasColors": {}, 689 | "bars": false, 690 | "dashLength": 10, 691 | "dashes": false, 692 | "datasource": "${prometheusds}", 693 | "decimals": 2, 694 | "description": "The 95th percentile of Argo workflows operation durations over a 5-minute window", 695 | "fill": 1, 696 | "fillGradient": 1, 697 | "gridPos": { 698 | "h": 9, 699 | "w": 24, 700 | "x": 0, 701 | "y": 25 702 | }, 703 | "hiddenSeries": false, 704 | "id": 14, 705 | "legend": { 706 | "alignAsTable": true, 707 | "avg": true, 708 | "current": true, 709 | "hideEmpty": true, 710 | "max": true, 711 | "min": true, 712 | "rightSide": false, 713 | "show": true, 714 | "sort": "current", 715 | "sortDesc": true, 716 | "total": false, 717 | "values": true 718 | }, 719 | "lines": true, 720 | "linewidth": 1, 721 | "nullPointMode": "null", 722 | "options": { 723 | "alertThreshold": true 724 | }, 725 | "percentage": false, 726 | "pluginVersion": "9.5.3", 727 | "pointradius": 2, 728 | "points": true, 729 | "renderer": "flot", 730 | "seriesOverrides": [], 731 | "spaceLength": 10, 732 | "stack": false, 733 | "steppedLine": false, 734 | "targets": [ 735 | { 736 | "datasource": "${prometheusds}", 737 | "exemplar": true, 738 | "expr": "histogram_quantile(0.95, sum by(le, origin_prometheus, kubernetes_namespace) (rate(argo_workflows_operation_duration_seconds_bucket{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\"}[5m])))", 739 | "interval": "1m", 740 | "legendFormat": "{{origin_prometheus}} : {{kubernetes_namespace}} : 95th ", 741 | "refId": "B" 742 | } 743 | ], 744 | "thresholds": [], 745 | "timeRegions": [], 746 | "title": "Workflow operation duration", 747 | "tooltip": { 748 | "shared": false, 749 | "sort": 2, 750 | "value_type": "individual" 751 | }, 752 | "transparent": true, 753 | "type": "graph", 754 | "xaxis": { 755 | "mode": "time", 756 | "show": true, 757 | "values": [] 758 | }, 759 | "yaxes": [ 760 | { 761 | "$$hashKey": "object:151", 762 | "decimals": 2, 763 | "format": "s", 764 | "logBase": 1, 765 | "show": true 766 | }, 767 | { 768 | "$$hashKey": "object:152", 769 | "format": "short", 770 | "logBase": 1, 771 | "show": true 772 | } 773 | ], 774 | "yaxis": { 775 | "align": false 776 | } 777 | }, 778 | { 779 | "collapsed": false, 780 | "datasource": "${prometheusds}", 781 | "gridPos": { 782 | "h": 1, 783 | "w": 24, 784 | "x": 0, 785 | "y": 34 786 | }, 787 | "id": 22, 788 | "panels": [], 789 | "targets": [ 790 | { 791 | "datasource": "${prometheusds}", 792 | "refId": "A" 793 | } 794 | ], 795 | "title": "Adds to the queue", 796 | "type": "row" 797 | }, 798 | { 799 | "aliasColors": {}, 800 | "bars": false, 801 | "dashLength": 10, 802 | "dashes": false, 803 | "datasource": "${prometheusds}", 804 | "decimals": 2, 805 | "description": "The rate of Argo workflows queue additions over a 2-minute window", 806 | "fill": 1, 807 | "fillGradient": 1, 808 | "gridPos": { 809 | "h": 9, 810 | "w": 24, 811 | "x": 0, 812 | "y": 35 813 | }, 814 | "hiddenSeries": false, 815 | "id": 15, 816 | "legend": { 817 | "alignAsTable": true, 818 | "avg": false, 819 | "current": true, 820 | "hideEmpty": true, 821 | "max": true, 822 | "min": false, 823 | "rightSide": false, 824 | "show": true, 825 | "sort": "current", 826 | "sortDesc": true, 827 | "total": false, 828 | "values": true 829 | }, 830 | "lines": true, 831 | "linewidth": 2, 832 | "nullPointMode": "null", 833 | "options": { 834 | "alertThreshold": true 835 | }, 836 | "percentage": false, 837 | "pluginVersion": "9.5.3", 838 | "pointradius": 2, 839 | "points": false, 840 | "renderer": "flot", 841 | "seriesOverrides": [], 842 | "spaceLength": 10, 843 | "stack": false, 844 | "steppedLine": false, 845 | "targets": [ 846 | { 847 | "datasource": "${prometheusds}", 848 | "exemplar": true, 849 | "expr": "delta(argo_workflows_queue_adds_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\"}[2m])", 850 | "interval": "1m", 851 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 852 | "queryType": "randomWalk", 853 | "refId": "A" 854 | } 855 | ], 856 | "thresholds": [], 857 | "timeRegions": [], 858 | "title": "Workflow queue adds rate", 859 | "tooltip": { 860 | "shared": false, 861 | "sort": 2, 862 | "value_type": "individual" 863 | }, 864 | "transparent": true, 865 | "type": "graph", 866 | "xaxis": { 867 | "mode": "time", 868 | "show": true, 869 | "values": [] 870 | }, 871 | "yaxes": [ 872 | { 873 | "$$hashKey": "object:151", 874 | "decimals": 0, 875 | "format": "none", 876 | "logBase": 1, 877 | "show": true 878 | }, 879 | { 880 | "$$hashKey": "object:152", 881 | "format": "short", 882 | "logBase": 1, 883 | "show": true 884 | } 885 | ], 886 | "yaxis": { 887 | "align": false 888 | } 889 | }, 890 | { 891 | "aliasColors": {}, 892 | "bars": false, 893 | "dashLength": 10, 894 | "dashes": false, 895 | "datasource": "${prometheusds}", 896 | "decimals": 2, 897 | "description": "The current depth of the Argo workflows queue", 898 | "fill": 1, 899 | "fillGradient": 1, 900 | "gridPos": { 901 | "h": 9, 902 | "w": 12, 903 | "x": 0, 904 | "y": 44 905 | }, 906 | "hiddenSeries": false, 907 | "id": 16, 908 | "legend": { 909 | "alignAsTable": true, 910 | "avg": false, 911 | "current": true, 912 | "hideEmpty": true, 913 | "max": true, 914 | "min": false, 915 | "rightSide": false, 916 | "show": true, 917 | "sort": "current", 918 | "sortDesc": true, 919 | "total": false, 920 | "values": true 921 | }, 922 | "lines": true, 923 | "linewidth": 2, 924 | "nullPointMode": "null", 925 | "options": { 926 | "alertThreshold": true 927 | }, 928 | "percentage": false, 929 | "pluginVersion": "9.5.3", 930 | "pointradius": 2, 931 | "points": false, 932 | "renderer": "flot", 933 | "seriesOverrides": [], 934 | "spaceLength": 10, 935 | "stack": false, 936 | "steppedLine": false, 937 | "targets": [ 938 | { 939 | "datasource": "${prometheusds}", 940 | "exemplar": true, 941 | "expr": "argo_workflows_queue_depth_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"$ns\",origin_prometheus=~\"^$dc$\"}", 942 | "interval": "1m", 943 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 944 | "queryType": "randomWalk", 945 | "refId": "A" 946 | } 947 | ], 948 | "thresholds": [], 949 | "timeRegions": [], 950 | "title": "Depth of the queue", 951 | "tooltip": { 952 | "shared": false, 953 | "sort": 2, 954 | "value_type": "individual" 955 | }, 956 | "transparent": true, 957 | "type": "graph", 958 | "xaxis": { 959 | "mode": "time", 960 | "show": true, 961 | "values": [] 962 | }, 963 | "yaxes": [ 964 | { 965 | "$$hashKey": "object:151", 966 | "decimals": 0, 967 | "format": "short", 968 | "logBase": 1, 969 | "min": "0", 970 | "show": true 971 | }, 972 | { 973 | "$$hashKey": "object:152", 974 | "format": "short", 975 | "logBase": 1, 976 | "show": true 977 | } 978 | ], 979 | "yaxis": { 980 | "align": false 981 | } 982 | }, 983 | { 984 | "aliasColors": {}, 985 | "bars": true, 986 | "dashLength": 10, 987 | "dashes": false, 988 | "datasource": "${prometheusds}", 989 | "decimals": 2, 990 | "description": "The average latency for each queue over a 2-minute window", 991 | "fill": 0, 992 | "fillGradient": 1, 993 | "gridPos": { 994 | "h": 9, 995 | "w": 12, 996 | "x": 12, 997 | "y": 44 998 | }, 999 | "hiddenSeries": false, 1000 | "id": 23, 1001 | "legend": { 1002 | "alignAsTable": true, 1003 | "avg": false, 1004 | "current": true, 1005 | "hideEmpty": true, 1006 | "max": true, 1007 | "min": false, 1008 | "rightSide": false, 1009 | "show": true, 1010 | "sort": "current", 1011 | "sortDesc": true, 1012 | "total": false, 1013 | "values": true 1014 | }, 1015 | "lines": false, 1016 | "linewidth": 1, 1017 | "nullPointMode": "null", 1018 | "options": { 1019 | "alertThreshold": true 1020 | }, 1021 | "percentage": false, 1022 | "pluginVersion": "9.5.3", 1023 | "pointradius": 2, 1024 | "points": false, 1025 | "renderer": "flot", 1026 | "seriesOverrides": [], 1027 | "spaceLength": 10, 1028 | "stack": false, 1029 | "steppedLine": false, 1030 | "targets": [ 1031 | { 1032 | "datasource": "${prometheusds}", 1033 | "exemplar": true, 1034 | "expr": "rate(argo_workflows_queue_latency_sum{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",queue_name=\"cron_wf_queue\"}[2m]) / rate(argo_workflows_queue_latency_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"cron_wf_queue\"}[2m])", 1035 | "interval": "1m", 1036 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 1037 | "queryType": "randomWalk", 1038 | "refId": "A" 1039 | }, 1040 | { 1041 | "datasource": "${prometheusds}", 1042 | "exemplar": true, 1043 | "expr": "rate(argo_workflows_queue_latency_sum{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"pod_cleanup_queue\"}[2m]) / rate(argo_workflows_queue_latency_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"pod_cleanup_queue\"}[2m])", 1044 | "interval": "1m", 1045 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 1046 | "refId": "B" 1047 | }, 1048 | { 1049 | "datasource": "${prometheusds}", 1050 | "exemplar": true, 1051 | "expr": "rate(argo_workflows_queue_latency_sum{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"cron_wf_queue\"}[2m]) / rate(argo_workflows_queue_latency_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"cron_wf_queue\"}[2m])", 1052 | "hide": false, 1053 | "interval": "1m", 1054 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 1055 | "refId": "C" 1056 | }, 1057 | { 1058 | "datasource": "${prometheusds}", 1059 | "exemplar": true, 1060 | "expr": "rate(argo_workflows_queue_latency_sum{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"workflow_queue\"}[2m]) / rate(argo_workflows_queue_latency_count{juju_application=~\"$juju_application\",juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_unit=~\"$juju_unit\",kubernetes_namespace=~\"^$ns$\",origin_prometheus=~\"^$dc$\",queue_name=\"workflow_queue\"}[2m])", 1061 | "hide": false, 1062 | "interval": "1m", 1063 | "legendFormat": "{{origin_prometheus}} : {{app}} : {{kubernetes_namespace}} : {{queue_name}}", 1064 | "refId": "D" 1065 | } 1066 | ], 1067 | "thresholds": [], 1068 | "timeRegions": [], 1069 | "title": "Average waiting time in each queue", 1070 | "tooltip": { 1071 | "shared": false, 1072 | "sort": 2, 1073 | "value_type": "individual" 1074 | }, 1075 | "transparent": true, 1076 | "type": "graph", 1077 | "xaxis": { 1078 | "mode": "time", 1079 | "show": true, 1080 | "values": [] 1081 | }, 1082 | "yaxes": [ 1083 | { 1084 | "$$hashKey": "object:151", 1085 | "decimals": 2, 1086 | "format": "s", 1087 | "label": "avg", 1088 | "logBase": 1, 1089 | "show": true 1090 | }, 1091 | { 1092 | "$$hashKey": "object:152", 1093 | "format": "short", 1094 | "logBase": 1, 1095 | "show": true 1096 | } 1097 | ], 1098 | "yaxis": { 1099 | "align": false 1100 | } 1101 | } 1102 | ], 1103 | "refresh": "1m", 1104 | "schemaVersion": 38, 1105 | "style": "dark", 1106 | "tags": [ 1107 | "ckf", 1108 | "argo", 1109 | "charm: grafana-agent-k8s" 1110 | ], 1111 | "templating": { 1112 | "list": [ 1113 | { 1114 | "current": { 1115 | "selected": false, 1116 | "text": "All", 1117 | "value": "$__all" 1118 | }, 1119 | "hide": 0, 1120 | "includeAll": true, 1121 | "label": "Loki datasource", 1122 | "multi": true, 1123 | "name": "lokids", 1124 | "options": [], 1125 | "query": "loki", 1126 | "refresh": 1, 1127 | "regex": "", 1128 | "skipUrlSync": false, 1129 | "type": "datasource" 1130 | }, 1131 | { 1132 | "current": { 1133 | "selected": false, 1134 | "text": "All", 1135 | "value": "$__all" 1136 | }, 1137 | "hide": 0, 1138 | "includeAll": true, 1139 | "label": "Prometheus datasource", 1140 | "multi": true, 1141 | "name": "prometheusds", 1142 | "options": [], 1143 | "query": "prometheus", 1144 | "refresh": 1, 1145 | "regex": "", 1146 | "skipUrlSync": false, 1147 | "type": "datasource" 1148 | }, 1149 | { 1150 | "allValue": ".*", 1151 | "current": { 1152 | "selected": false, 1153 | "text": "All", 1154 | "value": "$__all" 1155 | }, 1156 | "datasource": { 1157 | "uid": "${prometheusds}" 1158 | }, 1159 | "definition": "label_values(up{juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_application=~\"$juju_application\"},juju_unit)", 1160 | "hide": 0, 1161 | "includeAll": true, 1162 | "label": "Juju unit", 1163 | "multi": true, 1164 | "name": "juju_unit", 1165 | "options": [], 1166 | "query": { 1167 | "query": "label_values(up{juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\",juju_application=~\"$juju_application\"},juju_unit)", 1168 | "refId": "StandardVariableQuery" 1169 | }, 1170 | "refresh": 1, 1171 | "regex": "", 1172 | "skipUrlSync": false, 1173 | "sort": 0, 1174 | "tagValuesQuery": "", 1175 | "tags": [], 1176 | "tagsQuery": "", 1177 | "type": "query", 1178 | "useTags": false 1179 | }, 1180 | { 1181 | "allValue": ".*", 1182 | "current": { 1183 | "selected": false, 1184 | "text": "All", 1185 | "value": "$__all" 1186 | }, 1187 | "datasource": { 1188 | "uid": "${prometheusds}" 1189 | }, 1190 | "definition": "label_values(up{juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\"},juju_application)", 1191 | "hide": 0, 1192 | "includeAll": true, 1193 | "label": "Juju application", 1194 | "multi": true, 1195 | "name": "juju_application", 1196 | "options": [], 1197 | "query": { 1198 | "query": "label_values(up{juju_model=~\"$juju_model\",juju_model_uuid=~\"$juju_model_uuid\"},juju_application)", 1199 | "refId": "StandardVariableQuery" 1200 | }, 1201 | "refresh": 1, 1202 | "regex": "", 1203 | "skipUrlSync": false, 1204 | "sort": 0, 1205 | "tagValuesQuery": "", 1206 | "tags": [], 1207 | "tagsQuery": "", 1208 | "type": "query", 1209 | "useTags": false 1210 | }, 1211 | { 1212 | "allValue": ".*", 1213 | "current": { 1214 | "selected": false, 1215 | "text": "All", 1216 | "value": "$__all" 1217 | }, 1218 | "datasource": { 1219 | "uid": "${prometheusds}" 1220 | }, 1221 | "definition": "label_values(up{juju_model=~\"$juju_model\"},juju_model_uuid)", 1222 | "hide": 0, 1223 | "includeAll": true, 1224 | "label": "Juju model uuid", 1225 | "multi": true, 1226 | "name": "juju_model_uuid", 1227 | "options": [], 1228 | "query": { 1229 | "query": "label_values(up{juju_model=~\"$juju_model\"},juju_model_uuid)", 1230 | "refId": "StandardVariableQuery" 1231 | }, 1232 | "refresh": 1, 1233 | "regex": "", 1234 | "skipUrlSync": false, 1235 | "sort": 0, 1236 | "tagValuesQuery": "", 1237 | "tags": [], 1238 | "tagsQuery": "", 1239 | "type": "query", 1240 | "useTags": false 1241 | }, 1242 | { 1243 | "allValue": ".*", 1244 | "current": { 1245 | "selected": false, 1246 | "text": "All", 1247 | "value": "$__all" 1248 | }, 1249 | "datasource": { 1250 | "uid": "${prometheusds}" 1251 | }, 1252 | "definition": "label_values(up,juju_model)", 1253 | "hide": 0, 1254 | "includeAll": true, 1255 | "label": "Juju model", 1256 | "multi": true, 1257 | "name": "juju_model", 1258 | "options": [], 1259 | "query": { 1260 | "query": "label_values(up,juju_model)", 1261 | "refId": "StandardVariableQuery" 1262 | }, 1263 | "refresh": 1, 1264 | "regex": "", 1265 | "skipUrlSync": false, 1266 | "sort": 0, 1267 | "tagValuesQuery": "", 1268 | "tags": [], 1269 | "tagsQuery": "", 1270 | "type": "query", 1271 | "useTags": false 1272 | }, 1273 | { 1274 | "current": { 1275 | "isNone": true, 1276 | "selected": false, 1277 | "text": "None", 1278 | "value": "" 1279 | }, 1280 | "datasource": { 1281 | "uid": "${prometheusds}" 1282 | }, 1283 | "definition": "label_values(argo_workflows_count,origin_prometheus) ", 1284 | "description": "Kubernetes datacenter", 1285 | "hide": 0, 1286 | "includeAll": false, 1287 | "label": "k8s_dc", 1288 | "multi": true, 1289 | "name": "dc", 1290 | "options": [], 1291 | "query": { 1292 | "query": "label_values(argo_workflows_count,origin_prometheus) ", 1293 | "refId": "StandardVariableQuery" 1294 | }, 1295 | "refresh": 2, 1296 | "regex": "", 1297 | "skipUrlSync": false, 1298 | "sort": 0, 1299 | "tagValuesQuery": "", 1300 | "tagsQuery": "", 1301 | "type": "query", 1302 | "useTags": false 1303 | }, 1304 | { 1305 | "allValue": ".*", 1306 | "current": { 1307 | "selected": true, 1308 | "text": [ 1309 | "All" 1310 | ], 1311 | "value": [ 1312 | "$__all" 1313 | ] 1314 | }, 1315 | "datasource": { 1316 | "uid": "${prometheusds}" 1317 | }, 1318 | "definition": "label_values(argo_workflows_count{origin_prometheus=~\"^$dc$\"},kubernetes_namespace) ", 1319 | "description": "Kubernetes namespace", 1320 | "hide": 0, 1321 | "includeAll": true, 1322 | "label": "k8s_ns", 1323 | "multi": true, 1324 | "name": "ns", 1325 | "options": [], 1326 | "query": { 1327 | "query": "label_values(argo_workflows_count{origin_prometheus=~\"^$dc$\"},kubernetes_namespace) ", 1328 | "refId": "StandardVariableQuery" 1329 | }, 1330 | "refresh": 2, 1331 | "regex": "", 1332 | "skipUrlSync": false, 1333 | "sort": 0, 1334 | "tagValuesQuery": "", 1335 | "tagsQuery": "", 1336 | "type": "query", 1337 | "useTags": false 1338 | } 1339 | ] 1340 | }, 1341 | "time": { 1342 | "from": "now-3h", 1343 | "to": "now" 1344 | }, 1345 | "timepicker": {}, 1346 | "timezone": "", 1347 | "title": "ArgoWorkflow Metrics", 1348 | "uid": "a21866dd2b3b2e960944411b8860991ce7a2202c", 1349 | "version": 1, 1350 | "weekStart": "" 1351 | } 1352 | --------------------------------------------------------------------------------