From 7dcbe45d20b496d5ba217ee709293a84b822a180 Mon Sep 17 00:00:00 2001 From: day0hero Date: Thu, 3 Sep 2026 16:01:53 +0100 Subject: [PATCH 01/12] Patternizer upgrade to get ci hook updates. - Added tests/ci for testing --- .claude/skills/pattern-author/SKILL.md | 243 ++++++ .claude/skills/pattern-author/reference.md | 951 +++++++++++++++++++++ Makefile-common | 20 +- ansible.cfg | 15 +- pattern.sh | 69 +- tests/ci/README.md | 42 + tests/ci/__init__.py | 1 + tests/ci/conftest.py | 1 + tests/ci/requirements.txt | 15 + tests/ci/test_hub.py | 106 +++ tests/ci/test_standalone.py | 51 ++ 11 files changed, 1483 insertions(+), 31 deletions(-) create mode 100644 .claude/skills/pattern-author/SKILL.md create mode 100644 .claude/skills/pattern-author/reference.md create mode 100644 tests/ci/README.md create mode 100644 tests/ci/__init__.py create mode 100644 tests/ci/conftest.py create mode 100644 tests/ci/requirements.txt create mode 100644 tests/ci/test_hub.py create mode 100644 tests/ci/test_standalone.py diff --git a/.claude/skills/pattern-author/SKILL.md b/.claude/skills/pattern-author/SKILL.md new file mode 100644 index 00000000..281eef3b --- /dev/null +++ b/.claude/skills/pattern-author/SKILL.md @@ -0,0 +1,243 @@ +--- +name: pattern-author +description: > + Author and modify Validated Patterns — GitOps-based deployment configurations + for OpenShift. Use when creating new Patterns, adding applications/subscriptions/namespaces + to existing Patterns, configuring secrets, setting up hub/spoke clusters, or working with + clustergroup values files. +when_to_use: > + When the user asks to create a new Validated Pattern, add a helm chart or application to a + Pattern, configure secrets or vault, set up spoke clusters, modify clustergroup values, + or work with values-global.yaml, values-*.yaml, or values-secret.yaml.template files. + Also when the user mentions "Validated Patterns", "clustergroup", "pattern init", or + "patternizer". +allowed-tools: Read Bash(pattern *) Bash(helm *) Bash(find *) Bash(ls *) +--- + +# Validated Patterns Author + +You are helping author Validated Patterns — GitOps-based deployment configurations for OpenShift built on the [clustergroup helm chart](https://github.com/validatedpatterns/clustergroup-chart/). A Pattern is a Git repository containing values files that define what namespaces, operators, and applications to deploy on one or more OpenShift clusters via ArgoCD. + +For complete framework documentation, read [reference.md](reference.md) in this skill directory. Read it before your first Pattern authoring task in a session, or when you need details on a specific framework feature. + +## Authoring Workflow + +When creating a new Pattern: + +1. **Initialize** — Determine the Pattern name and whether secrets are needed. Run `pattern init` or `pattern init --with-secrets` in the Pattern directory. +2. **Identify requirements** — What operators, applications, and custom helm charts does this Pattern need? +3. **Define namespaces** — Add all required namespaces to the clustergroup values file (`values-.yaml`). Include OperatorGroup configuration for operator namespaces. +4. **Define subscriptions** — Add operator subscriptions with at minimum the operator `name`. Set `namespace`, `channel`, and `source` as needed. +5. **Define applications** — Wire in helm charts as applications: + - Local charts: set `path` to the chart location in the repository + - VP-published charts: set `chart` and `chartVersion` + - External Git charts: set `repoURL`, `path`, and `chartVersion` (Git ref) +6. **Configure secrets** (if applicable) — Define secrets in `values-secret.yaml.template` and create corresponding ExternalSecret CRDs in chart templates. +7. **Set up hub/spoke** (if multi-cluster) — Add ACM subscription and `managedClusterGroups` to the hub. Create spoke values files. +8. **Add imperative jobs** (if needed) — Configure Ansible playbooks in the imperative framework for tasks that don't fit the declarative model. + +When modifying an existing Pattern, read the current `values-global.yaml` and clustergroup values files first to understand the existing structure before making changes. + +## Rules + +These rules must always be followed: + +- **Map form for namespaces** — Always define namespaces as a map, never a list. Maps merge across values files; lists override entirely. +- **No secrets in Git** — Never put real secrets or credentials in the Pattern repository. Secrets belong in `~/values-secret-.yaml` on the user's machine. +- **`singleArgoCD: true`** — Always set this for new Patterns. +- **`multiSourceConfig.enabled: true`** — Always set this for new Patterns. +- **Vault only on hub** — The Vault application and namespace belong only on the hub/main cluster. Spoke clusters need ESO only (no Vault). The VP `openshift-external-secrets` chart auto-configures spokes to use the hub's Vault. +- **Chart values stubs** — A chart's `values.yaml` must include default stubs for any `.Values.global.*` or `.Values.clusterGroup.*` values referenced in its templates, so `helm template` works standalone during development. +- **ESO backtick escaping** — In ExternalSecret templates, escape ESO template expressions with backticks to prevent Helm from interpreting them: + + ```text + "{{ `{{ .field_name }}` }}" + ``` + +- **Idempotent imperative jobs** — All imperative jobs run on a schedule (every 10 minutes by default) and must be idempotent. +- **Re-run `pattern init`** — After adding new local helm charts, re-run `pattern init` to wire them into the clustergroup values file. It is idempotent. + +## Common Tasks + +### Adding an Operator + +Add three things to the clustergroup values file: a namespace, a subscription, and (if the operator needs its own chart for configuration) an application. + +```yaml +clusterGroup: + namespaces: + my-operator: + operatorGroup: true + targetNamespaces: [] + + subscriptions: + my-operator: + name: my-operator + namespace: my-operator + channel: stable + + applications: + my-operator-config: + name: my-operator-config + namespace: my-operator + path: charts/my-operator-config +``` + +Not every operator needs a local chart. If the operator requires no additional configuration beyond installation, the namespace and subscription are sufficient. + +### Adding a Local Helm Chart + +Place the chart anywhere in the repository (convention: `charts/`). Run `pattern init` to auto-discover it, or manually add it to the clustergroup values: + +```yaml +clusterGroup: + namespaces: + my-app: + + applications: + my-app: + name: my-app + namespace: my-app + path: charts/my-app +``` + +### Adding a VP-Published Chart + +```yaml +clusterGroup: + applications: + vault: + name: vault + namespace: vault + chart: hashicorp-vault + chartVersion: 0.1.* +``` + +### Adding a Chart from an External Git Repository + +```yaml +clusterGroup: + applications: + external-app: + name: external-app + namespace: external-app + repoURL: https://github.com/org/repo.git + chartVersion: main + path: charts/the-chart +``` + +### Adding a Secret + +1. Define the secret in `values-secret.yaml.template`: + + ```yaml + version: "2.0" + + secrets: + - name: my-secret + vaultPrefixes: + - global + fields: + - name: api-key + onMissingValue: prompt + - name: password + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + ``` + +2. Add `secretStore` defaults to the chart's `values.yaml`: + + ```yaml + secretStore: + name: vault-backend + kind: ClusterSecretStore + + mysecret: + key: secret/data/global/my-secret + refreshInterval: 2m0s + ``` + +3. Create an ExternalSecret template in the chart: + + ```yaml + apiVersion: external-secrets.io/v1 + kind: ExternalSecret + metadata: + name: my-secret + spec: + refreshInterval: {{ .Values.mysecret.refreshInterval }} + secretStoreRef: + name: {{ .Values.secretStore.name }} + kind: {{ .Values.secretStore.kind }} + target: + name: my-secret + template: + type: Opaque + data: + api-key: "{{ `{{ .api_key }}` }}" + password: "{{ `{{ .password }}` }}" + data: + - secretKey: api_key + remoteRef: + key: {{ .Values.mysecret.key }} + property: api-key + - secretKey: password + remoteRef: + key: {{ .Values.mysecret.key }} + property: password + ``` + +The Vault path is `secret/data//`. The `secretKey` values become the template variables in `target.template.data`. + +### Adding a Spoke Cluster + +1. Add ACM and `managedClusterGroups` to the hub clustergroup values: + + ```yaml + clusterGroup: + name: hub + + namespaces: + open-cluster-management: + + subscriptions: + acm: + name: advanced-cluster-management + namespace: open-cluster-management + channel: release-2.16 + + applications: + acm: + name: acm + namespace: open-cluster-management + chart: acm + chartVersion: 0.2.* + + managedClusterGroups: + region-one: + name: group-one + acmlabels: + - name: clusterGroup + value: group-one + ``` + +2. Create `values-group-one.yaml` with the spoke's namespaces, subscriptions, and applications. + +3. If using secrets on the spoke, include ESO components (without Vault) in the spoke values. + +### Adding Conditional Overrides + +Define a custom global variable and use `sharedValueFiles`: + +```yaml +# values-global.yaml +global: + device: gpu + +# values-.yaml +clusterGroup: + sharedValueFiles: + - /overrides/values-{{ $.Values.global.device }}.yaml +``` + +Create the override file (e.g., `/overrides/values-gpu.yaml`) with the conditional namespaces, subscriptions, and applications. diff --git a/.claude/skills/pattern-author/reference.md b/.claude/skills/pattern-author/reference.md new file mode 100644 index 00000000..1a24ddaf --- /dev/null +++ b/.claude/skills/pattern-author/reference.md @@ -0,0 +1,951 @@ +# What are Validated Patterns + +[Validated Patterns](https://validatedpatterns.io/) are an advanced form of reference architectures that offer a streamlined approach to deploying complex business solutions. + +Validated Patterns are GitOps-based. A Validated Pattern is a Git repository containing the values files for the [clustergroup helm chart](https://github.com/validatedpatterns/clustergroup-chart/). + +Validated Patterns are installed via the [Validated Patterns Operator](https://github.com/validatedpatterns/patterns-operator) (available as a community operator in OpenShift's operator catalog). The operator creates and manages a subscription for OpenShift GitOps (ArgoCD) and creates the app-of-apps Application in ArgoCD — the clustergroup chart with values taken from the Pattern repository (as a multi-source ArgoCD Application). + +## Creating a New Pattern + +1. Create an empty directory (or create a new repository on GitHub/GitLab and clone it). The directory name becomes the Pattern name. +2. Run `podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init` to create the new Pattern. Add `--with-secrets` to include the necessary components for the Validated Patterns secret framework: + `podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init --with-secrets` + +[The patternizer source is available on GitHub.](https://github.com/validatedpatterns/patternizer) + +You can shorten the patternizer command by adding a shell function to your shell's startup: + +```bash +pattern() { + podman run --pull=newer \ + -v "$PWD:$PWD:z" \ + -w "$PWD" \ + quay.io/validatedpatterns/patternizer "$@" +} +``` + +Then run `pattern init` or `pattern init --with-secrets`. + +`pattern init` is idempotent. You can add secrets to a Pattern initialized without them by running `pattern init --with-secrets` later. You can also re-run `pattern init` as you add helm charts to the repository and it will wire them into the clustergroup values file. + +## Pattern File Structure + +Given a fresh Pattern directory with a user-defined helm chart: + +```bash +mkdir -p fresh-pattern/charts +cd fresh-pattern/charts +helm create user-defined-chart +cd .. +pattern init --with-secrets +``` + +The resulting structure: + +```text +fresh-pattern +├── ansible.cfg +├── charts +│ └── user-defined-chart +├── Makefile +├── Makefile-common +├── pattern.sh +├── values-global.yaml +├── values-prod.yaml +└── values-secret.yaml.template +``` + +### File Descriptions + +`ansible.cfg` contains defaults for the Ansible playbooks invoked via the Makefile (e.g., `make install`). These playbooks are defined in the [rhvp.cluster_utils collection](https://github.com/validatedpatterns/rhvp.cluster_utils). + +`charts/` is the recommended location for local helm charts. `pattern init` discovers charts anywhere in the repository, but `charts/` is the convention. + +`Makefile` includes `Makefile-common` and provides a place for Pattern-specific make targets or overrides. Most Patterns never need to override the common targets, but it's useful for custom tests, linting, or convenience functions. + +`Makefile-common` provides the core make targets for the Patterns framework. Documented more fully on the [VP blog](https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/). The primary targets are `make install`, `make uninstall`, and `make load-secrets`. These targets are run via the `./pattern.sh` wrapper script, which executes them inside the [VP Utility Container](https://github.com/validatedpatterns/utility-container). The utility container includes make, oc, helm, aws, and other CLIs, so the only local dependency is podman. A user can run `./pattern.sh make install` to install a Pattern (assuming they are logged into an OpenShift cluster). + +`pattern.sh` is the wrapper script that runs make targets inside the utility container. + +`values-secret.yaml.template` is a template showing how the Pattern's secrets should be formatted for the [secrets framework](#the-secrets-framework). This file should never contain real secrets. Copy it to your home directory (`cp values-secret.yaml.template ~/values-secret-fresh-pattern.yaml`) so secrets stay on your local machine, not in your Git repository. + +### values-global.yaml + +```yaml +global: + pattern: fresh-pattern + singleArgoCD: true + secretLoader: + disabled: false +main: + clusterGroupName: prod + multiSourceConfig: + enabled: true + clusterGroupChartVersion: 0.9.* +``` + +#### Field Reference + +`global.pattern` — The Pattern name, taken from the directory name by patternizer. Used as a label on ArgoCD Applications and as part of the ArgoCD namespace name. + +`global.singleArgoCD` — When `true` (the patternizer default), each cluster uses a single ArgoCD instance for both the app-of-apps and all child applications. When `false` (legacy behavior), each cluster runs two ArgoCD instances: one for the clustergroup chart (app-of-apps) and a separate one for the applications it defines. A hub/spoke Pattern with `singleArgoCD: false` would have two ArgoCD instances on the hub and two on the spoke. With `singleArgoCD: true`, there is one ArgoCD instance on the hub and one on the spoke. New Patterns should always use `true`. + +`global.secretLoader.disabled` — When `true`, skip loading secrets. Useful for Patterns that don't use the secrets framework. + +`main.clusterGroupName` — The name of the primary clustergroup. Determines which `values-.yaml` file defines the main cluster. If you change this value, the next `pattern init` run creates the corresponding values file (you would need to manually delete the old unused one). + +`main.multiSourceConfig.enabled` — Enable ArgoCD multi-source applications. Should always be `true` for modern Patterns. + +`main.multiSourceConfig.clusterGroupChartVersion` — SemVer constraint for the clustergroup chart version from the VP chart repository. + +There are also some global options that can be set in `values-global.yaml` but are not included by default: + +`global.options.syncPolicy` — Controls the ArgoCD sync policy for all applications. `"Automatic"` (the default) enables auto-sync with retry. `"Manual"` disables auto-sync. Per-application `syncPolicy` overrides this global default. + +`global.options.installPlanApproval` — Default `installPlanApproval` for all subscriptions. `"Automatic"` (the default) allows operators to auto-upgrade when new updates are available in their channel. Set to `"Manual"` to prevent auto-upgrades. Per-subscription values override this. + +`global.options.useCSV` — When `True` (the default), subscriptions include a `startingCSV` field if their `.csv` value is set. + +### values-prod.yaml + +`values-prod.yaml` is the values file that defines the main clustergroup of the Pattern. In hub/spoke Patterns (see [Spoke clusters](#spoke-clusters)) this would be the hub cluster. In single cluster Patterns this defines the solitary cluster. If there is one file to look at which defines the Pattern, it is this one. Its contents are explored in [The clustergroup values](#the-clustergroup-values) section. + +### The Pattern CR + +When you run `./pattern.sh make install` to deploy the Pattern, the values in `values-global.yaml` are passed to the [pattern-install chart](https://github.com/validatedpatterns/pattern-install-chart) by the [rhvp.cluster_utils.install](https://github.com/validatedpatterns/rhvp.cluster_utils/blob/main/playbooks/install.yml) playbook. This chart creates a subscription for the Validated Patterns Operator, a configmap with default operator configuration, and the Pattern CR: + +```yaml +apiVersion: gitops.hybrid-cloud-patterns.io/v1alpha1 +kind: Pattern +metadata: + name: fresh-pattern + namespace: openshift-operators +spec: + clusterGroupName: prod + gitSpec: + targetRepo: https://github.com/validatedpatterns/fresh-pattern.git + targetRevision: main + multiSourceConfig: + enabled: true + clusterGroupChartVersion: 0.9.* +``` + +Your branch (`main` in this example) must have an upstream remote set and be pushed to that remote. + +## Values File Hierarchy + +When the Patterns Operator creates the app-of-apps representing the Pattern, it renders the [clustergroup chart](https://github.com/validatedpatterns/clustergroup-chart/) and automatically includes certain values files (if they exist) to customize resources for a given clustergroup, OCP version, and platform. + +The following values files are automatically included for each application, in this order: + +1. `values-global.yaml` — included for all clustergroups +2. `values-.yaml` — values specific to the clustergroup +3. `values-.yaml` — values specific to the platform (e.g., `values-AWS.yaml`) +4. `values--.yaml` — platform and version specific (e.g., `values-AWS-4.21.yaml`) +5. `values--.yaml` — platform and clustergroup specific (e.g., `values-AWS-hub.yaml`) +6. `values--.yaml` — version and clustergroup specific (e.g., `values-4.21-hub.yaml`) +7. `values-.yaml` — values for a specifically named cluster (e.g., `values-test-cluster.yaml`) +8. Files from `global.extraValueFiles` — additional global value files +9. Per-clustergroup `sharedValueFiles` — from `clusterGroup.sharedValueFiles` +10. Per-application `extraValueFiles` — from the application's `extraValueFiles` field + +ArgoCD is configured with `ignoreMissingValueFiles: true`, so it silently skips any of these files that do not exist. Only create the files you actually need. + +In multi-source mode (the default), values files from the Pattern repository are prefixed with `$patternref/` to tell ArgoCD which source they come from. This is handled automatically by the clustergroup chart. + +Some of these values files are cross-clustergroup. For example, `values-AWS.yaml` would apply to both hub and spoke clustergroups running on AWS. + +### Shared Value Files + +The clustergroup provides a `sharedValueFiles` field for including additional overrides for all applications in that clustergroup: + +```yaml +clusterGroup: + sharedValueFiles: + - '/overrides/values-{{ $.Values.global.clusterPlatform }}.yaml' + - '/overrides/values-{{ $.Values.global.clusterPlatform }}-{{ $.Values.global.clusterVersion }}.yaml' + - '/overrides/values-{{ $.Values.global.clusterPlatform }}-{{ $.Values.clusterGroup.name }}.yaml' + - '/overrides/values-{{ $.Values.global.clusterVersion }}-{{ $.Values.clusterGroup.name }}.yaml' + - '/overrides/values-{{ $.Values.global.localClusterName }}.yaml' +``` + +These paths support Helm template interpolation. The Validated Patterns operator handles resolving the global variables. As with the auto-included files, any that do not exist are silently skipped. + +By convention, extra value files are placed in an `overrides/` directory in the Pattern repository, but any location works. + +### Custom Globals for Conditional Overrides + +You can define your own global variables in `values-global.yaml` and use them in `sharedValueFiles` to enable conditional configuration. Consider this example: + +```yaml +# values-global.yaml +global: + pattern: ai-quickstart-rag + device: cpu # one of 'cpu' (no GPU) or 'gpu' (NVIDIA GPU) +main: + clusterGroupName: prod + multiSourceConfig: + enabled: true + clusterGroupChartVersion: 0.9.* +``` + +This enables conditional value file inclusion: + +```yaml +clusterGroup: + sharedValueFiles: + - /overrides/values-{{ $.Values.global.device }}.yaml + - /overrides/values-{{ $.Values.global.device }}-{{ $.Values.global.clusterPlatform }}.yaml +``` + +```yaml +# /overrides/values-gpu.yaml +global: + models: + llama-3-2-3b-instruct: + enabled: true + +llm-service: + device: gpu + +clusterGroup: + namespaces: + openshift-nfd: + nvidia-gpu-operator: + + subscriptions: + nfd: + name: nfd + namespace: openshift-nfd + nvidia: + name: gpu-operator-certified + namespace: nvidia-gpu-operator + source: certified-operators + + applications: + nfd: + name: nfd + namespace: openshift-nfd + path: charts/nfd + nvidia-config: + name: nvidia-config + namespace: nvidia-gpu-operator + path: charts/nvidia-config +``` + +```yaml +# /overrides/values-gpu-AWS.yaml +clusterGroup: + imperative: + jobs: + - name: deploy-nvidia-gpu + playbook: rhvp.cluster_utils.create_machineset + verbosity: -vvv + extravars: + - max_machineset_count=1 + - machineset_replicas=1 + - ensure_two_machine_minimum=false + - machineset_name=nvidia-gpu + - machineset_labels= + - machineset_instance_type=g6.2xlarge + - 'machineset_taints=[{"effect":"NoSchedule","key":"nvidia.com/gpu","value":"true"}]' + - 'machineset_node_labels={"node-role.kubernetes.io/nvidia-gpu":""}' + - machineset_api_version=machine.openshift.io/v1beta1 + clusterRoleYaml: + - apiGroups: + - "*" + resources: + - machinesets + - persistentvolumeclaims + - datavolumes + - dataimportcrons + - datasources + verbs: + - "*" + - apiGroups: + - "*" + resources: + - "*" + verbs: + - get + - list + - watch +``` + +This example leverages the `global.device` value to install GPU-specific subscriptions and create an additional machineset when deployed to AWS with a GPU. + +## The Clustergroup Values + +A Pattern IS a clustergroup. The Git repository containing a Validated Pattern contains the values files for the [clustergroup helm chart](https://github.com/validatedpatterns/clustergroup-chart/). The clustergroup values file (`values-.yaml`) is the central definition of what a Pattern deploys — which namespaces to create, which operators to install, and which applications (helm charts) to run. + +Here is the `values-prod.yaml` generated by `pattern init --with-secrets` for our example Pattern. It defines a single clustergroup named `prod` with the namespaces, operator subscriptions, and applications needed for the secrets framework alongside the user's own chart: + +```yaml +clusterGroup: + name: prod + namespaces: + fresh-pattern: + vault: + external-secrets-operator: + operatorGroup: true + targetNamespaces: [] + external-secrets: + subscriptions: + eso: + name: openshift-external-secrets-operator + namespace: external-secrets-operator + channel: stable-v1 + applications: + openshift-external-secrets: + name: openshift-external-secrets + namespace: external-secrets + chart: openshift-external-secrets + chartVersion: 0.0.* + user-defined-chart: + name: user-defined-chart + namespace: fresh-pattern + path: charts/user-defined-chart + vault: + name: vault + namespace: vault + chart: hashicorp-vault + chartVersion: 0.1.* +``` + +These three sections — `namespaces`, `subscriptions`, and `applications` — are the core building blocks of every clustergroup. Each is detailed below. + +### Namespaces + +The namespace section accepts simple strings or more complex mappings: + +```yaml +clusterGroup: + namespaces: + fresh-pattern: + external-secrets-operator: + operatorGroup: true + targetNamespaces: [] +``` + +Setting `operatorGroup: true` creates an OperatorGroup with the same name as the namespace using the specified `targetNamespaces`. + +Labels and annotations can also be set: + +```yaml +clusterGroup: + namespaces: + rag-llm: + labels: + opendatahub.io/dashboard: "true" + modelmesh-enabled: "false" +``` + +Namespaces can also be defined as a list: + +```yaml +clusterGroup: + namespaces: + - fresh-pattern + - vault + - external-secrets-operator: + operatorGroup: true + targetNamespaces: [] + - external-secrets +``` + +The map form is recommended over the list form. When merging values files, lists are overridden entirely whereas maps are merged. + +### Subscriptions + +Subscriptions define operators that should be installed on the cluster. + +The only required field is the name of the subscription: + +```yaml +clusterGroup: + subscriptions: + servicemesh-console: + name: kiali-ossm +``` + +The clustergroup chart provides defaults for the other fields, making this equivalent to: + +```yaml +clusterGroup: + subscriptions: + servicemesh-console: + name: kiali-ossm + namespace: openshift-operators + source: redhat-operators + sourceNamespace: openshift-marketplace + channel: stable + installPlanApproval: Automatic +``` + +You can also specify `.csv` for the `startingCSV` of the subscription if needed. + +The `name` must be the operator's name in the `source` catalog. The default source is `redhat-operators`, but some operators are in `certified-operators`, `community-operators`, or `redhat-marketplace`. + +`namespace` is where the operator is installed. For operators like ESO, this is an OperatorGroup namespace. + +`source` and `sourceNamespace` only need updating in disconnected or custom install scenarios where the standard catalog sources are unavailable. + +`channel` is operator-specific. Some operators publish on multiple channels (e.g., `fast`, `stable-3.x`). + +Set `installPlanApproval` to `Manual` to prevent the operator from upgrading automatically when new updates are available in its channel. The default is `Automatic`. + +### Applications + +#### Local Helm Charts + +If `path` is provided (and `repoURL` and `chartVersion` are not), the helm chart is sourced from the Pattern repository: + +```yaml +clusterGroup: + applications: + user-defined-chart: + name: user-defined-chart + namespace: fresh-pattern + path: charts/user-defined-chart +``` + +#### VP-Published Helm Charts + +If neither `repoURL` nor `path` are provided, charts are sourced from the [Validated Patterns chart repository](https://charts.validatedpatterns.io/). Specify a version with `chartVersion`: + +```yaml +clusterGroup: + applications: + vault: + name: vault + namespace: vault + chart: hashicorp-vault + chartVersion: 0.1.* +``` + +#### Helm Charts from External Git Repositories + +Helm charts from external Git repositories can be referenced with `repoURL` (the publicly reachable Git URL), `path` (the path within the repository), and `chartVersion` (the Git revision to use): + +```yaml +clusterGroup: + applications: + maas-quickstart: + name: maas-quickstart + repoURL: https://github.com/dminnear-rh/maas-code-assistant.git + chartVersion: main + path: charts/maas-code-assistant +``` + +#### Additional Application Fields + +Several additional fields are available for any application type: + +`extraValueFiles` — Paths (relative to the Pattern repository root) to additional values files passed to the helm chart: + +```yaml +extraValueFiles: + - /overrides/maas-quickstart.yaml +``` + +`overrides` — Direct helm value overrides: + +```yaml +overrides: + - name: grafana.namespace + value: grafana +``` + +`ignoreDifferences` — Instructs ArgoCD to ignore certain differences when computing the sync diff: + +```yaml +ignoreDifferences: + - kind: Secret + name: grafana-proxy + namespace: grafana + jsonPointers: + - /data/session_secret +``` + +A full example combining these: + +```yaml +clusterGroup: + applications: + maas-quickstart: + name: maas-quickstart + repoURL: https://github.com/dminnear-rh/maas-code-assistant.git + chartVersion: main + path: charts/maas-code-assistant + extraValueFiles: + - /overrides/maas-quickstart.yaml + overrides: + - name: grafana.namespace + value: grafana + ignoreDifferences: + - kind: Secret + name: grafana-proxy + namespace: grafana + jsonPointers: + - /data/session_secret +``` + +## Using Helm Charts in VP + +This section covers what the VP framework provides to your charts, not helm chart authoring in general. + +### Where Charts Live + +The default location is `charts/` in the Pattern repository. `patternizer init` auto-discovers charts anywhere in the repository, so you can organize them however you like. + +Charts can also live in separate Git repositories and be referenced via `repoURL` in the application definition. VP-published charts are available from [charts.validatedpatterns.io](https://charts.validatedpatterns.io/) (referenced with `chart:` and `chartVersion:`). + +### How Values Flow into Charts + +Every ArgoCD Application created by the clustergroup chart receives the full merged tree of values files described in [Values File Hierarchy](#values-file-hierarchy). This means every chart sees: + +- `.Values.global.*` — global Pattern values +- `.Values.clusterGroup.*` — clustergroup configuration +- Chart-specific values from the chart's own `values.yaml` + +In addition to the values files, the clustergroup chart injects helm parameters for cluster-specific values that are known at deploy time: + +- `global.repoURL` — Pattern repository URL +- `global.targetRevision` — Git branch/commit/ref +- `global.namespace` — the ArgoCD app namespace +- `global.pattern` — Pattern name +- `global.clusterDomain` — cluster FQDN +- `global.localClusterName` — local cluster identifier +- `global.clusterVersion` — OpenShift version +- `global.clusterPlatform` — platform type (e.g., AWS, Azure, GCP) +- `global.hubClusterDomain` — hub cluster FQDN +- `global.localClusterDomain` — local cluster FQDN + +These are available in templates as `.Values.global.`. + +A chart's `values.yaml` should include default stubs for any `global.*` or `clusterGroup.*` values referenced in its templates. These defaults enable standalone `helm template` to work during development and are overridden at deploy time by the merged values tree. + +### Example: config-demo Chart + +The [config-demo chart](https://github.com/validatedpatterns/multicloud-gitops/tree/main/charts/all/config-demo) from multicloud-gitops is a minimal working example. Its `values.yaml`: + +```yaml +secretStore: + name: vault-backend + kind: ClusterSecretStore + +configdemosecret: + key: secret/data/global/config-demo + refreshInterval: 2m0s + +global: + hubClusterDomain: hub.example.com + localClusterDomain: region-one.example.com + +clusterGroup: + isHubCluster: true + +image: + repository: registry.access.redhat.com/ubi10/httpd-24 + tag: "10.0-1755779646" + pullPolicy: IfNotPresent +``` + +The `global` and `clusterGroup` stubs provide defaults for development. The `secretStore` and `configdemosecret` sections are chart-specific values used by the ExternalSecret template (see [Consuming Secrets in Charts](#consuming-secrets-in-charts)). + +The deployment template uses chart-specific values: + +```yaml +containers: +- name: apache + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} +``` + +The ConfigMap template uses global values injected by the framework, demonstrating how charts can reference cluster-specific information without hardcoding it: + +```yaml +data: + "index.html": |- +

