From 6f8e535194f554c2606635fd5804a18eda5e9ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Jeanneret?= Date: Fri, 17 Jul 2026 10:54:35 +0200 Subject: [PATCH 1/2] fix(approve-installplan): version-gate InstallPlan approval against pinned startingCSV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Job could auto-approve an InstallPlan for a newer CSV than what pin-version pins. OLM treats startingCSV as a floor, not a ceiling — if the catalog offers a higher version in the same channel, OLM resolves to that version and the Job blindly approves it, causing version drift. Read the Subscription's spec.startingCSV before polling. When set, only approve InstallPlans whose clusterServiceVersionNames list contains the pinned CSV; fail fast with a clear error when a wrong-version InstallPlan is found. When startingCSV is not set (no pin-version component), fall back to the original behavior. Co-Authored-By: Claude Opus 4.6 --- .../utilities/approve-installplan/README.md | 22 +++++++++ .../utilities/approve-installplan/job.yaml | 47 +++++++++++++++++-- .../utilities/approve-installplan/rbac.yaml | 7 +++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/components/utilities/approve-installplan/README.md b/components/utilities/approve-installplan/README.md index 2e7c660..6dfb17b 100644 --- a/components/utilities/approve-installplan/README.md +++ b/components/utilities/approve-installplan/README.md @@ -25,3 +25,25 @@ Images: `registry.redhat.io/openshift4/ose-tools-rhel9:latest` (Job). In-tree overlays may reference this component with a local path until a release tag that contains `components/utilities/approve-installplan` is published; use the HTTPS URL above for reproducible builds outside the repository. + +## Version-gated approval + +When the `openstack` Subscription has `spec.startingCSV` set (as done by the `pin-version` +component), the Job only approves InstallPlans whose `spec.clusterServiceVersionNames` contains +the pinned CSV. If OLM resolves to a different (typically newer) version, the Job fails immediately +with a clear error showing expected vs. actual CSV names. + +When `spec.startingCSV` is not set, the Job falls back to approving the first unapproved +InstallPlan it finds (original behavior). + +## Environment variables + +| Variable | Default | Description | +| -------- | ------- | ----------- | +| `OS_OPERATORS_NAMESPACE` | `openstack-operators` | Namespace where operators are installed | +| `OS_NAMESPACE` | `openstack` | Namespace where the OpenStack control plane lives | +| `SUBSCRIPTION_NAME` | `openstack` | OLM Subscription name to read `startingCSV` from | +| `RETRIES` | `30` | Number of polling attempts for most checks | +| `DELAY` | `10` | Seconds between retries | +| `INSTALL_PLAN_RETRIES` | `60` | Retries for InstallPlan completion (longer due to image pulls) | +| `DEBUG` | `true` | Enable bash debug tracing (`set -x`) | diff --git a/components/utilities/approve-installplan/job.yaml b/components/utilities/approve-installplan/job.yaml index e5348c4..2fe5332 100644 --- a/components/utilities/approve-installplan/job.yaml +++ b/components/utilities/approve-installplan/job.yaml @@ -43,6 +43,8 @@ spec: value: "60" - name: DEBUG value: "true" + - name: SUBSCRIPTION_NAME + value: "openstack" command: - /bin/bash - -c @@ -63,6 +65,8 @@ spec: INSTALL_PLAN_RETRIES=${INSTALL_PLAN_RETRIES:-60} # Delay in seconds between retries. DELAY=${DELAY:-10} + # Name of the OLM Subscription to read startingCSV from. + SUBSCRIPTION_NAME=${SUBSCRIPTION_NAME:-"openstack"} # --- log() { @@ -70,11 +74,46 @@ spec: } find_and_approve_installplan() { + local pinned_csv="" + pinned_csv=$(oc get subscription "${SUBSCRIPTION_NAME}" \ + -n "${OS_OPERATORS_NAMESPACE}" \ + -o jsonpath='{.spec.startingCSV}' 2>/dev/null) || true + + if [ -n "$pinned_csv" ]; then + log "Subscription '${SUBSCRIPTION_NAME}' pins startingCSV: ${pinned_csv}" + else + log "Subscription '${SUBSCRIPTION_NAME}' has no startingCSV; will approve any unapproved InstallPlan." + fi + log "Waiting for unapproved InstallPlan..." + local install_plan_name="" for i in $(seq 1 $RETRIES); do - install_plan_name=$(oc get installplan -n $OS_OPERATORS_NAMESPACE -o json | jq -r '.items[] | select(.spec.approval=="Manual" and .spec.approved==false) | .metadata.name' | head -n1) || true + if [ -n "$pinned_csv" ]; then + install_plan_name=$(oc get installplan -n "${OS_OPERATORS_NAMESPACE}" -o json | \ + jq -r --arg csv "$pinned_csv" \ + '.items[] | select(.spec.approval=="Manual" and .spec.approved==false and (.spec.clusterServiceVersionNames | index($csv))) | .metadata.name' | head -n1) || true + + if [ -z "$install_plan_name" ]; then + local wrong_plan="" + wrong_plan=$(oc get installplan -n "${OS_OPERATORS_NAMESPACE}" -o json | \ + jq -r '.items[] | select(.spec.approval=="Manual" and .spec.approved==false) | .metadata.name' | head -n1) || true + + if [ -n "$wrong_plan" ]; then + local actual_csvs="" + actual_csvs=$(oc get installplan "${wrong_plan}" -n "${OS_OPERATORS_NAMESPACE}" -o json | \ + jq -r '.spec.clusterServiceVersionNames | join(", ")') || true + log "ERROR: InstallPlan '${wrong_plan}' targets CSV [${actual_csvs}], not pinned '${pinned_csv}'." + log "ERROR: The catalog resolved a newer version than what is pinned. Refusing to approve." + exit 1 + fi + fi + else + install_plan_name=$(oc get installplan -n "${OS_OPERATORS_NAMESPACE}" -o json | \ + jq -r '.items[] | select(.spec.approval=="Manual" and .spec.approved==false) | .metadata.name' | head -n1) || true + fi + [ -n "$install_plan_name" ] && break - log "Attempt $i/$RETRIES: No InstallPlan found, retrying..." + log "Attempt $i/$RETRIES: No matching InstallPlan found, retrying..." sleep $DELAY done @@ -83,8 +122,8 @@ spec: exit 1 fi - log "Found InstallPlan: $install_plan_name, approving..." - oc patch installplan $install_plan_name -n $OS_OPERATORS_NAMESPACE --type merge -p '{"spec":{"approved":true}}' >&2 + log "Found InstallPlan: ${install_plan_name}, approving..." + oc patch installplan "${install_plan_name}" -n "${OS_OPERATORS_NAMESPACE}" --type merge -p '{"spec":{"approved":true}}' >&2 echo "$install_plan_name" } diff --git a/components/utilities/approve-installplan/rbac.yaml b/components/utilities/approve-installplan/rbac.yaml index 0e256b8..723fd75 100644 --- a/components/utilities/approve-installplan/rbac.yaml +++ b/components/utilities/approve-installplan/rbac.yaml @@ -14,6 +14,13 @@ rules: - get - list - patch + - apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - list - apiGroups: - operator.openstack.org resources: From 78985ffaa6d785c8cabf1d30f4c3e66c86e0e20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Jeanneret?= Date: Fri, 17 Jul 2026 13:04:01 +0200 Subject: [PATCH 2/2] fix(approve-installplan): use deployedVersion for initial-vs-update detection The previous check (`oc get openstack openstack`) races with the openstack-operator-cr ArgoCD app which creates the OpenStack CR before the Job reaches its detection logic. On initial install this causes the Job to enter wait_for_new_version, which times out because deployedVersion is not set until the full controlplane deploys. Check OpenStackVersion.status.deployedVersion instead: if absent, this is an initial install (wait_for_crd); if present, it is a genuine update (wait_for_new_version). Co-Authored-By: Claude Opus 4.6 --- components/utilities/approve-installplan/job.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/utilities/approve-installplan/job.yaml b/components/utilities/approve-installplan/job.yaml index 2fe5332..22e10e2 100644 --- a/components/utilities/approve-installplan/job.yaml +++ b/components/utilities/approve-installplan/job.yaml @@ -203,11 +203,12 @@ spec: wait_for_installplan_completion "$install_plan_name" log "Checking if this is an initial installation or an update..." - if oc get openstack openstack -n $OS_OPERATORS_NAMESPACE; then - log "OpenStack CR 'openstack' exists. This is an update." + deployed_version=$(oc get openstackversion -n $OS_NAMESPACE -o jsonpath='{.items[0].status.deployedVersion}' 2>/dev/null) || true + if [ -n "$deployed_version" ]; then + log "Deployed version $deployed_version found. This is an update." wait_for_new_version else - log "OpenStack CR 'openstack' does not exist. This is an initial installation." + log "No deployed version found. This is an initial installation." wait_for_crd fi