From 449e9aa2968883b341aa3d48a20eebbddc4597be Mon Sep 17 00:00:00 2001 From: Ian Tewksbury Date: Fri, 7 Jun 2024 16:25:12 -0400 Subject: [PATCH 1/3] operators-installer - update to oc 4.15 tools --- charts/operators-installer/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/operators-installer/values.yaml b/charts/operators-installer/values.yaml index f75f17a1..dbef1dbc 100644 --- a/charts/operators-installer/values.yaml +++ b/charts/operators-installer/values.yaml @@ -10,7 +10,7 @@ approveManualInstallPlanViaHook: true # Image to use for the InstallPlan Approver and Verify Jobs -installPlanApproverAndVerifyJobsImage: registry.redhat.io/openshift4/ose-cli:v4.10@sha256:7804ea66ea8ca0f414148b8b3b52ae454800785e80a32bd8a5eb2db789014a00 +installPlanApproverAndVerifyJobsImage: registry.redhat.io/openshift4/ose-cli:v4.15@sha256:3f2123f42ae7358e1fece41d461bf331f144480da8b7711b9a93aca150f33f3f # EXAMPLE: declaratively controlled operator version operators: From 296cbacf2db4558a615b101d3c94c713510311f6 Mon Sep 17 00:00:00 2001 From: Ian Tewksbury Date: Fri, 7 Jun 2024 16:25:51 -0400 Subject: [PATCH 2/3] operators-installer - refactor to use python instead of bash this is a pre-work to enahnacing to be able to deal with incremental upgrades --- .../_scripts/installplan-approver.py | 66 +++++++++++++++ .../_scripts/installplan-verifier.py | 67 +++++++++++++++ .../_scripts/installplan_utils.py | 37 +++++++++ ...onfigMap_installplan-approver-scripts.yaml | 26 ++++++ .../templates/Job_installplan-approver.yaml | 82 +++++-------------- .../Job_installplan-complete-verifier.yaml | 82 +++++-------------- 6 files changed, 234 insertions(+), 126 deletions(-) create mode 100644 charts/operators-installer/_scripts/installplan-approver.py create mode 100644 charts/operators-installer/_scripts/installplan-verifier.py create mode 100644 charts/operators-installer/_scripts/installplan_utils.py create mode 100644 charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml diff --git a/charts/operators-installer/_scripts/installplan-approver.py b/charts/operators-installer/_scripts/installplan-approver.py new file mode 100644 index 00000000..0cbc5030 --- /dev/null +++ b/charts/operators-installer/_scripts/installplan-approver.py @@ -0,0 +1,66 @@ +#!/usr/bin/python + +import os +import sys +import installplan_utils + +namespace_name = os.environ["NAMESPACE"] +subscription_name = os.environ["SUBSCRIPTION"] +subscription_csv = os.environ["SUBSCRIPTION_CSV"] + +print() +print("******************************") +print("* START InstallPlan Approver *") +print("******************************") + +# find the subscription uid +print() +print(f"Get Subscription ({subscription_name}) UID") +subscription_uid = installplan_utils.get_subscription_uid(subscription_name) +print(f"Subscription ({subscription_name}) UID: {subscription_uid}") + +# if found subscription uid find InstallPlan for given CSV with owner of the given subscription +# else error +if subscription_uid: + # find the InstallPlan that has expected owner subscription id and expected target CSV name + # NOTE: if more then one InstallPlan matches, choose the first one + print( + f"Get InstallPlan for CSV ({subscription_csv}) with Subscription (${subscription_uid}) owner" + ) + target_install_plan = installplan_utils.get_installplan( + namespace_name, subscription_csv, subscription_uid + ) + # if found target InstallPlan, approve it + # else fail + if target_install_plan: + target_install_plan_name = target_install_plan.model.metadata.name + print( + f"InstallPlan for CSV ({subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}): {target_install_plan.model.metadata.name}" + ) + print() + print( + f"Current InstallPlan ({target_install_plan_name}) approval state: {target_install_plan.model.spec.approved}" + ) + # if already approved, just notify + # else approve + if target_install_plan.model.spec.approved: + print(f"InstallPlan ({target_install_plan_name}) is already approved") + else: + print(f"Approving InstallPlan ({target_install_plan_name})") + target_install_plan.model.spec.approved = True + target_install_plan.apply() + print(f"Approved InstallPlan ({target_install_plan_name})") + sys.exit(0) + else: + print() + print( + f"Could not find InstallPlan for CSV ${subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}) owner." + + "\nThis can happen if InstallPlan isn't created yet. Try again." + ) + sys.exit(1) +else: + print() + print( + "Failed to get Subscription ({subscription_name}) UID. This really shouldn't happen." + ) + sys.exit(1) diff --git a/charts/operators-installer/_scripts/installplan-verifier.py b/charts/operators-installer/_scripts/installplan-verifier.py new file mode 100644 index 00000000..e8e68edf --- /dev/null +++ b/charts/operators-installer/_scripts/installplan-verifier.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +import os +import sys +import installplan_utils + +namespace_name = os.environ["NAMESPACE"] +subscription_name = os.environ["SUBSCRIPTION"] +subscription_csv = os.environ["SUBSCRIPTION_CSV"] + +print() +print("******************************") +print("* START InstallPlan Approver *") +print("******************************") + +# find the subscription uid +print() +print(f"Get Subscription ({subscription_name}) UID") +subscription_uid = installplan_utils.get_subscription_uid(subscription_name) +print(f"Subscription ({subscription_name}) UID: {subscription_uid}") + +# if found subscription uid find InstallPlan for given CSV with owner of the given subscription +# else error +if subscription_uid: + # find the InstallPlan that has expected owner subscription id and expected target CSV name + # NOTE: if more then one InstallPlan matches, choose the first one + print( + f"Get InstallPlan for CSV ({subscription_csv}) with Subscription (${subscription_uid}) owner" + ) + target_install_plan = installplan_utils.get_installplan( + namespace_name, subscription_csv, subscription_uid + ) + # if found target InstallPlan, approve it + # else fail + if target_install_plan: + target_install_plan_name = target_install_plan.model.metadata.name + target_install_plan_phase = target_install_plan.model.status.phase + print( + f"InstallPlan for CSV ({subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}): {target_install_plan.model.metadata.name}" + ) + print() + print( + f"Current InstallPlan ({target_install_plan_name}) phase: {target_install_plan_phase}" + ) + # if complete, exit success + # if not yet complete, exit failure + if target_install_plan_phase == "Complete": + print(f"InstallPlan ({target_install_plan_name}) complete") + sys.exit(0) + else: + print( + f"InstallPlan ({target_install_plan_name}) not yet complete: ${target_install_plan_phase}" + ) + sys.exit(1) + else: + print() + print( + f"Could not find InstallPlan for CSV ${subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}) owner." + + "\nThis can happen if InstallPlan isn't created yet. Try again." + ) + sys.exit(1) +else: + print() + print( + "Failed to get Subscription ({subscription_name}) UID. This really shouldn't happen." + ) + sys.exit(1) diff --git a/charts/operators-installer/_scripts/installplan_utils.py b/charts/operators-installer/_scripts/installplan_utils.py new file mode 100644 index 00000000..a75c204c --- /dev/null +++ b/charts/operators-installer/_scripts/installplan_utils.py @@ -0,0 +1,37 @@ +#!/usr/bin/python + +import openshift_client as oc + + +def get_subscription_uid(subscription_name: str): + subscriptions_selector = oc.selector( + [f"subscriptions.operators.coreos.com/{subscription_name}"] + ) + subscription_uid = None + if subscriptions_selector.objects(): + subscription_uid = subscriptions_selector.objects()[0].model.metadata.uid + return subscription_uid + + +def get_installplan( + namespace_name: str, + subscription_csv: str, + subscription_uid: str, +): + with oc.project(namespace_name): + # find the InstallPlan that has expected owner subscription id and expected target CSV name + # NOTE: if more then one InstallPlan matches, choose the first one + install_plans_selector = oc.selector(["installplan.operators.coreos.com"]) + target_install_plan = None + for install_plan in install_plans_selector.objects(): + owner_uids = map( + lambda owner_reference: owner_reference.uid, + install_plan.model.metadata.ownerReferences, + ) + if ( + subscription_csv in install_plan.model.spec.clusterServiceVersionNames + ) and (subscription_uid in owner_uids): + target_install_plan = install_plan + break + + return target_install_plan diff --git a/charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml b/charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml new file mode 100644 index 00000000..8cc6fd7a --- /dev/null +++ b/charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml @@ -0,0 +1,26 @@ +{{- range .Values.operators }} +{{- if eq .installPlanApproval "Manual" }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: installplan-approver-scripts + namespace: {{ .namespace | default $.Release.Namespace }} + labels: + {{- include "operators-installer.labels" $ | nindent 4 }} + annotations: + {{- if $.Values.approveManualInstallPlanViaHook }} + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "helm.sh/hook-weight": "10" + {{- else }} + argocd.argoproj.io/sync-wave: "-30" + {{- end }} +data: + installplan_utils.py: |- +{{ tpl ( $.Files.Get "_scripts/installplan_utils.py" ) . | indent 4 }} + installplan-approver.py: |- +{{ tpl ( $.Files.Get "_scripts/installplan-approver.py" ) . | indent 4 }} + installplan-verifier.py: |- +{{ tpl ( $.Files.Get "_scripts/installplan-verifier.py" ) . | indent 4 }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/operators-installer/templates/Job_installplan-approver.yaml b/charts/operators-installer/templates/Job_installplan-approver.yaml index 8b8a842f..e8df2cda 100644 --- a/charts/operators-installer/templates/Job_installplan-approver.yaml +++ b/charts/operators-installer/templates/Job_installplan-approver.yaml @@ -27,75 +27,31 @@ spec: containers: - name: installplan-approver image: {{ $.Values.installPlanApproverAndVerifyJobsImage }} - command: - - /bin/bash - - -c - - | - export HOME=/tmp/approver - - echo - echo "Get Subscription (${SUBSCRIPTION}) UID" - subscriptionUID=$(oc get subscriptions.operators.coreos.com --field-selector metadata.name=${SUBSCRIPTION} -o=jsonpath="{.items[0].metadata.uid}") - echo "Subscription (${SUBSCRIPTION}) UID: ${subscriptionUID}" - if [ ! -z "subscriptionUID" ]; then - # this complicated go-template finds an InstallPlan where both the .spec.clusterServiceVersionNames contains the expected CSV - # and .metadata.ownerReferences contains the expected Subscription owner. JsonPath doesn't let you do and statements and go-templates - # done seem to have a function for checking if a value is in an array without iterating. so here we are. - # NOTE 1: if for whatever reason multiple InstallPlans are found that are associated with the Subscription for the correct CSV, only the first will be approved - # NOTE 2: this is nested in {{` `}} so that helm doesn't try to interpret the go template - {{`installPlanGoTemplate=$(cat << EOF - {{- \$installPlanName := "" -}} - {{- range .items -}} - {{- \$installPlanItem := . -}} - {{- range .spec.clusterServiceVersionNames -}} - {{- if and (eq . "${SUBSCRIPTION_CSV}") (not \$installPlanName) -}} - {{- range \$installPlanItem.metadata.ownerReferences -}} - {{- if eq .uid "${subscriptionUID}" -}} - {{- \$installPlanName = \$installPlanItem.metadata.name -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{ \$installPlanName }} - EOF - )`}} - - echo - echo "Get InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner" - installPlan=$(oc get installplan -o=go-template="${installPlanGoTemplate}") - echo "InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner: ${installPlan}" - if [ ! -z "${installPlan}" ]; then - echo - echo "Check InstallPlan (${installPlan}) approval" - if installPlanApproved=$(oc get installplan ${installPlan} -o=jsonpath="{.spec.approved}"); then - if [ "${installPlanApproved}" == "false" ]; then - echo "Approving InstallPlan (${installPlan})" - oc patch installplan ${installPlan} --type=json -p='[{"op":"replace","path": "/spec/approved", "value": true}]' - else - echo "InstallPlan (${installPlan})already approved" - fi - exit 0 - else - echo "Failed to look up InstallPlan (${installPlan}) approval" - exit 1 - fi - else - echo - echo "Could not find InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner. This can happen if InstallPlan isn't created yet. Try again." - exit 1 - fi - else - echo - echo "Failed to get Subscription ($SUBSCRIPTION) UID. This really shouldn't happen." - exit 1 - fi + command: ["/bin/sh","-c"] + args: + - >- + python3 -m venv /tmp/venv && + source /tmp/venv/bin/activate && + pip3 install openshift-client && + python3 /scripts/installplan-approver.py imagePullPolicy: Always env: - name: SUBSCRIPTION_CSV value: {{ .csv }} - name: SUBSCRIPTION value: {{ .name }} + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumeMounts: + - name: installplan-approver-scripts + mountPath: /scripts + volumes: + - name: installplan-approver-scripts + configMap: + name: installplan-approver-scripts + defaultMode: 0777 dnsPolicy: ClusterFirst restartPolicy: Never serviceAccount: {{ include "operators-installer.approverName" . }} diff --git a/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml b/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml index 50d8bf95..c8b21b25 100644 --- a/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml +++ b/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml @@ -27,75 +27,31 @@ spec: containers: - name: installplan-complete-verifier image: {{ $.Values.installPlanApproverAndVerifyJobsImage }} - command: - - /bin/bash - - -c - - | - export HOME=/tmp/approver - - echo - echo "Get Subscription (${SUBSCRIPTION}) UID" - subscriptionUID=$(oc get subscriptions.operators.coreos.com --field-selector metadata.name=${SUBSCRIPTION} -o=jsonpath="{.items[0].metadata.uid}") - echo "Subscription (${SUBSCRIPTION}) UID: ${subscriptionUID}" - if [ ! -z "subscriptionUID" ]; then - # this complicated go-template finds an InstallPlan where both the .spec.clusterServiceVersionNames contains the expected CSV - # and .metadata.ownerReferences contains the expected Subscription owner. JsonPath doesn't let you do and statements and go-templates - # done seem to have a function for checking if a value is in an array without iterating. so here we are. - # NOTE 1: if for whatever reason multiple InstallPlans are found that are associated with the Subscription for the correct CSV, only the first will be approved - # NOTE 2: this is nested in {{` `}} so that helm doesn't try to interpret the go template - {{`installPlanGoTemplate=$(cat << EOF - {{- \$installPlanName := "" -}} - {{- range .items -}} - {{- \$installPlanItem := . -}} - {{- range .spec.clusterServiceVersionNames -}} - {{- if and (eq . "${SUBSCRIPTION_CSV}") (not \$installPlanName) -}} - {{- range \$installPlanItem.metadata.ownerReferences -}} - {{- if eq .uid "${subscriptionUID}" -}} - {{- \$installPlanName = \$installPlanItem.metadata.name -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{- end -}} - {{ \$installPlanName }} - EOF - )`}} - - echo - echo "Get InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner" - installPlan=$(oc get installplan -o=go-template="${installPlanGoTemplate}") - echo "InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner: ${installPlan}" - if [ ! -z "${installPlan}" ]; then - echo - echo "Check InstallPlan (${installPlan}) phase" - if installPlanPhase=$(oc get installplan ${installPlan} -o=jsonpath="{.status.phase}"); then - if [ "${installPlanPhase}" == "Complete" ]; then - echo "InstallPlan (${installPlan}) complete" - exit 0 - else - echo "InstallPlan (${installPlan}) not yet complete: ${installPlanPhase}" - exit 1 - fi - else - echo "Failed to look up InstallPlan (${installPlan}) phase" - exit 1 - fi - else - echo - echo "Could not find InstallPlan for CSV (${SUBSCRIPTION_CSV}) with Subscription (${SUBSCRIPTION_CSV}) (${subscriptionUID}) owner. This can happen if InstallPlan isn't created yet. Try again." - exit 1 - fi - else - echo - echo "Failed to get Subscription ($SUBSCRIPTION) UID. This really shouldn't happen." - exit 1 - fi + command: ["/bin/sh","-c"] + args: + - >- + python3 -m venv /tmp/venv && + source /tmp/venv/bin/activate && + pip3 install openshift-client && + python3 /scripts/installplan-verifier.py imagePullPolicy: Always env: - name: SUBSCRIPTION_CSV value: {{ .csv }} - name: SUBSCRIPTION value: {{ .name }} + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumeMounts: + - name: installplan-approver-scripts + mountPath: /scripts + volumes: + - name: installplan-approver-scripts + configMap: + name: installplan-approver-scripts + defaultMode: 0777 dnsPolicy: ClusterFirst restartPolicy: Never serviceAccount: {{ include "operators-installer.approverName" . }} From 9df70a9f066e2d83d70559fcdffa1a2dbff431dc Mon Sep 17 00:00:00 2001 From: Ian Tewksbury Date: Tue, 11 Jun 2024 15:32:50 -0400 Subject: [PATCH 3/3] operators-installer - manual incremental upgrades * adds `automaticIntermediateManualUpgrades` option to allow the approver to automatically approve ever required update between currently installed CSV and target CSV --- ...integration-tests-operators-installer.yaml | 101 +++++++++ ...stall-test.yaml => install-unit-test.yaml} | 4 +- charts/operators-installer/Chart.yaml | 2 +- charts/operators-installer/README.md | 33 ++- ...c-intermediate-manual-upgrades-values.yaml | 24 +++ ...c-intermediate-manual-upgrades-values.yaml | 24 +++ .../_scripts/installplan-approver.py | 64 +++--- .../installplan-incremental-approver.py | 131 ++++++++++++ .../_scripts/installplan-verifier.py | 70 +++--- .../_scripts/installplan_utils.py | 201 ++++++++++++++++-- ...espaces-approve-via-helm-hook-values.yaml} | 18 +- ...mespace-approve-via-helm-hook-values.yaml} | 2 +- ...ator-approve-not-via-helm-hook-values.yaml | 6 +- ...operator-approve-via-helm-hook-values.yaml | 6 +- ...c-intermediate-manual-upgrades-values.yaml | 28 +++ ...rator-subscription-with-config-values.yaml | 23 ++ ...all-operator-subscription-with-config.yaml | 20 -- ...l-operator-with-channel-number-values.yaml | 19 ++ ...-install-operator-with-channel-number.yaml | 26 --- ...stall-operator-with-long-name-values.yaml} | 6 +- ...operators-installer-approver-scripts.yaml} | 10 +- .../templates/Job_installplan-approver.yaml | 31 ++- .../Job_installplan-complete-verifier.yaml | 16 +- .../templates/Role_installplan-approver.yaml | 1 + .../templates/_helpers.tpl | 9 +- charts/operators-installer/values.yaml | 20 ++ 26 files changed, 724 insertions(+), 171 deletions(-) create mode 100644 .github/workflows/install-integration-tests-operators-installer.yaml rename .github/workflows/{install-test.yaml => install-unit-test.yaml} (98%) create mode 100644 charts/operators-installer/_integration-tests/test-install-operator-0-automatic-intermediate-manual-upgrades-values.yaml create mode 100644 charts/operators-installer/_integration-tests/test-install-operator-1-automatic-intermediate-manual-upgrades-values.yaml create mode 100644 charts/operators-installer/_scripts/installplan-incremental-approver.py rename charts/operators-installer/ci/{test-install-multiple-operators-in-same-namespaces-approve-via-helm-hook-values.yaml => test-install-multiple-operators-in-different-namespaces-approve-via-helm-hook-values.yaml} (79%) rename charts/operators-installer/ci/{test-install-multiple-operators-in-different-namespace-approve-via-helm-hook-values.yaml => test-install-multiple-operators-in-same-namespace-approve-via-helm-hook-values.yaml} (98%) create mode 100644 charts/operators-installer/ci/test-install-operator-first-time-with-automatic-intermediate-manual-upgrades-values.yaml create mode 100644 charts/operators-installer/ci/test-install-operator-subscription-with-config-values.yaml delete mode 100644 charts/operators-installer/ci/test-install-operator-subscription-with-config.yaml create mode 100644 charts/operators-installer/ci/test-install-operator-with-channel-number-values.yaml delete mode 100644 charts/operators-installer/ci/test-install-operator-with-channel-number.yaml rename charts/operators-installer/ci/{test-install-operator-with-long-name.yaml => test-install-operator-with-long-name-values.yaml} (98%) rename charts/operators-installer/templates/{ConfigMap_installplan-approver-scripts.yaml => ConfigMap_operators-installer-approver-scripts.yaml} (62%) diff --git a/.github/workflows/install-integration-tests-operators-installer.yaml b/.github/workflows/install-integration-tests-operators-installer.yaml new file mode 100644 index 00000000..86836970 --- /dev/null +++ b/.github/workflows/install-integration-tests-operators-installer.yaml @@ -0,0 +1,101 @@ +--- +# these integration tests need to be per operator since they don't do clean up +# +# NOTE: can't use chart-testing because `ct` does not allow for a fixed release so you can't run two different tests that affect the same resources + +name: Install Integration Test - operators-installer + +on: + pull_request: + paths: + - .github/** + - _test/charts-integration-tests/operators-installer/** + - charts/operators-installer/** + +# Declare default permissions as read only. +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + install-integration-test: + runs-on: ubuntu-latest + env: + # renovate: datasource=github-releases depName=helm/helm + HELM_VERSION: v3.15.0 + # renovate: datasource=github-tags depName=python/cpython + PYTHON_VERSION: v3.12.3 + # renovate: datasource=github-releases depName=kubernetes-sigs/kind + KIND_VERSION: v0.23.0 + # renovate: datasource=github-releases depName=operator-framework/operator-lifecycle-manager + OLM_VERSION: v0.28.0 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + with: + fetch-depth: 0 + + - name: Setup Helm 🧰 + uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4 + with: + version: ${{ env.HELM_VERSION }} + + - name: Setup Python 🐍 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Setup kind cluster 🧰 + uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 + with: + version: ${{ env.KIND_VERSION }} + + # for helm charts we are testing that require installing operators + - name: Setup kind cluster - Install OLM 🧰 + run: | + curl -L https://github.com/operator-framework/operator-lifecycle-manager/releases/download/${OLM_VERSION}/install.sh -o install.sh + chmod +x install.sh + ./install.sh ${OLM_VERSION} + + # for helm charts we are testing that require ingress + - name: Setup kind cluster - Install ingress controller 🧰 + run: | + helm repo add haproxy-ingress https://haproxy-ingress.github.io/charts + helm install haproxy-ingress haproxy-ingress/haproxy-ingress \ + --create-namespace --namespace=ingress-controller \ + --set controller.hostNetwork=true + kubectl apply -f - <`false` to create manual InstallPlan approval resources as part of normal install

