From bc587a4d72ef5be88d18783bf88cd080f09e2529 Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Wed, 2 Sep 2026 10:33:16 -0400 Subject: [PATCH 1/8] feat: add ramendr-ci pipeline support Co-authored-by: Cursor --- README.md | 23 +++ templates/pipelines/_pipeline-common.tpl | 109 ++++++++++- templates/pipelines/_pipeline-multi-dr.tpl | 161 ++++++++++++++++ templates/pipelines/_pipeline-provision.tpl | 2 + templates/pipelines/standard-pipelines.yaml | 7 + templates/tasks/import-spoke-cluster.yaml | 15 +- templates/tasks/interop-test.yaml | 7 +- templates/tasks/prepare-dr-secrets.yaml | 181 ++++++++++++++++++ templates/tasks/provision-cluster.yaml | 119 +++++++++++- .../tasks/validate-pattern-metadata.yaml | 2 +- templates/tasks/wait-hub-dr.yaml | 41 ++++ values.yaml | 42 ++++ 12 files changed, 697 insertions(+), 12 deletions(-) create mode 100644 templates/pipelines/_pipeline-multi-dr.tpl create mode 100644 templates/tasks/prepare-dr-secrets.yaml create mode 100644 templates/tasks/wait-hub-dr.yaml diff --git a/README.md b/README.md index 15c76bd..c2129d3 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,18 @@ This chart is used to serve as the template for Validated Patterns Charts | qeCIPipelines.defaults.networking.clusterNetwork[0].cidr | string | `"10.128.0.0/14"` | | | qeCIPipelines.defaults.networking.clusterNetwork[0].hostPrefix | int | `23` | | | qeCIPipelines.defaults.networking.machineNetwork[0].cidr | string | `"10.0.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.hub.clusterHostPrefix | int | `23` | | +| qeCIPipelines.defaults.networking.multiDr.hub.clusterNetworkCidr | string | `"10.128.0.0/14"` | | +| qeCIPipelines.defaults.networking.multiDr.hub.machineNetworkCidr | string | `"10.0.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.hub.serviceNetworkCidr | string | `"172.30.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterHostPrefix | int | `23` | | +| qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterNetworkCidr | string | `"10.132.0.0/14"` | | +| qeCIPipelines.defaults.networking.multiDr.spokePrimary.machineNetworkCidr | string | `"10.1.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.spokePrimary.serviceNetworkCidr | string | `"172.31.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterHostPrefix | int | `23` | | +| qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterNetworkCidr | string | `"10.136.0.0/14"` | | +| qeCIPipelines.defaults.networking.multiDr.spokeSecondary.machineNetworkCidr | string | `"10.2.0.0/16"` | | +| qeCIPipelines.defaults.networking.multiDr.spokeSecondary.serviceNetworkCidr | string | `"172.32.0.0/16"` | | | qeCIPipelines.defaults.networking.networkType | string | `"OVNKubernetes"` | | | qeCIPipelines.defaults.networking.serviceNetwork[0] | string | `"172.30.0.0/16"` | | | qeCIPipelines.defaults.ocp_versions[0] | string | `"4.20"` | | @@ -75,6 +87,17 @@ This chart is used to serve as the template for Validated Patterns Charts | qeCIPipelines.patterns.mcg.flavors.multi | string | `nil` | | | qeCIPipelines.patterns.mcg.flavors.single.clusterGroup | string | `"standalone"` | | | qeCIPipelines.patterns.mcg.repo | string | `"https://github.com/validatedpatterns/multicloud-gitops.git"` | | +| qeCIPipelines.patterns.ramen.awsCredsSecret | string | `"qe-ci-aws-creds"` | | +| qeCIPipelines.patterns.ramen.flavors.multi-dr.clusterGroup | string | `"odf"` | | +| qeCIPipelines.patterns.ramen.ocp_versions[0] | string | `"4.20"` | | +| qeCIPipelines.patterns.ramen.ocp_versions[1] | string | `"4.22"` | | +| qeCIPipelines.patterns.ramen.platforms.aws.spokePrimaryRegion | string | `"us-west-1"` | | +| qeCIPipelines.patterns.ramen.platforms.aws.spokeSecondaryRegion | string | `"us-east-2"` | | +| qeCIPipelines.patterns.ramen.repo | string | `"https://github.com/validatedpatterns/ramendr-starter-kit.git"` | | +| qeCIPipelines.patterns.ramen.revision | string | `"main"` | | +| qeCIPipelines.patterns.ramen.secrets[0] | string | `"ramendr-secret-values-file"` | | +| qeCIPipelines.patterns.ramen.spokePrimaryAcmName | string | `"ocp-primary"` | | +| qeCIPipelines.patterns.ramen.spokeSecondaryAcmName | string | `"ocp-secondary"` | | | qeCIPipelines.scheduleDefaults.concurrencyPolicy | string | `"Forbid"` | | | qeCIPipelines.scheduleDefaults.failedJobsHistoryLimit | int | `3` | | | qeCIPipelines.scheduleDefaults.finallyTimeout | string | `"30m"` | | diff --git a/templates/pipelines/_pipeline-common.tpl b/templates/pipelines/_pipeline-common.tpl index 45f6533..b43c7ae 100644 --- a/templates/pipelines/_pipeline-common.tpl +++ b/templates/pipelines/_pipeline-common.tpl @@ -41,6 +41,35 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). {{- $patternSecrets = append $patternSecrets (dict "name" $name "workspace" $workspace "index" $i) -}} {{- end -}} {{- end -}} +{{- if eq .flavorName "multi-dr" }} +- name: prepare-dr-secrets + runAfter: + - provision-hub + - provision-spoke-primary + - provision-spoke-secondary + taskRef: + name: prepare-dr-secrets + params: + - name: spoke-primary-cluster-name + value: $(tasks.provision-spoke-primary.results.cluster-name) + - name: spoke-secondary-cluster-name + value: $(tasks.provision-spoke-secondary.results.cluster-name) + - name: aws-creds-secret-name + value: {{ default "" .app.awsCredsSecret | quote }} + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: output + workspace: shared-data + subPath: prepared-values-secret + {{- range $i, $secret := $patternSecrets }} + {{- if eq $i 0 }} + - name: base-values-secret + workspace: {{ $secret.workspace }} + {{- end }} + {{- end }} +{{- end }} - name: install-pattern onError: continue runAfter: @@ -49,6 +78,8 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). {{- else if eq .flavorName "multi" }} - provision-hub - provision-spoke + {{- else if eq .flavorName "multi-dr" }} + - prepare-dr-secrets {{- else }} - provision-hosted-cluster {{- end }} @@ -58,7 +89,7 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). {{- if eq .flavorName "single" }} - name: cluster-name value: $(tasks.provision-cluster.results.cluster-name) - {{- else if eq .flavorName "multi" }} + {{- else if or (eq .flavorName "multi") (eq .flavorName "multi-dr") }} - name: cluster-name value: $(tasks.provision-hub.results.cluster-name) {{- else }} @@ -74,10 +105,16 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - name: kubeconfig workspace: shared-data subPath: kubeconfig + {{- if eq .flavorName "multi-dr" }} + - name: values-secret-0 + workspace: shared-data + subPath: prepared-values-secret + {{- else }} {{- range $secret := $patternSecrets }} - name: values-secret-{{ $secret.index }} workspace: {{ $secret.workspace }} {{- end }} + {{- end }} {{- if eq .flavorName "multi" }} - name: import-spoke onError: continue @@ -99,12 +136,24 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - name: kubeconfig workspace: shared-data subPath: kubeconfig +{{- else if eq .flavorName "multi-dr" }} +- name: wait-hub-dr + onError: continue + runAfter: + - install-pattern + taskRef: + name: wait-hub-dr + params: + - name: install-status + value: $(tasks.install-pattern.results.outcome) {{- end }} - name: interop-test onError: continue runAfter: {{- if eq .flavorName "multi" }} - import-spoke + {{- else if eq .flavorName "multi-dr" }} + - wait-hub-dr {{- else }} - install-pattern {{- end }} @@ -119,6 +168,13 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). value: $(tasks.provision-hub.results.cluster-name) - name: spoke-cluster-name value: $(tasks.provision-spoke.results.cluster-name) + {{- else if eq .flavorName "multi-dr" }} + - name: hub-cluster-name + value: $(tasks.provision-hub.results.cluster-name) + - name: spoke-cluster-name + value: $(tasks.provision-spoke-primary.results.cluster-name) + - name: spoke-secondary-cluster-name + value: $(tasks.provision-spoke-secondary.results.cluster-name) {{- else }} - name: hub-cluster-name value: $(tasks.provision-hosted-cluster.results.cluster-name) @@ -128,6 +184,8 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - name: install-status {{- if eq .flavorName "multi" }} value: $(tasks.import-spoke.results.import-status) + {{- else if eq .flavorName "multi-dr" }} + value: $(tasks.wait-hub-dr.results.outcome) {{- else }} value: $(tasks.install-pattern.results.outcome) {{- end }} @@ -149,7 +207,7 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - name: cluster-name {{- if eq .flavorName "single" }} value: $(tasks.provision-cluster.results.cluster-name) - {{- else if eq .flavorName "multi" }} + {{- else if or (eq .flavorName "multi") (eq .flavorName "multi-dr") }} value: $(tasks.provision-hub.results.cluster-name) {{- else }} value: $(tasks.provision-hosted-cluster.results.cluster-name) @@ -179,12 +237,50 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - name: must-gather workspace: shared-data subPath: must-gather +{{- else if eq .flavorName "multi-dr" }} +- name: must-gather-spoke-primary + runAfter: + - interop-test + when: + - cel: "'$(tasks.install-pattern.results.outcome)' == 'failed' || '$(tasks.interop-test.results.outcome)' == 'failed'" + taskRef: + name: must-gather + params: + - name: cluster-name + value: $(tasks.provision-spoke-primary.results.cluster-name) + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: must-gather + workspace: shared-data + subPath: must-gather +- name: must-gather-spoke-secondary + runAfter: + - interop-test + when: + - cel: "'$(tasks.install-pattern.results.outcome)' == 'failed' || '$(tasks.interop-test.results.outcome)' == 'failed'" + taskRef: + name: must-gather + params: + - name: cluster-name + value: $(tasks.provision-spoke-secondary.results.cluster-name) + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: must-gather + workspace: shared-data + subPath: must-gather {{- end }} - name: upload-must-gather runAfter: - must-gather-hub {{- if eq .flavorName "multi" }} - must-gather-spoke +{{- else if eq .flavorName "multi-dr" }} + - must-gather-spoke-primary + - must-gather-spoke-secondary {{- end }} when: - input: $(tasks.must-gather-hub.results.outcome) @@ -194,6 +290,13 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). - input: $(tasks.must-gather-spoke.results.outcome) operator: in values: ["success"] +{{- else if eq .flavorName "multi-dr" }} + - input: $(tasks.must-gather-spoke-primary.results.outcome) + operator: in + values: ["success"] + - input: $(tasks.must-gather-spoke-secondary.results.outcome) + operator: in + values: ["success"] {{- end }} taskRef: name: upload-must-gather @@ -241,7 +344,7 @@ Shared finally tasks (not flavor-specific cleanup). - name: exact-ocp-version {{- if eq .flavorName "single" }} value: $(tasks.provision-cluster.results.exact-ocp-version) - {{- else if eq .flavorName "multi" }} + {{- else if or (eq .flavorName "multi") (eq .flavorName "multi-dr") }} value: $(tasks.provision-hub.results.exact-ocp-version) {{- else }} value: $(tasks.provision-hosted-cluster.results.exact-ocp-version) diff --git a/templates/pipelines/_pipeline-multi-dr.tpl b/templates/pipelines/_pipeline-multi-dr.tpl new file mode 100644 index 0000000..481b7ec --- /dev/null +++ b/templates/pipelines/_pipeline-multi-dr.tpl @@ -0,0 +1,161 @@ +{{/* +Multi-DR cluster flavor: hub + spoke-primary + spoke-secondary, provisioned in parallel. +Each cluster receives non-overlapping CIDRs to support Submariner and ODF multicluster replication. +*/}} +{{- define "pipelines.provision.multi-dr" -}} +{{- $hubParams := merge (deepCopy .) (dict + "clusterBaseName" (printf "%s" .patternName) + "clusterRole" "hub" + "namespace" (printf "%s" .pipelineNamespace) + ) -}} +{{- $spokePrimaryParams := merge (deepCopy .) (dict + "clusterBaseName" (printf "%s" .patternName) + "clusterRole" "pri" + "namespace" (printf "%s" .pipelineNamespace) + ) -}} +{{- $spokeSecondaryParams := merge (deepCopy .) (dict + "clusterBaseName" (printf "%s" .patternName) + "clusterRole" "sec" + "namespace" (printf "%s" .pipelineNamespace) + ) -}} +- name: provision-hub + runAfter: + - validate-pattern-metadata + retries: 3 + timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + taskRef: + name: provision-cluster + params: +{{ include "pipelines.provision.cluster.hive.params" $hubParams | nindent 4 }} + - name: ocp-version + value: {{ .ocpVersion | quote }} + - name: control-plane-config + value: $(tasks.validate-pattern-metadata.results.hub-control-plane[*]) + - name: compute-nodes-config + value: $(tasks.validate-pattern-metadata.results.hub-compute-nodes[*]) + - name: cluster-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.clusterNetworkCidr | quote }} + - name: cluster-host-prefix + value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.clusterHostPrefix | quote }} + - name: machine-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.machineNetworkCidr | quote }} + - name: service-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.serviceNetworkCidr | quote }} + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: install-config + workspace: shared-data + subPath: install-config/{{ $hubParams.clusterBaseName }}-hub +- name: provision-spoke-primary + runAfter: + - validate-pattern-metadata + retries: 3 + timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + taskRef: + name: provision-cluster + params: +{{ include "pipelines.provision.cluster.hive.params" $spokePrimaryParams | nindent 4 }} + - name: ocp-version + value: {{ .ocpVersion | quote }} + - name: control-plane-config + value: $(tasks.validate-pattern-metadata.results.spoke-control-plane[*]) + - name: compute-nodes-config + value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) + - name: cluster-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.clusterNetworkCidr | quote }} + - name: cluster-host-prefix + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.clusterHostPrefix | quote }} + - name: machine-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.machineNetworkCidr | quote }} + - name: service-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.serviceNetworkCidr | quote }} + - name: region + value: {{ default "" ((.app.platforms).aws).spokePrimaryRegion | quote }} + - name: skip-acm-auto-import + value: "true" + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: install-config + workspace: shared-data + subPath: install-config/{{ $spokePrimaryParams.clusterBaseName }}-spoke-primary +- name: provision-spoke-secondary + runAfter: + - validate-pattern-metadata + retries: 3 + timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + taskRef: + name: provision-cluster + params: +{{ include "pipelines.provision.cluster.hive.params" $spokeSecondaryParams | nindent 4 }} + - name: ocp-version + value: {{ .ocpVersion | quote }} + - name: control-plane-config + value: $(tasks.validate-pattern-metadata.results.spoke-control-plane[*]) + - name: compute-nodes-config + value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) + - name: cluster-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.clusterNetworkCidr | quote }} + - name: cluster-host-prefix + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.clusterHostPrefix | quote }} + - name: machine-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.machineNetworkCidr | quote }} + - name: service-network-cidr + value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.serviceNetworkCidr | quote }} + - name: region + value: {{ default "" ((.app.platforms).aws).spokeSecondaryRegion | quote }} + - name: skip-acm-auto-import + value: "true" + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig + - name: install-config + workspace: shared-data + subPath: install-config/{{ $spokeSecondaryParams.clusterBaseName }}-spoke-secondary +{{- end }} + +{{- define "pipelines.cleanup.multi-dr" -}} +- name: delete-spoke-secondary-if-succeeded + when: + - input: $(tasks.status) + operator: in + values: ["Completed"] + - input: "$(params.force-skip-cleanup)" + operator: in + values: ["false"] + taskRef: + name: delete-cluster + params: + - name: cluster-name + value: $(tasks.provision-spoke-secondary.results.cluster-name) +- name: delete-spoke-primary-if-succeeded + when: + - input: $(tasks.status) + operator: in + values: ["Completed"] + - input: "$(params.force-skip-cleanup)" + operator: in + values: ["false"] + taskRef: + name: delete-cluster + params: + - name: cluster-name + value: $(tasks.provision-spoke-primary.results.cluster-name) +- name: delete-hub-if-succeeded + when: + - input: $(tasks.status) + operator: in + values: ["Completed"] + - input: "$(params.force-skip-cleanup)" + operator: in + values: ["false"] + taskRef: + name: delete-cluster + params: + - name: cluster-name + value: $(tasks.provision-hub.results.cluster-name) +{{- end }} diff --git a/templates/pipelines/_pipeline-provision.tpl b/templates/pipelines/_pipeline-provision.tpl index fc99492..7c2e08d 100644 --- a/templates/pipelines/_pipeline-provision.tpl +++ b/templates/pipelines/_pipeline-provision.tpl @@ -17,5 +17,7 @@ value: $(context.pipeline.name) - name: pipelinerun-uid value: $(context.pipelineRun.uid) +- name: print-login-info + value: $(params.print-login-info) {{- end }} diff --git a/templates/pipelines/standard-pipelines.yaml b/templates/pipelines/standard-pipelines.yaml index 45ea1d5..5dbc1a4 100644 --- a/templates/pipelines/standard-pipelines.yaml +++ b/templates/pipelines/standard-pipelines.yaml @@ -64,6 +64,13 @@ spec: used so concurrent runs do not share a ClusterDeployment. Set to an existing postfix (e.g. srfbxw from a prior PipelineRun name) to reuse that cluster. default: "" + - name: print-login-info + type: string + description: > + When "true", print a ready-to-use oc login command and the kubeadmin password + to the provision task log after each cluster is provisioned. Useful for + manually accessing pipeline-provisioned clusters during debugging. + default: "false" workspaces: - name: shared-data {{- if $app.secrets }} diff --git a/templates/tasks/import-spoke-cluster.yaml b/templates/tasks/import-spoke-cluster.yaml index f7dc52a..cb4053a 100644 --- a/templates/tasks/import-spoke-cluster.yaml +++ b/templates/tasks/import-spoke-cluster.yaml @@ -16,7 +16,16 @@ spec: default: "" - name: spoke-cluster-name type: string - default: "success" + default: "" + - name: acm-cluster-name + type: string + default: "" + description: > + Desired ACM ManagedCluster name for the spoke (e.g. ocp-primary). + When set, the import playbook registers the cluster under this name + and adds the clusterset label so CA-bundle policies can select it. + When empty, the name is derived from the kubeconfig current-context + (unreliable for Hive-provisioned clusters). workspaces: - name: pattern-repo - name: kubeconfig @@ -28,8 +37,10 @@ spec: env: - name: VP_HUBCONFIG value: $(workspaces.kubeconfig.path)/$(params.hub-cluster-name)-kubeconfig - - name: VP_SPOKECONFIG + - name: VP_OCP_PRIMARY value: $(workspaces.kubeconfig.path)/$(params.spoke-cluster-name)-kubeconfig + - name: VP_ACM_CLUSTER_NAME + value: $(params.acm-cluster-name) script: | #!/usr/bin/env bash set -euo pipefail diff --git a/templates/tasks/interop-test.yaml b/templates/tasks/interop-test.yaml index 5e13dba..97dfcea 100644 --- a/templates/tasks/interop-test.yaml +++ b/templates/tasks/interop-test.yaml @@ -14,6 +14,9 @@ spec: - name: spoke-cluster-name type: string default: "" + - name: spoke-secondary-cluster-name + type: string + default: "" - name: install-status type: string default: "success" @@ -31,8 +34,10 @@ spec: env: - name: VP_HUBCONFIG value: $(workspaces.kubeconfig.path)/$(params.hub-cluster-name)-kubeconfig - - name: VP_SPOKECONFIG + - name: VP_OCP_PRIMARY value: $(workspaces.kubeconfig.path)/$(params.spoke-cluster-name)-kubeconfig + - name: VP_OCP_SECONDARY + value: $(workspaces.kubeconfig.path)/$(params.spoke-secondary-cluster-name)-kubeconfig - name: TARGET_CLUSTERGROUP value: $(params.target-clustergroup) script: | diff --git a/templates/tasks/prepare-dr-secrets.yaml b/templates/tasks/prepare-dr-secrets.yaml new file mode 100644 index 0000000..0dfb7bd --- /dev/null +++ b/templates/tasks/prepare-dr-secrets.yaml @@ -0,0 +1,181 @@ +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: prepare-dr-secrets + namespace: {{ .Values.pipelineNamespace }} + labels: + {{- include "pipelines.labels" . | nindent 4 }} +spec: + description: > + Prepare values-secret.yaml for a multi-DR pattern install by injecting + provisioned spoke kubeconfigs and AWS credentials from the pipeline cluster. + Writes a ready-to-use values-secret.yaml to the output workspace so that + install-pattern can consume it without needing any flavor-specific logic. + params: + - name: spoke-primary-cluster-name + type: string + description: Cluster name of the primary spoke (used to locate its kubeconfig in the kubeconfig workspace). + - name: spoke-secondary-cluster-name + type: string + description: Cluster name of the secondary spoke. + - name: aws-creds-secret-name + type: string + default: "" + description: > + Name of a Kubernetes Secret in the pipeline namespace that holds + aws_access_key_id and aws_secret_access_key. Leave empty to skip. + - name: spoke-primary-secret-name + type: string + default: "ocp-primary" + description: > + Vault secret key prefix for the primary spoke kubeconfig. + Must match clusterOverrides.primary.name in the pattern values. + - name: spoke-secondary-secret-name + type: string + default: "ocp-secondary" + description: > + Vault secret key prefix for the secondary spoke kubeconfig. + Must match clusterOverrides.secondary.name in the pattern values. + workspaces: + - name: kubeconfig + description: Workspace containing provisioned cluster kubeconfigs written by provision-cluster tasks. + - name: base-values-secret + optional: true + description: > + Optional workspace containing a user-supplied values-secret.yaml (e.g. SSH keys, + pull secrets, Vault config). Its contents are copied to the output workspace and + then augmented with DR-specific entries. + - name: output + description: > + Writable workspace where the fully prepared values-secret.yaml is written. + Bind this to shared-data with subPath prepared-values-secret in the pipeline + so install-pattern can mount it as values-secret-0. + steps: + - name: prepare-secrets + image: {{ .Values.pipelines.defaults.utilityContainerImg }} + script: | + #!/usr/bin/env bash + set -euo pipefail + + OUTPUT_DIR="$(workspaces.output.path)" + OUTPUT_FILE="${OUTPUT_DIR}/values-secret.yaml" + + # Start from the user-supplied base values-secret or create a minimal skeleton + if [ "$(workspaces.base-values-secret.bound)" == "true" ]; then + echo "Base workspace bound at: $(workspaces.base-values-secret.path)" + echo "Workspace contents (including symlinks):" + ls -la "$(workspaces.base-values-secret.path)/" || true + BASE_FILE=$(find -L "$(workspaces.base-values-secret.path)" -maxdepth 1 -name "*values-secret*" -type f 2>/dev/null | head -1 || true) + if [ -n "${BASE_FILE}" ]; then + echo "Using base values-secret: ${BASE_FILE}" + cp "${BASE_FILE}" "${OUTPUT_FILE}" + echo "Secrets found in base file:" + printf '%s\n' \ + 'import os, yaml' \ + 'vs = yaml.safe_load(open(os.environ["VS_PATH"])) or {}' \ + 'secrets = vs.get("secrets", [])' \ + 'names = ", ".join(s.get("name", "?") for s in secrets) or "(none)"' \ + 'print(f" {len(secrets)} secret(s): {names}")' \ + > /tmp/list_base_secrets.py + VS_PATH="${OUTPUT_FILE}" python3 /tmp/list_base_secrets.py 2>&1 || true + else + echo "Base workspace bound but no values-secret file found; creating skeleton" + printf 'version: "2.0"\nsecrets: []\n' > "${OUTPUT_FILE}" + fi + else + echo "No base values-secret workspace bound; creating skeleton" + printf 'version: "2.0"\nsecrets: []\n' > "${OUTPUT_FILE}" + fi + + # Inject provisioned spoke kubeconfigs + PRIMARY_KC="$(workspaces.kubeconfig.path)/$(params.spoke-primary-cluster-name)-kubeconfig" + SECONDARY_KC="$(workspaces.kubeconfig.path)/$(params.spoke-secondary-cluster-name)-kubeconfig" + PRIMARY_SECRET_NAME="$(params.spoke-primary-secret-name)_cluster_kubeconfig" + SECONDARY_SECRET_NAME="$(params.spoke-secondary-secret-name)_cluster_kubeconfig" + + if [ ! -f "${PRIMARY_KC}" ] || [ ! -f "${SECONDARY_KC}" ]; then + echo "ERROR: spoke kubeconfig(s) not found in kubeconfig workspace:" >&2 + echo " primary: ${PRIMARY_KC}" >&2 + echo " secondary: ${SECONDARY_KC}" >&2 + exit 1 + fi + + echo "Injecting spoke kubeconfigs into ${OUTPUT_FILE}" + printf '%s\n' \ + 'import os, yaml' \ + 'vs_path = os.environ["VS_PATH"]' \ + 'primary_kc_path = os.environ["PRIMARY_KC_PATH"]' \ + 'secondary_kc_path = os.environ["SECONDARY_KC_PATH"]' \ + 'primary_name = os.environ["PRIMARY_NAME"]' \ + 'secondary_name = os.environ["SECONDARY_NAME"]' \ + 'with open(vs_path) as f:' \ + ' vs = yaml.safe_load(f) or {}' \ + 'vs.setdefault("secrets", [])' \ + 'vs["secrets"] = [s for s in vs["secrets"] if s.get("name") not in (primary_name, secondary_name)]' \ + 'for name, kc_path in [(primary_name, primary_kc_path), (secondary_name, secondary_kc_path)]:' \ + ' with open(kc_path) as f:' \ + ' vs["secrets"].append({"name": name, "fields": [{"name": "kubeconfig", "value": f.read()}]})' \ + 'with open(vs_path, "w") as f:' \ + ' yaml.dump(vs, f, default_flow_style=False, allow_unicode=True)' \ + 'print("Injected:", primary_name, secondary_name)' \ + > /tmp/inject_kubeconfig.py + VS_PATH="${OUTPUT_FILE}" \ + PRIMARY_KC_PATH="${PRIMARY_KC}" \ + SECONDARY_KC_PATH="${SECONDARY_KC}" \ + PRIMARY_NAME="${PRIMARY_SECRET_NAME}" \ + SECONDARY_NAME="${SECONDARY_SECRET_NAME}" \ + python3 /tmp/inject_kubeconfig.py + + # Inject AWS credentials from the pipeline cluster. + # KUBECONFIG is not set here, so we use the in-cluster SA token explicitly + # to reach the pipeline cluster API (not the provisioned hub). + AWS_CREDS_SECRET="$(params.aws-creds-secret-name)" + if [ -n "${AWS_CREDS_SECRET}" ]; then + echo "Injecting AWS credentials from secret ${AWS_CREDS_SECRET}" + SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + SA_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + AWS_KEY_ID=$(oc get secret "${AWS_CREDS_SECRET}" -n "$(context.taskRun.namespace)" \ + --token="${SA_TOKEN}" --certificate-authority="${SA_CA}" --server="https://kubernetes.default.svc" \ + -o jsonpath='{.data.aws_access_key_id}' 2>/dev/null | base64 -d) || true + AWS_SECRET=$(oc get secret "${AWS_CREDS_SECRET}" -n "$(context.taskRun.namespace)" \ + --token="${SA_TOKEN}" --certificate-authority="${SA_CA}" --server="https://kubernetes.default.svc" \ + -o jsonpath='{.data.aws_secret_access_key}' 2>/dev/null | base64 -d) || true + if [ -z "${AWS_KEY_ID}" ] || [ -z "${AWS_SECRET}" ]; then + echo "WARNING: Could not read AWS credentials from secret ${AWS_CREDS_SECRET}, skipping injection" + else + printf '%s\n' \ + 'import os, yaml' \ + 'vs_path = os.environ["VS_PATH"]' \ + 'key_id = os.environ["AWS_KEY_ID"]' \ + 'secret = os.environ["AWS_SECRET"]' \ + 'with open(vs_path) as f:' \ + ' vs = yaml.safe_load(f) or {}' \ + 'vs.setdefault("secrets", [])' \ + 'vs["secrets"] = [s for s in vs["secrets"] if s.get("name") != "aws"]' \ + 'vs["secrets"].append({"name": "aws", "fields": [' \ + ' {"name": "aws_access_key_id", "value": key_id},' \ + ' {"name": "aws_secret_access_key", "value": secret},' \ + ']})' \ + 'with open(vs_path, "w") as f:' \ + ' yaml.dump(vs, f, default_flow_style=False, allow_unicode=True)' \ + 'print("Injected AWS credentials")' \ + > /tmp/inject_aws.py + VS_PATH="${OUTPUT_FILE}" \ + AWS_KEY_ID="${AWS_KEY_ID}" \ + AWS_SECRET="${AWS_SECRET}" \ + python3 /tmp/inject_aws.py + fi + fi + + echo "Final values-secret written to ${OUTPUT_FILE}:" + printf '%s\n' \ + 'import os, yaml' \ + 'vs = yaml.safe_load(open(os.environ["VS_PATH"])) or {}' \ + 'secrets = vs.get("secrets", [])' \ + 'print(f" {len(secrets)} secret(s) total:")' \ + 'for s in secrets:' \ + ' name = s.get("name", "?")' \ + ' fields = [fd.get("name", "?") for fd in s.get("fields", [])]' \ + ' print(f" - {name} (fields: {fields})")' \ + > /tmp/list_final_secrets.py + VS_PATH="${OUTPUT_FILE}" python3 /tmp/list_final_secrets.py 2>&1 || true diff --git a/templates/tasks/provision-cluster.yaml b/templates/tasks/provision-cluster.yaml index a4774d9..dfb3a77 100644 --- a/templates/tasks/provision-cluster.yaml +++ b/templates/tasks/provision-cluster.yaml @@ -70,6 +70,40 @@ spec: type: type: string default: "" + - name: cluster-network-cidr + type: string + default: "{{ (index .Values.pipelines.defaults.networking.clusterNetwork 0).cidr }}" + - name: cluster-host-prefix + type: string + default: "{{ (index .Values.pipelines.defaults.networking.clusterNetwork 0).hostPrefix }}" + - name: machine-network-cidr + type: string + default: "{{ (index .Values.pipelines.defaults.networking.machineNetwork 0).cidr }}" + - name: service-network-cidr + type: string + default: "{{ index .Values.pipelines.defaults.networking.serviceNetwork 0 }}" + - name: region + type: string + default: "" + description: > + AWS/GCP/Azure region override. When empty the platform default from + pipelines.defaults.platforms is used. Set to place a specific cluster + in a different region (e.g. spoke-primary in us-west-1). + - name: skip-acm-auto-import + type: string + default: "false" + description: > + When "true", omit the cluster.open-cluster-management.io/clusterset label + from the ClusterDeployment so Hive does NOT auto-create a ManagedCluster. + Use for patterns (like multi-DR) that manage their own ACM spoke import + through ArgoCD/ACM charts, to avoid conflicting ManagedCluster names. + - name: print-login-info + type: string + default: "false" + description: > + When "true", print a ready-to-use oc login command and the kubeadmin + password to the task log after the cluster is provisioned. Useful for + manual debugging of pipeline-provisioned clusters. results: - name: cluster-name @@ -84,6 +118,8 @@ spec: - name: kubeconfig - name: install-config description: Holds generated install-config.yaml and clusterdeployment.yaml + stepTemplate: + imagePullPolicy: IfNotPresent steps: - name: get-openshift-version ref: @@ -161,6 +197,11 @@ spec: exit 1 ;; esac + # Override region if explicitly specified + if [ -n "$(params.region)" ]; then + CLOUD_REGION="$(params.region)" + echo "Region overridden to: ${CLOUD_REGION}" + fi mkdir -p "$(workspaces.install-config.path)" @@ -204,13 +245,13 @@ spec: name: ${CLUSTER_NAME} networking: clusterNetwork: - - cidr: {{ (index .Values.qeCIPipelines.defaults.networking.clusterNetwork 0).cidr }} - hostPrefix: {{ (index .Values.qeCIPipelines.defaults.networking.clusterNetwork 0).hostPrefix }} + - cidr: $(params.cluster-network-cidr) + hostPrefix: $(params.cluster-host-prefix) machineNetwork: - - cidr: {{ (index .Values.qeCIPipelines.defaults.networking.machineNetwork 0).cidr }} + - cidr: $(params.machine-network-cidr) networkType: {{ .Values.qeCIPipelines.defaults.networking.networkType }} serviceNetwork: - - {{ index .Values.qeCIPipelines.defaults.networking.serviceNetwork 0 }} + - $(params.service-network-cidr) pullSecret: "" platform: EOF @@ -292,6 +333,10 @@ spec: exit 1 ;; esac + # Override region if explicitly specified + if [ -n "$(params.region)" ]; then + CLOUD_REGION="$(params.region)" + fi case "${PLATFORM}" in aws) CLOUD_LABEL="AWS" ;; @@ -299,6 +344,18 @@ spec: azure) CLOUD_LABEL="Azure" ;; esac + SKIP_ACM_IMPORT="$(params.skip-acm-auto-import)" + + # Build the clusterset label line conditionally. + # Omit it when skip-acm-auto-import=true so Hive does NOT auto-create a + # ManagedCluster; the pattern's own ACM chart will then import the cluster + # under the correct name (e.g. ocp-primary / ocp-secondary). + if [ "${SKIP_ACM_IMPORT}" != "true" ]; then + CLUSTERSET_LABEL=" cluster.open-cluster-management.io/clusterset: default" + else + CLUSTERSET_LABEL="" + fi + cat > "${OUT}" </dev/null || echo "") + + CONSOLE_URL=$(oc get clusterdeployment "${CLUSTER_NAME}" \ + -n "${NAMESPACE}" \ + -o jsonpath='{.status.webConsoleURL}' 2>/dev/null || echo "") + + PASSWORD_SECRET=$(oc get clusterdeployment "${CLUSTER_NAME}" \ + -n "${NAMESPACE}" \ + -o jsonpath='{.spec.clusterMetadata.adminPasswordSecretRef.name}' 2>/dev/null || echo "") + + PASSWORD="" + if [ -n "${PASSWORD_SECRET}" ]; then + PASSWORD=$(oc get secret "${PASSWORD_SECRET}" \ + -n "${NAMESPACE}" \ + -o jsonpath='{.data.password}' | base64 -d 2>/dev/null || echo "") + fi + + echo "" + echo "╔══════════════════════════════════════════════════╗" + echo "║ CLUSTER LOGIN INFORMATION ║" + echo "╠══════════════════════════════════════════════════╣" + echo " Cluster: ${CLUSTER_NAME}" + echo " Console: ${CONSOLE_URL}" + echo "" + if [ -n "${API_URL}" ] && [ -n "${PASSWORD}" ]; then + echo " CLI login command:" + echo " oc login ${API_URL} -u kubeadmin -p ${PASSWORD}" + echo "" + echo " Console password: ${PASSWORD}" + else + echo " WARNING: could not retrieve API URL or password." + echo " API URL: ${API_URL:-}" + fi + echo "╚══════════════════════════════════════════════════╝" + echo "" diff --git a/templates/tasks/validate-pattern-metadata.yaml b/templates/tasks/validate-pattern-metadata.yaml index 1e59763..12202be 100644 --- a/templates/tasks/validate-pattern-metadata.yaml +++ b/templates/tasks/validate-pattern-metadata.yaml @@ -139,7 +139,7 @@ spec: fi fi - if [ "${FLAVOR}" = "multi" ]; then + if [ "${FLAVOR}" = "multi" ] || [ "${FLAVOR}" = "multi-dr" ]; then SPOKE_SUPPORT=$(yq '.extra_features.spoke_support // false' "${PATTERN_METADATA}") if [ "${SPOKE_SUPPORT}" = "false" ]; then diff --git a/templates/tasks/wait-hub-dr.yaml b/templates/tasks/wait-hub-dr.yaml new file mode 100644 index 0000000..56cedc0 --- /dev/null +++ b/templates/tasks/wait-hub-dr.yaml @@ -0,0 +1,41 @@ +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: wait-hub-dr + namespace: {{ .Values.pipelineNamespace }} + labels: + {{- include "pipelines.labels" . | nindent 4 }} +spec: + description: > + Give hub-side DR resources (DRPolicy, DRCluster, DRPC) time to converge + after pattern install. Sleeps for 10 minutes, which is the expected + convergence window; if resources are not healthy by then the pipeline + should be considered failed. + params: + - name: install-status + type: string + default: "success" + description: Outcome of the install-pattern task. Skips the wait if not "success". + results: + - name: outcome + steps: + - name: wait-dr-resources + image: {{ .Values.pipelines.defaults.utilityContainerImg }} + script: | + #!/usr/bin/env bash + set -euo pipefail + + if [ "$(params.install-status)" != "success" ]; then + echo "Skipping DR wait: install-pattern did not succeed (status=$(params.install-status))" + echo -n "skipped" > $(results.outcome.path) + exit 0 + fi + + echo "Sleeping 10 minutes to allow DR resources to converge..." + for i in $(seq 1 10); do + echo "[${i}/10 min elapsed] waiting..." + sleep 60 + done + + echo "Wait complete — DR resources should be ready for tests" + echo -n "success" > $(results.outcome.path) diff --git a/values.yaml b/values.yaml index dcb724e..68d0013 100644 --- a/values.yaml +++ b/values.yaml @@ -90,6 +90,22 @@ qeCIPipelines: - cidr: 10.0.0.0/16 serviceNetwork: - 172.30.0.0/16 + multiDr: + hub: + clusterNetworkCidr: 10.128.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.0.0.0/16 + serviceNetworkCidr: 172.30.0.0/16 + spokePrimary: + clusterNetworkCidr: 10.132.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.1.0.0/16 + serviceNetworkCidr: 172.31.0.0/16 + spokeSecondary: + clusterNetworkCidr: 10.136.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.2.0.0/16 + serviceNetworkCidr: 172.32.0.0/16 mustGatherImg: quay.io/validatedpatterns/must-gather utilityContainerImg: quay.io/validatedpatterns/utility-container # Tekton per-task timeout for provision-* pipeline tasks (oc wait on clusterdeployment/hostedcluster). @@ -120,6 +136,32 @@ qeCIPipelines: - aeg-aap-manifest-file # oc create secret generic aeg-aap-manifest-file -n vp-qe-ci --from-file=aap_manifest.zip=/home/aeros/Downloads/manifest_vp-aeros_20260709T084257Z.zip - aeg-aap-ssh-file # oc create secret generic aeg-aap-ssh-file -n vp-qe-ci --from-file=id_rsa=aeg-qe-ssh --from-file=id_rsa.pub=aeg-qe-ssh.pub + ramen: + repo: https://github.com/validatedpatterns/ramendr-starter-kit.git + revision: main + ocp_versions: + - "4.20" + - "4.22" + awsCredsSecret: qe-ci-aws-creds + # ACM ManagedCluster names for the two DR spokes. + # These MUST match clusterOverrides.primary.name / secondary.name in + # the pattern's values-cluster-names.yaml so that ACM policies (CA bundle, + # DRCluster placement) select the right clusters. + spokePrimaryAcmName: ocp-primary + spokeSecondaryAcmName: ocp-secondary + platforms: + aws: + # Per-cluster region overrides for the multi-dr flavor. + # Hub uses the default region (us-east-1). + # Spokes must be in different regions for meaningful DR. + spokePrimaryRegion: us-west-1 + spokeSecondaryRegion: us-east-2 + flavors: + multi-dr: + clusterGroup: odf + secrets: + - ramendr-secret-values-file + # industrial-edge: # repo: https://github.com/validatedpatterns/industrial-edge.git # flavors: From f3f30100b0fb679e00b6f55a0fc184d14a98e96f Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Wed, 2 Sep 2026 10:52:16 -0400 Subject: [PATCH 2/8] security: remove print-login-info parameter Printing kubeadmin credentials to pipeline logs is a security flaw. Remove the print-login-info param, its pipeline declaration, and the task step that outputs the oc login command and plaintext password. Co-authored-by: Cursor --- templates/pipelines/_pipeline-provision.tpl | 2 - templates/pipelines/standard-pipelines.yaml | 7 --- templates/tasks/provision-cluster.yaml | 59 --------------------- 3 files changed, 68 deletions(-) diff --git a/templates/pipelines/_pipeline-provision.tpl b/templates/pipelines/_pipeline-provision.tpl index 7c2e08d..fc99492 100644 --- a/templates/pipelines/_pipeline-provision.tpl +++ b/templates/pipelines/_pipeline-provision.tpl @@ -17,7 +17,5 @@ value: $(context.pipeline.name) - name: pipelinerun-uid value: $(context.pipelineRun.uid) -- name: print-login-info - value: $(params.print-login-info) {{- end }} diff --git a/templates/pipelines/standard-pipelines.yaml b/templates/pipelines/standard-pipelines.yaml index 5dbc1a4..45ea1d5 100644 --- a/templates/pipelines/standard-pipelines.yaml +++ b/templates/pipelines/standard-pipelines.yaml @@ -64,13 +64,6 @@ spec: used so concurrent runs do not share a ClusterDeployment. Set to an existing postfix (e.g. srfbxw from a prior PipelineRun name) to reuse that cluster. default: "" - - name: print-login-info - type: string - description: > - When "true", print a ready-to-use oc login command and the kubeadmin password - to the provision task log after each cluster is provisioned. Useful for - manually accessing pipeline-provisioned clusters during debugging. - default: "false" workspaces: - name: shared-data {{- if $app.secrets }} diff --git a/templates/tasks/provision-cluster.yaml b/templates/tasks/provision-cluster.yaml index dfb3a77..3c2a268 100644 --- a/templates/tasks/provision-cluster.yaml +++ b/templates/tasks/provision-cluster.yaml @@ -97,13 +97,6 @@ spec: from the ClusterDeployment so Hive does NOT auto-create a ManagedCluster. Use for patterns (like multi-DR) that manage their own ACM spoke import through ArgoCD/ACM charts, to avoid conflicting ManagedCluster names. - - name: print-login-info - type: string - default: "false" - description: > - When "true", print a ready-to-use oc login command and the kubeadmin - password to the task log after the cluster is provisioned. Useful for - manual debugging of pipeline-provisioned clusters. results: - name: cluster-name @@ -495,55 +488,3 @@ spec: echo "Console: ${CONSOLE_URL}" echo "Kubeconfig: $(workspaces.kubeconfig.path)/${CLUSTER_NAME}-kubeconfig" echo "============================================" - - - name: print-login-info - image: {{ .Values.toolsImage }} - script: | - #!/bin/bash - set -euo pipefail - - if [ "$(params.print-login-info)" != "true" ]; then - echo "print-login-info not enabled, skipping." - exit 0 - fi - - CLUSTER_NAME=$(cat $(results.cluster-name.path)) - NAMESPACE="$(params.namespace)" - - API_URL=$(oc get clusterdeployment "${CLUSTER_NAME}" \ - -n "${NAMESPACE}" \ - -o jsonpath='{.status.apiURL}' 2>/dev/null || echo "") - - CONSOLE_URL=$(oc get clusterdeployment "${CLUSTER_NAME}" \ - -n "${NAMESPACE}" \ - -o jsonpath='{.status.webConsoleURL}' 2>/dev/null || echo "") - - PASSWORD_SECRET=$(oc get clusterdeployment "${CLUSTER_NAME}" \ - -n "${NAMESPACE}" \ - -o jsonpath='{.spec.clusterMetadata.adminPasswordSecretRef.name}' 2>/dev/null || echo "") - - PASSWORD="" - if [ -n "${PASSWORD_SECRET}" ]; then - PASSWORD=$(oc get secret "${PASSWORD_SECRET}" \ - -n "${NAMESPACE}" \ - -o jsonpath='{.data.password}' | base64 -d 2>/dev/null || echo "") - fi - - echo "" - echo "╔══════════════════════════════════════════════════╗" - echo "║ CLUSTER LOGIN INFORMATION ║" - echo "╠══════════════════════════════════════════════════╣" - echo " Cluster: ${CLUSTER_NAME}" - echo " Console: ${CONSOLE_URL}" - echo "" - if [ -n "${API_URL}" ] && [ -n "${PASSWORD}" ]; then - echo " CLI login command:" - echo " oc login ${API_URL} -u kubeadmin -p ${PASSWORD}" - echo "" - echo " Console password: ${PASSWORD}" - else - echo " WARNING: could not retrieve API URL or password." - echo " API URL: ${API_URL:-}" - fi - echo "╚══════════════════════════════════════════════════╝" - echo "" From cfb188af0411a343fa541c848d9a01a6736cb08a Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Wed, 2 Sep 2026 11:18:35 -0400 Subject: [PATCH 3/8] fix: correct Values path prefix from pipelines to qeCIPipelines All template references were using .Values.pipelines.* but the correct path is .Values.qeCIPipelines.*. Also fix template define names in _pipeline-multi-dr.tpl to use the qeCIPipelines namespace expected by standard-pipelines.yaml. Co-authored-by: Cursor --- templates/pipelines/_pipeline-multi-dr.tpl | 40 +++++++++++----------- templates/tasks/prepare-dr-secrets.yaml | 2 +- templates/tasks/provision-cluster.yaml | 8 ++--- templates/tasks/wait-hub-dr.yaml | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/templates/pipelines/_pipeline-multi-dr.tpl b/templates/pipelines/_pipeline-multi-dr.tpl index 481b7ec..c9ac49a 100644 --- a/templates/pipelines/_pipeline-multi-dr.tpl +++ b/templates/pipelines/_pipeline-multi-dr.tpl @@ -2,7 +2,7 @@ Multi-DR cluster flavor: hub + spoke-primary + spoke-secondary, provisioned in parallel. Each cluster receives non-overlapping CIDRs to support Submariner and ODF multicluster replication. */}} -{{- define "pipelines.provision.multi-dr" -}} +{{- define "qeCIPipelines.provision.multi-dr" -}} {{- $hubParams := merge (deepCopy .) (dict "clusterBaseName" (printf "%s" .patternName) "clusterRole" "hub" @@ -22,11 +22,11 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic runAfter: - validate-pattern-metadata retries: 3 - timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + timeout: {{ default "2h" .root.Values.qeCIPipelines.defaults.provisionTaskTimeout | quote }} taskRef: name: provision-cluster params: -{{ include "pipelines.provision.cluster.hive.params" $hubParams | nindent 4 }} +{{ include "qeCIPipelines.provision.cluster.hive.params" $hubParams | nindent 4 }} - name: ocp-version value: {{ .ocpVersion | quote }} - name: control-plane-config @@ -34,13 +34,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.hub-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.clusterNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.clusterHostPrefix | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.machineNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.hub.serviceNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.serviceNetworkCidr | quote }} workspaces: - name: kubeconfig workspace: shared-data @@ -52,11 +52,11 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic runAfter: - validate-pattern-metadata retries: 3 - timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + timeout: {{ default "2h" .root.Values.qeCIPipelines.defaults.provisionTaskTimeout | quote }} taskRef: name: provision-cluster params: -{{ include "pipelines.provision.cluster.hive.params" $spokePrimaryParams | nindent 4 }} +{{ include "qeCIPipelines.provision.cluster.hive.params" $spokePrimaryParams | nindent 4 }} - name: ocp-version value: {{ .ocpVersion | quote }} - name: control-plane-config @@ -64,13 +64,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.clusterNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.clusterHostPrefix | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.machineNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokePrimary.serviceNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.serviceNetworkCidr | quote }} - name: region value: {{ default "" ((.app.platforms).aws).spokePrimaryRegion | quote }} - name: skip-acm-auto-import @@ -86,11 +86,11 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic runAfter: - validate-pattern-metadata retries: 3 - timeout: {{ default "2h" .root.Values.pipelines.defaults.provisionTaskTimeout | quote }} + timeout: {{ default "2h" .root.Values.qeCIPipelines.defaults.provisionTaskTimeout | quote }} taskRef: name: provision-cluster params: -{{ include "pipelines.provision.cluster.hive.params" $spokeSecondaryParams | nindent 4 }} +{{ include "qeCIPipelines.provision.cluster.hive.params" $spokeSecondaryParams | nindent 4 }} - name: ocp-version value: {{ .ocpVersion | quote }} - name: control-plane-config @@ -98,13 +98,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.clusterNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.clusterHostPrefix | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.machineNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.pipelines.defaults.networking.multiDr.spokeSecondary.serviceNetworkCidr | quote }} + value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.serviceNetworkCidr | quote }} - name: region value: {{ default "" ((.app.platforms).aws).spokeSecondaryRegion | quote }} - name: skip-acm-auto-import @@ -118,7 +118,7 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic subPath: install-config/{{ $spokeSecondaryParams.clusterBaseName }}-spoke-secondary {{- end }} -{{- define "pipelines.cleanup.multi-dr" -}} +{{- define "qeCIPipelines.cleanup.multi-dr" -}} - name: delete-spoke-secondary-if-succeeded when: - input: $(tasks.status) diff --git a/templates/tasks/prepare-dr-secrets.yaml b/templates/tasks/prepare-dr-secrets.yaml index 0dfb7bd..a5fcf4a 100644 --- a/templates/tasks/prepare-dr-secrets.yaml +++ b/templates/tasks/prepare-dr-secrets.yaml @@ -52,7 +52,7 @@ spec: so install-pattern can mount it as values-secret-0. steps: - name: prepare-secrets - image: {{ .Values.pipelines.defaults.utilityContainerImg }} + image: {{ .Values.qeCIPipelines.defaults.utilityContainerImg }} script: | #!/usr/bin/env bash set -euo pipefail diff --git a/templates/tasks/provision-cluster.yaml b/templates/tasks/provision-cluster.yaml index 3c2a268..4fab205 100644 --- a/templates/tasks/provision-cluster.yaml +++ b/templates/tasks/provision-cluster.yaml @@ -72,16 +72,16 @@ spec: default: "" - name: cluster-network-cidr type: string - default: "{{ (index .Values.pipelines.defaults.networking.clusterNetwork 0).cidr }}" + default: "{{ (index .Values.qeCIPipelines.defaults.networking.clusterNetwork 0).cidr }}" - name: cluster-host-prefix type: string - default: "{{ (index .Values.pipelines.defaults.networking.clusterNetwork 0).hostPrefix }}" + default: "{{ (index .Values.qeCIPipelines.defaults.networking.clusterNetwork 0).hostPrefix }}" - name: machine-network-cidr type: string - default: "{{ (index .Values.pipelines.defaults.networking.machineNetwork 0).cidr }}" + default: "{{ (index .Values.qeCIPipelines.defaults.networking.machineNetwork 0).cidr }}" - name: service-network-cidr type: string - default: "{{ index .Values.pipelines.defaults.networking.serviceNetwork 0 }}" + default: "{{ index .Values.qeCIPipelines.defaults.networking.serviceNetwork 0 }}" - name: region type: string default: "" diff --git a/templates/tasks/wait-hub-dr.yaml b/templates/tasks/wait-hub-dr.yaml index 56cedc0..f22a554 100644 --- a/templates/tasks/wait-hub-dr.yaml +++ b/templates/tasks/wait-hub-dr.yaml @@ -20,7 +20,7 @@ spec: - name: outcome steps: - name: wait-dr-resources - image: {{ .Values.pipelines.defaults.utilityContainerImg }} + image: {{ .Values.qeCIPipelines.defaults.utilityContainerImg }} script: | #!/usr/bin/env bash set -euo pipefail From 3801255e857854a3ea0af244b67660406cbf7eee Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Thu, 3 Sep 2026 13:16:20 -0400 Subject: [PATCH 4/8] refactor: hardcode aws credentialName in prepare-dr-secrets Replace the aws-creds-secret-name param with the Helm-rendered qeCIPipelines.defaults.platforms.aws.credentialName value, consistent with how provision-cluster.yaml references it. Removes the param declaration from the task, the passthrough in _pipeline-common.tpl, and the now-redundant awsCredsSecret key from the ramen pattern entry. Co-authored-by: Cursor --- templates/pipelines/_pipeline-common.tpl | 2 -- templates/tasks/prepare-dr-secrets.yaml | 8 +------- values.yaml | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/templates/pipelines/_pipeline-common.tpl b/templates/pipelines/_pipeline-common.tpl index b43c7ae..cad4332 100644 --- a/templates/pipelines/_pipeline-common.tpl +++ b/templates/pipelines/_pipeline-common.tpl @@ -54,8 +54,6 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). value: $(tasks.provision-spoke-primary.results.cluster-name) - name: spoke-secondary-cluster-name value: $(tasks.provision-spoke-secondary.results.cluster-name) - - name: aws-creds-secret-name - value: {{ default "" .app.awsCredsSecret | quote }} workspaces: - name: kubeconfig workspace: shared-data diff --git a/templates/tasks/prepare-dr-secrets.yaml b/templates/tasks/prepare-dr-secrets.yaml index a5fcf4a..0e853e1 100644 --- a/templates/tasks/prepare-dr-secrets.yaml +++ b/templates/tasks/prepare-dr-secrets.yaml @@ -18,12 +18,6 @@ spec: - name: spoke-secondary-cluster-name type: string description: Cluster name of the secondary spoke. - - name: aws-creds-secret-name - type: string - default: "" - description: > - Name of a Kubernetes Secret in the pipeline namespace that holds - aws_access_key_id and aws_secret_access_key. Leave empty to skip. - name: spoke-primary-secret-name type: string default: "ocp-primary" @@ -129,7 +123,7 @@ spec: # Inject AWS credentials from the pipeline cluster. # KUBECONFIG is not set here, so we use the in-cluster SA token explicitly # to reach the pipeline cluster API (not the provisioned hub). - AWS_CREDS_SECRET="$(params.aws-creds-secret-name)" + AWS_CREDS_SECRET="{{ .Values.qeCIPipelines.defaults.platforms.aws.credentialName }}" if [ -n "${AWS_CREDS_SECRET}" ]; then echo "Injecting AWS credentials from secret ${AWS_CREDS_SECRET}" SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) diff --git a/values.yaml b/values.yaml index 68d0013..e0d1559 100644 --- a/values.yaml +++ b/values.yaml @@ -142,7 +142,6 @@ qeCIPipelines: ocp_versions: - "4.20" - "4.22" - awsCredsSecret: qe-ci-aws-creds # ACM ManagedCluster names for the two DR spokes. # These MUST match clusterOverrides.primary.name / secondary.name in # the pattern's values-cluster-names.yaml so that ACM policies (CA bundle, From ee59e1903af864be94167e63489280f408babb6b Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Thu, 3 Sep 2026 13:42:50 -0400 Subject: [PATCH 5/8] refactor: move multiDr networking to pattern-level Add ramen.networking.{hub,spokePrimary,spokeSecondary} so each pattern can declare its own non-overlapping CIDRs. _pipeline-multi-dr.tpl now resolves networking as: app.networking.[role] merged Co-authored-by: Cursor refactor: remove multiDr networking from defaults, pattern-owned only Co-authored-by: Cursor --- templates/pipelines/_pipeline-multi-dr.tpl | 28 +++++++++-------- values.yaml | 35 ++++++++++++---------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/templates/pipelines/_pipeline-multi-dr.tpl b/templates/pipelines/_pipeline-multi-dr.tpl index c9ac49a..36b9521 100644 --- a/templates/pipelines/_pipeline-multi-dr.tpl +++ b/templates/pipelines/_pipeline-multi-dr.tpl @@ -18,6 +18,10 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic "clusterRole" "sec" "namespace" (printf "%s" .pipelineNamespace) ) -}} +{{- $patternNet := default dict .app.networking -}} +{{- $hubNet := default dict $patternNet.hub -}} +{{- $spokePrimaryNet := default dict $patternNet.spokePrimary -}} +{{- $spokeSecondaryNet := default dict $patternNet.spokeSecondary -}} - name: provision-hub runAfter: - validate-pattern-metadata @@ -34,13 +38,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.hub-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.clusterNetworkCidr | quote }} + value: {{ $hubNet.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.clusterHostPrefix | quote }} + value: {{ $hubNet.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.machineNetworkCidr | quote }} + value: {{ $hubNet.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.hub.serviceNetworkCidr | quote }} + value: {{ $hubNet.serviceNetworkCidr | quote }} workspaces: - name: kubeconfig workspace: shared-data @@ -64,13 +68,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterNetworkCidr | quote }} + value: {{ $spokePrimaryNet.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.clusterHostPrefix | quote }} + value: {{ $spokePrimaryNet.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.machineNetworkCidr | quote }} + value: {{ $spokePrimaryNet.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokePrimary.serviceNetworkCidr | quote }} + value: {{ $spokePrimaryNet.serviceNetworkCidr | quote }} - name: region value: {{ default "" ((.app.platforms).aws).spokePrimaryRegion | quote }} - name: skip-acm-auto-import @@ -98,13 +102,13 @@ Each cluster receives non-overlapping CIDRs to support Submariner and ODF multic - name: compute-nodes-config value: $(tasks.validate-pattern-metadata.results.spoke-compute-nodes[*]) - name: cluster-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterNetworkCidr | quote }} + value: {{ $spokeSecondaryNet.clusterNetworkCidr | quote }} - name: cluster-host-prefix - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.clusterHostPrefix | quote }} + value: {{ $spokeSecondaryNet.clusterHostPrefix | quote }} - name: machine-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.machineNetworkCidr | quote }} + value: {{ $spokeSecondaryNet.machineNetworkCidr | quote }} - name: service-network-cidr - value: {{ .root.Values.qeCIPipelines.defaults.networking.multiDr.spokeSecondary.serviceNetworkCidr | quote }} + value: {{ $spokeSecondaryNet.serviceNetworkCidr | quote }} - name: region value: {{ default "" ((.app.platforms).aws).spokeSecondaryRegion | quote }} - name: skip-acm-auto-import diff --git a/values.yaml b/values.yaml index e0d1559..08d98a1 100644 --- a/values.yaml +++ b/values.yaml @@ -90,22 +90,6 @@ qeCIPipelines: - cidr: 10.0.0.0/16 serviceNetwork: - 172.30.0.0/16 - multiDr: - hub: - clusterNetworkCidr: 10.128.0.0/14 - clusterHostPrefix: 23 - machineNetworkCidr: 10.0.0.0/16 - serviceNetworkCidr: 172.30.0.0/16 - spokePrimary: - clusterNetworkCidr: 10.132.0.0/14 - clusterHostPrefix: 23 - machineNetworkCidr: 10.1.0.0/16 - serviceNetworkCidr: 172.31.0.0/16 - spokeSecondary: - clusterNetworkCidr: 10.136.0.0/14 - clusterHostPrefix: 23 - machineNetworkCidr: 10.2.0.0/16 - serviceNetworkCidr: 172.32.0.0/16 mustGatherImg: quay.io/validatedpatterns/must-gather utilityContainerImg: quay.io/validatedpatterns/utility-container # Tekton per-task timeout for provision-* pipeline tasks (oc wait on clusterdeployment/hostedcluster). @@ -155,6 +139,25 @@ qeCIPipelines: # Spokes must be in different regions for meaningful DR. spokePrimaryRegion: us-west-1 spokeSecondaryRegion: us-east-2 + # Per-cluster networking CIDRs for the multi-dr flavor. + # Each role must use non-overlapping ranges for Submariner and ODF replication. + # Falls back to qeCIPipelines.defaults.networking.multiDr if unset. + networking: + hub: + clusterNetworkCidr: 10.128.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.0.0.0/16 + serviceNetworkCidr: 172.30.0.0/16 + spokePrimary: + clusterNetworkCidr: 10.132.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.1.0.0/16 + serviceNetworkCidr: 172.31.0.0/16 + spokeSecondary: + clusterNetworkCidr: 10.136.0.0/14 + clusterHostPrefix: 23 + machineNetworkCidr: 10.2.0.0/16 + serviceNetworkCidr: 172.32.0.0/16 flavors: multi-dr: clusterGroup: odf From a1adf0bcad5bba349c6f6c180688df3fab0fd52c Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Thu, 3 Sep 2026 14:16:13 -0400 Subject: [PATCH 6/8] feat: poll DRPC health in wait-hub-dr instead of blind sleep Replace the 10-minute sleep with an active poll against all DRPlacementControl resources on the hub cluster. Checks every 30s for up to 15 minutes that every DRPC has phase=Deployed and condition Available=True. Times out gracefully (outcome=timeout, exit 0) so interop-test still runs. Adds hub-cluster-name param and kubeconfig workspace to connect to the hub. Co-authored-by: Cursor --- templates/pipelines/_pipeline-common.tpl | 6 ++ templates/tasks/wait-hub-dr.yaml | 76 ++++++++++++++++++++---- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/templates/pipelines/_pipeline-common.tpl b/templates/pipelines/_pipeline-common.tpl index cad4332..d7edd09 100644 --- a/templates/pipelines/_pipeline-common.tpl +++ b/templates/pipelines/_pipeline-common.tpl @@ -144,6 +144,12 @@ Install, optional spoke import, tests, and diagnostics (after provisioning). params: - name: install-status value: $(tasks.install-pattern.results.outcome) + - name: hub-cluster-name + value: $(tasks.provision-hub.results.cluster-name) + workspaces: + - name: kubeconfig + workspace: shared-data + subPath: kubeconfig {{- end }} - name: interop-test onError: continue diff --git a/templates/tasks/wait-hub-dr.yaml b/templates/tasks/wait-hub-dr.yaml index f22a554..2eb1151 100644 --- a/templates/tasks/wait-hub-dr.yaml +++ b/templates/tasks/wait-hub-dr.yaml @@ -7,23 +7,32 @@ metadata: {{- include "pipelines.labels" . | nindent 4 }} spec: description: > - Give hub-side DR resources (DRPolicy, DRCluster, DRPC) time to converge - after pattern install. Sleeps for 10 minutes, which is the expected - convergence window; if resources are not healthy by then the pipeline - should be considered failed. + Poll hub-side DRPlacementControl resources until all are Deployed and Available, + or time out after 15 minutes. On timeout, logs a warning and continues — does not + fail the pipeline so that interop-test can still run and report partial results. params: - name: install-status type: string default: "success" description: Outcome of the install-pattern task. Skips the wait if not "success". + - name: hub-cluster-name + type: string + description: Name of the hub cluster; used to locate its kubeconfig in the kubeconfig workspace. + workspaces: + - name: kubeconfig + description: Workspace containing provisioned cluster kubeconfigs written by provision-cluster. results: - name: outcome steps: - - name: wait-dr-resources + - name: wait-drpc-healthy image: {{ .Values.qeCIPipelines.defaults.utilityContainerImg }} + env: + - name: KUBECONFIG + value: $(workspaces.kubeconfig.path)/$(params.hub-cluster-name)-kubeconfig script: | #!/usr/bin/env bash - set -euo pipefail + set -uo pipefail + unset KUBERNETES_SERVICE_HOST if [ "$(params.install-status)" != "success" ]; then echo "Skipping DR wait: install-pattern did not succeed (status=$(params.install-status))" @@ -31,11 +40,54 @@ spec: exit 0 fi - echo "Sleeping 10 minutes to allow DR resources to converge..." - for i in $(seq 1 10); do - echo "[${i}/10 min elapsed] waiting..." - sleep 60 + TIMEOUT_MINUTES=15 + POLL_INTERVAL=30 + MAX_ATTEMPTS=$(( TIMEOUT_MINUTES * 60 / POLL_INTERVAL )) + + printf '%s\n' \ + 'import subprocess, json, sys' \ + 'try:' \ + ' raw = subprocess.check_output(' \ + ' ["oc", "get", "drpc", "--all-namespaces", "-o", "json"],' \ + ' stderr=subprocess.STDOUT,' \ + ' )' \ + 'except subprocess.CalledProcessError as e:' \ + ' print(" oc get drpc failed:", e.output.decode().strip())' \ + ' sys.exit(1)' \ + 'data = json.loads(raw)' \ + 'items = data.get("items", [])' \ + 'if not items:' \ + ' print(" No DRPCs found yet, still waiting...")' \ + ' sys.exit(1)' \ + 'all_healthy = True' \ + 'for drpc in items:' \ + ' ns = drpc["metadata"]["namespace"]' \ + ' name = drpc["metadata"]["name"]' \ + ' phase = drpc.get("status", {}).get("phase", "")' \ + ' conds = drpc.get("status", {}).get("conditions", [])' \ + ' available = next((c["status"] for c in conds if c.get("type") == "Available"), "False")' \ + ' healthy = (phase == "Deployed" and available == "True")' \ + ' mark = "OK" if healthy else "WAIT"' \ + ' print(" [{}] {}/{}: phase={}, Available={}".format(mark, ns, name, phase, available))' \ + ' if not healthy:' \ + ' all_healthy = False' \ + 'sys.exit(0 if all_healthy else 1)' \ + > /tmp/check_drpc.py + + echo "Polling DRPlacementControl health (timeout: ${TIMEOUT_MINUTES}m, interval: ${POLL_INTERVAL}s)..." + for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo "[attempt ${attempt}/${MAX_ATTEMPTS} — $(date -u '+%H:%M:%S UTC')]" + if python3 /tmp/check_drpc.py; then + echo "All DRPCs are Deployed and Available." + echo -n "success" > $(results.outcome.path) + exit 0 + fi + if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then + sleep $POLL_INTERVAL + fi done - echo "Wait complete — DR resources should be ready for tests" - echo -n "success" > $(results.outcome.path) + echo "WARNING: timed out after ${TIMEOUT_MINUTES} minutes waiting for DRPCs to become healthy." + echo "Continuing to interop-test — it may report partial results." + echo -n "timeout" > $(results.outcome.path) + exit 0 From 4002383ac764cffa9b57857441c567740a637d59 Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Thu, 3 Sep 2026 14:24:43 -0400 Subject: [PATCH 7/8] fix: rename VP_OCP_PRIMARY/SECONDARY to VP_SPOKECONFIG/VP_SPOKECONFIG_SECONDARY Co-authored-by: Cursor --- templates/tasks/interop-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/tasks/interop-test.yaml b/templates/tasks/interop-test.yaml index 97dfcea..a72ba3d 100644 --- a/templates/tasks/interop-test.yaml +++ b/templates/tasks/interop-test.yaml @@ -34,9 +34,9 @@ spec: env: - name: VP_HUBCONFIG value: $(workspaces.kubeconfig.path)/$(params.hub-cluster-name)-kubeconfig - - name: VP_OCP_PRIMARY + - name: VP_SPOKECONFIG value: $(workspaces.kubeconfig.path)/$(params.spoke-cluster-name)-kubeconfig - - name: VP_OCP_SECONDARY + - name: VP_SPOKECONFIG_SECONDARY value: $(workspaces.kubeconfig.path)/$(params.spoke-secondary-cluster-name)-kubeconfig - name: TARGET_CLUSTERGROUP value: $(params.target-clustergroup) From 39ccc19e6e6be689a16328fef41378f5f64ca76c Mon Sep 17 00:00:00 2001 From: OAharoni-RedHat Date: Thu, 3 Sep 2026 16:23:44 -0400 Subject: [PATCH 8/8] remove changes to import-spoke-cluster --- templates/tasks/import-spoke-cluster.yaml | 15 ++------------- values.yaml | 6 ------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/templates/tasks/import-spoke-cluster.yaml b/templates/tasks/import-spoke-cluster.yaml index cb4053a..f7dc52a 100644 --- a/templates/tasks/import-spoke-cluster.yaml +++ b/templates/tasks/import-spoke-cluster.yaml @@ -16,16 +16,7 @@ spec: default: "" - name: spoke-cluster-name type: string - default: "" - - name: acm-cluster-name - type: string - default: "" - description: > - Desired ACM ManagedCluster name for the spoke (e.g. ocp-primary). - When set, the import playbook registers the cluster under this name - and adds the clusterset label so CA-bundle policies can select it. - When empty, the name is derived from the kubeconfig current-context - (unreliable for Hive-provisioned clusters). + default: "success" workspaces: - name: pattern-repo - name: kubeconfig @@ -37,10 +28,8 @@ spec: env: - name: VP_HUBCONFIG value: $(workspaces.kubeconfig.path)/$(params.hub-cluster-name)-kubeconfig - - name: VP_OCP_PRIMARY + - name: VP_SPOKECONFIG value: $(workspaces.kubeconfig.path)/$(params.spoke-cluster-name)-kubeconfig - - name: VP_ACM_CLUSTER_NAME - value: $(params.acm-cluster-name) script: | #!/usr/bin/env bash set -euo pipefail diff --git a/values.yaml b/values.yaml index 08d98a1..97f782e 100644 --- a/values.yaml +++ b/values.yaml @@ -126,12 +126,6 @@ qeCIPipelines: ocp_versions: - "4.20" - "4.22" - # ACM ManagedCluster names for the two DR spokes. - # These MUST match clusterOverrides.primary.name / secondary.name in - # the pattern's values-cluster-names.yaml so that ACM policies (CA bundle, - # DRCluster placement) select the right clusters. - spokePrimaryAcmName: ocp-primary - spokeSecondaryAcmName: ocp-secondary platforms: aws: # Per-cluster region overrides for the multi-dr flavor.