Repository: operator-framework/operator-sdk
Pull request #4741 (2021-04-09 18:34:00)

workflows/deploy-manual: automate ansible-operator base image bump PR creation
**Description of the change:**
- .github/workflows/deploy-manual.yml: patch ansible-operator Dockerfile with new base image tag and create a PR on successful base image build

**Motivation for the change:** automates a step that needs doing on each ansible-operator-base image build.

**Checklist**

If the pull request includes user-facing changes, extra documentation is required:
- [ ] Add a new changelog fragment in `changelog/fragments` (see [`changelog/fragments/00-template.yaml`](https://github.com/operator-framework/operator-sdk/tree/master/changelog/fragments/00-template.yaml))
- [ ] Add or update relevant sections of the docs website in [`website/content/en/docs`](https://github.com/operator-framework/operator-sdk/tree/master/website/content/en/docs)



Patch:
diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml
index 37132228..2a4c1881 100644
--- a/.github/workflows/deploy-manual.yml
+++ b/.github/workflows/deploy-manual.yml
@@ -51,3 +51,20 @@ jobs:
         platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
         push: true
         tags: ${{ steps.tag.outputs.tag }}
+
+    # This change will be staged and committed in the PR pushed below.
+    # The script below will fail if no change was made.
+    - name: update ansible-operator base
+      run: |
+        set -e
+        sed -i -E 's|FROM quay\.io/operator-framework/ansible-operator-base:.+|FROM '"${{ steps.tag.outputs.tag }}"'|' images/ansible-operator/Dockerfile
+        git diff --exit-code --quiet && echo "Failed to update images/ansible-operator/Dockerfile" && exit 1
+
+    - name: create PR
+      uses: peter-evans/create-pull-request@v3
+      with:
+        title: "[auto] image(ansible-operator): bump base to ${{ steps.tag.outputs.tag }}"
+        commit-message: "[auto] image(ansible-operator): bump base to ${{ steps.tag.outputs.tag }}"
+        body: "New ansible-operator-base image built by https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+        delete-branch: true
+        branch-suffix: -ansible-operator-base
diff --git a/changelog/fragments/ca-secret-name-run-bundle.yaml b/changelog/fragments/ca-secret-name-run-bundle.yaml
new file mode 100644
index 00000000..222abf84
--- /dev/null
+++ b/changelog/fragments/ca-secret-name-run-bundle.yaml
@@ -0,0 +1,5 @@
+entries:
+  - description: >
+      Added `--ca-secret-name` to `run bundle` and `run bundle-upgrade` to configure
+      the registry Pod with an in-cluster certificate Secret to use TLS with a private registry.
+    kind: addition
diff --git a/changelog/fragments/helm-uninstall-wait-anno.yml b/changelog/fragments/helm-uninstall-wait-anno.yml
new file mode 100644
index 00000000..19e9b835
--- /dev/null
+++ b/changelog/fragments/helm-uninstall-wait-anno.yml
@@ -0,0 +1,6 @@
+entries:
+  - description: >
+      For Helm based-operators, added annotation `helm.sdk.operatorframework.io/uninstall-wait: "true"`
+      to allow all resources to be deleted before removing the custom resource's finalizer.
+    kind: "addition"
+    breaking: false
diff --git a/changelog/fragments/run-bundle-with-secret.yaml b/changelog/fragments/run-bundle-with-secret.yaml
index ffe121b2..9ecb4f26 100644
--- a/changelog/fragments/run-bundle-with-secret.yaml
+++ b/changelog/fragments/run-bundle-with-secret.yaml
@@ -4,7 +4,7 @@ entries:
       registry objects to a non-default service account.
     kind: addition
   - description: >
-      Added `--secret-name` to `run bundle` and `run bundle-upgrade` to configure
+      Added `--pull-secret-name` to `run bundle` and `run bundle-upgrade` to configure
       the registry Pod with an in-cluster docker config Secret
       to pull bundle images from private registries.
     kind: addition
diff --git a/go.mod b/go.mod
index f74f0e6f..6c0338fb 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/markbates/inflect v1.0.4
 	github.com/onsi/ginkgo v1.15.0
 	github.com/onsi/gomega v1.10.5
-	github.com/operator-framework/api v0.5.3
+	github.com/operator-framework/api v0.8.0
 	github.com/operator-framework/operator-lib v0.4.0
 	github.com/operator-framework/operator-registry v1.15.3
 	github.com/prometheus/client_golang v1.7.1
@@ -36,7 +36,7 @@ require (
 	rsc.io/letsencrypt v0.0.3 // indirect
 	sigs.k8s.io/controller-runtime v0.8.2
 	sigs.k8s.io/controller-tools v0.5.0
-	sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210407152553-d5e70a57ccb0
+	sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210411191801-c7525d748153
 	sigs.k8s.io/yaml v1.2.0
 )
 
diff --git a/go.sum b/go.sum
index d615f8dd..aa1f88dd 100644
--- a/go.sum
+++ b/go.sum
@@ -802,8 +802,8 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
 github.com/operator-framework/api v0.3.22/go.mod h1:GVNiB6AQucwdZz3ZFXNv9HtcLOzcFnr6O/QldzKG93g=
 github.com/operator-framework/api v0.5.2 h1:NLgOoi70+iyz4vVJeeJUKaFT8wZaCbCHzS1eExCqX7A=
 github.com/operator-framework/api v0.5.2/go.mod h1:L7IvLd/ckxJEJg/t4oTTlnHKAJIP/p51AvEslW3wYdY=
-github.com/operator-framework/api v0.5.3 h1:1RDw2FbuEDtSC7ONKLKgAxtq+iRsV7rRTumD4RLQfSI=
-github.com/operator-framework/api v0.5.3/go.mod h1:L7IvLd/ckxJEJg/t4oTTlnHKAJIP/p51AvEslW3wYdY=
+github.com/operator-framework/api v0.8.0 h1:S1R5BaPKeZoACbu0913mPnG33s7GOA2VrT9gvOeBjbU=
+github.com/operator-framework/api v0.8.0/go.mod h1:L7IvLd/ckxJEJg/t4oTTlnHKAJIP/p51AvEslW3wYdY=
 github.com/operator-framework/operator-lib v0.4.0 h1:g7tGRo+FikHgFZDmRdHkOxyTv3sViI+Ujiqbfd9Tfsk=
 github.com/operator-framework/operator-lib v0.4.0/go.mod h1:kOjV7h01DCSw3RZAqYdHyHyVwuJL8hvG53tSZoDZfsQ=
 github.com/operator-framework/operator-registry v1.15.3 h1:C+u+zjDh6yQAKN+DbUvPeLjojZtJftvp/J28rRqiWWU=
@@ -1557,8 +1557,8 @@ sigs.k8s.io/controller-tools v0.4.1 h1:VkuV0MxlRPmRu5iTgBZU4UxUX2LiR99n3sdQGRxZF
 sigs.k8s.io/controller-tools v0.4.1/go.mod h1:G9rHdZMVlBDocIxGkK3jHLWqcTMNvveypYJwrvYKjWU=
 sigs.k8s.io/controller-tools v0.5.0 h1:3u2RCwOlp0cjCALAigpOcbAf50pE+kHSdueUosrC/AE=
 sigs.k8s.io/controller-tools v0.5.0/go.mod h1:JTsstrMpxs+9BUj6eGuAaEb6SDSPTeVtUyp0jmnAM/I=
-sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210407152553-d5e70a57ccb0 h1:7xXfjzq8NfOwBEo3ZGa/+G5uABPfHIDXdoy3ERn5kxQ=
-sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210407152553-d5e70a57ccb0/go.mod h1:eVtLdWzmvL1ixDYLlVrvQe8wjpikJVoSOg5PghTk2Lw=
+sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210411191801-c7525d748153 h1:zNScO/WTy8ZTg3lxnjm1y/oHcZgKJx0IY4axJCHKSE0=
+sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210411191801-c7525d748153/go.mod h1:eVtLdWzmvL1ixDYLlVrvQe8wjpikJVoSOg5PghTk2Lw=
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
 sigs.k8s.io/kustomize/kyaml v0.10.10 h1:caAxDDkaXZp+0kDsZVik4leFJV8LCy09PdVqpaoNeF4=
diff --git a/images/ansible-operator/Dockerfile b/images/ansible-operator/Dockerfile
index d54da6b5..740719af 100644
--- a/images/ansible-operator/Dockerfile
+++ b/images/ansible-operator/Dockerfile
@@ -17,7 +17,7 @@ COPY . .
 RUN GOOS=linux GOARCH=$TARGETARCH make build/ansible-operator
 
 # Final image.
-FROM quay.io/operator-framework/ansible-operator-base:b187f86157fbdc115da60d275628b3dd633e55c2
+FROM quay.io/operator-framework/ansible-operator-base:master-d31e4e800c0f159a2cd9f5ae375dbda00789f474
 
 ENV HOME=/opt/ansible \
     USER_NAME=ansible \
diff --git a/internal/cmd/operator-sdk/alpha/config3alphato3/convert_config_3-alpha_to_3.go b/internal/cmd/operator-sdk/alpha/config3alphato3/convert_config_3-alpha_to_3.go
index 23abf241..fd19c6cf 100644
--- a/internal/cmd/operator-sdk/alpha/config3alphato3/convert_config_3-alpha_to_3.go
+++ b/internal/cmd/operator-sdk/alpha/config3alphato3/convert_config_3-alpha_to_3.go
@@ -34,8 +34,14 @@ var (
 	versionRe = regexp.MustCompile(`version:[ ]*(?:")?3-alpha(?:")?`)
 )
 
+type templObj struct {
+	IsGo      bool
+	Resources []resource.Resource
+}
+
 // convertConfig3AlphaTo3 returns cfgBytes converted to 3 iff cfgBytes is version 3-alpha.
 func convertConfig3AlphaTo3(cfgBytes []byte) (_ []byte, err error) {
+	tObj := templObj{}
 	cfgObj := make(map[string]interface{}, 5)
 	if err := yaml.Unmarshal(cfgBytes, &cfgObj); err != nil {
 		return nil, err
@@ -109,9 +115,12 @@ func convertConfig3AlphaTo3(cfgBytes []byte) (_ []byte, err error) {
 			}
 		}
 
+		tObj.Resources = resources
+		tObj.IsGo = isGo
+
 		out := bytes.Buffer{}
 		t := template.Must(template.New("").Parse(tmpl))
-		if err := t.Execute(&out, resources); err != nil {
+		if err := t.Execute(&out, tObj); err != nil {
 			return nil, err
 		}
 
@@ -152,7 +161,8 @@ var getModulePath = func() (string, error) {
 
 // Comment-heavy "resources" template.
 const tmpl = `resources:
-{{- range $i, $res := . }}
+{{- $isGo := .IsGo }}
+{{- range $i, $res := .Resources }}
 -{{- if $res.API }} api:
     {{- if $res.API.CRDVersion }}
     crdVersion: {{ $res.API.CRDVersion }}
@@ -163,8 +173,10 @@ const tmpl = `resources:
     # TODO(user): Uncomment the below line if this resource's CRD is namespace scoped, else delete it.
     # namespaced: true
   {{- end }}
+  {{- if $isGo }}
   # TODO(user): Uncomment the below line if this resource implements a controller, else delete it.
   # controller: true
+  {{- end }}
   {{- if $res.Domain }}
   domain: {{ $res.Domain }}
   {{- end }}
diff --git a/internal/cmd/operator-sdk/cli/cli.go.orig b/internal/cmd/operator-sdk/cli/cli.go.orig
deleted file mode 100644
index 15174e4b..00000000
--- a/internal/cmd/operator-sdk/cli/cli.go.orig
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright 2019 The Operator-SDK Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package cli
-
-import (
-	log "github.com/sirupsen/logrus"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-	"sigs.k8s.io/kubebuilder/v3/pkg/cli"
-	cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
-	cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
-	kustomizev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1"
-	declarativev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/declarative/v1"
-
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
-	golangv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2"
-	golangv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3"
-
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/alpha/config3alphato3"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/bundle"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/cleanup"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/generate"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/olm"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/run"
-	"github.com/operator-framework/operator-sdk/internal/cmd/operator-sdk/scorecard"
-	"github.com/operator-framework/operator-sdk/internal/flags"
-	"github.com/operator-framework/operator-sdk/internal/plugins"
-	ansiblev1 "github.com/operator-framework/operator-sdk/internal/plugins/ansible/v1"
-	envtestv1 "github.com/operator-framework/operator-sdk/internal/plugins/envtest/v1"
-	helmv1 "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1"
-	manifestsv2 "github.com/operator-framework/operator-sdk/internal/plugins/manifests/v2"
-	scorecardv2 "github.com/operator-framework/operator-sdk/internal/plugins/scorecard/v2"
-	"github.com/operator-framework/operator-sdk/internal/util/projutil"
-)
-
-var (
-	commands = []*cobra.Command{
-		bundle.NewCmd(),
-		cleanup.NewCmd(),
-		generate.NewCmd(),
-		olm.NewCmd(),
-		run.NewCmd(),
-		scorecard.NewCmd(),
-	}
-	alphaCommands = []*cobra.Command{
-		config3alphato3.NewCmd(),
-	}
-)
-
-func Run() error {
-	c, _ := GetPluginsCLIAndRoot()
-	return c.Run()
-}
-
-// GetPluginsCLIAndRoot returns the plugins based CLI configured to use operator-sdk as the root command
-// This CLI can run kubebuilder commands and certain SDK specific commands that are aligned for
-// the kubebuilder project layout
-func GetPluginsCLIAndRoot() (*cli.CLI, *cobra.Command) {
-	ansibleBundle, _ := plugin.NewBundle("ansible"+plugins.DefaultNameQualifier, plugin.Version{Number: 1},
-		kustomizev1.Plugin{},
-		ansiblev1.Plugin{},
-		manifestsv2.Plugin{},
-		scorecardv2.Plugin{},
-	)
-
-	// todo: Export the bundles KB and then change here to use the bundles exported instead
-	// more info: https://github.com/kubernetes-sigs/kubebuilder/pull/2112
-	gov2Bundle, _ := plugin.NewBundle(golang.DefaultNameQualifier, golangv2.Plugin{}.Version(),
-		golangv2.Plugin{},
-		envtestv1.Plugin{},
-		manifestsv2.Plugin{},
-		scorecardv2.Plugin{},
-	)
-	gov3Bundle, _ := plugin.NewBundle(golang.DefaultNameQualifier, golangv3.Plugin{}.Version(),
-		kustomizev1.Plugin{},
-		golangv3.Plugin{},
-		manifestsv2.Plugin{},
-		scorecardv2.Plugin{},
-	)
-	helmBundle, _ := plugin.NewBundle("helm"+plugins.DefaultNameQualifier, plugin.Version{Number: 1},
-		kustomizev1.Plugin{},
-		helmv1.Plugin{},
-		manifestsv2.Plugin{},
-		scorecardv2.Plugin{},
-	)
-	c, err := cli.New(
-		cli.WithCommandName("operator-sdk"),
-		cli.WithVersion(makeVersionString()),
-		cli.WithPlugins(
-			ansibleBundle,
-			gov2Bundle,
-			gov3Bundle,
-			helmBundle,
-<<<<<<< HEAD
-			kustomizev1.Plugin{},
-=======
-			declarativev1.Plugin{},
->>>>>>> plugins: add the optional kb declative plugin
-		),
-		cli.WithDefaultPlugins(cfgv2.Version, gov2Bundle),
-		cli.WithDefaultPlugins(cfgv3.Version, gov3Bundle),
-		cli.WithDefaultProjectVersion(cfgv3.Version),
-		cli.WithExtraCommands(commands...),
-		cli.WithExtraAlphaCommands(alphaCommands...),
-		cli.WithCompletion(),
-	)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// We can get the whole CLI for doc-gen/completion from the root of any
-	// command added to a CLI.
-	root := commands[0].Root()
-
-	// Configure --verbose globally.
-	// TODO(estroz): upstream PR for global --verbose.
-	root.PersistentFlags().Bool(flags.VerboseOpt, false, "Enable verbose logging")
-	if err := viper.BindPFlags(root.PersistentFlags()); err != nil {
-		log.Fatalf("Failed to bind %s flags: %v", root.Name(), err)
-	}
-	root.PersistentPreRun = rootPersistentPreRun
-
-	return c, root
-}
-
-func rootPersistentPreRun(cmd *cobra.Command, args []string) {
-	if viper.GetBool(flags.VerboseOpt) {
-		if err := projutil.SetGoVerbose(); err != nil {
-			log.Fatalf("Could not set GOFLAGS: (%v)", err)
-		}
-		log.SetLevel(log.DebugLevel)
-		log.Debug("Debug logging is set")
-	}
-
-	config3alphato3.RootPersistentPreRun(cmd, args)
-}
diff --git a/internal/cmd/operator-sdk/generate/kustomize/manifests.go b/internal/cmd/operator-sdk/generate/kustomize/manifests.go
index 092f2cf9..84a41842 100644
--- a/internal/cmd/operator-sdk/generate/kustomize/manifests.go
+++ b/internal/cmd/operator-sdk/generate/kustomize/manifests.go
@@ -15,6 +15,7 @@
 package kustomize
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -210,6 +211,11 @@ func (c manifestsCmd) run(cfg config.Config) error {
 	if err != nil {
 		return fmt.Errorf("error marshaling CSV base: %v", err)
 	}
+
+	// todo: remove it when the OLM starts to support https://github.com/operator-framework/api/pull/100
+	const cleanup = "cleanup:\n    enabled: false\n  "
+	csvBytes = bytes.ReplaceAll(csvBytes, []byte(cleanup), []byte(""))
+
 	if err = os.MkdirAll(filepath.Join(c.outputDir, "bases"), 0755); err != nil {
 		return err
 	}
diff --git a/internal/generate/internal/genutil.go b/internal/generate/internal/genutil.go
index 2a046de9..630fece9 100644
--- a/internal/generate/internal/genutil.go
+++ b/internal/generate/internal/genutil.go
@@ -15,6 +15,7 @@
 package genutil
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"io"
@@ -61,6 +62,11 @@ func WriteObject(w io.Writer, obj interface{}) error {
 	if err != nil {
 		return err
 	}
+
+	// todo: remove it when the OLM starts to support https://github.com/operator-framework/api/pull/100
+	const cleanup = "cleanup:\n    enabled: false\n  "
+	b = bytes.ReplaceAll(b, []byte(cleanup), []byte(""))
+
 	return write(w, b)
 }
 
diff --git a/internal/helm/controller/reconcile.go b/internal/helm/controller/reconcile.go
index 8fb55224..3917f86d 100644
--- a/internal/helm/controller/reconcile.go
+++ b/internal/helm/controller/reconcile.go
@@ -60,6 +60,9 @@ const (
 	uninstallFinalizer = "helm.sdk.operatorframework.io/uninstall-release"
 	// Deprecated: use uninstallFinalizer. This will be removed in operator-sdk v2.0.0.
 	uninstallFinalizerLegacy = "uninstall-helm-release"
+
+	helmUpgradeForceAnnotation  = "helm.sdk.operatorframework.io/upgrade-force"
+	helmUninstallWaitAnnotation = "helm.sdk.operatorframework.io/uninstall-wait"
 )
 
 // Reconcile reconciles the requested resource by installing, updating, or
@@ -122,25 +125,58 @@ func (r HelmOperatorReconciler) Reconcile(ctx context.Context, request reconcile
 		}
 		status.RemoveCondition(types.ConditionReleaseFailed)
 
+		wait := hasAnnotation(helmUninstallWaitAnnotation, o)
 		if errors.Is(err, driver.ErrReleaseNotFound) {
-			log.Info("Release not found, removing finalizer")
+			log.Info("Release not found")
 		} else {
 			log.Info("Uninstalled release")
 			if log.V(0).Enabled() && uninstalledRelease != nil {
 				fmt.Println(diff.Generate(uninstalledRelease.Manifest, ""))
 			}
+			if !wait {
+				status.SetCondition(types.HelmAppCondition{
+					Type:   types.ConditionDeployed,
+					Status: types.StatusFalse,
+					Reason: types.ReasonUninstallSuccessful,
+				})
+				status.DeployedRelease = nil
+			}
+		}
+		if wait {
 			status.SetCondition(types.HelmAppCondition{
-				Type:   types.ConditionDeployed,
-				Status: types.StatusFalse,
-				Reason: types.ReasonUninstallSuccessful,
+				Type:    types.ConditionDeployed,
+				Status:  types.StatusFalse,
+				Reason:  types.ReasonUninstallSuccessful,
+				Message: "Waiting until all resources are deleted.",
 			})
-			status.DeployedRelease = nil
 		}
 		if err := r.updateResourceStatus(ctx, o, status); err != nil {
 			log.Info("Failed to update CR status")
 			return reconcile.Result{}, err
 		}
 
+		if wait && status.DeployedRelease != nil && status.DeployedRelease.Manifest != "" {
+			log.Info("Uninstall wait")
+			isAllResourcesDeleted, err := manager.CleanupRelease(ctx, status.DeployedRelease.Manifest)
+			if err != nil {
+				log.Error(err, "Failed to cleanup release")
+				status.SetCondition(types.HelmAppCondition{
+					Type:    types.ConditionReleaseFailed,
+					Status:  types.StatusTrue,
+					Reason:  types.ReasonUninstallError,
+					Message: err.Error(),
+				})
+				_ = r.updateResourceStatus(ctx, o, status)
+				return reconcile.Result{}, err
+			}
+			if !isAllResourcesDeleted {
+				log.Info("Waiting until all resources are deleted")
+				return reconcile.Result{RequeueAfter: r.ReconcilePeriod}, nil
+			}
+			status.RemoveCondition(types.ConditionReleaseFailed)
+		}
+
+		log.Info("Removing finalizer")
 		controllerutil.RemoveFinalizer(o, uninstallFinalizer)
 		controllerutil.RemoveFinalizer(o, uninstallFinalizerLegacy)
 		if err := r.updateResource(ctx, o); err != nil {
@@ -254,7 +290,7 @@ func (r HelmOperatorReconciler) Reconcile(ctx context.Context, request reconcile
 			r.EventRecorder.Eventf(o, "Warning", "OverrideValuesInUse",
 				"Chart value %q overridden to %q by operator's watches.yaml", k, v)
 		}
-		force := hasHelmUpgradeForceAnnotation(o)
+		force := hasAnnotation(helmUpgradeForceAnnotation, o)
 		previousRelease, upgradedRelease, err := manager.UpgradeRelease(ctx, release.ForceUpgrade(force))
 		if err != nil {
 			log.Error(err, "Release failed")
@@ -357,16 +393,15 @@ func (r HelmOperatorReconciler) Reconcile(ctx context.Context, request reconcile
 
 // returns the boolean representation of the annotation string
 // will return false if annotation is not set
-func hasHelmUpgradeForceAnnotation(o *unstructured.Unstructured) bool {
-	const helmUpgradeForceAnnotation = "helm.sdk.operatorframework.io/upgrade-force"
-	force := o.GetAnnotations()[helmUpgradeForceAnnotation]
-	if force == "" {
+func hasAnnotation(anno string, o *unstructured.Unstructured) bool {
+	boolStr := o.GetAnnotations()[anno]
+	if boolStr == "" {
 		return false
 	}
 	value := false
-	if i, err := strconv.ParseBool(force); err != nil {
+	if i, err := strconv.ParseBool(boolStr); err != nil {
 		log.Info("Could not parse annotation as a boolean",
-			"annotation", helmUpgradeForceAnnotation, "value informed", force)
+			"annotation", anno, "value informed", boolStr)
 	} else {
 		value = i
 	}
diff --git a/internal/helm/manifestutil/resource_policy.go b/internal/helm/manifestutil/resource_policy.go
new file mode 100644
index 00000000..320d9f4d
--- /dev/null
+++ b/internal/helm/manifestutil/resource_policy.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2021 The Operator-SDK Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package manifestutil
+
+import (
+	"strings"
+
+	"helm.sh/helm/v3/pkg/kube"
+	"helm.sh/helm/v3/pkg/releaseutil"
+)
+
+// Source from https://github.com/helm/helm/blob/v3.4.2/pkg/action/resource_policy.go
+func FilterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) {
+	for _, m := range manifests {
+		if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 {
+			remaining = append(remaining, m)
+			continue
+		}
+
+		resourcePolicyType, ok := m.Head.Metadata.Annotations[kube.ResourcePolicyAnno]
+		if !ok {
+			remaining = append(remaining, m)
+			continue
+		}
+
+		resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType))
+		if resourcePolicyType == kube.KeepPolicy {
+			keep = append(keep, m)
+		}
+
+	}
+	return keep, remaining
+}
diff --git a/internal/helm/release/manager.go b/internal/helm/release/manager.go
index c2968e4d..2d494f42 100644
--- a/internal/helm/release/manager.go
+++ b/internal/helm/release/manager.go
@@ -26,8 +26,8 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	cpb "helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/kube"
-	helmkube "helm.sh/helm/v3/pkg/kube"
 	rpb "helm.sh/helm/v3/pkg/release"
+	"helm.sh/helm/v3/pkg/releaseutil"
 	"helm.sh/helm/v3/pkg/storage"
 	"helm.sh/helm/v3/pkg/storage/driver"
 	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -36,10 +36,13 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	apitypes "k8s.io/apimachinery/pkg/types"
+	apiutilerrors "k8s.io/apimachinery/pkg/util/errors"
 	"k8s.io/apimachinery/pkg/util/strategicpatch"
 	"k8s.io/cli-runtime/pkg/resource"
+	"k8s.io/client-go/discovery"
 
 	"github.com/operator-framework/operator-sdk/internal/helm/internal/types"
+	"github.com/operator-framework/operator-sdk/internal/helm/manifestutil"
 )
 
 // Manager manages a Helm release. It can install, upgrade, reconcile,
@@ -53,6 +56,7 @@ type Manager interface {
 	UpgradeRelease(context.Context, ...UpgradeOption) (*rpb.Release, *rpb.Release, error)
 	ReconcileRelease(context.Context) (*rpb.Release, error)
 	UninstallRelease(context.Context, ...UninstallOption) (*rpb.Release, error)
+	CleanupRelease(context.Context, string) (bool, error)
 }
 
 type manager struct {
@@ -293,7 +297,7 @@ func createPatch(existing runtime.Object, expected *resource.Info) ([]byte, apit
 	}
 
 	// Get a versioned object
-	versionedObject := helmkube.AsVersioned(expected)
+	versionedObject := kube.AsVersioned(expected)
 
 	// Unstructured objects, such as CRDs, may not have an not registered error
 	// returned from ConvertToVersion. Anything that's unstructured should
@@ -363,3 +367,50 @@ func (m manager) UninstallRelease(ctx context.Context, opts ...UninstallOption)
 	}
 	return uninstallResponse.Release, err
 }
+
+// CleanupRelease deletes resources if they are not deleted already.
+// Return true if all the resources are deleted, false otherwise.
+func (m manager) CleanupRelease(ctx context.Context, manifest string) (bool, error) {
+	dc, err := m.actionConfig.RESTClientGetter.ToDiscoveryClient()
+	if err != nil {
+		return false, fmt.Errorf("failed to get Kubernetes discovery client: %w", err)
+	}
+	apiVersions, err := action.GetVersionSet(dc)
+	if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
+		return false, fmt.Errorf("failed to get apiVersions from Kubernetes: %w", err)
+	}
+	manifests := releaseutil.SplitManifests(manifest)
+	_, files, err := releaseutil.SortManifests(manifests, apiVersions, releaseutil.UninstallOrder)
+	if err != nil {
+		return false, fmt.Errorf("failed to sort manifests: %w", err)
+	}
+	// do not delete resources that are annotated with the Helm resource policy 'keep'
+	_, filesToDelete := manifestutil.FilterManifestsToKeep(files)
+	var builder strings.Builder
+	for _, file := range filesToDelete {
+		builder.WriteString("\n---\n" + file.Content)
+	}
+	resources, err := m.kubeClient.Build(strings.NewReader(builder.String()), false)
+	if err != nil {
+		return false, fmt.Errorf("failed to build resources from manifests: %w", err)
+	}
+	if resources == nil || len(resources) <= 0 {
+		return true, nil
+	}
+	for _, resource := range resources {
+		err = resource.Get()
+		if err != nil {
+			if apierrors.IsNotFound(err) {
+				continue // resource is already delete, check the next one.
+			}
+			return false, fmt.Errorf("failed to get resource: %w", err)
+		}
+		// found at least one resource that is not deleted so just delete everything again.
+		_, errs := m.kubeClient.Delete(resources)
+		if len(errs) > 0 {
+			return false, fmt.Errorf("failed to delete resources: %v", apiutilerrors.NewAggregate(errs))
+		}
+		return false, nil
+	}
+	return true, nil
+}
diff --git a/internal/olm/operator/registry/index/registry_pod.go b/internal/olm/operator/registry/index/registry_pod.go
index af39e6f2..5345aadc 100644
--- a/internal/olm/operator/registry/index/registry_pod.go
+++ b/internal/olm/operator/registry/index/registry_pod.go
@@ -72,6 +72,11 @@ type RegistryPod struct {
 	// can pull bundle images from a private registry.
 	SecretName string
 
+	// SecretName holds the name of a secret for a CA cert file containing root certificates.
+	// This file is transiently added to the registry Pod's cert pool via `opm registry add --ca-file`.
+	// The secret's key for this file must be "cert.pem".
+	CASecretName string
+
 	// pod represents a kubernetes *corev1.pod that will be created on a cluster using an index image
 	pod *corev1.Pod
 
@@ -113,11 +118,11 @@ func (rp *RegistryPod) Create(ctx context.Context, cfg *operator.Configuration,
 
 	// make catalog source the owner of registry pod object
 	if err := controllerutil.SetOwnerReference(cs, rp.pod, rp.cfg.Scheme); err != nil {
-		return nil, fmt.Errorf("set registry pod owner reference: %v", err)
+		return nil, fmt.Errorf("error setting owner reference: %w", err)
 	}
 
 	if err := rp.cfg.Client.Create(ctx, rp.pod); err != nil {
-		return nil, fmt.Errorf("create registry pod: %v", err)
+		return nil, fmt.Errorf("error creating pod: %w", err)
 	}
 
 	// get registry pod key
@@ -225,37 +230,58 @@ func (rp *RegistryPod) podForBundleRegistry() (*corev1.Pod, error) {
 	}
 
 	addImagePullSecret(rp.pod, rp.SecretName)
+	addCertSecret(rp.pod, rp.CASecretName)
 
 	return rp.pod, nil
 }
 
-// addImagePullSecret creates and mounts an image pull secret volume
-// for a docker config secret "secretName" in each container in pod.
+// addImagePullSecret creates a docker config volume for secretName
+// and a volumeMount for that secret in each container in pod.
 func addImagePullSecret(pod *corev1.Pod, secretName string) {
 	if secretName == "" {
 		return
 	}
 
-	pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
+	// Require a non-legacy docker config secret.
+	volume := makeSecretVolume(secretName, corev1.KeyToPath{Key: ".dockerconfigjson", Path: ".docker/config.json"})
+	pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
+
+	addVolumeMountForSecret(pod, volume.Name, "/root")
+}
+
+// addCertSecret creates and mounts a volume containing a CA root certificate
+// to pass to `opm registry add`.
+func addCertSecret(pod *corev1.Pod, secretName string) {
+	if secretName == "" {
+		return
+	}
+
+	// Ensure the secret contains a key "cert.pem".
+	volume := makeSecretVolume(secretName, corev1.KeyToPath{Key: "cert.pem", Path: "cert.pem"})
+	pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
+
+	addVolumeMountForSecret(pod, volume.Name, "/certs")
+}
+
+func makeSecretVolume(secretName string, items ...corev1.KeyToPath) corev1.Volume {
+	return corev1.Volume{
 		Name: secretName,
 		VolumeSource: corev1.VolumeSource{
 			Secret: &corev1.SecretVolumeSource{
 				SecretName:  secretName,
 				DefaultMode: newInt32(0400),
 				Optional:    newBool(false),
-				Items: []corev1.KeyToPath{
-					// Require a non-legacy docker config secret.
-					{Key: ".dockerconfigjson", Path: ".docker/config.json"},
-				},
+				Items:       items,
 			},
 		},
-	})
+	}
+}
 
+func addVolumeMountForSecret(pod *corev1.Pod, secretName, mountPath string) {
 	volumeMount := corev1.VolumeMount{
-		Name:     secretName,
-		ReadOnly: true,
-		// Mount in $HOME.
-		MountPath: "/root",
+		Name:      secretName,
+		ReadOnly:  true,
+		MountPath: mountPath,
 	}
 	for i := range pod.Spec.Containers {
 		pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount)
@@ -274,9 +300,9 @@ func newBool(b bool) *bool {
 	return bp
 }
 
-const containerCommand = `/bin/mkdir -p {{ dirname .DBPath }} && \
+const cmdTemplate = `/bin/mkdir -p {{ dirname .DBPath }} && \
 {{- range $i, $item := .BundleItems }}
-/bin/opm registry add -d {{ $.DBPath }} -b {{ $item.ImageTag }} --mode={{ $item.AddMode }} && \
+/bin/opm registry add -d {{ $.DBPath }} -b {{ $item.ImageTag }} --mode={{ $item.AddMode }}{{ if $.CASecretName }} --ca-file=/certs/cert.pem{{ end }} && \
 {{- end }}
 /bin/opm registry serve -d {{ .DBPath }} -p {{ .GRPCPort }}
 `
@@ -291,13 +317,13 @@ func (rp *RegistryPod) getContainerCmd() (string, error) {
 	}
 
 	// add the custom dirname template function to the
-	// template's FuncMap and parse the containerCommand
-	tmp := template.Must(template.New("cmd").Funcs(funcMap).Parse(containerCommand))
+	// template's FuncMap and parse the cmdTemplate
+	t := template.Must(template.New("cmd").Funcs(funcMap).Parse(cmdTemplate))
 
-	// execute the command by applying the parsed tmp to command
+	// execute the command by applying the parsed t to command
 	// and write command output to out
 	out := &bytes.Buffer{}
-	if err := tmp.Execute(out, rp); err != nil {
+	if err := t.Execute(out, rp); err != nil {
 		return "", fmt.Errorf("parse container command: %w", err)
 	}
 
diff --git a/internal/olm/operator/registry/index_image.go b/internal/olm/operator/registry/index_image.go
index 2d842b86..a009c3ba 100644
--- a/internal/olm/operator/registry/index_image.go
+++ b/internal/olm/operator/registry/index_image.go
@@ -64,6 +64,7 @@ type IndexImageCatalogCreator struct {
 	BundleImage   string
 	BundleAddMode index.BundleAddMode
 	SecretName    string
+	CASecretName  string
 
 	cfg *operator.Configuration
 }
@@ -78,10 +79,14 @@ func NewIndexImageCatalogCreator(cfg *operator.Configuration) *IndexImageCatalog
 }
 
 func (c *IndexImageCatalogCreator) BindFlags(fs *pflag.FlagSet) {
-	fs.StringVar(&c.SecretName, "secret-name", "",
+	fs.StringVar(&c.SecretName, "pull-secret-name", "",
 		"Name of image pull secret (\"type: kubernetes.io/dockerconfigjson\") required "+
 			"to pull bundle images. This secret *must* be both in the namespace and an "+
 			"imagePullSecret of the service account that this command is configured to run in")
+	fs.StringVar(&c.CASecretName, "ca-secret-name", "",
+		"Name of a generic secret containing a PEM root certificate file required to pull bundle images. "+
+			"This secret *must* be in the namespace that this command is configured to run in, "+
+			"and the file *must* be encoded under the key \"cert.pem\"")
 }
 
 func (c IndexImageCatalogCreator) CreateCatalog(ctx context.Context, name string) (*v1alpha1.CatalogSource, error) {
@@ -153,7 +158,7 @@ func (c IndexImageCatalogCreator) UpdateCatalog(ctx context.Context, cs *v1alpha
 
 	if prevRegistryPodName != "" {
 		if err = c.deleteRegistryPod(ctx, prevRegistryPodName); err != nil {
-			return fmt.Errorf("error cleaning up previous registry pod: %v", err)
+			return fmt.Errorf("error cleaning up previous registry: %v", err)
 		}
 	}
 
@@ -182,16 +187,17 @@ func (c IndexImageCatalogCreator) createAnnotatedRegistry(ctx context.Context, c
 	}
 	// Initialize and create registry pod
 	registryPod := index.RegistryPod{
-		BundleItems: items,
-		IndexImage:  c.IndexImage,
-		SecretName:  c.SecretName,
+		BundleItems:  items,
+		IndexImage:   c.IndexImage,
+		SecretName:   c.SecretName,
+		CASecretName: c.CASecretName,
 	}
 	if registryPod.DBPath, err = c.getDBPath(ctx); err != nil {
 		return fmt.Errorf("get database path: %v", err)
 	}
 	pod, err := registryPod.Create(ctx, c.cfg, cs)
 	if err != nil {
-		return fmt.Errorf("error creating registry pod: %v", err)
+		return err
 	}
 
 	// JSON marshal injected bundles
diff --git a/website/content/en/docs/building-operators/ansible/migration.md b/website/content/en/docs/building-operators/ansible/migration.md
index d0da2c3a..cce3fe71 100644
--- a/website/content/en/docs/building-operators/ansible/migration.md
+++ b/website/content/en/docs/building-operators/ansible/migration.md
@@ -49,8 +49,7 @@ The easy migration path is to initialize a new project, re-recreate APIs, then c
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 ### Creating a new project
 
@@ -297,7 +296,7 @@ kubectl logs deployment.apps/memcached-operator-controller-manager -n memcached-
 For further steps regarding the deployment of the operator, creation of custom resources, and cleaning up of resources, see the [tutorial][tutorial-deploy].
 
 [install-guide]: /docs/building-operators/ansible/installation
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [kustomize]: https://github.com/kubernetes-sigs/kustomize
 [kube-auth-proxy]: https://github.com/brancz/kube-rbac-proxy
 [metrics]: https://book.kubebuilder.io/reference/metrics.html?highlight=metr#metrics
@@ -306,4 +305,3 @@ For further steps regarding the deployment of the operator, creation of custom r
 [testing-guide]: /docs/building-operators/ansible/testing-guide
 [migration-doc]: /docs/upgrading-sdk-version/
 [tutorial-deploy]: /docs/building-operators/ansible/tutorial/#run-the-operator
-
diff --git a/website/content/en/docs/building-operators/ansible/quickstart.md b/website/content/en/docs/building-operators/ansible/quickstart.md
index 478d8cf8..9d762a85 100644
--- a/website/content/en/docs/building-operators/ansible/quickstart.md
+++ b/website/content/en/docs/building-operators/ansible/quickstart.md
@@ -15,8 +15,7 @@ This guide walks through an example of building a simple memcached-operator powe
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 
 ## Steps
@@ -56,14 +55,10 @@ This guide walks through an example of building a simple memcached-operator powe
   make bundle-build bundle-push
   ```
 
-1. Run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+1. Run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
   ```sh
-  kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
   operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
   ```
 
@@ -110,6 +105,6 @@ Read the [full tutorial][tutorial] for an in-depth walkthough of building a Ansi
 
 [ansible-link]:https://www.ansible.com/
 [install-guide]:/docs/building-operators/ansible/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/ansible/tutorial/
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/website/content/en/docs/building-operators/ansible/tutorial.md b/website/content/en/docs/building-operators/ansible/tutorial.md
index e8dacffe..a312398f 100644
--- a/website/content/en/docs/building-operators/ansible/tutorial.md
+++ b/website/content/en/docs/building-operators/ansible/tutorial.md
@@ -16,8 +16,7 @@ please [migrate][migration-guide], or consult the [legacy docs][legacy-quickstar
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 
 ## Overview
@@ -209,14 +208,10 @@ in the `bundle` directory containing manifests and metadata defining your operat
 make bundle bundle-build bundle-push
 ```
 
-Finally, run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+Finally, run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
 ```sh
-kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
 operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
 ```
 
@@ -356,6 +351,7 @@ OLM will manage creation of most if not all resources required to run your opera
 [legacy-quickstart-doc]:https://v0-19-x.sdk.operatorframework.io/docs/ansible/quickstart/
 [migration-guide]:/docs/building-operators/ansible/migration
 [install-guide]:/docs/building-operators/ansible/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [ansible-developer-tips]:/docs/building-operators/ansible/development-tips/
 [ansible-watches]:/docs/building-operators/ansible/reference/watches
 [custom-resources]:https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
@@ -363,4 +359,3 @@ OLM will manage creation of most if not all resources required to run your opera
 [doc-bundle]:https://github.com/operator-framework/operator-registry/blob/v1.16.1/docs/design/operator-bundle.md#operator-bundle
 [quickstart-bundle]:/docs/olm-integration/quickstart-bundle
 [doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/website/content/en/docs/building-operators/golang/migration.md b/website/content/en/docs/building-operators/golang/migration.md
index ecedf558..543cd702 100644
--- a/website/content/en/docs/building-operators/golang/migration.md
+++ b/website/content/en/docs/building-operators/golang/migration.md
@@ -57,8 +57,7 @@ The easy migration path is to initialize a new project, re-recreate APIs, then c
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 ### Create a new project
 
@@ -417,7 +416,7 @@ For further steps regarding the deployment of the operator, creation of custom r
 
 
 [install-guide]: /docs/building-operators/ansible/installation
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [metrics]: https://book.kubebuilder.io/reference/metrics.html?highlight=metr#metrics
 [memcached_controller]: https://github.com/operator-framework/operator-sdk/tree/master/testdata/go/v3/memcached-operator
 [rbac_markers]: https://book.kubebuilder.io/reference/markers/rbac.html
diff --git a/website/content/en/docs/building-operators/golang/quickstart.md b/website/content/en/docs/building-operators/golang/quickstart.md
index 130925ea..c38e143b 100644
--- a/website/content/en/docs/building-operators/golang/quickstart.md
+++ b/website/content/en/docs/building-operators/golang/quickstart.md
@@ -15,8 +15,7 @@ This guide walks through an example of building a simple memcached-operator usin
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 
 ## Steps
@@ -56,14 +55,10 @@ This guide walks through an example of building a simple memcached-operator usin
   make bundle-build bundle-push
   ```
 
-1. Run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+1. Run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
   ```sh
-  kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
   operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
   ```
 
@@ -109,6 +104,6 @@ Read the [full tutorial][tutorial] for an in-depth walkthough of building a Go o
 
 
 [install-guide]:/docs/building-operators/golang/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/golang/tutorial/
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/website/content/en/docs/building-operators/golang/tutorial.md b/website/content/en/docs/building-operators/golang/tutorial.md
index 1b6aa6ae..bd70fcc9 100644
--- a/website/content/en/docs/building-operators/golang/tutorial.md
+++ b/website/content/en/docs/building-operators/golang/tutorial.md
@@ -16,8 +16,7 @@ please [migrate][migration-guide], or consult the [legacy docs][legacy-quickstar
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 ## Overview
 
@@ -339,14 +338,10 @@ in the `bundle` directory containing manifests and metadata defining your operat
 make bundle bundle-build bundle-push
 ```
 
-Finally, run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+Finally, run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
 ```sh
-kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
 operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
 ```
 
@@ -448,6 +443,7 @@ Next, check out the following:
 [legacy-quickstart-doc]:https://v0-19-x.sdk.operatorframework.io/docs/golang/legacy/quickstart/
 [migration-guide]:/docs/building-operators/golang/migration
 [install-guide]:/docs/building-operators/golang/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [enqueue_requests_from_map_func]: https://godoc.org/sigs.k8s.io/controller-runtime/pkg/handler#EnqueueRequestsFromMapFunc
 [event_handler_godocs]: https://godoc.org/sigs.k8s.io/controller-runtime/pkg/handler#hdr-EventHandlers
 [event_filtering]:/docs/building-operators/golang/references/event-filtering/
@@ -492,4 +488,3 @@ Next, check out the following:
 [openapi-validation]: /docs/building-operators/golang/references/openapi-validation
 [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime
 [kb-doc-gkvs]: https://book.kubebuilder.io/cronjob-tutorial/gvks.html
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/website/content/en/docs/building-operators/helm/migration.md b/website/content/en/docs/building-operators/helm/migration.md
index 12750227..53fc45cc 100644
--- a/website/content/en/docs/building-operators/helm/migration.md
+++ b/website/content/en/docs/building-operators/helm/migration.md
@@ -48,8 +48,7 @@ The easy migration path is to initialize a new project, re-recreate APIs, then c
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 ### Creating a new project
 
@@ -183,7 +182,7 @@ kubectl logs deployment.apps/nginx-operator-controller-manager -n nginx-operator
 For further steps regarding the deployment of the operator, creation of custom resources, and cleaning up of resources, see the [tutorial][tutorial-deploy].
 
 [install-guide]: /docs/building-operators/helm/installation
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [kustomize]: https://github.com/kubernetes-sigs/kustomize
 [kube-auth-proxy]: https://github.com/brancz/kube-rbac-proxy
 [metrics]: https://book.kubebuilder.io/reference/metrics.html?highlight=metr#metrics
diff --git a/website/content/en/docs/building-operators/helm/quickstart.md b/website/content/en/docs/building-operators/helm/quickstart.md
index 7b41d317..06d57804 100644
--- a/website/content/en/docs/building-operators/helm/quickstart.md
+++ b/website/content/en/docs/building-operators/helm/quickstart.md
@@ -15,8 +15,7 @@ This guide walks through an example of building a simple nginx-operator powered
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 
 ## Steps
@@ -57,14 +56,10 @@ This guide walks through an example of building a simple nginx-operator powered
   make bundle-build bundle-push
   ```
 
-1. Run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+1. Run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
   ```sh
-  kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
   operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
   ```
 
@@ -110,6 +105,6 @@ Read the [full tutorial][tutorial] for an in-depth walkthough of building a Helm
 
 [helm-official]:https://helm.sh/docs/
 [install-guide]:/docs/building-operators/helm/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/helm/tutorial/
-[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/website/content/en/docs/building-operators/helm/reference/advanced_features/annotations.md b/website/content/en/docs/building-operators/helm/reference/advanced_features/annotations.md
index 09445546..604bd059 100644
--- a/website/content/en/docs/building-operators/helm/reference/advanced_features/annotations.md
+++ b/website/content/en/docs/building-operators/helm/reference/advanced_features/annotations.md
@@ -33,3 +33,115 @@ log message when an upgrade succeeds:
 ```
 {"level":"info","ts":1591198931.1703992,"logger":"helm.controller","msg":"Upgraded release","namespace":"helm-nginx","name":"example-nginx","apiVersion":"cache.example.com/v1alpha1","kind":"Nginx","release":"example-nginx","force":true}
 ```
+
+## `helm.sdk.operatorframework.io/uninstall-wait`
+
+This annotation can be set to `"true"` on custom resources to enable the deletion to wait until all the resources in the
+`status.deployedRelease.manifest` are deleted. 
+
+**Example**
+
+```yaml
+apiVersion: example.com/v1alpha1
+kind: Nginx
+metadata:
+  name: nginx-sample
+  annotations:
+    helm.sdk.operatorframework.io/uninstall-wait: "true"
+spec:
+...
+status:
+  ...
+  deployedRelease:
+    manifest: |
+      ---
+      # Source: nginx/templates/serviceaccount.yaml
+      apiVersion: v1
+      kind: ServiceAccount
+      metadata:
+        name: nginx-sample
+        labels:
+          helm.sh/chart: nginx-0.1.0
+          app.kubernetes.io/name: nginx
+          app.kubernetes.io/instance: nginx-sample
+          app.kubernetes.io/version: "1.16.0"
+          app.kubernetes.io/managed-by: Helm
+      ---
+      # Source: nginx/templates/service.yaml
+      apiVersion: v1
+      kind: Service
+      metadata:
+        name: nginx-sample
+        labels:
+          helm.sh/chart: nginx-0.1.0
+          app.kubernetes.io/name: nginx
+          app.kubernetes.io/instance: nginx-sample
+          app.kubernetes.io/version: "1.16.0"
+          app.kubernetes.io/managed-by: Helm
+      spec:
+        type: ClusterIP
+        ports:
+          - port: 80
+            targetPort: http
+            protocol: TCP
+            name: http
+        selector:
+          app.kubernetes.io/name: nginx
+          app.kubernetes.io/instance: nginx-sample
+      ---
+      # Source: nginx/templates/deployment.yaml
+      apiVersion: apps/v1
+      kind: Deployment
+      metadata:
+        name: nginx-sample
+        labels:
+          helm.sh/chart: nginx-0.1.0
+          app.kubernetes.io/name: nginx
+          app.kubernetes.io/instance: nginx-sample
+          app.kubernetes.io/version: "1.16.0"
+          app.kubernetes.io/managed-by: Helm
+      spec:
+        replicas: 1
+        selector:
+          matchLabels:
+            app.kubernetes.io/name: nginx
+            app.kubernetes.io/instance: nginx-sample
+        template:
+          metadata:
+            labels:
+              app.kubernetes.io/name: nginx
+              app.kubernetes.io/instance: nginx-sample
+          spec:
+            serviceAccountName: nginx-sample
+            securityContext:
+              {}
+            containers:
+              - name: nginx
+                securityContext:
+                  {}
+                image: "nginx:1.16.0"
+                imagePullPolicy: IfNotPresent
+                ports:
+                  - name: http
+                    containerPort: 80
+                    protocol: TCP
+                livenessProbe:
+                  httpGet:
+                    path: /
+                    port: http
+                readinessProbe:
+                  httpGet:
+                    path: /
+                    port: http
+                resources:
+                  {}
+```
+
+Setting this annotation to `true` and deleting the custom resource will cause the custom resource to be reconciled
+continuously until all the resources in `status.deployedRelease.manifest` are deleted. This can be verified in the
+log message when a delete has been triggered:
+
+```
+{"level":"info","ts":1612294054.5845876,"logger":"helm.controller","msg":"Uninstall wait","namespace":"default","name":"nginx-sample","apiVersion":"example.com/v1alpha1","kind":"Nginx","release":"nginx-sample"}
+
+```
diff --git a/website/content/en/docs/building-operators/helm/tutorial.md b/website/content/en/docs/building-operators/helm/tutorial.md
index 7842fce6..fe9e4d1b 100644
--- a/website/content/en/docs/building-operators/helm/tutorial.md
+++ b/website/content/en/docs/building-operators/helm/tutorial.md
@@ -16,8 +16,7 @@ please [migrate][migration-guide], or consult the [legacy docs][legacy-quickstar
 [quay.io](https://quay.io/)) and be logged in in your command line environment.
   - `example.com` is used as the registry Docker Hub namespace in these examples.
   Replace it with another value if using a different registry or namespace.
-  - The registry/namespace must be public, or the cluster must be provisioned with an
-  [image pull secret][k8s-image-pull-sec] if the image namespace is private.
+  - [Authentication and certificates][image-reg-config] if the registry is private or uses a custom CA.
 
 ## Overview
 
@@ -242,14 +241,10 @@ in the `bundle` directory containing manifests and metadata defining your operat
 make bundle bundle-build bundle-push
 ```
 
-Finally, run your bundle. If your bundle image is hosted in a private registry,
-add the image pull secret for that registry host to the service account in use
-and set `--secret-name` to the secret name:
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+Finally, run your bundle. If your bundle image is hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
 ```sh
-kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"<reg secret name>"}]}'
 operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
 ```
 
@@ -366,6 +361,7 @@ Next, check out the following:
 [legacy-quickstart-doc]:https://v0-19-x.sdk.operatorframework.io/docs/helm/quickstart/
 [migration-guide]:/docs/building-operators/helm/migration
 [install-guide]:/docs/building-operators/helm/installation
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [layout-doc]: /docs/overview/project-layout
 [helm-charts]:https://helm.sh/docs/topics/charts/
 [helm-values]:https://helm.sh/docs/intro/using_helm/#customizing-the-chart-before-installing
diff --git a/website/content/en/docs/cli/operator-sdk_run_bundle-upgrade.md b/website/content/en/docs/cli/operator-sdk_run_bundle-upgrade.md
index 2aa458d2..461e0f2a 100644
--- a/website/content/en/docs/cli/operator-sdk_run_bundle-upgrade.md
+++ b/website/content/en/docs/cli/operator-sdk_run_bundle-upgrade.md
@@ -17,12 +17,13 @@ operator-sdk run bundle-upgrade <bundle-image> [flags]
 ### Options
 
 ```
-  -h, --help                     help for bundle-upgrade
-      --kubeconfig string        Path to the kubeconfig file to use for CLI requests.
-  -n, --namespace string         If present, namespace scope for this CLI request
-      --secret-name string       Name of image pull secret ("type: kubernetes.io/dockerconfigjson") required to pull bundle images. This secret *must* be both in the namespace and an imagePullSecret of the service account that this command is configured to run in
-      --service-account string   Service account name to bind registry objects to. If unset, the default service account is used. This value does not override the operator's service account
-      --timeout duration         Duration to wait for the command to complete before failing (default 2m0s)
+      --ca-secret-name string     Name of a generic secret containing a PEM root certificate file required to pull bundle images. This secret *must* be in the namespace that this command is configured to run in, and the file *must* be encoded under the key "cert.pem"
+  -h, --help                      help for bundle-upgrade
+      --kubeconfig string         Path to the kubeconfig file to use for CLI requests.
+  -n, --namespace string          If present, namespace scope for this CLI request
+      --pull-secret-name string   Name of image pull secret ("type: kubernetes.io/dockerconfigjson") required to pull bundle images. This secret *must* be both in the namespace and an imagePullSecret of the service account that this command is configured to run in
+      --service-account string    Service account name to bind registry objects to. If unset, the default service account is used. This value does not override the operator's service account
+      --timeout duration          Duration to wait for the command to complete before failing (default 2m0s)
 ```
 
 ### Options inherited from parent commands
diff --git a/website/content/en/docs/cli/operator-sdk_run_bundle.md b/website/content/en/docs/cli/operator-sdk_run_bundle.md
index 20603011..c80e1a35 100644
--- a/website/content/en/docs/cli/operator-sdk_run_bundle.md
+++ b/website/content/en/docs/cli/operator-sdk_run_bundle.md
@@ -17,12 +17,13 @@ operator-sdk run bundle <bundle-image> [flags]
 ### Options
 
 ```
+      --ca-secret-name string           Name of a generic secret containing a PEM root certificate file required to pull bundle images. This secret *must* be in the namespace that this command is configured to run in, and the file *must* be encoded under the key "cert.pem"
   -h, --help                            help for bundle
       --index-image string              index image in which to inject bundle (default "quay.io/operator-framework/upstream-opm-builder:latest")
       --install-mode InstallModeValue   install mode
       --kubeconfig string               Path to the kubeconfig file to use for CLI requests.
   -n, --namespace string                If present, namespace scope for this CLI request
-      --secret-name string              Name of image pull secret ("type: kubernetes.io/dockerconfigjson") required to pull bundle images. This secret *must* be both in the namespace and an imagePullSecret of the service account that this command is configured to run in
+      --pull-secret-name string         Name of image pull secret ("type: kubernetes.io/dockerconfigjson") required to pull bundle images. This secret *must* be both in the namespace and an imagePullSecret of the service account that this command is configured to run in
       --service-account string          Service account name to bind registry objects to. If unset, the default service account is used. This value does not override the operator's service account
       --timeout duration                Duration to wait for the command to complete before failing (default 2m0s)
 ```
diff --git a/website/content/en/docs/olm-integration/cli-overview.md b/website/content/en/docs/olm-integration/cli-overview.md
index 032649c3..e3f5caec 100644
--- a/website/content/en/docs/olm-integration/cli-overview.md
+++ b/website/content/en/docs/olm-integration/cli-overview.md
@@ -8,9 +8,7 @@ This document gives an overview of using `operator-sdk` to work with Operator ma
 namely [bundles][bundle] and [package manifests][package-manifests]. See the [manifests generation][doc-olm-generate]
 doc for an in-depth discussion of these commands.
 
-## Commands
-
-### OLM installation
+## OLM installation
 
 The following `operator-sdk` subcommands manage an OLM installation:
 
@@ -20,16 +18,16 @@ can infer the version of an error-free OLM installation.
 - [`olm uninstall`][cli-olm-uninstall]: uninstall a particular version of OLM running in a cluster. This command
 can infer the version of an error-free OLM installation.
 
-### Manifests and metadata
+## Manifests and metadata
 
 The following `make` recipes and `operator-sdk` subcommands create or interact with Operator package manifests and bundles:
 
 - [`generate kustomize manifests`][cli-gen-kustomize-manifests]: creates kustomize bases and a `kustomization.yaml` in `config/manifests`.
 
-##### Bundles
+### Bundles
 
 - `make bundle`: runs the following commands:
-  - `generate kustomize manifests`: see [above](#commands).
+  - `generate kustomize manifests`: see [above](#manifests-and-metadata).
   - [`generate bundle`][cli-gen-bundle]: creates a new or updates an existing bundle in the `<project-root>/bundle`
   directory. This command generates both manifests and metadata.
   - [`bundle validate`][cli-bundle-validate]: validates an Operator bundle image or unpacked manifests and metadata.
@@ -38,7 +36,53 @@ The following `make` recipes and `operator-sdk` subcommands create or interact w
   existing OLM installation.
 - [`run bundle-upgrade`][cli-run-bundle-upgrade]: upgrades the Operator bundle to a specified newer version.
 
-##### Package Manifests
+#### Private bundle and catalog image registries
+
+By default, projects are configured to push to and pull from a [docker.io] registry
+with namespace being the value passed to `operator-sdk init --domain=<value>`.
+Modify this value to push/pull bundle and catalog images with a different registry host or namespace.
+
+All bundle and catalog image-related commands invoke [`opm`][opm] (except for bundle image builds,
+for which `docker` is used directly). `opm` leverages the host's image build/pull tools indirectly
+to perform various image tasks, so if your image registry is private or has a custom CA you
+must ensure the in-use build tool is able to push to/pull from the registry:
+- `docker`: [`config.json`][docker-pull-sec], [certificate configuration][docker-certs]
+- `podman`: [`auth.json`][podman-pull-sec], [certificate configuration][podman-certs]
+- `none` (containerd): uses docker's `config.json`, [certificate configuration][none-certs]
+
+The `run bundle` or `run bundle-upgrade` commands use the `none` image tool, described above, in-cluster.
+These commands accept the names of secrets available in the deployment namesace that contain configuration file data.
+Ideally a cluster admin will provision a namespace and service account for bundle testing,
+such that they include and reference these secrets:
+- Create an [image pull secret][k8s-image-pull-sec] for your `config.json` and [add it to your service account][k8s-pull-sec-sa].
+<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
+minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+- Create a [generic secret][k8s-gen-sec] with a `cert.pem` key containing root certificate(s) for your registry.
+
+Once the above secrets have been created, run the either command with `--pull-secret-name=<image pull secret>` and `--ca-secret-name=<certificate secret>`:
+
+```sh
+operator-sdk run bundle private-custom-ca-reg.com/memcached-operator-bundle:v0.0.2 \
+    --index-image private-custom-ca-reg.com/memcached-operator-catalog:v0.0.1 \
+    --pull-secret-name foo-pull-sec \
+    --ca-secret-name foo-cert-sec
+```
+
+You may have to set `--namespace=<provisioned namespace>` if the namespace encoded in your kubeconfig's current context
+was not provisioned with these secrets.
+
+[docker.io]:https://hub.docker.com/
+[opm]:https://github.com/operator-framework/operator-registry/blob/496ccce/docs/design/opm-tooling.md
+[docker-pull-sec]:https://docs.docker.com/engine/reference/commandline/login/
+[docker-certs]:https://docs.docker.com/engine/security/certificates/
+[podman-pull-sec]:http://docs.podman.io/en/latest/markdown/podman-login.1.html#description
+[podman-certs]:http://docs.podman.io/en/latest/markdown/podman-image-sign.1.html#cert-dir-path
+[none-certs]:https://github.com/operator-framework/operator-registry/blob/master/docs/design/opm-tooling.md#add
+[k8s-image-pull-sec]:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+[k8s-pull-sec-sa]:https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-image-pull-secret-to-service-account
+[k8s-gen-sec]:https://kubernetes.io/docs/tasks/configmap-secret/managing-secret-using-kubectl/#create-a-secret
+
+### Package Manifests
 
 - [`generate packagemanifests`][cli-gen-packagemanifests]: creates a new or updates an existing versioned
 directory as part of the package manifests in the `<project-root>/packagemanifests` directory.
diff --git a/website/content/en/docs/olm-integration/quickstart-bundle.md b/website/content/en/docs/olm-integration/quickstart-bundle.md
index c0e114ba..4dcc61c7 100644
--- a/website/content/en/docs/olm-integration/quickstart-bundle.md
+++ b/website/content/en/docs/olm-integration/quickstart-bundle.md
@@ -110,14 +110,8 @@ to ensure the on-disk bundle representation is correct.
 At this point in development we've generated all files necessary to build the memcached-operator bundle.
 Now we're ready to test and deploy the Operator with OLM.
 
-**Note:** if testing a bundle hosted in a private registry, add the image pull
-secret for that registry host [to the service account][add-sa-secret] in use
-and set `--secret-name` to the secret name. You may have to set `--namespace`
-if the service account is in a different namespace than that configured in your kubeconfig.
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
-
-[add-sa-secret]:https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
+**Note:** If testing a bundle whose image will be hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
 ### Testing bundles
 
@@ -287,6 +281,7 @@ In-depth discussions of OLM concepts mentioned here:
 [bundle]:https://github.com/operator-framework/operator-registry/blob/v1.16.1/docs/design/operator-bundle.md
 [bundle-metadata]:https://github.com/operator-framework/operator-registry/blob/v1.12.6/docs/design/operator-bundle.md#bundle-annotations
 [bundle-dockerfile]:https://github.com/operator-framework/operator-registry/blob/v1.12.6/docs/design/operator-bundle.md#bundle-dockerfile
+[image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
 [cli-olm]:/docs/cli/operator-sdk_olm
 [cli-run-bundle]:/docs/cli/operator-sdk_run_bundle
 [doc-cli-overview]:/docs/olm-integration/cli-overview
diff --git a/website/content/en/docs/overview/cheat-sheet.md b/website/content/en/docs/overview/cheat-sheet.md
index bd244ef0..409e0bfd 100644
--- a/website/content/en/docs/overview/cheat-sheet.md
+++ b/website/content/en/docs/overview/cheat-sheet.md
@@ -38,7 +38,7 @@ For further information check [Operator SDK Integration with Operator Lifecycle
 | `make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>` | To build your bundle operator image. |
 | `make bundle-build bundle-push BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>` | To build and push your bundle operator image. |
 | `operator-sdk run bundle <some-registry>/<project-name-bundle>:<tag>` | To deploy your bundle operator using OLM on your cluster for development purposes. |
-| `operator-sdk run bundle --service-account sa-with-secret --secret-name registry-pull-secret private-registry.org/bundle:v1.2.3` | Configure `run bundle` (and `run bundle-upgrade`) to use an image pull secret and non-default service account configured with that secret |
+| `operator-sdk run bundle private-registry.org/bundle:v1.2.3 --service-account sa-with-secret --pull-secret-name regcred --ca-secret-name cert-sec` | Configure `run bundle` (and `run bundle-upgrade`) to use an image pull secret, non-default service account configured with that secret, and custom CA certificate secret |
 <!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
 minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
 


Test patch:
diff --git a/internal/helm/controller/reconcile_test.go b/internal/helm/controller/reconcile_test.go
index 130b1d5f..188e936f 100644
--- a/internal/helm/controller/reconcile_test.go
+++ b/internal/helm/controller/reconcile_test.go
@@ -21,8 +21,8 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-func TestHasHelmUpgradeForceAnnotation(t *testing.T) {
-	tests := []struct {
+func TestHasAnnotation(t *testing.T) {
+	upgradeForceTests := []struct {
 		input       map[string]interface{}
 		expectedVal bool
 		expectedOut string
@@ -33,47 +33,101 @@ func TestHasHelmUpgradeForceAnnotation(t *testing.T) {
 				"helm.sdk.operatorframework.io/upgrade-force": "True",
 			},
 			expectedVal: true,
-			name:        "base case true",
+			name:        "upgrade force base case true",
 		},
 		{
 			input: map[string]interface{}{
 				"helm.sdk.operatorframework.io/upgrade-force": "False",
 			},
 			expectedVal: false,
-			name:        "base case false",
+			name:        "upgrade force base case false",
 		},
 		{
 			input: map[string]interface{}{
 				"helm.sdk.operatorframework.io/upgrade-force": "1",
 			},
 			expectedVal: true,
-			name:        "true as int",
+			name:        "upgrade force true as int",
 		},
 		{
 			input: map[string]interface{}{
 				"helm.sdk.operatorframework.io/upgrade-force": "0",
 			},
 			expectedVal: false,
-			name:        "false as int",
+			name:        "upgrade force false as int",
 		},
 		{
 			input: map[string]interface{}{
 				"helm.sdk.operatorframework.io/wrong-annotation": "true",
 			},
 			expectedVal: false,
-			name:        "annotation not set",
+			name:        "upgrade force annotation not set",
 		},
 		{
 			input: map[string]interface{}{
 				"helm.sdk.operatorframework.io/upgrade-force": "invalid",
 			},
 			expectedVal: false,
-			name:        "invalid value",
+			name:        "upgrade force invalid value",
 		},
 	}
 
-	for _, test := range tests {
-		assert.Equal(t, test.expectedVal, hasHelmUpgradeForceAnnotation(annotations(test.input)), test.name)
+	for _, test := range upgradeForceTests {
+		assert.Equal(t, test.expectedVal, hasAnnotation(helmUpgradeForceAnnotation, annotations(test.input)), test.name)
+	}
+
+	uninstallWaitTests := []struct {
+		input       map[string]interface{}
+		expectedVal bool
+		expectedOut string
+		name        string
+	}{
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/uninstall-wait": "True",
+			},
+			expectedVal: true,
+			name:        "uninstall wait base case true",
+		},
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/uninstall-wait": "False",
+			},
+			expectedVal: false,
+			name:        "uninstall wait base case false",
+		},
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/uninstall-wait": "1",
+			},
+			expectedVal: true,
+			name:        "uninstall wait true as int",
+		},
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/uninstall-wait": "0",
+			},
+			expectedVal: false,
+			name:        "uninstall wait false as int",
+		},
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/wrong-annotation": "true",
+			},
+			expectedVal: false,
+			name:        "uninstall wait annotation not set",
+		},
+		{
+			input: map[string]interface{}{
+				"helm.sdk.operatorframework.io/uninstall-wait": "invalid",
+			},
+			expectedVal: false,
+			name:        "uninstall wait invalid value",
+		},
+	}
+
+	for _, test := range uninstallWaitTests {
+		assert.Equal(t, test.expectedVal, hasAnnotation(helmUninstallWaitAnnotation, annotations(test.input)), test.name)
 	}
 }
 
diff --git a/internal/olm/operator/registry/index/registry_pod_test.go b/internal/olm/operator/registry/index/registry_pod_test.go
index 1c0a61d4..836936a5 100644
--- a/internal/olm/operator/registry/index/registry_pod_test.go
+++ b/internal/olm/operator/registry/index/registry_pod_test.go
@@ -45,10 +45,10 @@ func TestCreateRegistryPod(t *testing.T) {
 
 var _ = Describe("RegistryPod", func() {
 
-	var defaultBundleItem = BundleItem{
+	var defaultBundleItems = []BundleItem{{
 		ImageTag: "quay.io/example/example-operator-bundle:0.2.0",
 		AddMode:  SemverBundleAddMode,
-	}
+	}}
 
 	Describe("creating registry pod", func() {
 
@@ -67,7 +67,7 @@ var _ = Describe("RegistryPod", func() {
 					Namespace: "test-default",
 				}
 				rp = &RegistryPod{
-					BundleItems: []BundleItem{defaultBundleItem},
+					BundleItems: defaultBundleItems,
 					IndexImage:  testIndexImageTag,
 				}
 				By("initializing the RegistryPod")
@@ -94,21 +94,27 @@ var _ = Describe("RegistryPod", func() {
 			It("should return a valid container command for one image", func() {
 				output, err := rp.getContainerCmd()
 				Expect(err).To(BeNil())
-				Expect(output).Should(Equal(containerCommandFor(defaultDBPath, []BundleItem{defaultBundleItem})))
+				Expect(output).Should(Equal(containerCommandFor(defaultDBPath, defaultBundleItems, false)))
+			})
+
+			It("should return a container command with --ca-file", func() {
+				rp.CASecretName = "foo-secret"
+				output, err := rp.getContainerCmd()
+				Expect(err).To(BeNil())
+				Expect(output).Should(Equal(containerCommandFor(defaultDBPath, defaultBundleItems, true)))
 			})
 
 			It("should return a valid container command for three images", func() {
-				bundleItems := []BundleItem{
-					defaultBundleItem,
-					{
+				bundleItems := append(defaultBundleItems,
+					BundleItem{
 						ImageTag: "quay.io/example/example-operator-bundle:0.3.0",
 						AddMode:  ReplacesBundleAddMode,
 					},
-					{
+					BundleItem{
 						ImageTag: "quay.io/example/example-operator-bundle:1.0.1",
 						AddMode:  SemverBundleAddMode,
 					},
-				}
+				)
 				rp2 := RegistryPod{
 					DBPath:      defaultDBPath,
 					GRPCPort:    defaultGRPCPort,
@@ -116,7 +122,7 @@ var _ = Describe("RegistryPod", func() {
 				}
 				output, err := rp2.getContainerCmd()
 				Expect(err).To(BeNil())
-				Expect(output).Should(Equal(containerCommandFor(defaultDBPath, bundleItems)))
+				Expect(output).Should(Equal(containerCommandFor(defaultDBPath, bundleItems, false)))
 			})
 
 			It("check pod status should return successfully when pod check is true", func() {
@@ -188,7 +194,7 @@ var _ = Describe("RegistryPod", func() {
 
 			It("checkPodStatus should return error when pod check is false and context is done", func() {
 				rp := &RegistryPod{
-					BundleItems: []BundleItem{defaultBundleItem},
+					BundleItems: defaultBundleItems,
 					IndexImage:  testIndexImageTag,
 				}
 				Expect(rp.init(cfg)).To(Succeed())
@@ -211,10 +217,14 @@ var _ = Describe("RegistryPod", func() {
 })
 
 // containerCommandFor returns the expected container command for a db path and set of bundle items.
-func containerCommandFor(dbPath string, items []BundleItem) string {
+func containerCommandFor(dbPath string, items []BundleItem, hasCA bool) string {
+	var caFlag string
+	if hasCA {
+		caFlag = " --ca-file=/certs/cert.pem"
+	}
 	additions := &strings.Builder{}
 	for _, item := range items {
-		additions.WriteString(fmt.Sprintf("/bin/opm registry add -d %s -b %s --mode=%s && \\\n", dbPath, item.ImageTag, item.AddMode))
+		additions.WriteString(fmt.Sprintf("/bin/opm registry add -d %s -b %s --mode=%s%s && \\\n", dbPath, item.ImageTag, item.AddMode, caFlag))
 	}
 	return fmt.Sprintf("/bin/mkdir -p /database && \\\n%s/bin/opm registry serve -d /database/index.db -p 50051\n", additions.String())
 }
diff --git a/test/e2e/helm/cluster_test.go b/test/e2e/helm/cluster_test.go
index b3213efe..f9bb2ab5 100644
--- a/test/e2e/helm/cluster_test.go
+++ b/test/e2e/helm/cluster_test.go
@@ -276,17 +276,84 @@ var _ = Describe("Running Helm projects", func() {
 				tc.Version)
 			Eventually(getCurlLogs, time.Minute, time.Second).Should(ContainSubstring(metricExportedCR))
 
+			By("annotate CR with uninstall-wait")
+			cmdOpts = []string{
+				"annotate", tc.Kind, fmt.Sprintf("%s-sample", strings.ToLower(tc.Kind)),
+				"helm.sdk.operatorframework.io/uninstall-wait=true",
+			}
+			_, err = tc.Kubectl.Command(cmdOpts...)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("adding a finalizer to statefulset")
+			cmdOpts = []string{
+				"patch", "statefulset", releaseName, "-p",
+				"{\"metadata\":{\"finalizers\":[\"helm.sdk.operatorframework.io/fake-finalizer\"]}}",
+				"--type=merge",
+			}
+			_, err = tc.Kubectl.Command(cmdOpts...)
+			Expect(err).NotTo(HaveOccurred())
+
 			By("deleting CR manifest")
+			_, err = tc.Kubectl.Delete(false, "-f", sampleFile, "--wait=false")
+			Expect(err).NotTo(HaveOccurred())
+
+			By("ensuring the CR gets reconciled and uninstall-wait is enabled")
+			managerContainerLogsAfterDeleteCR := func() string {
+				logOutput, err := tc.Kubectl.Logs(controllerPodName, "-c", "manager")
+				Expect(err).NotTo(HaveOccurred())
+				return logOutput
+			}
+			Eventually(managerContainerLogsAfterDeleteCR, time.Minute, time.Second).Should(ContainSubstring("Uninstall wait"))
+			Eventually(managerContainerLogsAfterDeleteCR, time.Minute, time.Second).Should(ContainSubstring("Waiting until all resources are deleted"))
+
+			By("removing the finalizer from statefulset")
+			cmdOpts = []string{
+				"patch", "statefulset", releaseName, "-p",
+				"{\"metadata\":{\"finalizers\":[]}}",
+				"--type=merge",
+			}
+			_, err = tc.Kubectl.Command(cmdOpts...)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("ensuring the CR gets reconciled and CR finalizer is removed")
+			Eventually(managerContainerLogsAfterDeleteCR, 2*time.Minute, time.Second).Should(ContainSubstring("Removing finalizer"))
+
+			By("ensuring the CR is deleted")
+			verifyDeletedCR := func() error {
+				_, err := tc.Kubectl.Get(
+					true,
+					tc.Kind, fmt.Sprintf("%s-sample", strings.ToLower(tc.Kind)))
+				if err == nil {
+					return fmt.Errorf("the %s CR is not deleted", tc.Kind)
+				}
+				return nil
+			}
+			Eventually(verifyDeletedCR, time.Minute, time.Second).Should(Succeed())
+
+			By("creating an instance of release(CR) again after a delete with uninstall-wait")
+			_, err = tc.Kubectl.Apply(false, "-f", sampleFile)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("ensuring the CR gets reconciled and the release was Installed again")
+			managerContainerLogs = func() string {
+				logOutput, err := tc.Kubectl.Logs(controllerPodName, "-c", "manager")
+				Expect(err).NotTo(HaveOccurred())
+				return logOutput
+			}
+			Eventually(managerContainerLogs, time.Minute, time.Second).Should(ContainSubstring("Installed release"))
+
+			By("deleting CR manifest again without uninstall-wait")
 			_, err = tc.Kubectl.Delete(false, "-f", sampleFile)
 			Expect(err).NotTo(HaveOccurred())
 
 			By("ensuring the CR gets reconciled and the release was Uninstalled")
-			managerContainerLogsAfterDeleteCR := func() string {
+			managerContainerLogsAfterDeleteCR = func() string {
 				logOutput, err := tc.Kubectl.Logs(controllerPodName, "-c", "manager")
 				Expect(err).NotTo(HaveOccurred())
 				return logOutput
 			}
 			Eventually(managerContainerLogsAfterDeleteCR, time.Minute, time.Second).Should(ContainSubstring("Uninstalled release"))
+			Eventually(managerContainerLogsAfterDeleteCR, time.Minute, time.Second).Should(ContainSubstring("Removing finalizer"))
 		})
 	})
 })
diff --git a/website/content/en/docs/olm-integration/testing-deployment.md b/website/content/en/docs/olm-integration/testing-deployment.md
index 5b058d13..8862c21a 100644
--- a/website/content/en/docs/olm-integration/testing-deployment.md
+++ b/website/content/en/docs/olm-integration/testing-deployment.md
@@ -17,13 +17,8 @@ to inform OLM of that registry and which Operator versions it can deploy and whe
 - `run bundle` and `run packagemanifests` can only deploy one Operator and one version of that Operator at a time, 
 and `run bundle-upgrade` can only upgrade one Operator and one version of that Operator at a time, 
 hence their intended purpose being testing only.
-- If testing a bundle hosted in a private registry, set the `--secret-name` flag to
-the name of the appropriate in-cluster image pull secret, and add the same secret
-[to a service account][add-sa-secret] and set `--service-account` to that service account's name;
-you may have to set `--namespace` if the service account is in a different namespace
-than that configured in your kubeconfig.
-<!-- TODO(estroz): remove the service account requirement once OLM releases a patch or new
-minor release containing https://github.com/operator-framework/operator-lifecycle-manager/pull/1941 -->
+- If testing a bundle or catalog whose image will be hosted in a registry that is private and/or
+has a custom CA, these [configuration steps][image-reg-config] must be complete.
 
 
 ## `operator-sdk run bundle` command overview