+ Hub Cluster domain is '{{ .Values.global.hubClusterDomain }}'
+ Pod is running on Local Cluster Domain '{{ .Values.global.localClusterDomain }}'
+

+``` + +## The Secrets Framework + +Secrets in the VP framework are stored in Vault and consumed in charts via External Secrets Operator (ESO). The workflow is: define secrets in `values-secret.yaml.template`, load them into Vault with `./pattern.sh make install` or `./pattern.sh make load-secrets`, and consume them in charts using ExternalSecret CRDs. + +### Defining Secrets + +The `values-secret.yaml.template` file defines the secrets a Pattern needs. The install/load-secrets command looks for secrets in `~/values-secret-.yaml` and falls back to the template in the Pattern repository. This encourages users to copy the template to their home directory and keep secrets out of the Git repository. + +Example from [multicloud-gitops](https://github.com/validatedpatterns/multicloud-gitops/blob/main/values-secret.yaml.template): + +```yaml +version: "2.0" + +secrets: + - name: config-demo + vaultPrefixes: + - global + fields: + - name: secret + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy +``` + +#### Secret Field Reference + +Each secret entry supports these fields: + +- `name` — secret name (becomes the Vault path segment) +- `vaultPrefixes` — list of Vault path prefixes controlling which clustergroups can read the secret +- `fields[].name` — field name within the secret +- `fields[].value` — literal value +- `fields[].path` — path to a file containing the value +- `fields[].ini_file`, `ini_section`, `ini_key` — read a value from an INI file +- `fields[].onMissingValue` — set to `generate` to auto-generate the value +- `fields[].vaultPolicy` — policy name for generation (either `validatedPatternDefaultPolicy` or a custom policy) + +Example with various field types: + +```yaml +secrets: + # AWS credentials from INI file + - name: aws + fields: + - name: aws_access_key_id + ini_file: ~/.aws/credentials + ini_section: default + ini_key: aws_access_key_id + - name: aws_secret_access_key + ini_file: ~/.aws/credentials + ini_key: aws_secret_access_key + + # SSH keys from files + - name: publickey + fields: + - name: content + path: ~/.ssh/id_rsa.pub + - name: privatekey + fields: + - name: content + path: ~/.ssh/id_rsa + + # OpenShift pull secret from file + - name: openshiftPullSecret + fields: + - name: content + path: ~/.pullsecret.json +``` + +### Vault Prefixes and Access Control + +The `vaultPrefixes` field controls which clustergroups can access a secret. The Vault path convention is `secret/data//`. + +- `global` — readable by all clustergroups (hub and spoke) +- A clustergroup name (e.g., `hub`) — readable only by that clustergroup +- Multiple prefixes write the secret to multiple paths, making it accessible from multiple clustergroups + +### Secret Generation and Policies + +Secrets can be auto-generated if no value is provided by setting `onMissingValue: generate`. The generation is controlled by a vault policy: + +```yaml +version: "2.0" + +vaultPolicies: + basicPolicy: | + length=16 + rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 } + rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 } + rule "charset" { charset = "0123456789" min-chars = 1 } + +secrets: + - name: pgvector + fields: + - name: user + value: postgres + - name: password + onMissingValue: generate + vaultPolicy: basicPolicy + - name: dbname + value: rag_blueprint + - name: host + value: pgvector + - name: port + value: "5432" +``` + +### Consuming Secrets in Charts + +Secrets stored in Vault are consumed in charts using ESO ExternalSecret CRDs. The VP `openshift-external-secrets` chart sets up a `ClusterSecretStore` named `vault-backend` that points to the Vault instance. + +A chart that needs secrets should include `secretStore` defaults in its `values.yaml`: + +```yaml +secretStore: + name: vault-backend + kind: ClusterSecretStore + +configdemosecret: + key: secret/data/global/config-demo + refreshInterval: 2m0s +``` + +The ExternalSecret template maps secrets from Vault into Kubernetes Secrets: + +```yaml +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: config-demo-secret + namespace: config-demo +spec: + refreshInterval: {{ .Values.configdemosecret.refreshInterval }} + secretStoreRef: + name: {{ .Values.secretStore.name }} + kind: {{ .Values.secretStore.kind }} + target: + name: config-demo-secret + template: + type: Opaque + data: + secret: "{{ `{{ .configdemo_secret }}` }}" + data: + - secretKey: configdemo_secret + remoteRef: + key: {{ .Values.configdemosecret.key }} + property: secret +``` + +#### How the Mapping Works + +Given a secret defined in `values-secret.yaml.template` as: + +```yaml +secrets: + - name: config-demo + vaultPrefixes: + - global + fields: + - name: secret +``` + +The Vault path is: `secret/data/global/config-demo` (constructed as `secret/data//`). + +In the ExternalSecret: + +- `remoteRef.key` is set to this Vault path (`secret/data/global/config-demo`) +- `remoteRef.property` is the field name (`secret`) +- `data[].secretKey` (`configdemo_secret`) is the local key used to reference the fetched value in the `target.template` + +#### Backtick Escaping for ESO Templates + +The `target.template.data` section uses ESO's own template syntax (`{{ .fieldname }}`) to map fetched values into the Kubernetes Secret. Since the ExternalSecret is itself rendered by Helm, the ESO template expressions must be escaped to prevent Helm from interpreting them. The pattern is: + +```text +"{{ `{{ .configdemo_secret }}` }}" +``` + +The backticks create a Go raw string literal that Helm passes through unchanged. ESO then processes `{{ .configdemo_secret }}` at runtime. + +### Hub vs. Spoke Secret Infrastructure + +The Vault/ESO components should only be defined on the hub/main cluster: + +```yaml +# Hub cluster needs both Vault and ESO +clusterGroup: + namespaces: + vault: + external-secrets-operator: + operatorGroup: true + targetNamespaces: [] + external-secrets: + subscriptions: + eso: + name: openshift-external-secrets-operator + namespace: external-secrets-operator + channel: stable-v1 + applications: + openshift-external-secrets: + name: openshift-external-secrets + namespace: external-secrets + chart: openshift-external-secrets + chartVersion: 0.0.* + vault: + name: vault + namespace: vault + chart: hashicorp-vault + chartVersion: 0.1.* +``` + +Spoke clusters only need ESO — no Vault: + +```yaml +# Spoke cluster needs ESO only +clusterGroup: + namespaces: + external-secrets-operator: + operatorGroup: true + targetNamespaces: [] + external-secrets: + subscriptions: + eso: + name: openshift-external-secrets-operator + namespace: external-secrets-operator + channel: stable-v1 + applications: + openshift-external-secrets: + name: openshift-external-secrets + namespace: external-secrets + chart: openshift-external-secrets + chartVersion: 0.0.* +``` + +The VP `openshift-external-secrets` chart automatically configures spoke clusters to use the Vault instance on the hub cluster as the external secret store. + +For more information, see [Secrets management in the Validated Patterns framework](https://validatedpatterns.io/learn/secrets-management-in-the-validated-patterns-framework/). + +## Spoke Clusters + +Hub/spoke cluster support uses ACM (Advanced Cluster Management). Include the ACM components in your hub clustergroup: + +```yaml +clusterGroup: + name: hub + + namespaces: + open-cluster-management: + + subscriptions: + acm: + name: advanced-cluster-management + namespace: open-cluster-management + channel: release-2.16 + + applications: + acm: + name: acm + namespace: open-cluster-management + chart: acm + chartVersion: 0.2.* + + managedClusterGroups: + exampleRegion: + name: group-one + acmlabels: + - name: clusterGroup + value: group-one +``` + +This installs ACM on the hub cluster via the Validated Patterns ACM chart. The `managedClusterGroups` defines the mapping the framework uses to determine which clusters imported into ACM correspond to which clustergroup values files. + +To create a spoke clustergroup, create `values-group-one.yaml` with its own namespaces, subscriptions, and applications. For secrets, include the ESO components (without Vault) as described in [Hub vs. Spoke Secret Infrastructure](#hub-vs-spoke-secret-infrastructure). + +```yaml +clusterGroup: + name: group-one + + namespaces: + config-demo: + hello-world: + external-secrets-operator: + operatorGroup: true + targetNamespaces: [] + external-secrets: + + subscriptions: + eso: + name: openshift-external-secrets-operator + namespace: external-secrets-operator + channel: stable-v1 + + applications: + openshift-external-secrets: + name: openshift-external-secrets + namespace: external-secrets + chart: openshift-external-secrets + chartVersion: 0.0.* + config-demo: + name: config-demo + namespace: config-demo + path: charts/all/config-demo + hello-world: + name: hello-world + namespace: hello-world + path: charts/all/hello-world +``` + +Any cluster imported into ACM with the label `clusterGroup: group-one` will have the clustergroup chart applied using `values-global.yaml` and `values-group-one.yaml` (plus all the other automatically included platform/version values files). + +In short, spoke and hub clusters are both just clustergroups. The difference is that the hub cluster includes ACM and the Vault components. While the Patterns Operator installs the Pattern via the clustergroup chart in ArgoCD on the hub cluster, the ACM chart pushes policies to spoke clusters for installing OpenShift GitOps (ArgoCD) and the clustergroup chart for that spoke cluster. + +## The Imperative Framework + +Sometimes tasks don't fit neatly into a declarative framework. The imperative framework runs Ansible playbooks on a schedule against the cluster. + +```yaml +clusterGroup: + imperative: + jobs: + - name: trilio-enable-cr + playbook: ansible/playbooks/imperative-enable-cr.yaml + timeout: 900 + + - name: trilio-cr-backup + playbook: ansible/playbooks/imperative-cr-backup.yaml + timeout: 1200 + + - name: trilio-backup + playbook: ansible/playbooks/imperative-backup.yaml + timeout: 1200 + + - name: trilio-restore-standard + playbook: ansible/playbooks/imperative-restore-standard.yaml + timeout: 1800 + + - name: trilio-e2e-status + playbook: ansible/playbooks/imperative-e2e-status.yaml + timeout: 120 +``` + +Imperative jobs typically reference playbooks stored in the `ansible/` directory of the Pattern repository, or playbooks from the `rhvp.cluster_utils` Ansible collection. Jobs are defined as a list since they run in order — if one fails, the imperative job fails and remaining jobs are aborted. Jobs must be idempotent since they run on a schedule (every 10 minutes by default). + +The full set of imperative framework defaults: + +```yaml +imperative: + jobs: [] + image: quay.io/validatedpatterns/imperative-container:v1 + ansibleDevMode: + enabled: false + requirementsFile: "requirements.yml" + requirementsContent: "" + ansibleCfgFile: "ansible.cfg" + ansibleCfgContent: "" + namespace: "imperative" + valuesConfigMap: "helm-values-configmap" + cronJobName: "imperative-cronjob" + jobName: "imperative-job" + imagePullPolicy: Always + activeDeadlineSeconds: 3600 + schedule: "*/10 * * * *" + insecureUnsealVaultInsideClusterSchedule: "*/5 * * * *" + verbosity: "" + extraPlaybookArgs: [] + serviceAccountCreate: true + serviceAccountName: imperative-sa + clusterRoleName: imperative-cluster-role + clusterRoleYaml: "" + roleName: imperative-role + roleYaml: "" + adminServiceAccountCreate: true + adminServiceAccountName: imperative-admin-sa + adminClusterRoleName: imperative-admin-cluster-role + vaultNamespace: "vault" +``` diff --git a/Makefile-common b/Makefile-common index 537ca883..cf9b7bfe 100644 --- a/Makefile-common +++ b/Makefile-common @@ -1,6 +1,6 @@ MAKEFLAGS += --no-print-directory -ANSIBLE_STDOUT_CALLBACK ?= null # null silences all ansible output. Override this with default, minimal, oneline, etc. when debugging. -ANSIBLE_RUN := ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) +ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluster_utils.readable +ANSIBLE_RUN ?= ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) DOCS_URL := https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/ .PHONY: help @@ -20,9 +20,9 @@ operator-deploy operator-upgrade: ## Installs/updates the pattern on a cluster ( .PHONY: install install: pattern-install ## Installs the pattern onto a cluster (Loads secrets as well if configured) -.PHONY: uninstall ## Prints a notice that patterns cannot currently be uninstalled -uninstall: - @echo "Uninstall is not possible at the moment so this target is empty. We are working to implement it as well as we can." +.PHONY: uninstall +uninstall: ## (EXPERIMENTAL) See https://validatedpatterns.io/blog/2026-02-16-pattern-uninstall/. + @$(ANSIBLE_RUN) rhvp.cluster_utils.uninstall .PHONY: pattern-install pattern-install: @@ -32,6 +32,11 @@ pattern-install: load-secrets: ## Loads secrets onto the cluster (unless explicitly disabled in values-global.yaml) @$(ANSIBLE_RUN) rhvp.cluster_utils.load_secrets +##@ Debug Tasks +.PHONY: display-secrets-info +display-secrets-info: ## Display your secret material on terminal or show secret loading error. Use with caution! + @$(ANSIBLE_RUN) rhvp.cluster_utils.display_secrets_info + ##@ Validation Tasks .PHONY: validate-prereq validate-prereq: ## verify pre-requisites @@ -52,3 +57,8 @@ validate-schema: ## validates values files against schema in common/clustergroup .PHONY: argo-healthcheck argo-healthcheck: ## Checks if all argo applications are synced @$(ANSIBLE_RUN) rhvp.cluster_utils.argo_healthcheck + +##@ Testing (CI) Tasks +.PHONY: run-ci-tests +run-ci-tests: ## To run ci-tests, set any needed env vars + @$(ANSIBLE_RUN) rhvp.cluster_utils.run_ci_tests diff --git a/ansible.cfg b/ansible.cfg index 516f8b84..528a8cb6 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,6 +1,15 @@ [defaults] localhost_warning=False retry_files_enabled=False -library=~/.ansible/plugins/modules:./ansible/plugins/modules:./common/ansible/plugins/modules:/usr/share/ansible/plugins/modules -roles_path=~/.ansible/roles:./ansible/roles:./common/ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles -filter_plugins=~/.ansible/plugins/filter:./ansible/plugins/filter:./common/ansible/plugins/filter:/usr/share/ansible/plugins/filter +# Retry files disabled to avoid cluttering CI/CD environments +interpreter_python=auto_silent +timeout=30 +library=~/.ansible/plugins/modules:./ansible/plugins/modules:/usr/share/ansible/plugins/modules +roles_path=~/.ansible/roles:./ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles +filter_plugins=~/.ansible/plugins/filter:./ansible/plugins/filter:/usr/share/ansible/plugins/filter +# use the collections from the util. container, +# change below if you want to test local collections +collections_path=/usr/share/ansible/collections + +[inventory] +inventory_unparsed_warning=False diff --git a/pattern.sh b/pattern.sh index d6daa15e..b2172fd4 100755 --- a/pattern.sh +++ b/pattern.sh @@ -1,19 +1,33 @@ #!/bin/bash +set -euo pipefail function is_available { - command -v $1 >/dev/null 2>&1 || { echo >&2 "$1 is required but it's not installed. Aborting."; exit 1; } + command -v "$1" >/dev/null 2>&1 || { echo >&2 "$1 is required but it's not installed. Aborting."; exit 1; } } function version { - echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' + echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' } -if [ -z "$PATTERN_UTILITY_CONTAINER" ]; then +# We need this check mostly for CI testing, we do not want to run container in container +function is_container() { + [ -n "${KUBERNETES_SERVICE_HOST:-}" ] && return 0 + [ -f /.dockerenv ] && return 0 + [ -f /run/.containerenv ] && return 0 + return 1 +} + +if is_container; then + echo "Already running in a container" + exec "$@" +fi + +if [ -z "${PATTERN_UTILITY_CONTAINER:-}" ]; then PATTERN_UTILITY_CONTAINER="quay.io/validatedpatterns/utility-container" fi # If PATTERN_DISCONNECTED_HOME is set it will be used to populate both PATTERN_UTILITY_CONTAINER # and PATTERN_INSTALL_CHART automatically -if [ -n "${PATTERN_DISCONNECTED_HOME}" ]; then +if [ -n "${PATTERN_DISCONNECTED_HOME:-}" ]; then PATTERN_UTILITY_CONTAINER="${PATTERN_DISCONNECTED_HOME}/utility-container" PATTERN_INSTALL_CHART="oci://${PATTERN_DISCONNECTED_HOME}/pattern-install" echo "PATTERN_DISCONNECTED_HOME is set to ${PATTERN_DISCONNECTED_HOME}" @@ -23,10 +37,10 @@ if [ -n "${PATTERN_DISCONNECTED_HOME}" ]; then fi readonly commands=(podman) -for cmd in ${commands[@]}; do is_available "$cmd"; done +for cmd in "${commands[@]}"; do is_available "$cmd"; done UNSUPPORTED_PODMAN_VERSIONS="1.6 1.5" -PODMAN_VERSION_STR=$(podman --version) +PODMAN_VERSION_STR=$(podman --version) || { echo "Failed to get podman version"; exit 1; } for i in ${UNSUPPORTED_PODMAN_VERSIONS}; do # We add a space if echo "${PODMAN_VERSION_STR}" | grep -q -E "\b${i}"; then @@ -41,19 +55,20 @@ done PODMAN_VERSION=$(echo "${PODMAN_VERSION_STR}" | awk '{ print $NF }') # podman < 4.3.0 do not support keep-id:uid=... -if [ $(version "${PODMAN_VERSION}") -lt $(version "4.3.0") ]; then - PODMAN_ARGS="-v ${HOME}:/root" +PODMAN_ARGS=() +if [ "$(version "${PODMAN_VERSION}")" -lt "$(version "4.3.0")" ]; then + PODMAN_ARGS=(-v "${HOME}:/root") else # We do not rely on bash's $UID and $GID because on MacOSX $GID is not set MYNAME=$(id -n -u) MYUID=$(id -u) MYGID=$(id -g) - PODMAN_ARGS="--passwd-entry ${MYNAME}:x:${MYUID}:${MYGID}::/pattern-home:/bin/bash --user ${MYUID}:${MYGID} --userns keep-id:uid=${MYUID},gid=${MYGID}" - + PODMAN_ARGS=(--passwd-entry "${MYNAME}:x:${MYUID}:${MYGID}::/pattern-home:/bin/bash" --user "${MYUID}:${MYGID}" --userns "keep-id:uid=${MYUID},gid=${MYGID}") fi -if [ -n "$KUBECONFIG" ]; then - if [[ ! "${KUBECONFIG}" =~ ^$HOME* ]]; then +if [ -n "${KUBECONFIG:-}" ]; then + # Check if KUBECONFIG path starts with HOME directory + if [[ ! "${KUBECONFIG}" =~ ^"${HOME}" ]]; then echo "${KUBECONFIG} is pointing outside of the HOME folder, this will make it unavailable from the container." echo "Please move it somewhere inside your $HOME folder, as that is what gets bind-mounted inside the container" exit 1 @@ -62,20 +77,26 @@ fi # Detect if we use podman machine. If we do not then we bind mount local host ssl folders # if we are using podman machine then we do not bind mount anything (for now!) -REMOTE_PODMAN=$(podman system connection list | tail -n +2 | wc -l) -if [ $REMOTE_PODMAN -eq 0 ]; then # If we are not using podman machine we check the hosts folders +REMOTE_PODMAN=$(podman system connection list | tail -n +2 | wc -l) || REMOTE_PODMAN=0 +PKI_HOST_MOUNT_ARGS=() +if [ "${REMOTE_PODMAN}" -eq 0 ]; then # If we are not using podman machine we check the hosts folders # We check /etc/pki/tls because on ubuntu /etc/pki/fwupd sometimes # exists but not /etc/pki/tls and we do not want to bind mount in such a case # as it would find no certificates at all. if [ -d /etc/pki/tls ]; then - PKI_HOST_MOUNT_ARGS="-v /etc/pki:/etc/pki:ro" + PKI_HOST_MOUNT_ARGS=(-v /etc/pki:/etc/pki:ro) elif [ -d /etc/ssl ]; then - PKI_HOST_MOUNT_ARGS="-v /etc/ssl:/etc/ssl:ro" + PKI_HOST_MOUNT_ARGS=(-v /etc/ssl:/etc/ssl:ro) else - PKI_HOST_MOUNT_ARGS="-v /usr/share/ca-certificates:/usr/share/ca-certificates:ro" + PKI_HOST_MOUNT_ARGS=(-v /usr/share/ca-certificates:/usr/share/ca-certificates:ro) fi -else - PKI_HOST_MOUNT_ARGS="" +fi + +# Parse EXTRA_ARGS into an array if set +EXTRA_ARGS_ARRAY=() +if [ -n "${EXTRA_ARGS:-}" ]; then + # shellcheck disable=SC2206 + EXTRA_ARGS_ARRAY=(${EXTRA_ARGS}) fi # Copy Kubeconfig from current environment. The utilities will pick up ~/.kube/config if set so it's not mandatory @@ -101,17 +122,19 @@ podman run -it --rm --pull=newer \ -e PATTERN_NAME \ -e TARGET_BRANCH \ -e TARGET_CLUSTERGROUP \ + -e TARGET_VARIANT \ -e TARGET_ORIGIN \ -e TOKEN_NAMESPACE \ -e TOKEN_SECRET \ -e UUID_FILE \ -e VALUES_SECRET \ - ${PKI_HOST_MOUNT_ARGS} \ + -e 'VP_*' \ + "${PKI_HOST_MOUNT_ARGS[@]}" \ -v "$(pwd -P)":"$(pwd -P)" \ -v "${HOME}":"${HOME}" \ -v "${HOME}":/pattern-home \ - ${PODMAN_ARGS} \ - ${EXTRA_ARGS} \ + "${PODMAN_ARGS[@]}" \ + "${EXTRA_ARGS_ARRAY[@]}" \ -w "$(pwd -P)" \ "$PATTERN_UTILITY_CONTAINER" \ - $@ + "$@" diff --git a/tests/ci/README.md b/tests/ci/README.md new file mode 100644 index 00000000..c62c7fc0 --- /dev/null +++ b/tests/ci/README.md @@ -0,0 +1,42 @@ +# CI Tests + +The requirements.txt file is just a placeholder to show the installed packages in the utility container: +[utility-container requirements.txt](https://github.com/validatedpatterns/utility-container/blob/main/requirements.txt) + +## The ci will run pytest based on file pattern + +pytest -lv test\_\.py --junit-xml .results/test\_\.xml + +## To run upstream tests locally + +Set the env variables pointing to the clusters needed to run the tests on:\ +`VP_HUBCONFIG` (`VP_SPOKECONFIG` if applicable) pointing to the kubconfig of hub (and spoke) clusters\ +(all VP\_\* env var will be available inside the container)\ +`TARGET_CLUSTERGROUP` if its different from the default (values-global.yaml main.clusterGroupName)\ +Test logs and junit-xml will be saved in ci/.results/\ +Run from root repository: + +```bash +./pattern.sh make run-ci-tests +``` + +## Writing additional tests + +The openshift_dyn_client fixture will return a DynamicClient which can be used inside test functions.\ +It requires only an env variable param to use it as a kubeconfig. + +```python +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +``` + +If your tests requires additional python packages, you might want to either run them fully locally in a venv or similar.\ +Or if you want to use the pattern framework (make/Ansible wrappers) you need to use your own util container + +```bash +PATTERN_UTILITY_CONTAINER="your_utility container" ./pattern.sh make run-ci-tests + +``` diff --git a/tests/ci/__init__.py b/tests/ci/__init__.py new file mode 100644 index 00000000..3dc1f76b --- /dev/null +++ b/tests/ci/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/tests/ci/conftest.py b/tests/ci/conftest.py new file mode 100644 index 00000000..12cc3c06 --- /dev/null +++ b/tests/ci/conftest.py @@ -0,0 +1 @@ +from validatedpatterns_tests.interop.conftest_openshift import * # noqa: F401,F403 diff --git a/tests/ci/requirements.txt b/tests/ci/requirements.txt new file mode 100644 index 00000000..0a68e1e2 --- /dev/null +++ b/tests/ci/requirements.txt @@ -0,0 +1,15 @@ +ansible-core==2.18.* +ansible-runner +awxkit +kubernetes +openshift +boto3>=1.21 +botocore>=1.24 +awscli>=1.22 +azure-cli>=2.34 +gcloud +humanize +pytz +pytest +junitparser +vp-qe-test-common @ git+https://github.com/validatedpatterns/vp-qe-test-common.git@v1 \ No newline at end of file diff --git a/tests/ci/test_hub.py b/tests/ci/test_hub.py new file mode 100644 index 00000000..36c80bb4 --- /dev/null +++ b/tests/ci/test_hub.py @@ -0,0 +1,106 @@ +import pytest +from validatedpatterns_tests.interop import application, components, subscription + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_subscription_status_hub(openshift_dyn_client): + expected_subs = { + "openshift-gitops-operator": ["openshift-gitops-operator"], + "advanced-cluster-management": ["open-cluster-management"], + "multicluster-engine": ["multicluster-engine"], + } + subscription.assert_subscription_status(openshift_dyn_client, expected_subs) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_SPOKECONFIG"], + indirect=True, +) +def test_subscription_status_spoke(openshift_dyn_client): + expected_subs = { + "openshift-gitops-operator": ["openshift-gitops-operator"], + } + + subscription.assert_subscription_status(openshift_dyn_client, expected_subs) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG", "VP_SPOKECONFIG"], + indirect=True, +) +def test_site_reachable(openshift_dyn_client): + + components.assert_site_reachable(openshift_dyn_client) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_pod_status_hub(openshift_dyn_client): + projects = [ + "patterns-operator", + "open-cluster-management", + "open-cluster-management-hub", + "vp-gitops", + "vault", + "hello-world", + "config-demo", + "external-secrets", + ] + + components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_SPOKECONFIG"], + indirect=True, +) +def test_pod_status_spoke(openshift_dyn_client): + projects = [ + "open-cluster-management-agent", + "open-cluster-management-agent-addon", + "vp-gitops", + "hello-world", + "config-demo", + "external-secrets", + ] + + components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_managed_clusters(openshift_dyn_client): + components.assert_managed_clusters(openshift_dyn_client, ["group-one"]) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG", "VP_SPOKECONFIG"], + indirect=True, +) +def test_argocd_reachable(openshift_dyn_client): + components.assert_argocd_reachable(openshift_dyn_client) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG", "VP_SPOKECONFIG"], + indirect=True, +) +def test_argocd_applications_health(openshift_dyn_client): + projects = ["vp-gitops", "multicloud-gitops-hub"] + + application.assert_argocd_applications(openshift_dyn_client, projects) diff --git a/tests/ci/test_standalone.py b/tests/ci/test_standalone.py new file mode 100644 index 00000000..aea8772f --- /dev/null +++ b/tests/ci/test_standalone.py @@ -0,0 +1,51 @@ +import pytest +from validatedpatterns_tests.interop import components, subscription + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_subscription_status_standalone(openshift_dyn_client): + expected_subs = { + "openshift-gitops-operator": ["openshift-gitops-operator"], + } + + subscription.assert_subscription_status(openshift_dyn_client, expected_subs) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_site_reachable_hub(openshift_dyn_client): + components.assert_site_reachable(openshift_dyn_client) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_argocd_reachable_standalone(openshift_dyn_client): + components.assert_argocd_reachable(openshift_dyn_client) + + +@pytest.mark.parametrize( + "openshift_dyn_client", + ["VP_HUBCONFIG"], + indirect=True, +) +def test_pod_status_standalone(openshift_dyn_client): + projects = [ + "patterns-operator", + "vp-gitops", + "vault", + "hello-world", + "config-demo", + "external-secrets", + # "non-existing" + ] + components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) From 6745cf81f23d68f508e82a6d194eb46767a56ed7 Mon Sep 17 00:00:00 2001 From: day0hero Date: Thu, 3 Sep 2026 18:12:05 +0100 Subject: [PATCH 02/12] updating subscriptions,namespaces..etc --- tests/ci/test_standalone.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/ci/test_standalone.py b/tests/ci/test_standalone.py index aea8772f..47299bc0 100644 --- a/tests/ci/test_standalone.py +++ b/tests/ci/test_standalone.py @@ -10,6 +10,9 @@ def test_subscription_status_standalone(openshift_dyn_client): expected_subs = { "openshift-gitops-operator": ["openshift-gitops-operator"], + "tempo-product": ["openshift-tempo-operator"], + "cluster-observability-operator": ["openshift-cluster-observability-operator"], + "opentelemetry-product": ["openshift-opentelemetry-operator"], } subscription.assert_subscription_status(openshift_dyn_client, expected_subs) @@ -43,9 +46,12 @@ def test_pod_status_standalone(openshift_dyn_client): "patterns-operator", "vp-gitops", "vault", - "hello-world", - "config-demo", - "external-secrets", - # "non-existing" + "golang-external-secrets", + "travel-agency", + "travel-control", + "travel-portal", + "openshift-tempo-operator", + "openshift-opentelemetry-operator", + "openshift-cluster-observability-operator", ] components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) From 17e32701ee77751947371531ce31f8b4560d11f9 Mon Sep 17 00:00:00 2001 From: day0hero Date: Tue, 29 Sep 2026 15:06:33 +0100 Subject: [PATCH 03/12] Updated tests - Added test to lookup gateway for travel-control since it isn't a route. - Converted UI tests from typescript to python. --- pattern.sh | 1 + tests/ci/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 192 bytes .../conftest.cpython-311-pytest-9.1.1.pyc | Bin 0 -> 383 bytes tests/ci/__pycache__/helpers.cpython-311.pyc | Bin 0 -> 3095 bytes .../test_hub.cpython-311-pytest-9.1.1.pyc | Bin 0 -> 3902 bytes ...st_standalone.cpython-311-pytest-9.1.1.pyc | Bin 0 -> 3692 bytes .../test_standalone.cpython-311.pyc | Bin 0 -> 3552 bytes tests/ci/helpers.py | 55 ++++++++++++++++++ tests/ci/requirements.txt | 1 + tests/ci/test_hub.py | 34 +++++++++++ tests/ci/test_standalone.py | 34 +++++++++++ tests/ci/ui/conftest.py | 9 +++ tests/ci/ui/test_travelops.py.j2 | 47 +++++++++++++++ 13 files changed, 181 insertions(+) create mode 100644 tests/ci/__pycache__/__init__.cpython-311.pyc create mode 100644 tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc create mode 100644 tests/ci/__pycache__/helpers.cpython-311.pyc create mode 100644 tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc create mode 100644 tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc create mode 100644 tests/ci/__pycache__/test_standalone.cpython-311.pyc create mode 100644 tests/ci/helpers.py create mode 100644 tests/ci/ui/conftest.py create mode 100644 tests/ci/ui/test_travelops.py.j2 diff --git a/pattern.sh b/pattern.sh index b2172fd4..18aabe3d 100755 --- a/pattern.sh +++ b/pattern.sh @@ -30,6 +30,7 @@ fi if [ -n "${PATTERN_DISCONNECTED_HOME:-}" ]; then PATTERN_UTILITY_CONTAINER="${PATTERN_DISCONNECTED_HOME}/utility-container" PATTERN_INSTALL_CHART="oci://${PATTERN_DISCONNECTED_HOME}/pattern-install" + export PATTERN_UTILITY_CONTAINER PATTERN_INSTALL_CHART echo "PATTERN_DISCONNECTED_HOME is set to ${PATTERN_DISCONNECTED_HOME}" echo "Setting the following variables:" echo " PATTERN_UTILITY_CONTAINER: ${PATTERN_UTILITY_CONTAINER}" diff --git a/tests/ci/__pycache__/__init__.cpython-311.pyc b/tests/ci/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a4677a429e858a759d3bcf30f454d2616e34bf4 GIT binary patch literal 192 zcmZ3^%ge<81m+bpv&4Y(V-N=h7@>^MASKfoQW&BbQW%37G?}Vc4fG844E!`1Z*j-R zm!%dJXXfX{$FF4g4ASw-UOyv0H&s8YC^I=bu_#49peR2pHMyi%KP9o!AS1OXU%#X% zu`D$wzo1yZB()eQk({X?AD@|*SrQ+wS5Wzj!zMRBr8Fniu80k29>^KRd_dv@Gb1D8 P4L;EZZV)VD0SW;C4Tmwv literal 0 HcmV?d00001 diff --git a/tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31ab68a1e916d61a740853bd1e58d3a33a810881 GIT binary patch literal 383 zcmZ`zu};G<5VaGarE1yv1HuraE-VQ81qg{wT_O`-6$i(*Y`2h&zd(Ee;%^v{S0*;3 zZk@0r(gBIH&v)oAIWdCISI`(Vu)dZ7PzZPla>ia&E8@k zMkNB*nhpH$ZpDS;()#gCx5W+}7rJ#|T~4h9q11Ee;* zyr+hd8>4C{oh6L6XH^grXXqM)(w20dq~xTx%XPaQUNnlW1YDY<;D7&-6`tk(QWVXVEk=~BIGe_XLIe>N7fzzsu;D~;l)_LATSX87g|Oj{B+Aq- zGqbcSngWQx2L;d}0TNh196%>WfpgHU2HJyxUV2;!1WGJmz(AXWZ*-Mhbn1JvOz^Yfgs!xk@ z#|92ypWyS}zkz@~=@#)NU^{Io=If*`)f3+(?t=8aSi>Z_3zGM`;$&6!)%bzb<-4%{ zUaZ5BuL=M7!0%w^^~6`iQR@;=?Q6AH&nHq{cJ$#`)lzJ6N$I{^Rs2+!C#0@`|H003m^{dRa7Ja|+GS6FoxN^yw_n2i{uDwhh z>-QDvUR^BB`<95_STS}7^x(FRz<=*BklSQ|)X5)}RjDq0o>*6}l5YvqVIrRh6Jz6{ z$}s#KLtt6^BZZ3X`;@r_c<1v%u~b;3Fy)DpX~M)wCtt&%I=#EaprG|JGyQ1;7wI#e;L zJXCPlm)|NB@;bu<4CMvthjN*^p;23W2sp`VW8h;`&50^?Wn+o5tkB+zXq^Cf%CZ$(?HEP6g?6YagtAa33tR4h;sW z6VNvX?+iy=pqya+u<4)A;J?C|{TebD@-_)hbQ&3Cnr}p2KAkQG7lZmhnFvtWu zI^fShUG32>z5=+ugGGmHXZ{Bkb)ek3aQS_!%iE5_DChqdE(uiXKodhJA?u>AL)Uti6Ol|I4o=Yvj8NbS{j)E|W7)o70T$Tm( zp)xfybD2GdmWF`jQw-sq<&fZ~h@q=&1W2ewsv^vhIiQtMVV>_5y>h5lD}dsmdfhHp zY2IMRan%7_8Qn-n>Q3V9C=v?@$HRzl%d#TmN<9^SR)A^T4*zqomT zT{*tuYm0*VL!!_4LNTu5mOFy`@iYDS{Xsq&fg*MtYYG9t zE;F|wt>0{BUTRD|G>0~fJDIIx+s=-8rfHrD%nSEfTnSJB_>$}+tLH0sxU}#Nygs}N4^zMRJqQ^jaRb0Hs zXCZvWZkYuAdgfv@_r<+=?#pO*_lsAypP~JQ>4{5I41sezD z(kGHbpa{{XFG7i+U8k%xzY^+xX_-Q$@c8c_Tg%2*+_p}Qk6ZK1Tee1PeWTV#Ro`0h zd@C=92BRNWv5F9vHB3dfbFSz)6lP_p9N2F`8OrgB=W@!?VYp|}6&tiUr_9F;pJUW6 zF4`ZJDVqQhT*WT~X^@ARq4oO4J6r1ZjUaP=Cv(1;Ip3IiWSC83WK-TXUJ68S>605g z&?lj9EjGvAj##`#Cz@RukQ8T;m)JPyh8#H!Ls;;N6|m2}Dl1Yx*1h-L(WU+kk6kOd z3v<`rA&!=#D-#EMMIn^>f$M8mAy~FVWv}H+> sS|rmDzZQ8X=yNn>pilh+mLU8zm}8x_I$4zW@LL literal 0 HcmV?d00001 diff --git a/tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44c3988436705567db18bafdbb7609de2955ef00 GIT binary patch literal 3902 zcmcIm&2Jk;6rWkI9ovcX*|cfWHYv1e-QuKz7D1bS1p%c2CshH7NXXjkPVG(X-PP30ynB{kmL+)n9lMT;clS)h`kuPl-k}nkS1?{7Z$9Dp6997sJ#)i7ZCoM9Oy+ zqKnZ&Y%x}dFUBchk>&0H@Q0pY)%>r8G&xJP@UzH9h>$Iq{-t$2AsevV3O2}M;y`H8 zwrH^ywB7@u$yzikwUI8~g4cI2ytdMHx1jYO2(7)YJuPSh2SRJ_heQk7;K9&3SgyAP zZ|IHi`daXgX!Jfw_Wy&ClTL=bZpE_lx~e$3X?StfELf(&49E6*?BbHGa@`UqM@)+u zc20lbWVAISqvmyB@_N2r$lUpA_M`dqC!c-#Ws-VP-OzN-R7a(a&?H2LHOB6%Ss;)t z6#Oi~48fGY4}C|SChq0N`Z0M-Hz2>ZVEPy46G$IU*N!*jNo94%wCogYaK$ls?HC@V z)O^u)7*7=xL&>rNNLw2!6!VTA955rR8!Q>(nB}E0MO#%2m1%7rX#fTv=s#&}G_THXNwBSY&d=r73I8JAu3H-ir);5Z;0U+Bbo$lWH$HH%r~Kvz7C= z-E*^*i7W2-`_(RaI8r4**1y<^lkh-UzUaPl>qTl?UMR~8ZZKW}1o3*ox%U_5=RWq` z+uoyBffGH7KGo@=Jt?Jy`FR3hcps3uwfF!^aH^xmwq>p`eiWF7@S#gUgwC-u?&NLI zx*C#%lF`mea(r@+QW&BlOs8P+lynNp4G7S!Mv(d;xk=MWiDR65!$E-P4$E++s5xaR z&o-%u4I7`E@UKLd-W zBnw{o@wFbwl4sK1zEaPxP&yec4Oof;31sYM7RPQ?8taf=DTu;aY}KmgqZeLPAl+)+ zIhN0xsfQ-dYqf6GG#=>Ll*S5Xt)D$Yd?lZ0~FfLiIA7(%Y{+PAQsZ8vG`N@p{)V}wi-ONt7P+A0D zX}I2A&7v0cWGD%sCR(b|H|z2@eRbHakvT3Rvkhw|$CSL2GL5_A8EXySJmp6g zd=X7?_K-t`_F|1KuPfM#!^3q!y@{^x{T zQ22`1Z7EzSFo)|UCOWxjZK4@)EL|LPl4HPfbhl7{lL&Q0FR4Xm3Z_=fvl|@SKgPaY zgzt`Oh*DZX3w8J<*2S+v?zx>uHA1B5ld(VP$RBj1B9B(WL#}+Jk{GCV5$X6#B1_W0 zNUHG&y+^AA>C5hYyMy~NhF$q+JB+ZvK-!EUcKi2Z47tN+U3sz{j6|5tj zwAZDFE`Tmbn<2#Rsr?w8bcqWLq|F#&cXU5SC!13GImm&u8AI$wTQNG?gVO7Wa(yqz W_3B|TLUay9LE4P6yX&8cu>24Dxqm_c literal 0 HcmV?d00001 diff --git a/tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86d391f4e868aa69351826fb3f044aadf43a63f5 GIT binary patch literal 3692 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 O0ubFN2 Date: Wed, 30 Sep 2026 14:16:00 +0100 Subject: [PATCH 04/12] removed unnecessary test file --- tests/ci/test_hub.py | 90 ++++++++---------------------------- tests/ci/test_standalone.py | 91 ------------------------------------- 2 files changed, 20 insertions(+), 161 deletions(-) delete mode 100644 tests/ci/test_standalone.py diff --git a/tests/ci/test_hub.py b/tests/ci/test_hub.py index 0a02e604..d14c0478 100644 --- a/tests/ci/test_hub.py +++ b/tests/ci/test_hub.py @@ -1,5 +1,5 @@ import pytest -from validatedpatterns_tests.interop import application, components, subscription +from validatedpatterns_tests.interop import components, subscription from .helpers import assert_url_reachable, gateway_url, route_url @@ -9,23 +9,12 @@ ["VP_HUBCONFIG"], indirect=True, ) -def test_subscription_status_hub(openshift_dyn_client): - expected_subs = { - "openshift-gitops-operator": ["openshift-gitops-operator"], - "advanced-cluster-management": ["open-cluster-management"], - "multicluster-engine": ["multicluster-engine"], - } - subscription.assert_subscription_status(openshift_dyn_client, expected_subs) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_SPOKECONFIG"], - indirect=True, -) -def test_subscription_status_spoke(openshift_dyn_client): +def test_subscription_status(openshift_dyn_client): expected_subs = { "openshift-gitops-operator": ["openshift-gitops-operator"], + "tempo-product": ["openshift-tempo-operator"], + "cluster-observability-operator": ["openshift-cluster-observability-operator"], + "opentelemetry-product": ["openshift-opentelemetry-operator"], } subscription.assert_subscription_status(openshift_dyn_client, expected_subs) @@ -33,11 +22,10 @@ def test_subscription_status_spoke(openshift_dyn_client): @pytest.mark.parametrize( "openshift_dyn_client", - ["VP_HUBCONFIG", "VP_SPOKECONFIG"], + ["VP_HUBCONFIG"], indirect=True, ) -def test_site_reachable(openshift_dyn_client): - +def test_site_reachable_hub(openshift_dyn_client): components.assert_site_reachable(openshift_dyn_client) @@ -46,68 +34,31 @@ def test_site_reachable(openshift_dyn_client): ["VP_HUBCONFIG"], indirect=True, ) -def test_pod_status_hub(openshift_dyn_client): - projects = [ - "patterns-operator", - "open-cluster-management", - "open-cluster-management-hub", - "vp-gitops", - "vault", - "hello-world", - "config-demo", - "external-secrets", - ] - - components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) +def test_argocd_reachable(openshift_dyn_client): + components.assert_argocd_reachable(openshift_dyn_client) @pytest.mark.parametrize( "openshift_dyn_client", - ["VP_SPOKECONFIG"], + ["VP_HUBCONFIG"], indirect=True, ) -def test_pod_status_spoke(openshift_dyn_client): +def test_pod_status(openshift_dyn_client): projects = [ - "open-cluster-management-agent", - "open-cluster-management-agent-addon", + "patterns-operator", "vp-gitops", - "hello-world", - "config-demo", - "external-secrets", + "vault", + "golang-external-secrets", + "travel-agency", + "travel-control", + "travel-portal", + "openshift-tempo-operator", + "openshift-opentelemetry-operator", + "openshift-cluster-observability-operator", ] - components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_managed_clusters(openshift_dyn_client): - components.assert_managed_clusters(openshift_dyn_client, ["group-one"]) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG", "VP_SPOKECONFIG"], - indirect=True, -) -def test_argocd_reachable(openshift_dyn_client): - components.assert_argocd_reachable(openshift_dyn_client) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG", "VP_SPOKECONFIG"], - indirect=True, -) -def test_argocd_applications_health(openshift_dyn_client): - projects = ["vp-gitops", "multicloud-gitops-hub"] - - application.assert_argocd_applications(openshift_dyn_client, projects) - - @pytest.mark.parametrize( "openshift_dyn_client", ["VP_HUBCONFIG"], @@ -127,7 +78,6 @@ def test_kiali_route(openshift_dyn_client): url = route_url(openshift_dyn_client, "istio-system", "kiali") assert_url_reachable(url) - @pytest.mark.parametrize( "openshift_dyn_client", ["VP_HUBCONFIG"], diff --git a/tests/ci/test_standalone.py b/tests/ci/test_standalone.py deleted file mode 100644 index 73324301..00000000 --- a/tests/ci/test_standalone.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest -from validatedpatterns_tests.interop import components, subscription - -from .helpers import assert_url_reachable, gateway_url, route_url - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_subscription_status_standalone(openshift_dyn_client): - expected_subs = { - "openshift-gitops-operator": ["openshift-gitops-operator"], - "tempo-product": ["openshift-tempo-operator"], - "cluster-observability-operator": ["openshift-cluster-observability-operator"], - "opentelemetry-product": ["openshift-opentelemetry-operator"], - } - - subscription.assert_subscription_status(openshift_dyn_client, expected_subs) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_site_reachable_hub(openshift_dyn_client): - components.assert_site_reachable(openshift_dyn_client) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_argocd_reachable_standalone(openshift_dyn_client): - components.assert_argocd_reachable(openshift_dyn_client) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_pod_status_standalone(openshift_dyn_client): - projects = [ - "patterns-operator", - "vp-gitops", - "vault", - "golang-external-secrets", - "travel-agency", - "travel-control", - "travel-portal", - "openshift-tempo-operator", - "openshift-opentelemetry-operator", - "openshift-cluster-observability-operator", - ] - components.assert_pod_status(openshift_dyn_client, projects, skip_check=[]) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_console_route(openshift_dyn_client): - url = route_url(openshift_dyn_client, "openshift-console", "console") - assert_url_reachable(url) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_kiali_route(openshift_dyn_client): - url = route_url(openshift_dyn_client, "istio-system", "kiali") - assert_url_reachable(url) - - -@pytest.mark.parametrize( - "openshift_dyn_client", - ["VP_HUBCONFIG"], - indirect=True, -) -def test_travel_control_gateway(openshift_dyn_client): - url = gateway_url( - openshift_dyn_client, "travel-control", "travel-control-gateway" - ) - assert_url_reachable(url) From b1b5456162159cc8f904d906d92782447dfa7eb0 Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 14:17:57 +0100 Subject: [PATCH 05/12] variant implementation --- values-global.yaml | 3 +-- values-hub.yaml => variants/hub/values-hub.yaml | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename values-hub.yaml => variants/hub/values-hub.yaml (100%) diff --git a/values-global.yaml b/values-global.yaml index 768164cd..decd49e7 100644 --- a/values-global.yaml +++ b/values-global.yaml @@ -1,7 +1,6 @@ --- global: pattern: travelops - singleArgoCD: true options: @@ -11,7 +10,7 @@ global: autoApproveManualInstallPlans: true main: - clusterGroupName: hub + variant: hub multiSourceConfig: enabled: true clusterGroupChartVersion: "0.9.*" diff --git a/values-hub.yaml b/variants/hub/values-hub.yaml similarity index 100% rename from values-hub.yaml rename to variants/hub/values-hub.yaml From 96adb463a963ac1e026db2259db5bf6d72909776 Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 14:29:35 +0100 Subject: [PATCH 06/12] add s4 chart to the mix --- variants/hub/values-hub.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/variants/hub/values-hub.yaml b/variants/hub/values-hub.yaml index fb252292..a623dbb6 100644 --- a/variants/hub/values-hub.yaml +++ b/variants/hub/values-hub.yaml @@ -8,6 +8,7 @@ clusterGroup: operatorGroup: true targetNamespaces: [] external-secrets: + s4-storage: travel-agency: labels: istio-discovery: enabled @@ -127,6 +128,28 @@ clusterGroup: path: charts/all/distributed-tracing extraValueFiles: - /values-distributed-tracing.yaml + vp-s4-storage: + name: vp-s4-storage + namespace: vp-s4-storage + argoProject: hub + chart: vp-s4-storage + chartVersion: 0.2.* + overrides: + # Vault paths from examples/secrets/values-secret.v2.yaml (adjust prefix as needed) + - name: s4UICredentials.vaultKey + value: secret/data/global/s4-ui-credentials + - name: s4APICredentials.vaultKey + value: secret/data/global/s4-api-credentials + # Bucket names for the imperative Job/CronJob playbook + - name: s4Role.buckets[0] + value: my-app-data + - name: s4Role.buckets[1] + value: my-app-logs + # vp-rbac Role/RoleBinding namespace must match the Argo CD app namespace + - name: vp-rbac.serviceAccounts.vp-s4-storage-sa.namespace + value: vp-s4-storage + - name: vp-rbac.roles.external-secrets-validator.namespace + value: vp-s4-storage servicemesh: istio: From c784f2e4bb1a49426a0f513216fcfccd06e12178 Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 15:07:18 +0100 Subject: [PATCH 07/12] fix typo in namespace --- variants/hub/values-hub.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/hub/values-hub.yaml b/variants/hub/values-hub.yaml index a623dbb6..8a1e4b87 100644 --- a/variants/hub/values-hub.yaml +++ b/variants/hub/values-hub.yaml @@ -8,7 +8,7 @@ clusterGroup: operatorGroup: true targetNamespaces: [] external-secrets: - s4-storage: + vp-s4-storage: travel-agency: labels: istio-discovery: enabled From c53502f2a393f17c408ce42f1ba773f68856ce4c Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 15:33:29 +0100 Subject: [PATCH 08/12] configure tempostack to use vp-s4-storage --- charts/all/tempostack/Chart.yaml | 3 +- charts/all/tempostack/templates/minio.yaml | 4 +-- .../tempostack/templates/storage-secret.yaml | 30 +++++++++++++++++++ charts/all/tempostack/values.yaml | 11 +++++-- values-distributed-tracing.yaml | 7 ++++- values-secret.yaml.template | 21 ++++++++++--- variants/hub/values-hub.yaml | 4 +-- 7 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 charts/all/tempostack/templates/storage-secret.yaml diff --git a/charts/all/tempostack/Chart.yaml b/charts/all/tempostack/Chart.yaml index a6f9371e..8a1eb905 100644 --- a/charts/all/tempostack/Chart.yaml +++ b/charts/all/tempostack/Chart.yaml @@ -1,7 +1,8 @@ apiVersion: v2 name: tempostack description: | - Deploys a multi-tenant TempoStack instance, optionally bundled with a MinIO backend. + Deploys a multi-tenant TempoStack instance backed by S3-compatible object storage + (vp-s4-storage by default), with an optional bundled MinIO backend for local demos. Ideal as a tracing backend with Red Hat Service Mesh 3 and Tempo. type: application diff --git a/charts/all/tempostack/templates/minio.yaml b/charts/all/tempostack/templates/minio.yaml index 1a1982c9..080881df 100644 --- a/charts/all/tempostack/templates/minio.yaml +++ b/charts/all/tempostack/templates/minio.yaml @@ -110,10 +110,10 @@ spec: data: - secretKey: access_key_id remoteRef: - key: {{ .Values.secretStore.remoteKey }} + key: {{ .Values.tempostack.minio.remoteKey | default "secret/data/global/minio-credentials" }} property: accessKey - secretKey: access_key_secret remoteRef: - key: {{ .Values.secretStore.remoteKey }} + key: {{ .Values.tempostack.minio.remoteKey | default "secret/data/global/minio-credentials" }} property: secretKey {{- end }} diff --git a/charts/all/tempostack/templates/storage-secret.yaml b/charts/all/tempostack/templates/storage-secret.yaml new file mode 100644 index 00000000..09f07e98 --- /dev/null +++ b/charts/all/tempostack/templates/storage-secret.yaml @@ -0,0 +1,30 @@ +{{- if not .Values.tempostack.minio.enabled }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.tempostack.storage.secretName }} + namespace: {{ .Values.tempostack.namespace | default .Release.Namespace | quote }} +spec: + refreshInterval: 15s + secretStoreRef: + name: {{ .Values.secretStore.name }} + kind: {{ .Values.secretStore.kind }} + target: + name: {{ .Values.tempostack.storage.secretName }} + template: + type: Opaque + data: + bucket: {{ .Values.tempostack.storage.bucket | quote }} + endpoint: {{ .Values.tempostack.storage.endpoint | quote }} + access_key_id: "{{ `{{ .access_key_id }}` }}" + access_key_secret: "{{ `{{ .access_key_secret }}` }}" + data: + - secretKey: access_key_id + remoteRef: + key: {{ .Values.secretStore.remoteKey }} + property: {{ .Values.secretStore.accessKeyProperty }} + - secretKey: access_key_secret + remoteRef: + key: {{ .Values.secretStore.remoteKey }} + property: {{ .Values.secretStore.secretKeyProperty }} +{{- end }} diff --git a/charts/all/tempostack/values.yaml b/charts/all/tempostack/values.yaml index e99f3578..b29d895b 100644 --- a/charts/all/tempostack/values.yaml +++ b/charts/all/tempostack/values.yaml @@ -3,6 +3,9 @@ tempostack: type: s3 secretName: tempostack-default-secret size: 10Gi + # Object-storage backend used when minio.enabled is false (vp-s4-storage by default) + bucket: tempo-data + endpoint: http://vp-s4-storage.vp-s4-storage.svc:7480 resources: # Resources are autonomously split between all deployments in the tempostack. Override resources per-deployment in the tempate value. limits: @@ -25,11 +28,15 @@ tempostack: resourceNames: [] # - traces + # Bundled MinIO is optional; prefer cluster S3 (vp-s4-storage) for OpenShift demos minio: - enabled: true + enabled: false bucket: tempo-data secretStore: name: vault-backend kind: ClusterSecretStore - remoteKey: secret/data/global/minio-credentials + # Defaults target vp-s4-storage API credentials in Vault + remoteKey: secret/data/global/s4-api-credentials + accessKeyProperty: AWS_ACCESS_KEY_ID + secretKeyProperty: AWS_SECRET_ACCESS_KEY diff --git a/values-distributed-tracing.yaml b/values-distributed-tracing.yaml index 7ff65c90..91fbaf3f 100644 --- a/values-distributed-tracing.yaml +++ b/values-distributed-tracing.yaml @@ -2,7 +2,12 @@ tempostack: namespace: tempo storage: - secretName: tempostack-dev-minio + secretName: tempostack-s3 + bucket: tempo-data + endpoint: http://vp-s4-storage.vp-s4-storage.svc:7480 + + minio: + enabled: false resources: limits: diff --git a/values-secret.yaml.template b/values-secret.yaml.template index 4915f3db..70f4c984 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -13,12 +13,25 @@ secrets: - name: rootpasswd onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - - name: minio-credentials + # S4 Web UI login (merged into Kubernetes Secret s4-credentials by vp-s4-storage) + - name: s4-ui-credentials vaultPrefixes: - global fields: - - name: accessKey - value: minioAccessKey - - name: secretKey + - name: UI_USERNAME + value: s4admin + onMissingValue: error + - name: UI_PASSWORD + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + # S4 S3 API credentials (used by TempoStack object storage and S4 itself) + - name: s4-api-credentials + vaultPrefixes: + - global + fields: + - name: AWS_ACCESS_KEY_ID + value: s4admin + onMissingValue: error + - name: AWS_SECRET_ACCESS_KEY onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy diff --git a/variants/hub/values-hub.yaml b/variants/hub/values-hub.yaml index 8a1e4b87..5db04bb8 100644 --- a/variants/hub/values-hub.yaml +++ b/variants/hub/values-hub.yaml @@ -142,9 +142,7 @@ clusterGroup: value: secret/data/global/s4-api-credentials # Bucket names for the imperative Job/CronJob playbook - name: s4Role.buckets[0] - value: my-app-data - - name: s4Role.buckets[1] - value: my-app-logs + value: tempo-data # vp-rbac Role/RoleBinding namespace must match the Argo CD app namespace - name: vp-rbac.serviceAccounts.vp-s4-storage-sa.namespace value: vp-s4-storage From 300a899b659576fe8df90785103e516e53cbe1ed Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 18:00:36 +0100 Subject: [PATCH 09/12] update gh workflows and gitignore --- .github/workflows/jsonschema.yaml | 2 +- .gitignore | 1 + tests/ci/test_hub.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jsonschema.yaml b/.github/workflows/jsonschema.yaml index 8b669f93..ac73da80 100644 --- a/.github/workflows/jsonschema.yaml +++ b/.github/workflows/jsonschema.yaml @@ -42,7 +42,7 @@ jobs: - name: Verify ClusterGroup values.schema.json against values-*yaml files run: | - set -e; for i in values-hub.yaml; do + set -e; for i in variants/hub/values-hub.yaml; do echo "$i" # disable shellcheck of single quotes in yq # shellcheck disable=2016 diff --git a/.gitignore b/.gitignore index 1bf6d7eb..9222576b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ vault.init super-linter.log common/pattern-vault.init scratch +tests/ci/__pycache__/* diff --git a/tests/ci/test_hub.py b/tests/ci/test_hub.py index d14c0478..2c58e725 100644 --- a/tests/ci/test_hub.py +++ b/tests/ci/test_hub.py @@ -78,6 +78,7 @@ def test_kiali_route(openshift_dyn_client): url = route_url(openshift_dyn_client, "istio-system", "kiali") assert_url_reachable(url) + @pytest.mark.parametrize( "openshift_dyn_client", ["VP_HUBCONFIG"], From 71d5e287b03860813df010e7709d5ec1594656d6 Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 18:00:36 +0100 Subject: [PATCH 10/12] updated values-secret template - added vp-s4-storage chart and needed to update the secrets template to account for new requirements --- values-secret.yaml.template | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/values-secret.yaml.template b/values-secret.yaml.template index 70f4c984..b3b73844 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -2,6 +2,22 @@ # https://github.com/hybrid-cloud-patterns/common/tree/main/ansible/roles/vault_utils#values-secret-file-format version: "2.0" +backingStore: vault + +vaultPolicies: + basicPolicy: | + length=10 + rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 } + rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 } + rule "charset" { charset = "0123456789" min-chars = 1 } + + advancedPolicy: | + length=20 + rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 } + rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 } + rule "charset" { charset = "0123456789" min-chars = 1 } + rule "charset" { charset = "!@#$%^&*" min-chars = 1 } + # Ideally you NEVER COMMIT THESE VALUES TO GIT (although if all passwords are # automatically generated inside the vault this should not really matter) From a5f254bc4b9bd40c7305810619ee36a4836038af Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 30 Sep 2026 21:08:11 +0100 Subject: [PATCH 11/12] adding vp-s4-storage ns to list of checks --- tests/ci/test_hub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ci/test_hub.py b/tests/ci/test_hub.py index 2c58e725..bb9547c6 100644 --- a/tests/ci/test_hub.py +++ b/tests/ci/test_hub.py @@ -49,6 +49,7 @@ def test_pod_status(openshift_dyn_client): "vp-gitops", "vault", "golang-external-secrets", + "vp-s4-storage", "travel-agency", "travel-control", "travel-portal", From 00170a4ddb5b0fac57352fc134684538964ac908 Mon Sep 17 00:00:00 2001 From: Martin Jackson Date: Wed, 30 Sep 2026 15:25:41 -0500 Subject: [PATCH 12/12] Remove pycache dir --- tests/ci/__pycache__/__init__.cpython-311.pyc | Bin 192 -> 0 bytes .../conftest.cpython-311-pytest-9.1.1.pyc | Bin 383 -> 0 bytes tests/ci/__pycache__/helpers.cpython-311.pyc | Bin 3095 -> 0 bytes .../test_hub.cpython-311-pytest-9.1.1.pyc | Bin 3902 -> 0 bytes .../test_standalone.cpython-311-pytest-9.1.1.pyc | Bin 3692 -> 0 bytes .../__pycache__/test_standalone.cpython-311.pyc | Bin 3552 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/ci/__pycache__/__init__.cpython-311.pyc delete mode 100644 tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc delete mode 100644 tests/ci/__pycache__/helpers.cpython-311.pyc delete mode 100644 tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc delete mode 100644 tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc delete mode 100644 tests/ci/__pycache__/test_standalone.cpython-311.pyc diff --git a/tests/ci/__pycache__/__init__.cpython-311.pyc b/tests/ci/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 5a4677a429e858a759d3bcf30f454d2616e34bf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192 zcmZ3^%ge<81m+bpv&4Y(V-N=h7@>^MASKfoQW&BbQW%37G?}Vc4fG844E!`1Z*j-R zm!%dJXXfX{$FF4g4ASw-UOyv0H&s8YC^I=bu_#49peR2pHMyi%KP9o!AS1OXU%#X% zu`D$wzo1yZB()eQk({X?AD@|*SrQ+wS5Wzj!zMRBr8Fniu80k29>^KRd_dv@Gb1D8 P4L;EZZV)VD0SW;C4Tmwv diff --git a/tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/conftest.cpython-311-pytest-9.1.1.pyc deleted file mode 100644 index 31ab68a1e916d61a740853bd1e58d3a33a810881..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 383 zcmZ`zu};G<5VaGarE1yv1HuraE-VQ81qg{wT_O`-6$i(*Y`2h&zd(Ee;%^v{S0*;3 zZk@0r(gBIH&v)oAIWdCISI`(Vu)dZ7PzZPla>ia&E8@k zMkNB*nhpH$ZpDS;()#gCx5W+}7rJ#|T~4h9q11Ee;* zyr+hd8>4C{oh6L6XH^grXXqM)(w20dq~xTx%XPaQUNnlW1YDY<;D7&-6`tk(QWVXVEk=~BIGe_XLIe>N7fzzsu;D~;l)_LATSX87g|Oj{B+Aq- zGqbcSngWQx2L;d}0TNh196%>WfpgHU2HJyxUV2;!1WGJmz(AXWZ*-Mhbn1JvOz^Yfgs!xk@ z#|92ypWyS}zkz@~=@#)NU^{Io=If*`)f3+(?t=8aSi>Z_3zGM`;$&6!)%bzb<-4%{ zUaZ5BuL=M7!0%w^^~6`iQR@;=?Q6AH&nHq{cJ$#`)lzJ6N$I{^Rs2+!C#0@`|H003m^{dRa7Ja|+GS6FoxN^yw_n2i{uDwhh z>-QDvUR^BB`<95_STS}7^x(FRz<=*BklSQ|)X5)}RjDq0o>*6}l5YvqVIrRh6Jz6{ z$}s#KLtt6^BZZ3X`;@r_c<1v%u~b;3Fy)DpX~M)wCtt&%I=#EaprG|JGyQ1;7wI#e;L zJXCPlm)|NB@;bu<4CMvthjN*^p;23W2sp`VW8h;`&50^?Wn+o5tkB+zXq^Cf%CZ$(?HEP6g?6YagtAa33tR4h;sW z6VNvX?+iy=pqya+u<4)A;J?C|{TebD@-_)hbQ&3Cnr}p2KAkQG7lZmhnFvtWu zI^fShUG32>z5=+ugGGmHXZ{Bkb)ek3aQS_!%iE5_DChqdE(uiXKodhJA?u>AL)Uti6Ol|I4o=Yvj8NbS{j)E|W7)o70T$Tm( zp)xfybD2GdmWF`jQw-sq<&fZ~h@q=&1W2ewsv^vhIiQtMVV>_5y>h5lD}dsmdfhHp zY2IMRan%7_8Qn-n>Q3V9C=v?@$HRzl%d#TmN<9^SR)A^T4*zqomT zT{*tuYm0*VL!!_4LNTu5mOFy`@iYDS{Xsq&fg*MtYYG9t zE;F|wt>0{BUTRD|G>0~fJDIIx+s=-8rfHrD%nSEfTnSJB_>$}+tLH0sxU}#Nygs}N4^zMRJqQ^jaRb0Hs zXCZvWZkYuAdgfv@_r<+=?#pO*_lsAypP~JQ>4{5I41sezD z(kGHbpa{{XFG7i+U8k%xzY^+xX_-Q$@c8c_Tg%2*+_p}Qk6ZK1Tee1PeWTV#Ro`0h zd@C=92BRNWv5F9vHB3dfbFSz)6lP_p9N2F`8OrgB=W@!?VYp|}6&tiUr_9F;pJUW6 zF4`ZJDVqQhT*WT~X^@ARq4oO4J6r1ZjUaP=Cv(1;Ip3IiWSC83WK-TXUJ68S>605g z&?lj9EjGvAj##`#Cz@RukQ8T;m)JPyh8#H!Ls;;N6|m2}Dl1Yx*1h-L(WU+kk6kOd z3v<`rA&!=#D-#EMMIn^>f$M8mAy~FVWv}H+> sS|rmDzZQ8X=yNn>pilh+mLU8zm}8x_I$4zW@LL diff --git a/tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/test_hub.cpython-311-pytest-9.1.1.pyc deleted file mode 100644 index 44c3988436705567db18bafdbb7609de2955ef00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3902 zcmcIm&2Jk;6rWkI9ovcX*|cfWHYv1e-QuKz7D1bS1p%c2CshH7NXXjkPVG(X-PP30ynB{kmL+)n9lMT;clS)h`kuPl-k}nkS1?{7Z$9Dp6997sJ#)i7ZCoM9Oy+ zqKnZ&Y%x}dFUBchk>&0H@Q0pY)%>r8G&xJP@UzH9h>$Iq{-t$2AsevV3O2}M;y`H8 zwrH^ywB7@u$yzikwUI8~g4cI2ytdMHx1jYO2(7)YJuPSh2SRJ_heQk7;K9&3SgyAP zZ|IHi`daXgX!Jfw_Wy&ClTL=bZpE_lx~e$3X?StfELf(&49E6*?BbHGa@`UqM@)+u zc20lbWVAISqvmyB@_N2r$lUpA_M`dqC!c-#Ws-VP-OzN-R7a(a&?H2LHOB6%Ss;)t z6#Oi~48fGY4}C|SChq0N`Z0M-Hz2>ZVEPy46G$IU*N!*jNo94%wCogYaK$ls?HC@V z)O^u)7*7=xL&>rNNLw2!6!VTA955rR8!Q>(nB}E0MO#%2m1%7rX#fTv=s#&}G_THXNwBSY&d=r73I8JAu3H-ir);5Z;0U+Bbo$lWH$HH%r~Kvz7C= z-E*^*i7W2-`_(RaI8r4**1y<^lkh-UzUaPl>qTl?UMR~8ZZKW}1o3*ox%U_5=RWq` z+uoyBffGH7KGo@=Jt?Jy`FR3hcps3uwfF!^aH^xmwq>p`eiWF7@S#gUgwC-u?&NLI zx*C#%lF`mea(r@+QW&BlOs8P+lynNp4G7S!Mv(d;xk=MWiDR65!$E-P4$E++s5xaR z&o-%u4I7`E@UKLd-W zBnw{o@wFbwl4sK1zEaPxP&yec4Oof;31sYM7RPQ?8taf=DTu;aY}KmgqZeLPAl+)+ zIhN0xsfQ-dYqf6GG#=>Ll*S5Xt)D$Yd?lZ0~FfLiIA7(%Y{+PAQsZ8vG`N@p{)V}wi-ONt7P+A0D zX}I2A&7v0cWGD%sCR(b|H|z2@eRbHakvT3Rvkhw|$CSL2GL5_A8EXySJmp6g zd=X7?_K-t`_F|1KuPfM#!^3q!y@{^x{T zQ22`1Z7EzSFo)|UCOWxjZK4@)EL|LPl4HPfbhl7{lL&Q0FR4Xm3Z_=fvl|@SKgPaY zgzt`Oh*DZX3w8J<*2S+v?zx>uHA1B5ld(VP$RBj1B9B(WL#}+Jk{GCV5$X6#B1_W0 zNUHG&y+^AA>C5hYyMy~NhF$q+JB+ZvK-!EUcKi2Z47tN+U3sz{j6|5tj zwAZDFE`Tmbn<2#Rsr?w8bcqWLq|F#&cXU5SC!13GImm&u8AI$wTQNG?gVO7Wa(yqz W_3B|TLUay9LE4P6yX&8cu>24Dxqm_c diff --git a/tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc b/tests/ci/__pycache__/test_standalone.cpython-311-pytest-9.1.1.pyc deleted file mode 100644 index 86d391f4e868aa69351826fb3f044aadf43a63f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3692 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 O0ubFN2