The hook method is nice to not have lingering resources needed for the manual InstallPlan approval but has the downside that no CustomResources using CustomResourceDefinitions installed by the operator can be used in the same chart because the operator InstallPlan wont be approved, and therefor the operator wont be installed, until the post-install,post-upgrade phase which means you will never get to that phase because your CustomResources wont be able to apply because the Operator isn't installed.

This is is ultimately a trade off between cleaning up these resources or being able to install and configure the operator in the same helm chart that has a dependency on this helm chart. +| installRequiredPythonLibraries | `true` | No | If `true`, install the required Python libraries (openshift-client, semver==2.13.0) dynamically from the given `pythonIndexURL` and `pythonExtraIndexURL` into the `installPlanApproverAndVerifyJobsImage` at run time +| pythonIndexURL | https://pypi.org/simple/ | No | If `installRequiredPythonLibraries` is `true` then use this python index to pull required libraries +| pythonExtraIndexURL | https://pypi.org/simple/ | No | If `installRequiredPythonLibraries` is `true` then use this python extra index to pull required library dependencies | commonLabels | `{}` | No | Common labels to add to all chart created resources. Implements the same idea from Kustomize for this chart. | global.commonLabels | `{}` | No | Common labels coming from global values to add to all chart created resources. Implements the same idea from Kustomize for this chart. ## Warnings -### Can not install / upgrade different operators in same namespace independently +### Disconnected Use +If wanting use this chart in a disconnected environment you need to either: + +#### Option 1: local python index +Set the `pythonIndexURL` and `pythonExtraIndexURL` values to a local disconnected python index that minimally includes (and their dependencies): +* openshift-client +* semver==2.13.0 + +#### Option 2: custom `installPlanApproverAndVerifyJobsImage` with required dependencies +Build a custom container image with: +* binary - `oc` +* python lib - `openshift-client` +* python lib - `semver==2.13.0` +Suggestion is to build such an image on top of the latest `registry.redhat.io/openshift4/ose-cli` image + +Then provide that custom image to `installPlanApproverAndVerifyJobsImage` and set `installRequiredPythonLibraries` to false. + +### Can not install / upgrade different operators in same namespace independently As documented in [How can Operators be updated independently from each other?](https://access.redhat.com/solutions/6389681) when more then one operator install or update is pending in the same namespace the Operator Lifecycle Manager (OLM) will combine those installs/updates into a single InstallPlan and there is no way to separate them. Therefor if you use this helm chart in namespace ZZZ to install operator A at v1.0 and it has a pending update to v1.1 and then update the configuration to also install operator B at v42.0 in namespace ZZZ the ZZZ v42.0 InstallPlan and the A v1.1 InstallPlan will get merged (by OLM) and this helm chart will then approve that InstallPlan as it will match on the ZZZ v42.0 pending install, which will incidentally install the A v1.1 update. There is no way for this or any helm chart, automation, or even click ops to prevent this, as documented in [How can Operators be updated independently from each other?](https://access.redhat.com/solutions/6389681) this is currently considered "a feature of OLM". diff --git a/charts/operators-installer/_integration-tests/test-install-operator-0-automatic-intermediate-manual-upgrades-values.yaml b/charts/operators-installer/_integration-tests/test-install-operator-0-automatic-intermediate-manual-upgrades-values.yaml new file mode 100644 index 00000000..2244fec2 --- /dev/null +++ b/charts/operators-installer/_integration-tests/test-install-operator-0-automatic-intermediate-manual-upgrades-values.yaml @@ -0,0 +1,24 @@ +approveManualInstallPlanViaHook: true + +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 + +operatorGroups: +- name: argocd-operator + createNamespace: true + targetOwnNamespace: false + otherTargetNamespaces: + +operators: +- name: argocd-operator + channel: alpha + csv: argocd-operator.v0.8.0 + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + namespace: argocd-operator + installPlanVerifierActiveDeadlineSeconds: 1200 + automaticIntermediateManualUpgrades: true + config: + env: + - name: DISABLE_DEFAULT_ARGOCD_INSTANCE + value: "true" diff --git a/charts/operators-installer/_integration-tests/test-install-operator-1-automatic-intermediate-manual-upgrades-values.yaml b/charts/operators-installer/_integration-tests/test-install-operator-1-automatic-intermediate-manual-upgrades-values.yaml new file mode 100644 index 00000000..bc6c55b0 --- /dev/null +++ b/charts/operators-installer/_integration-tests/test-install-operator-1-automatic-intermediate-manual-upgrades-values.yaml @@ -0,0 +1,24 @@ +approveManualInstallPlanViaHook: true + +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 + +operatorGroups: +- name: argocd-operator + createNamespace: true + targetOwnNamespace: false + otherTargetNamespaces: + +operators: +- name: argocd-operator + channel: alpha + csv: argocd-operator.v0.10.1 + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + namespace: argocd-operator + installPlanVerifierActiveDeadlineSeconds: 1200 + automaticIntermediateManualUpgrades: true + config: + env: + - name: DISABLE_DEFAULT_ARGOCD_INSTANCE + value: "true" diff --git a/charts/operators-installer/_scripts/installplan-approver.py b/charts/operators-installer/_scripts/installplan-approver.py index 0cbc5030..b78c3596 100644 --- a/charts/operators-installer/_scripts/installplan-approver.py +++ b/charts/operators-installer/_scripts/installplan-approver.py @@ -4,20 +4,29 @@ import sys import installplan_utils -namespace_name = os.environ["NAMESPACE"] -subscription_name = os.environ["SUBSCRIPTION"] -subscription_csv = os.environ["SUBSCRIPTION_CSV"] +NAMESPACE_NAME = os.getenv("NAMESPACE") or installplan_utils.error_and_exit( + "env is missing expected value: NAMESPACE", 2 +) +SUBSCRIPTION_NAME = os.getenv("SUBSCRIPTION") or installplan_utils.error_and_exit( + "env is missing expected value: SUBSCRIPTION", 2 +) +CSV = os.getenv("CSV") or installplan_utils.error_and_exit( + "env is missing expected value: CSV", 2 +) print() -print("******************************") -print("* START InstallPlan Approver *") -print("******************************") +print("********************************************************************") +print("* START InstallPlan approver") +print(f"*\t- NAMESPACE_NAME: {NAMESPACE_NAME}") +print(f"*\t- SUBSCRIPTION_NAME: {SUBSCRIPTION_NAME}") +print(f"*\t- CSV: {CSV}") +print("********************************************************************") # find the subscription uid print() -print(f"Get Subscription ({subscription_name}) UID") -subscription_uid = installplan_utils.get_subscription_uid(subscription_name) -print(f"Subscription ({subscription_name}) UID: {subscription_uid}") +print(f"Get Subscription ({SUBSCRIPTION_NAME}) UID") +subscription_uid = installplan_utils.get_subscription_uid(SUBSCRIPTION_NAME) +print(f"\t- Subscription ({SUBSCRIPTION_NAME}) UID: {subscription_uid}") # if found subscription uid find InstallPlan for given CSV with owner of the given subscription # else error @@ -25,42 +34,29 @@ # find the InstallPlan that has expected owner subscription id and expected target CSV name # NOTE: if more then one InstallPlan matches, choose the first one print( - f"Get InstallPlan for CSV ({subscription_csv}) with Subscription (${subscription_uid}) owner" + f"Find InstallPlan in Namespace ({NAMESPACE_NAME}) for CSV ({CSV}) with Subscription (${subscription_uid}) owner" ) - target_install_plan = installplan_utils.get_installplan( - namespace_name, subscription_csv, subscription_uid + target_installplan = installplan_utils.get_installplan( + NAMESPACE_NAME, CSV, subscription_uid ) - # if found target InstallPlan, approve it + + # if found target InstallPlan, approve it, and success exit # else fail - if target_install_plan: - target_install_plan_name = target_install_plan.model.metadata.name - print( - f"InstallPlan for CSV ({subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}): {target_install_plan.model.metadata.name}" - ) - print() - print( - f"Current InstallPlan ({target_install_plan_name}) approval state: {target_install_plan.model.spec.approved}" - ) - # if already approved, just notify - # else approve - if target_install_plan.model.spec.approved: - print(f"InstallPlan ({target_install_plan_name}) is already approved") - else: - print(f"Approving InstallPlan ({target_install_plan_name})") - target_install_plan.model.spec.approved = True - target_install_plan.apply() - print(f"Approved InstallPlan ({target_install_plan_name})") + if target_installplan: + print(f"\t- Found InstallPlan: {target_installplan.model.metadata.name}") + installplan_utils.approve_installplan(target_installplan) sys.exit(0) else: print() print( - f"Could not find InstallPlan for CSV ${subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}) owner." - + "\nThis can happen if InstallPlan isn't created yet. Try again." + f"ERROR: Could not find next InstallPlan to reach CSV ${CSV}) with Subscription ({SUBSCRIPTION_NAME}) ({subscription_uid}) owner." + + "\nThis can happen if InstallPlan isn't created yet or no valid upgrade path between current CSV and target CSV." + + "\nTry again." ) sys.exit(1) else: print() print( - "Failed to get Subscription ({subscription_name}) UID. This really shouldn't happen." + f"ERROR: Failed to get Subscription ({SUBSCRIPTION_NAME}) UID. This really shouldn't happen." ) sys.exit(1) diff --git a/charts/operators-installer/_scripts/installplan-incremental-approver.py b/charts/operators-installer/_scripts/installplan-incremental-approver.py new file mode 100644 index 00000000..5cbb5d21 --- /dev/null +++ b/charts/operators-installer/_scripts/installplan-incremental-approver.py @@ -0,0 +1,131 @@ +#!/usr/bin/python + +import os +import sys +import installplan_utils +import time + +NAMESPACE_NAME = os.getenv("NAMESPACE") or installplan_utils.error_and_exit( + "env is missing expected value: NAMESPACE", 2 +) +SUBSCRIPTION_NAME = os.getenv("SUBSCRIPTION") or installplan_utils.error_and_exit( + "env is missing expected value: SUBSCRIPTION", 2 +) +CSV = os.getenv("CSV") or installplan_utils.error_and_exit( + "env is missing expected value: CSV", 2 +) +INCREMENTAL_INSTALL_BACKOFF_LIMIT = ( + int(os.getenv("INCREMENTAL_INSTALL_BACKOFF_LIMIT")) or 10 +) +INCREMENTAL_INSTALL_DELAY_INCREMENT = ( + int(os.getenv("INCREMENTAL_INSTALL_DELAY_INCREMENT")) or 5 +) + + +print() +print("********************************************************************") +print("* START InstallPlan approver including intermediates") +print(f"*\t- NAMESPACE_NAME: {NAMESPACE_NAME}") +print(f"*\t- SUBSCRIPTION_NAME: {SUBSCRIPTION_NAME}") +print(f"*\t- CSV: {CSV}") +print(f"*\t- INCREMENTAL_INSTALL_BACKOFF_LIMIT: {INCREMENTAL_INSTALL_BACKOFF_LIMIT}") +print( + f"*\t- INCREMENTAL_INSTALL_DELAY_INCREMENT: {INCREMENTAL_INSTALL_DELAY_INCREMENT}" +) +print("********************************************************************") + +# find the subscription uid +print() +print(f"Get Subscription ({SUBSCRIPTION_NAME}) UID") +subscription_uid = installplan_utils.get_subscription_uid(SUBSCRIPTION_NAME) +print(f"\t- Subscription ({SUBSCRIPTION_NAME}) UID: {subscription_uid}") + +# if found subscription uid incrementally find, approve, and verify the next InstallPlan for given CSV with owner of the given subscription +# until reaching the desired Subscription CSV +# else error +if subscription_uid: + print( + f"Find and approve every InstallPlan between currently installed CSV and target CSV ({CSV})" + ) + target_installplan = None + approved_install_plans = [] + attempt = 0 + while (target_installplan is None) or ( + CSV not in target_installplan.model.spec.clusterServiceVersionNames + ): + # find the next InstallPlan that has expected owner subscription id and expected target CSV name + print( + f"\nFind next InstallPlan in Namespace ({NAMESPACE_NAME}) for CSV ({CSV}) with Subscription (${subscription_uid}) owner" + ) + target_installplan = installplan_utils.get_next_installplan( + NAMESPACE_NAME, CSV, subscription_uid + ) + + # if found next InstallPlan, approve it + # else fail + if target_installplan: + # if target install plan is not one we have already approved, then approve it + # else wait and loop for next InstallPlan that installs a CSV less then or equal to our target CSV + if target_installplan.model.metadata.name not in map( + lambda approved_install_plan: approved_install_plan.model.metadata.name, + approved_install_plans, + ): + attempt = 0 + print( + f"\t- Found next InstallPlan: {target_installplan.model.metadata.name}" + ) + approved_install_plans.append(target_installplan) + installplan_utils.approve_installplan(target_installplan) + installplan_installed = ( + installplan_utils.verify_installplan_and_csv_installed( + target_installplan, + INCREMENTAL_INSTALL_BACKOFF_LIMIT, + INCREMENTAL_INSTALL_DELAY_INCREMENT, + ) + ) + else: + attempt += 1 + # let everyone know what InstallPlans are currently available + print("\t- Currently available InstallPlans:") + for available_installplan in installplan_utils.get_all_installplans( + NAMESPACE_NAME + ): + print( + f"\t\t-- {available_installplan.model.metadata.name} - {available_installplan.model.spec.clusterServiceVersionNames}: {available_installplan.model.status.phase}" + ) + + # if attempts left, do another loop waiting for target InstallPlan to appear + if attempt <= INCREMENTAL_INSTALL_BACKOFF_LIMIT: + delay = attempt * INCREMENTAL_INSTALL_DELAY_INCREMENT + print( + f"\t- Attempt ({attempt} of {INCREMENTAL_INSTALL_BACKOFF_LIMIT}) waiting ({delay} seconds) for next unapproved InstallPlan including target CSV less then or equal to be created. Target CSV: {CSV}" + ) + sys.stdout.flush() + time.sleep(delay) + else: + installplan_utils.error_and_exit( + f"Timed out waiting for next unapproved InstallPlan including target CSV less then or equal to be created. Target CSV: {CSV}", + 1, + ) + else: + installplan_utils.error_and_exit( + f"Could not find next InstallPlan to reach CSV ${CSV}) with Subscription ({SUBSCRIPTION_NAME}) ({subscription_uid}) owner." + + "\nThis can happen if InstallPlan isn't created yet or no valid upgrade path between current CSV and target CSV." + + "\nTry again.", + 1, + ) + + # report success + print() + print( + f"Successfully installed target CSV ({CSV}) installed, approved intermediate InstallPlans include:" + ) + for approved_install_plan in approved_install_plans: + print( + f"\t- {approved_install_plan.model.metadata.name}: {approved_install_plan.model.spec.clusterServiceVersionNames}" + ) + sys.exit(0) +else: + installplan_utils.error_and_exit( + f"Failed to get Subscription ({SUBSCRIPTION_NAME}) UID. This really shouldn't happen." + ) diff --git a/charts/operators-installer/_scripts/installplan-verifier.py b/charts/operators-installer/_scripts/installplan-verifier.py index e8e68edf..7ea49a39 100644 --- a/charts/operators-installer/_scripts/installplan-verifier.py +++ b/charts/operators-installer/_scripts/installplan-verifier.py @@ -4,20 +4,30 @@ import sys import installplan_utils -namespace_name = os.environ["NAMESPACE"] -subscription_name = os.environ["SUBSCRIPTION"] -subscription_csv = os.environ["SUBSCRIPTION_CSV"] +NAMESPACE_NAME = os.getenv("NAMESPACE") or installplan_utils.error_and_exit( + "env is missing expected value: NAMESPACE", 2 +) +SUBSCRIPTION_NAME = os.getenv("SUBSCRIPTION") or installplan_utils.error_and_exit( + "env is missing expected value: SUBSCRIPTION", 2 +) +CSV = os.getenv("CSV") or installplan_utils.error_and_exit( + "env is missing expected value: CSV", 2 +) print() -print("******************************") +print("********************************************************************") print("* START InstallPlan Approver *") -print("******************************") +print(f"*\t- NAMESPACE_NAME: {NAMESPACE_NAME}") +print(f"*\t- SUBSCRIPTION_NAME: {SUBSCRIPTION_NAME}") +print(f"*\t- CSV: {CSV}") +print("********************************************************************") + # find the subscription uid print() -print(f"Get Subscription ({subscription_name}) UID") -subscription_uid = installplan_utils.get_subscription_uid(subscription_name) -print(f"Subscription ({subscription_name}) UID: {subscription_uid}") +print(f"Get Subscription ({SUBSCRIPTION_NAME}) UID") +subscription_uid = installplan_utils.get_subscription_uid(SUBSCRIPTION_NAME) +print(f"\t- Subscription ({SUBSCRIPTION_NAME}) UID: {subscription_uid}") # if found subscription uid find InstallPlan for given CSV with owner of the given subscription # else error @@ -25,43 +35,45 @@ # find the InstallPlan that has expected owner subscription id and expected target CSV name # NOTE: if more then one InstallPlan matches, choose the first one print( - f"Get InstallPlan for CSV ({subscription_csv}) with Subscription (${subscription_uid}) owner" + f"\tFind InstallPlan in Namespace ({NAMESPACE_NAME}) for CSV ({CSV}) with Subscription (${subscription_uid}) owner" ) - target_install_plan = installplan_utils.get_installplan( - namespace_name, subscription_csv, subscription_uid + target_installplan = installplan_utils.get_installplan( + NAMESPACE_NAME, CSV, subscription_uid ) - # if found target InstallPlan, approve it + + # if found target InstallPlan, check if its installed # else fail - if target_install_plan: - target_install_plan_name = target_install_plan.model.metadata.name - target_install_plan_phase = target_install_plan.model.status.phase - print( - f"InstallPlan for CSV ({subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}): {target_install_plan.model.metadata.name}" - ) - print() - print( - f"Current InstallPlan ({target_install_plan_name}) phase: {target_install_plan_phase}" + if target_installplan: + print(f"\t- Found InstallPlan: {target_installplan.model.metadata.name}") + + installplan_installed = installplan_utils.verify_installplan_and_csv_installed( + target_installplan, + 1, + 1, ) - # if complete, exit success - # if not yet complete, exit failure - if target_install_plan_phase == "Complete": - print(f"InstallPlan ({target_install_plan_name}) complete") + if installplan_installed: + print() + print( + f"InstallPlan ({target_installplan.model.metadata.name}) installation verified" + ) sys.exit(0) else: + print() print( - f"InstallPlan ({target_install_plan_name}) not yet complete: ${target_install_plan_phase}" + f"InstallPlan ({target_installplan.model.metadata.name}) not yet installed. Suggest retry verification." ) sys.exit(1) else: print() print( - f"Could not find InstallPlan for CSV ${subscription_csv}) with Subscription ({subscription_name}) ({subscription_uid}) owner." - + "\nThis can happen if InstallPlan isn't created yet. Try again." + f"ERROR: Could not find next InstallPlan to reach CSV ${CSV}) with Subscription ({SUBSCRIPTION_NAME}) ({subscription_uid}) owner." + + "\nThis can happen if InstallPlan isn't created yet or no valid upgrade path between current CSV and target CSV." + + "\nTry again." ) sys.exit(1) else: print() print( - "Failed to get Subscription ({subscription_name}) UID. This really shouldn't happen." + f"ERROR: Failed to get Subscription ({SUBSCRIPTION_NAME}) UID. This really shouldn't happen." ) sys.exit(1) diff --git a/charts/operators-installer/_scripts/installplan_utils.py b/charts/operators-installer/_scripts/installplan_utils.py index a75c204c..26b5b0f9 100644 --- a/charts/operators-installer/_scripts/installplan_utils.py +++ b/charts/operators-installer/_scripts/installplan_utils.py @@ -1,37 +1,210 @@ #!/usr/bin/python import openshift_client as oc +import semver # assumes semver 2.13 because thats whats supported on python3.6 which is what is supported by oc tools image +import re +import time +import sys -def get_subscription_uid(subscription_name: str): +def get_subscription(subscription_name: str): subscriptions_selector = oc.selector( [f"subscriptions.operators.coreos.com/{subscription_name}"] ) - subscription_uid = None + subscription = None if subscriptions_selector.objects(): - subscription_uid = subscriptions_selector.objects()[0].model.metadata.uid + subscription = subscriptions_selector.objects()[0] + + return subscription + + +def get_subscription_uid(subscription_name: str): + """Get the Subscription UID for the given Subscription name""" + subscription = get_subscription(subscription_name) + if subscription: + subscription_uid = subscription.model.metadata.uid return subscription_uid +def get_csv(csv_name: str): + """Get the ClusterServiceVersion object for a given CSV name""" + csvs_selector = oc.selector( + [f"clusterserviceversions.operators.coreos.com/{csv_name}"] + ) + csv = None + if csvs_selector.objects(): + csv = csvs_selector.objects()[0] + + return csv + + +def get_all_installplans( + namespace_name: str, +): + """Get all InstallPlans in a given Namespace""" + installplans_selector = oc.selector(["installplan.operators.coreos.com"]) + return installplans_selector.objects() + + def get_installplan( namespace_name: str, - subscription_csv: str, + csv_name: str, subscription_uid: str, ): + """Get InstallPlan in a given Namespace, with a given target ClusterServiceVersion name, and a given Subscription owner""" with oc.project(namespace_name): # find the InstallPlan that has expected owner subscription id and expected target CSV name # NOTE: if more then one InstallPlan matches, choose the first one - install_plans_selector = oc.selector(["installplan.operators.coreos.com"]) - target_install_plan = None - for install_plan in install_plans_selector.objects(): - owner_uids = map( + installplans = get_all_installplans(namespace_name) + target_installplan = None + for installplan in installplans: + installplan_owner_uids = map( lambda owner_reference: owner_reference.uid, - install_plan.model.metadata.ownerReferences, + installplan.model.metadata.ownerReferences, ) - if ( - subscription_csv in install_plan.model.spec.clusterServiceVersionNames - ) and (subscription_uid in owner_uids): - target_install_plan = install_plan + if (csv_name in installplan.model.spec.clusterServiceVersionNames) and ( + subscription_uid in installplan_owner_uids + ): + target_installplan = installplan break - return target_install_plan + return target_installplan + + +def get_next_installplan( + namespace_name: str, + csv_name: str, + subscription_uid: str, +): + """Find the next InstallPlan that installs a ClusterServiceVersion that is less then or equal to the given ClusterServiceVersion name and owned by the given Subscription""" + target_csv_name_semver = get_csv_semver(csv_name) + with oc.project(namespace_name): + # search each InstallPlan for the one that is owned by given subscription and has the highest CSV less then or equal to our target csv + installplans_selector = oc.selector(["installplan.operators.coreos.com"]) + latest_installplan = None + latest_installplan_csv_semver = None + for installplan in installplans_selector.objects(): + installplan_owner_uids = map( + lambda owner_reference: owner_reference.uid, + installplan.model.metadata.ownerReferences, + ) + # if InstallPlan owned by given subscription search its CSVs + if subscription_uid in installplan_owner_uids: + # searching InstallPlan's CSVs for one that is less then or equal to our target CSV and greater then any already found valid InstallPlan + for ( + installplan_csv + ) in installplan.model.spec.clusterServiceVersionNames: + installplan_csv_semver = get_csv_semver(installplan_csv) + # if pending InstallPlan CSV semver is less then or equal to target subscription CSV semver and greater then last found valid pending InstallPlan + if (installplan_csv_semver <= target_csv_name_semver) and ( + latest_installplan_csv_semver is None + or installplan_csv_semver > latest_installplan_csv_semver + ): + latest_installplan = installplan + latest_installplan_csv_semver = installplan_csv_semver + + return latest_installplan + + +def approve_installplan(installplan: oc.APIObject): + """Approves a given install plan""" + installplan_name = installplan.model.metadata.name + print( + "Approve InstallPlan:" + + f"\n\t- name: {installplan_name}" + + f"\n\t- CSVs: {installplan.model.spec.clusterServiceVersionNames}" + + f"\n\t- approval state: {installplan.model.spec.approved}" + ) + # if already approved, just notify + # else approve + if installplan.model.spec.approved: + print( + f"\t- InstallPlan ({installplan_name}) is already approved, nothing to do." + ) + else: + print(f"\t- Approving InstallPlan ({installplan_name})") + installplan.modify_and_apply(installplan_approve_modify_and_apply, 4) + print(f"\t- Approved InstallPlan ({installplan_name})") + + +def installplan_approve_modify_and_apply(installplan: oc.APIObject): + """Helper function for oc.APIObject.modify_and_apply to approve an InstallPlan""" + installplan.model.spec.approved = True + + +def get_csv_semver( + csv_name: str, +): + """Get the semver section of a given ClusterServiceVersion name""" + csv_version_regex_pattern = re.compile("[^\.]*.v?(.*)") + csv_version = csv_version_regex_pattern.match(csv_name).group(1) + return semver.VersionInfo.parse(csv_version) + + +def verify_installplan_and_csv_installed( + installplan: oc.APIObject, + backoffLimit: int = 1, + delay_increment: int = 2, +): + """Wait until a given InstallPlan and its associated ClusterServiceVersions are installed""" + installplan_name = installplan.model.metadata.name + print( + "Verify InstallPlan and CSV installed:" + + f"\n\t- InstallPlan name: {installplan_name}" + + f"\n\t- InstallPlan CSVs: {installplan.model.spec.clusterServiceVersionNames}" + ) + installplan_installed = False + csvs_installed = False + for attempt in range(backoffLimit): + # progressively wait longer with each attempt + time.sleep(attempt * delay_increment) + + if not installplan_installed: + # refresh and check if installed + installplan.refresh() + installplan_installed = installplan.model.status.conditions.can_match( + { + "type": "Installed", + "status": "True", + } + ) + if installplan_installed: + print( + f"\t- Verify InstallPlan ({installplan_name}) installed attempt ({attempt} of {backoffLimit}) success, current phase: {installplan.model.status.phase}" + ) + else: + print( + f"\t- Verify InstallPlan ({installplan_name}) installed attempt ({attempt} of {backoffLimit}) failed, current phase: {installplan.model.status.phase}" + ) + + if not csvs_installed: + for csv_name in installplan.model.spec.clusterServiceVersionNames: + # get the CSV and check if installed + csv = get_csv(csv_name) + csvs_installed = csv.model.status.conditions.can_match( + { + "reason": "InstallSucceeded", + "phase": "Succeeded", + } + ) + if csvs_installed: + print( + f"\t- Verify CSV ({csv_name}) installed attempt ({attempt} of {backoffLimit}) success, current phase: {csv.model.status.phase}" + ) + else: + print( + f"\t- Verify CSV ({csv_name}) installed attempt ({attempt} of {backoffLimit}) failed, current phase: {csv.model.status.phase}" + ) + + sys.stdout.flush() + if installplan_installed and csvs_installed: + break + + return installplan_installed and csvs_installed + + +def error_and_exit(message: str, code: int): + """Print and error and exit""" + print() + print(f"ERROR: {message}") + sys.exit(code) diff --git a/charts/operators-installer/ci/test-install-multiple-operators-in-same-namespaces-approve-via-helm-hook-values.yaml b/charts/operators-installer/ci/test-install-multiple-operators-in-different-namespaces-approve-via-helm-hook-values.yaml similarity index 79% rename from charts/operators-installer/ci/test-install-multiple-operators-in-same-namespaces-approve-via-helm-hook-values.yaml rename to charts/operators-installer/ci/test-install-multiple-operators-in-different-namespaces-approve-via-helm-hook-values.yaml index dc122597..7374bd06 100644 --- a/charts/operators-installer/ci/test-install-multiple-operators-in-same-namespaces-approve-via-helm-hook-values.yaml +++ b/charts/operators-installer/ci/test-install-multiple-operators-in-different-namespaces-approve-via-helm-hook-values.yaml @@ -1,9 +1,9 @@ approveManualInstallPlanViaHook: true -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 operatorGroups: -- name: external-secrets-operator3 +- name: external-secrets-operator-3 createNamespace: true targetOwnNamespace: false otherTargetNamespaces: @@ -13,20 +13,20 @@ operatorGroups: otherTargetNamespaces: operators: -- channel: stable +- name: external-secrets-operator + channel: stable + csv: external-secrets-operator.v0.8.1 installPlanApproval: Manual - name: external-secrets-operator source: operatorhubio-catalog sourceNamespace: olm - csv: external-secrets-operator.v0.8.1 - namespace: external-secrets-operator3 + namespace: external-secrets-operator-3 installPlanVerifierActiveDeadlineSeconds: 1200 -- channel: alpha +- name: argocd-operator + channel: alpha + csv: argocd-operator.v0.6.0 installPlanApproval: Manual - name: argocd-operator source: operatorhubio-catalog sourceNamespace: olm - csv: argocd-operator.v0.6.0 namespace: argocd-operator installPlanVerifierActiveDeadlineSeconds: 1200 commonLabels: diff --git a/charts/operators-installer/ci/test-install-multiple-operators-in-different-namespace-approve-via-helm-hook-values.yaml b/charts/operators-installer/ci/test-install-multiple-operators-in-same-namespace-approve-via-helm-hook-values.yaml similarity index 98% rename from charts/operators-installer/ci/test-install-multiple-operators-in-different-namespace-approve-via-helm-hook-values.yaml rename to charts/operators-installer/ci/test-install-multiple-operators-in-same-namespace-approve-via-helm-hook-values.yaml index 1296d858..bd62c887 100644 --- a/charts/operators-installer/ci/test-install-multiple-operators-in-different-namespace-approve-via-helm-hook-values.yaml +++ b/charts/operators-installer/ci/test-install-multiple-operators-in-same-namespace-approve-via-helm-hook-values.yaml @@ -1,6 +1,6 @@ approveManualInstallPlanViaHook: true -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 operatorGroups: community: diff --git a/charts/operators-installer/ci/test-install-old-operator-approve-not-via-helm-hook-values.yaml b/charts/operators-installer/ci/test-install-old-operator-approve-not-via-helm-hook-values.yaml index 63699dff..98b19db7 100644 --- a/charts/operators-installer/ci/test-install-old-operator-approve-not-via-helm-hook-values.yaml +++ b/charts/operators-installer/ci/test-install-old-operator-approve-not-via-helm-hook-values.yaml @@ -1,9 +1,9 @@ approveManualInstallPlanViaHook: false -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 operatorGroups: -- name: external-secrets-operator2 +- name: external-secrets-operator-2 createNamespace: true targetOwnNamespace: false otherTargetNamespaces: @@ -15,7 +15,7 @@ operators: source: operatorhubio-catalog sourceNamespace: olm csv: external-secrets-operator.v0.8.1 - namespace: external-secrets-operator2 + namespace: external-secrets-operator-2 installPlanVerifierActiveDeadlineSeconds: 1200 commonLabels: test-label: xyz123 diff --git a/charts/operators-installer/ci/test-install-old-operator-approve-via-helm-hook-values.yaml b/charts/operators-installer/ci/test-install-old-operator-approve-via-helm-hook-values.yaml index de97a08e..2a4836b8 100644 --- a/charts/operators-installer/ci/test-install-old-operator-approve-via-helm-hook-values.yaml +++ b/charts/operators-installer/ci/test-install-old-operator-approve-via-helm-hook-values.yaml @@ -1,9 +1,9 @@ approveManualInstallPlanViaHook: true -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 operatorGroups: -- name: external-secrets-operator1 +- name: external-secrets-operator-1 createNamespace: true targetOwnNamespace: false otherTargetNamespaces: @@ -15,7 +15,7 @@ operators: source: operatorhubio-catalog sourceNamespace: olm csv: external-secrets-operator.v0.8.1 - namespace: external-secrets-operator1 + namespace: external-secrets-operator-1 installPlanVerifierActiveDeadlineSeconds: 1200 commonLabels: test-label: xyz123 diff --git a/charts/operators-installer/ci/test-install-operator-first-time-with-automatic-intermediate-manual-upgrades-values.yaml b/charts/operators-installer/ci/test-install-operator-first-time-with-automatic-intermediate-manual-upgrades-values.yaml new file mode 100644 index 00000000..81966578 --- /dev/null +++ b/charts/operators-installer/ci/test-install-operator-first-time-with-automatic-intermediate-manual-upgrades-values.yaml @@ -0,0 +1,28 @@ +# NOTE: +# this doesn't REALLY do a hard core test of the `automaticIntermediateManualUpgrades` option +# because the operator is not already installed, but it does at least test some of the code path + +approveManualInstallPlanViaHook: true + +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 + +operatorGroups: +- name: argocd-operator-1 + createNamespace: true + targetOwnNamespace: true + otherTargetNamespaces: + +operators: +- name: argocd-operator + channel: alpha + csv: argocd-operator.v0.6.0 + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + namespace: argocd-operator-1 + installPlanVerifierActiveDeadlineSeconds: 1200 + automaticIntermediateManualUpgrades: true + config: + env: + - name: DISABLE_DEFAULT_ARGOCD_INSTANCE + value: "true" diff --git a/charts/operators-installer/ci/test-install-operator-subscription-with-config-values.yaml b/charts/operators-installer/ci/test-install-operator-subscription-with-config-values.yaml new file mode 100644 index 00000000..050b14e2 --- /dev/null +++ b/charts/operators-installer/ci/test-install-operator-subscription-with-config-values.yaml @@ -0,0 +1,23 @@ +approveManualInstallPlanViaHook: true + +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 + +operatorGroups: +- name: argocd-operator-2 + createNamespace: true + targetOwnNamespace: true + otherTargetNamespaces: + +operators: +- name: argocd-operator + channel: alpha + csv: argocd-operator.v0.6.0 + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + namespace: argocd-operator-2 + installPlanVerifierActiveDeadlineSeconds: 1200 + config: + env: + - name: DISABLE_DEFAULT_ARGOCD_INSTANCE + value: "true" diff --git a/charts/operators-installer/ci/test-install-operator-subscription-with-config.yaml b/charts/operators-installer/ci/test-install-operator-subscription-with-config.yaml deleted file mode 100644 index 90804b39..00000000 --- a/charts/operators-installer/ci/test-install-operator-subscription-with-config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -approveManualInstallPlanViaHook: true - -installPlanApproverAndVerifyJobsImage: registry.redhat.io/openshift4/ose-cli:v4.12 - -operators: -- channel: gitops-1.10 - installPlanApproval: Manual - name: openshift-gitops-operator - source: redhat-operators - sourceNamespace: openshift-marketplace - csv: openshift-gitops-operator.v1.10.1 - namespace: openshift-gitops-operator - config: - env: - - name: DISABLE_DEFAULT_ARGOCD_INSTANCE - value: "true" - -operatorGroups: -- name: openshift-gitops-operator - createNamespace: openshift-gitops-operator diff --git a/charts/operators-installer/ci/test-install-operator-with-channel-number-values.yaml b/charts/operators-installer/ci/test-install-operator-with-channel-number-values.yaml new file mode 100644 index 00000000..a623b5d0 --- /dev/null +++ b/charts/operators-installer/ci/test-install-operator-with-channel-number-values.yaml @@ -0,0 +1,19 @@ +approveManualInstallPlanViaHook: true + +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 + +operatorGroups: +- name: aqua-operator + createNamespace: true + targetOwnNamespace: true + otherTargetNamespaces: + +operators: +- name: aqua + channel: 2022.4.0 + csv: aqua-operator.2022.4.14 + installPlanApproval: Manual + source: operatorhubio-catalog + sourceNamespace: olm + namespace: aqua-operator + installPlanVerifierActiveDeadlineSeconds: 1200 diff --git a/charts/operators-installer/ci/test-install-operator-with-channel-number.yaml b/charts/operators-installer/ci/test-install-operator-with-channel-number.yaml deleted file mode 100644 index 944b88b7..00000000 --- a/charts/operators-installer/ci/test-install-operator-with-channel-number.yaml +++ /dev/null @@ -1,26 +0,0 @@ -approveManualInstallPlanViaHook: false - -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 - -commonLabels: - localLabelOne: hello - -global: - commonLabels: - globalLabelOne: world - -operatorGroups: - aqua: - name: aqua - createNamespace: true - targetOwnNamespace: false - otherTargetNamespaces: [] - -operators: - aqua: - channel: "2022.4.0" - installPlanApproval: Manual - name: aqua-operator-certified - source: operatorhubio-catalog - sourceNamespace: openshift-marketplace - csv: aqua-operator.v2022.4.348 diff --git a/charts/operators-installer/ci/test-install-operator-with-long-name.yaml b/charts/operators-installer/ci/test-install-operator-with-long-name-values.yaml similarity index 98% rename from charts/operators-installer/ci/test-install-operator-with-long-name.yaml rename to charts/operators-installer/ci/test-install-operator-with-long-name-values.yaml index 07604608..b9293879 100644 --- a/charts/operators-installer/ci/test-install-operator-with-long-name.yaml +++ b/charts/operators-installer/ci/test-install-operator-with-long-name-values.yaml @@ -1,6 +1,6 @@ approveManualInstallPlanViaHook: false -installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.12 +installPlanApproverAndVerifyJobsImage: quay.io/openshift/origin-cli:4.15 operatorGroups: cmo: @@ -11,11 +11,11 @@ operatorGroups: operators: cmo: + name: costmanagement-metrics-operator channel: stable + csv: costmanagement-metrics-operator.2.0.0 installPlanApproval: Manual - name: costmanagement-metrics-operator source: operatorhubio-catalog sourceNamespace: olm - csv: costmanagement-metrics-operator.2.0.0 namespace: costmanagement-metrics-operator installPlanVerifierActiveDeadlineSeconds: 1200 diff --git a/charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml b/charts/operators-installer/templates/ConfigMap_operators-installer-approver-scripts.yaml similarity index 62% rename from charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml rename to charts/operators-installer/templates/ConfigMap_operators-installer-approver-scripts.yaml index 8cc6fd7a..53770e33 100644 --- a/charts/operators-installer/templates/ConfigMap_installplan-approver-scripts.yaml +++ b/charts/operators-installer/templates/ConfigMap_operators-installer-approver-scripts.yaml @@ -4,7 +4,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: installplan-approver-scripts + name: {{ include "operators-installer.scriptsName" . }} namespace: {{ .namespace | default $.Release.Namespace }} labels: {{- include "operators-installer.labels" $ | nindent 4 }} @@ -17,10 +17,12 @@ metadata: {{- end }} data: installplan_utils.py: |- -{{ tpl ( $.Files.Get "_scripts/installplan_utils.py" ) . | indent 4 }} +{{ tpl ( $.Files.Get "_scripts/installplan_utils.py" ) $ | indent 4 }} installplan-approver.py: |- -{{ tpl ( $.Files.Get "_scripts/installplan-approver.py" ) . | indent 4 }} +{{ tpl ( $.Files.Get "_scripts/installplan-approver.py" ) $ | indent 4 }} + installplan-incremental-approver.py: |- +{{ tpl ( $.Files.Get "_scripts/installplan-incremental-approver.py" ) $ | indent 4 }} installplan-verifier.py: |- -{{ tpl ( $.Files.Get "_scripts/installplan-verifier.py" ) . | indent 4 }} +{{ tpl ( $.Files.Get "_scripts/installplan-verifier.py" ) $ | indent 4 }} {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/operators-installer/templates/Job_installplan-approver.yaml b/charts/operators-installer/templates/Job_installplan-approver.yaml index e8df2cda..4d6a7b03 100644 --- a/charts/operators-installer/templates/Job_installplan-approver.yaml +++ b/charts/operators-installer/templates/Job_installplan-approver.yaml @@ -2,10 +2,11 @@ {{- if eq .installPlanApproval "Manual" }} --- # create one installplan-approver job per manual operator +# Finds and approves InstallPlan matching the given subscription CSV apiVersion: batch/v1 kind: Job metadata: - name: {{ printf "%s-%s" .csv "approver" | trunc -63 | trimAll "-" }} + name: {{ printf "%s-%s" .csv "approver" | trunc -63 | replace "." "-" | trimAll "-" }} namespace: {{ .namespace | default $.Release.Namespace }} labels: {{- include "operators-installer.labels" $ | nindent 4 }} @@ -21,7 +22,7 @@ spec: completions: 1 parallelism: 1 backoffLimit: {{ .installPlanApproverRetries | default 10 }} - activeDeadlineSeconds: {{ .installPlanApproverActiveDeadlineSeconds | default 240 }} + activeDeadlineSeconds: {{ .installPlanApproverActiveDeadlineSeconds }} template: spec: containers: @@ -30,27 +31,39 @@ spec: command: ["/bin/sh","-c"] args: - >- + {{- if $.Values.installRequiredPythonLibraries }} python3 -m venv /tmp/venv && source /tmp/venv/bin/activate && - pip3 install openshift-client && + python3 -m pip install openshift-client semver==2.13.0 --index-url {{ $.Values.pythonIndexURL }} --extra-index-url {{ $.Values.pythonExtraIndexURL }} && + {{- end }} + {{- if .automaticIntermediateManualUpgrades }} + python3 /scripts/installplan-incremental-approver.py + {{- else }} python3 /scripts/installplan-approver.py + {{- end }} imagePullPolicy: Always env: - - name: SUBSCRIPTION_CSV - value: {{ .csv }} + - name: CSV + value: "{{ .csv }}" - name: SUBSCRIPTION - value: {{ .name }} + value: "{{ .name }}" - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace + {{- if .automaticIntermediateManualUpgrades }} + - name: INCREMENTAL_INSTALL_BACKOFF_LIMIT + value: "{{ .automaticIntermediateManualUpgradesIncrementalInstallBackoffLimit | default 10 }}" + - name: INCREMENTAL_INSTALL_DELAY_INCREMENT + value: "{{ .automaticIntermediateManualUpgradesIncrementalInstallDelayIncrement | default 5 }}" + {{- end }} volumeMounts: - - name: installplan-approver-scripts + - name: operators-installer-approver-scripts mountPath: /scripts volumes: - - name: installplan-approver-scripts + - name: operators-installer-approver-scripts configMap: - name: installplan-approver-scripts + name: {{ include "operators-installer.scriptsName" . }} defaultMode: 0777 dnsPolicy: ClusterFirst restartPolicy: Never diff --git a/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml b/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml index c8b21b25..5e549e24 100644 --- a/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml +++ b/charts/operators-installer/templates/Job_installplan-complete-verifier.yaml @@ -5,7 +5,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: {{ printf "%s-%s" .csv "verifier" | trunc -63 | trimAll "-" }} + name: {{ printf "%s-%s" .csv "verifier" | trunc -63 | replace "." "-" | trimAll "-" }} namespace: {{ .namespace | default $.Release.Namespace }} labels: {{- include "operators-installer.labels" $ | nindent 4 }} @@ -21,7 +21,7 @@ spec: completions: 1 parallelism: 1 backoffLimit: {{ .installPlanVerifierRetries | default 10 }} - activeDeadlineSeconds: {{ .installPlanVerifierActiveDeadlineSeconds | default 240 }} + activeDeadlineSeconds: {{ .installPlanVerifierActiveDeadlineSeconds }} template: spec: containers: @@ -30,13 +30,15 @@ spec: command: ["/bin/sh","-c"] args: - >- + {{- if $.Values.installRequiredPythonLibraries }} python3 -m venv /tmp/venv && source /tmp/venv/bin/activate && - pip3 install openshift-client && + python3 -m pip install openshift-client semver==2.13.0 --index-url {{ $.Values.pythonIndexURL }} --extra-index-url {{ $.Values.pythonExtraIndexURL }} && + {{- end }} python3 /scripts/installplan-verifier.py imagePullPolicy: Always env: - - name: SUBSCRIPTION_CSV + - name: CSV value: {{ .csv }} - name: SUBSCRIPTION value: {{ .name }} @@ -45,12 +47,12 @@ spec: fieldRef: fieldPath: metadata.namespace volumeMounts: - - name: installplan-approver-scripts + - name: operators-installer-approver-scripts mountPath: /scripts volumes: - - name: installplan-approver-scripts + - name: operators-installer-approver-scripts configMap: - name: installplan-approver-scripts + name: {{ include "operators-installer.scriptsName" . }} defaultMode: 0777 dnsPolicy: ClusterFirst restartPolicy: Never diff --git a/charts/operators-installer/templates/Role_installplan-approver.yaml b/charts/operators-installer/templates/Role_installplan-approver.yaml index 033127bb..50138ba1 100644 --- a/charts/operators-installer/templates/Role_installplan-approver.yaml +++ b/charts/operators-installer/templates/Role_installplan-approver.yaml @@ -27,6 +27,7 @@ rules: resources: - installplans - subscriptions + - clusterserviceversions verbs: - get - list diff --git a/charts/operators-installer/templates/_helpers.tpl b/charts/operators-installer/templates/_helpers.tpl index 4f20952b..ba2fdde6 100644 --- a/charts/operators-installer/templates/_helpers.tpl +++ b/charts/operators-installer/templates/_helpers.tpl @@ -60,5 +60,12 @@ app.kubernetes.io/instance: {{ .Release.Name }} Name to use for approver SA, Role, and RoleBinding */}} {{- define "operators-installer.approverName" -}} -{{- printf "%s-%s" .csv "-approver" | trunc -63 | trimAll "-" }} +{{- printf "%s-%s" .csv "approver" | trunc -63 | replace "." "-" | trimAll "-" }} {{- end }} + +{{/* +Name to use for approver SA, Role, and RoleBinding +*/}} +{{- define "operators-installer.scriptsName" -}} +{{- printf "%s-%s" .csv "scripts" | trunc -63 | replace "." "-" | trimAll "-" }} +{{- end }} \ No newline at end of file diff --git a/charts/operators-installer/values.yaml b/charts/operators-installer/values.yaml index dbef1dbc..60379a3d 100644 --- a/charts/operators-installer/values.yaml +++ b/charts/operators-installer/values.yaml @@ -12,6 +12,26 @@ approveManualInstallPlanViaHook: true # Image to use for the InstallPlan Approver and Verify Jobs installPlanApproverAndVerifyJobsImage: registry.redhat.io/openshift4/ose-cli:v4.15@sha256:3f2123f42ae7358e1fece41d461bf331f144480da8b7711b9a93aca150f33f3f +# If `true`, install the required Python libraries (openshift-client, semver==2.13.0) dynamically +# from the given `pythonIndexURL` and `pythonExtraIndexURL` into the `installPlanApproverAndVerifyJobsImage` at run time +# +# This is because the supported ose-cli image from red hat does not include the Python libraries +# and it is beyond the scope of this helm chart to provide an image that provides those tools. +# +# If `false`, such as if you are running in a disconnected environment, you either need to change `installPlanApproverAndVerifyJobsImage` to be an image that includes `oc` and the required python libraires. +# Or change `pythonIndexURL` and `pythonExtraIndexURL` to be a local python index with the required python libraries. +# +# Required Python libraries +# * openshift-client +# * semver==2.13.0 +installRequiredPythonLibraries: true + +# If `installRequiredPythonLibraries` is `true` then use this python index to pull required libraries +pythonIndexURL: https://pypi.org/simple/ + +# If `installRequiredPythonLibraries` is `true` then use this python extra index to pull required library dependencies +pythonExtraIndexURL: https://pypi.org/simple/ + # EXAMPLE: declaratively controlled operator version operators: # - channel: stable