Repository: operator-framework/operator-sdk
Pull request #5086 (2021-07-23 19:09:48)

Refactoring code for label-based filtering for Ansible Operators
**Description of the change:**
Removed some of the code from https://github.com/operator-framework/operator-sdk/pull/3275 and instead obtained the same functionality from controller runtime.

**Motivation for the change:**
When this was originally created in Operator SDK, controller-runtime did not support this feature. Now that it's a part of controller-runtime (from [this](https://github.com/kubernetes-sigs/controller-runtime/pull/1121) PR), we leverage it in SDK and remove the extra code.

**Notes**
1. This feature involves removing the entire "predicate" folder under `internal/ansible`. The predicate folder only contains`predicate.go`. The code from `predicate.go` is present upstream.
2. This exact feature has been implemented for Helm: https://github.com/operator-framework/operator-sdk/pull/4997.
3. Since this is just refactoring a previous feature, the documentation and changelog fragment already exist.


Patch:
diff --git a/Makefile b/Makefile
index de6e426f..670356d2 100644
--- a/Makefile
+++ b/Makefile
@@ -162,7 +162,7 @@ test-e2e-teardown:
 # Repeated rules are executed in the order they appear.
 $(e2e_targets):: test-e2e-setup image/scorecard-test
 test-e2e:: $(e2e_tests) ## Run e2e tests
-	
+
 test-e2e-go:: image/custom-scorecard-tests ## Run Go e2e tests
 	go test ./test/e2e/go -v -ginkgo.v
 test-e2e-ansible:: image/ansible-operator ## Run Ansible e2e tests
diff --git a/changelog/fragments/ansible-set-local-rolespath.yaml b/changelog/fragments/ansible-set-local-rolespath.yaml
new file mode 100644
index 00000000..ff409232
--- /dev/null
+++ b/changelog/fragments/ansible-set-local-rolespath.yaml
@@ -0,0 +1,18 @@
+# entries is a list of entries to include in
+# release notes and/or the migration guide
+entries:
+  - description: >
+      (ansible/v1) Previously, when scaffolding an Ansible-based operator with
+      both Roles and Playbooks, the roles imported by the playbook could not be
+      found when running locally (`make run`). This change prepends the
+      `ANSIBLE_ROLES_PATH` environment variable with the path to the local
+      roles directory.
+    kind: "bugfix"
+    breaking: false
+    migration:
+      header: (optional) Add local Ansible Roles path to Env in `make run`
+      body: >
+        If you would like to run your operator locally using `make run`, modify
+        the `run` target in the `Makefile` to:
+        `ANSIBLE_ROLES_PATH="$(ANSIBLE_ROLES_PATH):$(shell pwd)/roles"
+        $(ANSIBLE_OPERATOR) run`
diff --git a/changelog/fragments/bump-kb.yaml b/changelog/fragments/bump-kb.yaml
new file mode 100644
index 00000000..09f7edc2
--- /dev/null
+++ b/changelog/fragments/bump-kb.yaml
@@ -0,0 +1,78 @@
+# entries is a list of entries to include in
+# release notes and/or the migration guide
+entries:
+  - description: >
+      (go/v3) Fixed a bug in `make test` caused by incorrect envtest asset setup.
+
+    kind: "change"
+
+    # Is this a breaking change?
+    breaking: false
+
+    migration:
+      header: (go/v3) Export `KUBEBUILDER_ASSETS` in `Makefile`
+      body: >
+        In `go/v3` projects, after installing `setup-envtest` binary, the path to
+        `KUBEBUILDER_ASSETS` needs to be exported. In `Makefile`, add the following:
+
+          ```diff
+            +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+            +ENVTEST_K8S_VERSION = 1.21
+
+            test: manifests generate fmt vet envtest ## Run tests.
+            - go test ./... -coverprofile cover.out
+            + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
+          ```
+  - description: >
+      (go/v3, ansible/v1, helm/v1) Added `containerPort` protocol field required for server-side apply in manifests.
+
+    kind: "addition"
+
+    # Is this a breaking change?
+    breaking: false
+
+    migration:
+      header: (go/v3, ansible/v1, helm/v1) Add `containerPort` protocol field in manifests.
+      body: >
+        In `go/v3`, `ansible/v1`, `helm/v1` projects, specify the `containerPort` protocol in the following files:
+        - `config/default/manager_auth_proxy_patch.yaml`
+        - `config/rbac/auth_proxy_service.yaml`
+
+        ```diff
+          ports:
+         - containerPort: 8443
+         + protocol: TCP
+           name: https
+        ```
+
+        Additionally, in `go/v3` projects, add the `containerPort` protocol to `config/webhook/service.yaml`:
+
+        ```diff
+           ports:
+            - port: 443
+            + protocol: TCP
+              targetPort: 9443
+        ```
+
+  - description: >
+      (go/v3, ansible/v1, helm/v1) Increased resource limits for controller manager.
+
+    kind: "change"
+
+    # Is this a breaking change?
+    breaking: false
+
+    migration:
+      header: (go/v3, ansible/v1, helm/v1) Increase resource limits for controller manager.
+      body: >
+        In `go/v3`, `ansible/v1`, `helm/v1` projects, increase the `cpu` and `memory` resource limits to `200m` and
+        `100Mi` respectively in `config/manager/manager.yaml`.
+
+        ```diff
+          resources:
+           limits:
+            - cpu: 100m
+            - memory: 30Mi
+            + cpu: 200m
+            + memory: 100Mi
+        ```
diff --git a/changelog/fragments/bump-of-api-0.10.5.yaml b/changelog/fragments/bump-of-api-0.10.5.yaml
new file mode 100644
index 00000000..a8ddb1ff
--- /dev/null
+++ b/changelog/fragments/bump-of-api-0.10.5.yaml
@@ -0,0 +1,6 @@
+entries:
+  - description: >
+      Updated operator-framework dependency to 0.10.5. This specific update includes a
+      fix for bundle validation when checking for invalid service accounts.
+    kind: change
+    breaking: false
diff --git a/changelog/fragments/fix-bundle-service-accounts.yaml b/changelog/fragments/fix-bundle-service-accounts.yaml
new file mode 100644
index 00000000..e434966c
--- /dev/null
+++ b/changelog/fragments/fix-bundle-service-accounts.yaml
@@ -0,0 +1,4 @@
+entries:
+  - description: >
+      In `generate bundle`, exclude ServiceAccounts already in a CSV from generated bundle.
+    kind: bugfix
diff --git a/changelog/fragments/helm-overrides-template.yaml b/changelog/fragments/helm-overrides-template.yaml
new file mode 100644
index 00000000..9a2cd4ad
--- /dev/null
+++ b/changelog/fragments/helm-overrides-template.yaml
@@ -0,0 +1,6 @@
+# entries is a list of entries to include in
+# release notes and/or the migration guide
+entries:
+  - description: >
+      For helm-based operators, support go `text/template` expansion of override values
+    kind: addition
diff --git a/changelog/fragments/revert-helm-equality-comparison.yaml b/changelog/fragments/revert-helm-equality-comparison.yaml
new file mode 100644
index 00000000..3827baf0
--- /dev/null
+++ b/changelog/fragments/revert-helm-equality-comparison.yaml
@@ -0,0 +1,20 @@
+# entries is a list of entries to include in
+# release notes and/or the migration guide
+entries:
+  - description: >
+      For helm-based operators, reverted the following PRs which modified helm release
+      equality comparison.
+      - https://github.com/operator-framework/operator-sdk/pull/5042
+      - https://github.com/operator-framework/operator-sdk/pull/4937
+
+    # kind is one of:
+    # - addition
+    # - change
+    # - deprecation
+    # - removal
+    # - bugfix
+    kind: change
+
+    # Is this a breaking change?
+    breaking: false
+
diff --git a/go.mod b/go.mod
index fafd3f49..77892943 100644
--- a/go.mod
+++ b/go.mod
@@ -6,13 +6,14 @@ require (
 	github.com/blang/semver/v4 v4.0.0
 	github.com/fatih/structtag v1.1.0
 	github.com/go-logr/logr v0.4.0
+	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
 	github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
 	github.com/kr/text v0.2.0
 	github.com/markbates/inflect v1.0.4
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2
 	github.com/onsi/ginkgo v1.16.4
 	github.com/onsi/gomega v1.13.0
-	github.com/operator-framework/api v0.10.2
+	github.com/operator-framework/api v0.10.5
 	github.com/operator-framework/java-operator-plugins v0.0.0-20210708174638-463fb91f3d5e
 	github.com/operator-framework/operator-lib v0.5.0
 	github.com/operator-framework/operator-registry v1.17.4
@@ -39,7 +40,7 @@ require (
 	k8s.io/kubectl v0.21.0
 	sigs.k8s.io/controller-runtime v0.9.2
 	sigs.k8s.io/controller-tools v0.6.0
-	sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210707171043-359e8aa70c2b
+	sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210803185103-51e4a9aa5055
 	sigs.k8s.io/yaml v1.2.0
 )
 
diff --git a/go.sum b/go.sum
index 5e52c8fc..0dbb88d7 100644
--- a/go.sum
+++ b/go.sum
@@ -409,6 +409,7 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ=
 github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
@@ -823,8 +824,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
 github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
 github.com/operator-framework/api v0.7.1/go.mod h1:L7IvLd/ckxJEJg/t4oTTlnHKAJIP/p51AvEslW3wYdY=
 github.com/operator-framework/api v0.10.0/go.mod h1:tV0BUNvly7szq28ZPBXhjp1Sqg5yHCOeX19ui9K4vjI=
-github.com/operator-framework/api v0.10.2 h1:fo8Bhyx1v46NdJIz2rUzfzNUpe1KDNPtVpHVpuxZnk0=
-github.com/operator-framework/api v0.10.2/go.mod h1:tV0BUNvly7szq28ZPBXhjp1Sqg5yHCOeX19ui9K4vjI=
+github.com/operator-framework/api v0.10.5 h1:/WvLKOPo8zZMyEmuW0kLC0PJBt4Xal8HZkFioKIxqTA=
+github.com/operator-framework/api v0.10.5/go.mod h1:tV0BUNvly7szq28ZPBXhjp1Sqg5yHCOeX19ui9K4vjI=
 github.com/operator-framework/java-operator-plugins v0.0.0-20210708174638-463fb91f3d5e h1:LMsT59IJqaLn7kD6DnZFy0IouRufXyJHTT+mXQrl9Ps=
 github.com/operator-framework/java-operator-plugins v0.0.0-20210708174638-463fb91f3d5e/go.mod h1:sGKGELFkUeRqElcyvyPC89bC76YnCL7MPMa13P0AQcw=
 github.com/operator-framework/operator-lib v0.5.0 h1:Jmhz/WjcstEyBBM9IFUiHEgKg5bd43uF4ej/ZY2S0rM=
@@ -1624,8 +1625,8 @@ sigs.k8s.io/controller-tools v0.6.0 h1:o2Fm1K7CmIp8OVaBtXsWB/ssBAzyoKZPPAGR3Vuxa
 sigs.k8s.io/controller-tools v0.6.0/go.mod h1:baRMVPrctU77F+rfAuH2uPqW93k6yQnZA2dhUOr7ihc=
 sigs.k8s.io/kind v0.10.0/go.mod h1:fb32zUw7ewC47bPwLnwhf47wd/vADtv3c38KP7sjIlo=
 sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210702145813-742983631190/go.mod h1:pUhjQx9f/+cn1OtSa5zMohY1lgk9s/9Mbcvwj82lrNk=
-sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210707171043-359e8aa70c2b h1:fGbw9og/lq/fNi4afnVwSinfaQXkonlbTar9nG95u44=
-sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210707171043-359e8aa70c2b/go.mod h1:pUhjQx9f/+cn1OtSa5zMohY1lgk9s/9Mbcvwj82lrNk=
+sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210803185103-51e4a9aa5055 h1:3f5m0xp0DEsg+Sdrt1Rld8TCEFOH5GKsoYrEpdOzA+s=
+sigs.k8s.io/kubebuilder/v3 v3.0.0-alpha.0.0.20210803185103-51e4a9aa5055/go.mod h1:pUhjQx9f/+cn1OtSa5zMohY1lgk9s/9Mbcvwj82lrNk=
 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/api v0.8.5 h1:bfCXGXDAbFbb/Jv5AhMj2BB8a5VAJuuQ5/KU69WtDjQ=
diff --git a/internal/ansible/controller/controller.go b/internal/ansible/controller/controller.go
index 4c7a0d78..2e1b4188 100644
--- a/internal/ansible/controller/controller.go
+++ b/internal/ansible/controller/controller.go
@@ -17,6 +17,7 @@ package controller
 import (
 	"fmt"
 	"os"
+	"reflect"
 	"strings"
 	"time"
 
@@ -33,7 +34,6 @@ import (
 
 	"github.com/operator-framework/operator-sdk/internal/ansible/events"
 	"github.com/operator-framework/operator-sdk/internal/ansible/handler"
-	"github.com/operator-framework/operator-sdk/internal/ansible/predicate"
 	"github.com/operator-framework/operator-sdk/internal/ansible/runner"
 )
 
@@ -103,12 +103,17 @@ func Add(mgr manager.Manager, options Options) *controller.Controller {
 	predicates := []ctrlpredicate.Predicate{
 		ctrlpredicate.Or(ctrlpredicate.GenerationChangedPredicate{}, libpredicate.NoGenerationPredicate{}),
 	}
-	filterPredicate, err := predicate.NewResourceFilterPredicate(options.Selector)
+
+	p, err := parsePredicateSelector(options.Selector)
+
 	if err != nil {
-		log.Error(err, "Error creating resource filter predicate")
+		log.Error(err, "")
 		os.Exit(1)
 	}
-	predicates = append(predicates, filterPredicate)
+
+	if p != nil {
+		predicates = append(predicates, p)
+	}
 
 	u := &unstructured.Unstructured{}
 	u.SetGroupVersionKind(options.GVK)
@@ -120,3 +125,17 @@ func Add(mgr manager.Manager, options Options) *controller.Controller {
 
 	return &c
 }
+
+// parsePredicateSelector parses the selector in the WatchOptions and creates a predicate
+// that is used to filter resources based on the specified selector
+func parsePredicateSelector(selector metav1.LabelSelector) (ctrlpredicate.Predicate, error) {
+	// If a selector has been specified in watches.yaml, add it to the watch's predicates.
+	if !reflect.ValueOf(selector).IsZero() {
+		p, err := ctrlpredicate.LabelSelectorPredicate(selector)
+		if err != nil {
+			return nil, fmt.Errorf("error constructing predicate from watches selector: %v", err)
+		}
+		return p, nil
+	}
+	return nil, nil
+}
diff --git a/internal/ansible/predicate/predicate.go b/internal/ansible/predicate/predicate.go
deleted file mode 100644
index 9f0e3723..00000000
--- a/internal/ansible/predicate/predicate.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2018 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 predicate
-
-import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
-	"sigs.k8s.io/controller-runtime/pkg/event"
-	"sigs.k8s.io/controller-runtime/pkg/predicate"
-)
-
-type resourceFilterPredicate struct {
-	predicate.Funcs
-	Selector labels.Selector
-}
-
-// Skips events that have labels matching selectors defined in watches.yaml
-func (r resourceFilterPredicate) eventFilter(eventLabels map[string]string) bool {
-	return r.Selector.Matches(labels.Set(eventLabels))
-}
-
-func NewResourceFilterPredicate(s metav1.LabelSelector) (predicate.Predicate, error) {
-	selectorSpecs, err := metav1.LabelSelectorAsSelector(&s)
-	requirements := resourceFilterPredicate{Selector: selectorSpecs}
-	return requirements, err
-
-}
-
-// Predicate functions that call the EventFilter Function
-func (r resourceFilterPredicate) Update(e event.UpdateEvent) bool {
-	return r.eventFilter(e.ObjectNew.GetLabels())
-}
-
-func (r resourceFilterPredicate) Create(e event.CreateEvent) bool {
-	return r.eventFilter(e.Object.GetLabels())
-}
-
-func (r resourceFilterPredicate) Delete(e event.DeleteEvent) bool {
-	return r.eventFilter(e.Object.GetLabels())
-}
-
-func (r resourceFilterPredicate) Generic(e event.GenericEvent) bool {
-	return r.eventFilter(e.Object.GetLabels())
-}
diff --git a/internal/ansible/watches/watches.go b/internal/ansible/watches/watches.go
index c42fae03..48204687 100644
--- a/internal/ansible/watches/watches.go
+++ b/internal/ansible/watches/watches.go
@@ -85,36 +85,6 @@ var (
 	ansibleVerbosityDefault        = 2
 )
 
-// Creates, populates, and returns a LabelSelector object. Used in Unmarshal().
-func parseLabelSelector(dls tempLabelSelector) metav1.LabelSelector {
-	obj := metav1.LabelSelector{}
-	obj.MatchLabels = dls.MatchLabels
-
-	for _, v := range dls.MatchExpressions {
-		requirement := metav1.LabelSelectorRequirement{
-			Key:      v.Key,
-			Operator: v.Operator,
-			Values:   v.Values,
-		}
-
-		obj.MatchExpressions = append(obj.MatchExpressions, requirement)
-	}
-
-	return obj
-}
-
-// Temporary structs created to store yaml parsing
-type tempLabelSelector struct {
-	MatchLabels      map[string]string `yaml:"matchLabels,omitempty"`
-	MatchExpressions []tempRequirement `json:"matchExpressions,omitempty"`
-}
-
-type tempRequirement struct {
-	Key      string                       `json:"key"`
-	Operator metav1.LabelSelectorOperator `json:"operator"`
-	Values   []string                     `json:"values,omitempty"`
-}
-
 // Use an alias struct to handle complex types
 type alias struct {
 	Group                       string                    `yaml:"group"`
@@ -132,7 +102,7 @@ type alias struct {
 	MarkUnsafe                  *bool                     `yaml:"markUnsafe"`
 	Blacklist                   []schema.GroupVersionKind `yaml:"blacklist,omitempty"`
 	Finalizer                   *Finalizer                `yaml:"finalizer"`
-	Selector                    tempLabelSelector         `yaml:"selector"`
+	Selector                    metav1.LabelSelector      `yaml:"selector"`
 }
 
 // buildWatch will build Watch based on the values parsed from alias
@@ -201,7 +171,7 @@ func (w *Watch) setValuesFromAlias(tmp alias) error {
 		return err
 	}
 	w.addRolePlaybookPaths(wd)
-	w.Selector = parseLabelSelector(tmp.Selector)
+	w.Selector = tmp.Selector
 
 	return nil
 }
diff --git a/internal/cmd/operator-sdk/generate/internal/manifests.go b/internal/cmd/operator-sdk/generate/internal/manifests.go
index 7e9ffb3a..5834684f 100644
--- a/internal/cmd/operator-sdk/generate/internal/manifests.go
+++ b/internal/cmd/operator-sdk/generate/internal/manifests.go
@@ -31,23 +31,6 @@ func GetManifestObjects(c *collector.Manifests, extraSAs []string) (objs []clien
 		objs = append(objs, &c.V1beta1CustomResourceDefinitions[i])
 	}
 
-	// All ServiceAccounts passed in should be written.
-	saSet := make(map[string]struct{}, len(extraSAs))
-	for _, saName := range extraSAs {
-		saSet[saName] = struct{}{}
-	}
-	for i := range c.ServiceAccounts {
-		sa := c.ServiceAccounts[i]
-		saSet[sa.GetName()] = struct{}{}
-		objs = append(objs, &sa)
-	}
-	extraSAs = make([]string, len(saSet))
-	i := 0
-	for saName := range saSet {
-		extraSAs[i] = saName
-		i++
-	}
-
 	// All Services passed in should be written.
 	for i := range c.Services {
 		objs = append(objs, &c.Services[i])
@@ -61,7 +44,7 @@ func GetManifestObjects(c *collector.Manifests, extraSAs []string) (objs []clien
 		}
 	}
 
-	// RBAC objects that are not a part of the CSV should be written.
+	// RBAC objects (including ServiceAccounts) that are not a part of the CSV should be written.
 	_, _, rbacObjs := c.SplitCSVPermissionsObjects(extraSAs)
 	objs = append(objs, rbacObjs...)
 
diff --git a/internal/generate/collector/clusterserviceversion.go b/internal/generate/collector/clusterserviceversion.go
index 46436235..c658aeea 100644
--- a/internal/generate/collector/clusterserviceversion.go
+++ b/internal/generate/collector/clusterserviceversion.go
@@ -38,16 +38,16 @@ const (
 // are added to out. Any bindings with some associations, but with non-associations, are added to out unmodified.
 func (c *Manifests) SplitCSVPermissionsObjects(extraSAs []string) (inPerms, inCPerms, out []client.Object) {
 	// Create a set of ServiceAccount names to match against below.
-	saNameSet := make(map[string]struct{})
+	csvSAs := make(map[string]struct{})
 	for _, dep := range c.Deployments {
 		saName := dep.Spec.Template.Spec.ServiceAccountName
 		if saName == "" {
 			saName = defaultServiceAccountName
 		}
-		saNameSet[saName] = struct{}{}
+		csvSAs[saName] = struct{}{}
 	}
 	for _, extraSA := range extraSAs {
-		saNameSet[extraSA] = struct{}{}
+		csvSAs[extraSA] = struct{}{}
 	}
 
 	// Construct sets for lookups.
@@ -86,9 +86,9 @@ func (c *Manifests) SplitCSVPermissionsObjects(extraSAs []string) (inPerms, inCP
 	// bound Subjects from partial bindings to easily find concrete bindings that bind non-CSV RBAC.
 	// Those with no Subjects left will be removed from the partial binding lists; their concrete counterparts should
 	// be added to out.
-	inRoleNames := pRoleBindings.getRolesBoundToPartialBindings(roleKind, roleNameMap, saNameSet)
-	inCRoleNamesNScope := pRoleBindings.getRolesBoundToPartialBindings(cRoleKind, cRoleNameMap, saNameSet)
-	inCRoleNamesCScope := pCRoleBindings.getRolesBoundToPartialBindings(cRoleKind, cRoleNameMap, saNameSet)
+	inRoleNames := pRoleBindings.getRolesBoundToPartialBindings(roleKind, roleNameMap, csvSAs)
+	inCRoleNamesNScope := pRoleBindings.getRolesBoundToPartialBindings(cRoleKind, cRoleNameMap, csvSAs)
+	inCRoleNamesCScope := pCRoleBindings.getRolesBoundToPartialBindings(cRoleKind, cRoleNameMap, csvSAs)
 
 	// Add {Cluster}Roles bound to a ServiceAccount to either namespace- or cluster-scoped permission sets.
 	for _, roleName := range inRoleNames {
@@ -120,6 +120,14 @@ func (c *Manifests) SplitCSVPermissionsObjects(extraSAs []string) (inPerms, inCP
 		out = append(out, cRoleBindingMap[pBinding.Name])
 	}
 
+	// All ServiceAccounts not in the CSV should be in out.
+	for i := range c.ServiceAccounts {
+		sa := c.ServiceAccounts[i]
+		if _, csvHasSA := csvSAs[sa.Name]; !csvHasSA {
+			out = append(out, &sa)
+		}
+	}
+
 	return inPerms, inCPerms, out
 }
 
diff --git a/internal/helm/release/manager.go b/internal/helm/release/manager.go
index ffeb40b2..07641ccc 100644
--- a/internal/helm/release/manager.go
+++ b/internal/helm/release/manager.go
@@ -20,7 +20,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"reflect"
 	"strings"
 
 	jsonpatch "gomodules.xyz/jsonpatch/v3"
@@ -33,7 +32,6 @@ import (
 	"helm.sh/helm/v3/pkg/storage/driver"
 	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
-	apiequality "k8s.io/apimachinery/pkg/api/equality"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -127,10 +125,15 @@ func (m *manager) Sync(ctx context.Context) error {
 	m.deployedRelease = deployedRelease
 	m.isInstalled = true
 
-	m.isUpgradeRequired, err = m.isUpgrade(deployedRelease)
+	// Get the next candidate release to determine if an upgrade is necessary.
+	candidateRelease, err := m.getCandidateRelease(m.namespace, m.releaseName, m.chart, m.values)
 	if err != nil {
-		return fmt.Errorf("failed to get upgrade status: %w", err)
+		return fmt.Errorf("failed to get candidate release: %w", err)
 	}
+	if deployedRelease.Manifest != candidateRelease.Manifest {
+		m.isUpgradeRequired = true
+	}
+
 	return nil
 }
 
@@ -138,43 +141,6 @@ func notFoundErr(err error) bool {
 	return err != nil && strings.Contains(err.Error(), "not found")
 }
 
-// This is caused by the different logic of loading from local and loading from secret
-// For example, the Raw field, which has the tag `json:"-"`, causes the Unmarshal to be lost when it into Release
-// We need to make them follow the JSON tag
-// see: https://github.com/helm/helm/blob/cf0c6fed519d48101cd69ce01a355125215ee46f/pkg/storage/driver/util.go#L81
-func equalJSONStruct(a, b interface{}) (bool, error) {
-	if reflect.ValueOf(a).IsNil() || reflect.ValueOf(b).IsNil() {
-		return apiequality.Semantic.DeepEqual(a, b), nil
-	}
-
-	aBuf, bBuf := &bytes.Buffer{}, &bytes.Buffer{}
-	err := json.NewEncoder(aBuf).Encode(a)
-	if err != nil {
-		return false, err
-	}
-	err = json.NewEncoder(bBuf).Encode(b)
-	return aBuf.String() == bBuf.String(), err
-}
-
-func (m manager) isUpgrade(deployedRelease *rpb.Release) (bool, error) {
-	if deployedRelease == nil {
-		return false, nil
-	}
-
-	// Judging whether to skip updates
-	skip := m.namespace == deployedRelease.Namespace
-	skip = skip && m.releaseName == deployedRelease.Name
-
-	ok, err := equalJSONStruct(m.chart, deployedRelease.Chart)
-	if err != nil {
-		return false, err
-	}
-	skip = skip && ok
-
-	ok, err = equalJSONStruct(m.values, deployedRelease.Config)
-	return !(skip && ok), err
-}
-
 func (m manager) getDeployedRelease() (*rpb.Release, error) {
 	deployedRelease, err := m.storageBackend.Deployed(m.releaseName)
 	if err != nil {
@@ -186,6 +152,14 @@ func (m manager) getDeployedRelease() (*rpb.Release, error) {
 	return deployedRelease, nil
 }
 
+func (m manager) getCandidateRelease(namespace, name string, chart *cpb.Chart,
+	values map[string]interface{}) (*rpb.Release, error) {
+	upgrade := action.NewUpgrade(m.actionConfig)
+	upgrade.Namespace = namespace
+	upgrade.DryRun = true
+	return upgrade.Run(name, chart, values)
+}
+
 // InstallRelease performs a Helm release install.
 func (m manager) InstallRelease(ctx context.Context, opts ...InstallOption) (*rpb.Release, error) {
 	install := action.NewInstall(m.actionConfig)
diff --git a/internal/helm/watches/watches.go b/internal/helm/watches/watches.go
index 8aa0e650..e2f43a1d 100644
--- a/internal/helm/watches/watches.go
+++ b/internal/helm/watches/watches.go
@@ -15,12 +15,15 @@
 package watches
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"text/template"
 
+	sprig "github.com/go-task/slim-sprig"
 	"helm.sh/helm/v3/pkg/chartutil"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -112,21 +115,34 @@ func LoadReader(reader io.Reader) ([]Watch, error) {
 			trueVal := true
 			w.WatchDependentResources = &trueVal
 		}
-		w.OverrideValues = expandOverrideEnvs(w.OverrideValues)
+		w.OverrideValues, err = expandOverrideValues(w.OverrideValues)
+		if err != nil {
+			return nil, fmt.Errorf("failed to expand override values: %v", err)
+		}
 		watches[i] = w
 	}
 	return watches, nil
 }
 
-func expandOverrideEnvs(in map[string]string) map[string]string {
+func expandOverrideValues(in map[string]string) (map[string]string, error) {
 	if in == nil {
-		return nil
+		return nil, nil
 	}
 	out := make(map[string]string)
 	for k, v := range in {
-		out[k] = os.ExpandEnv(v)
+		envV := os.ExpandEnv(v)
+
+		v := &bytes.Buffer{}
+		tmplV, err := template.New(k).Funcs(sprig.TxtFuncMap()).Parse(envV)
+		if err != nil {
+			return nil, fmt.Errorf("invalid template string %q: %v", envV, err)
+		}
+		if err := tmplV.Execute(v, nil); err != nil {
+			return nil, fmt.Errorf("failed to execute template %q: %v", envV, err)
+		}
+		out[k] = v.String()
 	}
-	return out
+	return out, nil
 }
 
 func verifyGVK(gvk schema.GroupVersionKind) error {
diff --git a/internal/plugins/ansible/v1/init.go b/internal/plugins/ansible/v1/init.go
index 207c8dd4..680c7a9e 100644
--- a/internal/plugins/ansible/v1/init.go
+++ b/internal/plugins/ansible/v1/init.go
@@ -177,8 +177,8 @@ func addInitCustomizations(projectName string) error {
 	// todo: remove it when we solve the issue operator-framework/operator-sdk#3573
 	const resourcesLimitsFragment = `  resources:
           limits:
-            cpu: 100m
-            memory: 30Mi
+            cpu: 200m
+            memory: 100Mi
           requests:
             cpu: 100m
             memory: 20Mi
diff --git a/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go b/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go
index f3f5d394..08d7ebca 100644
--- a/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go
+++ b/internal/plugins/ansible/v1/scaffolds/internal/templates/makefile.go
@@ -89,7 +89,7 @@ help: ## Display this help.
 ##@ Build
 
 run: ansible-operator ## Run against the configured Kubernetes cluster in ~/.kube/config
-	$(ANSIBLE_OPERATOR) run
+	ANSIBLE_ROLES_PATH="$(ANSIBLE_ROLES_PATH):$(shell pwd)/roles" $(ANSIBLE_OPERATOR) run
 
 docker-build: ## Build docker image with the manager.
 	docker build -t ${IMG} .
diff --git a/internal/plugins/helm/v1/init.go b/internal/plugins/helm/v1/init.go
index f8ede16c..f2db1d4b 100644
--- a/internal/plugins/helm/v1/init.go
+++ b/internal/plugins/helm/v1/init.go
@@ -199,10 +199,6 @@ func addInitCustomizations(projectName string) error {
 	}
 
 	// Increase the default memory required.
-	err = sdkutil.ReplaceInFile(managerFile, "memory: 30Mi", "memory: 90Mi")
-	if err != nil {
-		return err
-	}
 	err = sdkutil.ReplaceInFile(managerFile, "memory: 20Mi", "memory: 60Mi")
 	if err != nil {
 		return err
diff --git a/website/content/en/docs/building-operators/ansible/installation.md b/website/content/en/docs/building-operators/ansible/installation.md
index 7840117f..04a4e305 100644
--- a/website/content/en/docs/building-operators/ansible/installation.md
+++ b/website/content/en/docs/building-operators/ansible/installation.md
@@ -13,7 +13,7 @@ Follow the steps in the [installation guide][install-guide] to learn how to inst
 - [docker][docker_tool] version 17.03+.
 - [python][python] version 3.8.6+
 - [ansible][ansible] version v2.9.0+
-- [ansible-runner][ansible-runner] version v1.1.0+
+- [ansible-runner][ansible-runner] version >=1.1.0,<2.0
 - [ansible-runner-http][ansible-runner-http-plugin] version v1.0.0+
 - [openshift][openshift-module] version v0.11.2+
 - [kubectl][kubectl_tool] and access to a Kubernetes cluster of a [compatible version][k8s-version-compat].
diff --git a/website/content/en/docs/building-operators/ansible/quickstart.md b/website/content/en/docs/building-operators/ansible/quickstart.md
index 9d762a85..3b002ed2 100644
--- a/website/content/en/docs/building-operators/ansible/quickstart.md
+++ b/website/content/en/docs/building-operators/ansible/quickstart.md
@@ -106,5 +106,5 @@ 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
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/ansible/tutorial/
diff --git a/website/content/en/docs/building-operators/ansible/tutorial.md b/website/content/en/docs/building-operators/ansible/tutorial.md
index a312398f..fcfd5080 100644
--- a/website/content/en/docs/building-operators/ansible/tutorial.md
+++ b/website/content/en/docs/building-operators/ansible/tutorial.md
@@ -164,7 +164,7 @@ There are three ways to run the operator:
 
 - As Go program outside a cluster
 - As a Deployment inside a Kubernetes cluster
-- Managed by the [Operator Lifecycle Manager (OLM)][doc-olm] in [bundle][quickstart-bundle] format
+- Managed by the [Operator Lifecycle Manager (OLM)][doc-olm] in [bundle][tutorial-bundle] format
 
 ### 1. Run locally outside the cluster
 
@@ -215,7 +215,7 @@ has a custom CA, these [configuration steps][image-reg-config] must be complete.
 operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
 ```
 
-Check out the [docs][quickstart-bundle] for a deep dive into `operator-sdk`'s OLM integration.
+Check out the [docs][tutorial-bundle] for a deep dive into `operator-sdk`'s OLM integration.
 
 
 ## Create a Memcached CR
@@ -346,7 +346,7 @@ todo(camilamacedo86): Create an Ansible operator scope document.
 https://github.com/operator-framework/operator-sdk/issues/3447
 -->
 
-OLM will manage creation of most if not all resources required to run your operator, using a bit of setup from other operator-sdk commands. Check out the [OLM integration guide][quickstart-bundle].
+OLM will manage creation of most if not all resources required to run your operator, using a bit of setup from other operator-sdk commands. Check out the [OLM integration guide][tutorial-bundle].
 
 [legacy-quickstart-doc]:https://v0-19-x.sdk.operatorframework.io/docs/ansible/quickstart/
 [migration-guide]:/docs/building-operators/ansible/migration
@@ -357,5 +357,5 @@ OLM will manage creation of most if not all resources required to run your opera
 [custom-resources]:https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
 [layout-doc]:/docs/building-operators/ansible/reference/scaffolding
 [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
+[tutorial-bundle]:/docs/olm-integration/tutorial-bundle
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
diff --git a/website/content/en/docs/building-operators/golang/operator-scope.md b/website/content/en/docs/building-operators/golang/operator-scope.md
index 1717562b..0b435f61 100644
--- a/website/content/en/docs/building-operators/golang/operator-scope.md
+++ b/website/content/en/docs/building-operators/golang/operator-scope.md
@@ -268,7 +268,7 @@ if strings.Contains(watchNamespace, ",") {
 If your operator is [integrated with OLM][olm-integration], you will want to update your [CSV base's][csv-base]
 `spec.installModes` list to support the desired namespacing requirements. Support for multiple types of namespacing
 is allowed, so supporting multiple install modes in a CSV is permitted. After doing so, update your
-[bundle][bundle-quickstart] or [package manifests][packagemanifests-quickstart] by following the linked guides.
+[bundle][bundle-quickstart] or [package manifests][packagemanifests-tutorial] by following the linked guides.
 
 ### Watching resources in all Namespaces (default)
 
@@ -311,4 +311,4 @@ If the operator can watch multiple namespaces, set the following in your `spec.i
 [olm-integration]: /docs/olm-integration
 [csv-base]: /docs/olm-integration/generation/#kustomize-files
 [bundle-quickstart]: /docs/olm-integration/quickstart-bundle
-[packagemanifests-quickstart]: /docs/olm-integration/quickstart-package-manifests
+[packagemanifests-tutorial]: /docs/olm-integration/tutorial-package-manifests
diff --git a/website/content/en/docs/building-operators/golang/quickstart.md b/website/content/en/docs/building-operators/golang/quickstart.md
index c38e143b..b8d70eb9 100644
--- a/website/content/en/docs/building-operators/golang/quickstart.md
+++ b/website/content/en/docs/building-operators/golang/quickstart.md
@@ -105,5 +105,5 @@ 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
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/golang/tutorial/
diff --git a/website/content/en/docs/building-operators/golang/tutorial.md b/website/content/en/docs/building-operators/golang/tutorial.md
index be05e3c8..f50745a2 100644
--- a/website/content/en/docs/building-operators/golang/tutorial.md
+++ b/website/content/en/docs/building-operators/golang/tutorial.md
@@ -345,7 +345,7 @@ has a custom CA, these [configuration steps][image-reg-config] must be complete.
 operator-sdk run bundle example.com/memcached-operator-bundle:v0.0.1
 ```
 
-Check out the [docs][quickstart-bundle] for a deep dive into `operator-sdk`'s OLM integration.
+Check out the [docs][tutorial-bundle] for a deep dive into `operator-sdk`'s OLM integration.
 
 
 ## Create a Memcached CR
@@ -478,8 +478,9 @@ Next, check out the following:
 [role-based-access-control]: https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control#iam-rolebinding-bootstrap
 [multigroup-kubebuilder-doc]: https://book.kubebuilder.io/migration/multi-group.html
 [doc-bundle]:https://github.com/operator-framework/operator-registry/blob/v1.16.1/docs/design/operator-bundle.md#operator-bundle
+[tutorial-bundle]:/docs/olm-integration/tutorial-bundle
 [quickstart-bundle]:/docs/olm-integration/quickstart-bundle
-[doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
 [conditionals]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
 [kubernetes-extend-api]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
 [reconcile-godoc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
diff --git a/website/content/en/docs/building-operators/helm/quickstart.md b/website/content/en/docs/building-operators/helm/quickstart.md
index 06d57804..be7afdbc 100644
--- a/website/content/en/docs/building-operators/helm/quickstart.md
+++ b/website/content/en/docs/building-operators/helm/quickstart.md
@@ -106,5 +106,5 @@ 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
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
 [tutorial]:/docs/building-operators/helm/tutorial/
diff --git a/website/content/en/docs/building-operators/helm/reference/advanced_features/override_values.md b/website/content/en/docs/building-operators/helm/reference/advanced_features/override_values.md
index cbfce953..16b69d08 100644
--- a/website/content/en/docs/building-operators/helm/reference/advanced_features/override_values.md
+++ b/website/content/en/docs/building-operators/helm/reference/advanced_features/override_values.md
@@ -14,8 +14,8 @@ or the helm binary (helm v3) for security reasons.
 With the helm Operator this becomes possible by override values. This enforces that certain
 template values provided by the chart's default `values.yaml` or by a CR spec are always set
 when rendering the chart. If the value is set by a CR it gets overridden by the global override value.
-The override value can be static but can also refer to an environment variable. To pass down environment
-variables to the chart override values is currently the only way.
+The override value can be static but can also refer to an environment variable and use go templates.
+To pass down environment variables to the chart override values is currently the only way.
 
 An example use case of this is when your helm chart references container images by chart variables,
 which is a good practice.
@@ -27,6 +27,8 @@ versus individually per CR / chart release.
 > and then also associate these with an environment variable of your Operator like shown below.
 > This allows your Operator to be mirrored for offline usage when packaged for OLM.
 
+## Basic usage
+
 To configure your operator with override values, add an `overrideValues` map to your
 `watches.yaml` file for the GVK and chart you need to override. For example, to change
 the repository used by the nginx chart, you would update your `watches.yaml` to the
@@ -39,14 +41,16 @@ following:
   kind: Nginx
   chart: helm-charts/nginx
   overrideValues:
-    image.repository: quay.io/mycustomrepo
+    image.repository: quay.io/mycustomrepo/myimage
 ```
 
-By setting `image.repository` to `quay.io/mycustomrepo` you are ensuring that
-`quay.io/mycustomrepo` will always be used instead of the chart's default repository
+By setting `image.repository` to `quay.io/mycustomrepo/myimage` you are ensuring that
+`quay.io/mycustomrepo/myimage` will always be used instead of the chart's default repository
 (`nginx`). If the CR attempts to set this value, it will be ignored.
 
-It is now possible to reference environment variables in the `overrideValues` section:
+## Using environment variables
+
+It is also possible to reference environment variables in the `overrideValues` section:
 
 ```yaml
   overrideValues:
@@ -61,7 +65,7 @@ following snippet to the container spec:
 ```yaml
 env:
   - name: IMAGE_REPOSITORY
-    value: quay.io/mycustomrepo
+    value: quay.io/mycustomrepo/myimage
 ```
 
 If an environment variable reference is listed in `overrideValues`, but is not present
@@ -70,6 +74,32 @@ override all other values. Therefore, these environment variables should _always
 set. It is suggested to update the Dockerfile to set these environment variables to
 the same defaults that are defined by the chart.
 
+## Using Go templates
+
+Lastly, you can use Go `text/template` strings along with
+[slim-sprig](https://go-task.github.io/slim-sprig/) functions to provide even more
+flexibility when building override values.
+
+For example, consider a situation where your operator has an environment variable,
+`$IMAGE`, set to `quay.io/mycustomrepo/myimage:latest`. You can use sprig template
+functions to split that environment variable into its repo and tag:
+
+```yaml
+  overrideValues:
+    image.repository: '{{ ("$IMAGE" | split ":")._0 }}'
+    image.tag: '{{ ("$IMAGE" | split ":")._1 }}'
+```
+
+The resulting override values sent to the helm installation would look like:
+
+```yaml
+  overrideValues:
+    image.repository: quay.io/mycustomrepo/myimage
+    image.tag: latest
+```
+
+## Event generation
+
 To warn users that their CR settings may be ignored, the Helm operator creates events on
 the CR that include the name and value of each overridden value. For example:
 
diff --git a/website/content/en/docs/building-operators/helm/tutorial.md b/website/content/en/docs/building-operators/helm/tutorial.md
index 81a93e50..7cca752d 100644
--- a/website/content/en/docs/building-operators/helm/tutorial.md
+++ b/website/content/en/docs/building-operators/helm/tutorial.md
@@ -197,7 +197,7 @@ There are three ways to run the operator:
 
 - As Go program outside a cluster
 - As a Deployment inside a Kubernetes cluster
-- Managed by the [Operator Lifecycle Manager (OLM)][doc-olm] in [bundle][quickstart-bundle] format
+- Managed by the [Operator Lifecycle Manager (OLM)][doc-olm] in [bundle][tutorial-bundle] format
 
 ### 1. Run locally outside the cluster
 
@@ -248,7 +248,7 @@ has a custom CA, these [configuration steps][image-reg-config] must be complete.
 operator-sdk run bundle example.com/nginx-operator-bundle:v0.0.1
 ```
 
-Check out the [docs][quickstart-bundle] for a deep dive into `operator-sdk`'s OLM integration.
+Check out the [docs][tutorial-bundle] for a deep dive into `operator-sdk`'s OLM integration.
 
 
 ## Create a Nginx CR
@@ -366,7 +366,7 @@ Next, check out the following:
 [helm-charts]:https://helm.sh/docs/topics/charts/
 [helm-values]:https://helm.sh/docs/intro/using_helm/#customizing-the-chart-before-installing
 [helm-official]:https://helm.sh/docs/
-[quickstart-bundle]:/docs/olm-integration/quickstart-bundle
-[doc-olm]:/docs/olm-integration/quickstart-bundle/#enabling-olm
+[tutorial-bundle]:/docs/olm-integration/tutorial-bundle
+[doc-olm]:/docs/olm-integration/tutorial-bundle/#enabling-olm
 [olm-integration]:/docs/olm-integration
 [advanced-features]:/docs/building-operators/helm/reference/advanced_features
diff --git a/website/content/en/docs/faqs/_index.md b/website/content/en/docs/faqs/_index.md
index 6f197a6d..bfdc384c 100644
--- a/website/content/en/docs/faqs/_index.md
+++ b/website/content/en/docs/faqs/_index.md
@@ -20,7 +20,7 @@ For further context about the relationship between Kubebuilder and Operator SDK,
 ## Can I use the Kubebuilder docs?
 
 Yes, you can use [https://book.kubebuilder.io/](https://book.kubebuilder.io/). Just keep in mind that when you see an instruction such as:
-`$ kubebuilder <command>` you will use `$ operator-sdk <command>`. 
+`$ kubebuilder <command>` you will use `$ operator-sdk <command>`.
 
 ## Controller Runtime FAQ
 
@@ -154,5 +154,5 @@ SHELL := /bin/bash
 [owner-references-permission-enforcement]: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement
 [rbac-markers]: https://book.kubebuilder.io/reference/markers/rbac.html
 [rbac]:https://kubernetes.io/docs/reference/access-authn-authz/rbac/
-[scorecard-doc]: https://sdk.operatorframework.io/docs/advanced-topics/scorecard/
+[scorecard-doc]: https://sdk.operatorframework.io/docs/testing-operators/scorecard/
 [project-doc]: /docs/overview/project-layout
diff --git a/website/content/en/docs/olm-integration/quickstart-bundle.md b/website/content/en/docs/olm-integration/quickstart-bundle.md
index 4dcc61c7..0f0fad8f 100644
--- a/website/content/en/docs/olm-integration/quickstart-bundle.md
+++ b/website/content/en/docs/olm-integration/quickstart-bundle.md
@@ -8,288 +8,59 @@ The [Operator Lifecycle Manager (OLM)][olm] is a set of cluster resources that m
 The Operator SDK supports both creating manifests for OLM deployment, and testing your Operator on an OLM-enabled
 Kubernetes cluster.
 
-This document succinctly walks through getting an Operator OLM-ready with [bundles][bundle], and glosses over
-explanations of certain steps for brevity. The following documents contain more detail on these steps:
-- All operator-framework manifest commands supported by the SDK: [CLI overview][doc-cli-overview].
-- Generating operator-framework manifests: [generation overview][doc-olm-generate].
-
-If you are working with package manifests, see the [package manifests quickstart][quickstart-package-manifests]
-once you have completed the *Setup* section below.
+This document is intended to quickly walk through the steps to generate an OLM bundle. For further explanation,
+or if you're using package manifests, see the [Bundle Tutorial][tutorial-bundle].
 
 **Important:** this guide assumes your project was scaffolded with `operator-sdk init --project-version=3`.
 These features are unavailable to projects of version `2` or less; this information can be found by inspecting
 your `PROJECT` file's `version` value.
 
-## Setup
-
-Let's first walk through creating an Operator for `memcached`, a distributed key-value store.
-
-Follow one of the user guides to develop the memcached-operator in either [Go][sdk-user-guide-go],
-[Ansible][sdk-user-guide-ansible], or [Helm][sdk-user-guide-helm], depending on which Operator type you are interested in.
-This guide assumes memcached-operator is on version `0.0.1`, which is set in the `Makefile` variable `VERSION`.
-
-### Enabling OLM
+## Prerequisites
+- Have a working operator that you have uploaded to a container registry. This guide assumes the simple Golang Memcached operator from [the building operators section][sdk-user-guid-go] at version `0.0.1`.
+- User authorized with 'cluster-admin' permissions.
+- OLM installed on your cluster. The command `operator-sdk olm install` will attempt to install a basic OLM deployment on your cluster.
 
-Ensure OLM is enabled on your cluster before following this guide. [`operator-sdk olm`][cli-olm]
-has several subcommands that can install, uninstall, and check the status of particular OLM versions in a cluster.
+## Steps
 
-**Note:** Certain cluster types may already have OLM enabled, but under a non-default (`"olm"`) namespace,
-which can be configured by setting `--olm-namespace=[non-default-olm-namespace]` for `operator-sdk olm status|uninstall` subcommands.
+1. Export environment variables
 
-You can check if OLM is already installed by running the following command,
-which will detect the installed OLM version automatically (0.15.1 in this example):
-
-```console
-$ operator-sdk olm status
-INFO[0000] Fetching CRDs for version "0.15.1"
-INFO[0002] Fetching resources for version "0.15.1"
-INFO[0002] Successfully got OLM status for version "0.15.1"
-
-NAME                                            NAMESPACE    KIND                        STATUS
-olm                                                          Namespace                   Installed
-operatorgroups.operators.coreos.com                          CustomResourceDefinition    Installed
-catalogsources.operators.coreos.com                          CustomResourceDefinition    Installed
-subscriptions.operators.coreos.com                           CustomResourceDefinition    Installed
-...
+```sh
+$ export USERNAME=<container-registry-username>
+$ export VERSON=0.0.1
+$ export IMG=docker.io/$USERNAME/memcached-operator:v$VERSION // location where your operator image is hosted
+$ export BUNDLE_IMG=docker.io/$USERNAME/memcached-operator-bundle:v$VERSION // location where your bundle will be hosted
 ```
 
-All resources listed should have status `Installed`.
-
-If OLM is not already installed, go ahead and install the latest version:
-
-```console
-$ operator-sdk olm install
-INFO[0000] Fetching CRDs for version "latest"
-INFO[0001] Fetching resources for version "latest"
-INFO[0007] Creating CRDs and resources
-INFO[0007]   Creating CustomResourceDefinition "clusterserviceversions.operators.coreos.com"
-INFO[0007]   Creating CustomResourceDefinition "installplans.operators.coreos.com"
-INFO[0007]   Creating CustomResourceDefinition "subscriptions.operators.coreos.com"
-...
-NAME                                            NAMESPACE    KIND                        STATUS
-clusterserviceversions.operators.coreos.com                  CustomResourceDefinition    Installed
-installplans.operators.coreos.com                            CustomResourceDefinition    Installed
-subscriptions.operators.coreos.com                           CustomResourceDefinition    Installed
-catalogsources.operators.coreos.com                          CustomResourceDefinition    Installed
-...
-```
-
-**Note:** By default, `olm status` and `olm uninstall` auto-detect the OLM version installed in your cluster.
-This can fail if the installation is broken in some way, so the version of OLM can be overridden using the
-`--version` flag provided with these commands.
-
-## Creating a bundle
-
-_If working with package manifests, see the [package manifests quickstart][quickstart-package-manifests]._
-
-We will now create bundle manifests by running `make bundle` in the root of the memcached-operator project.
+1. Create a bundle from the root directory of your project
 
-```console
+```sh
 $ make bundle
-/home/user/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
-operator-sdk generate kustomize manifests -q
-kustomize build config/manifests | operator-sdk generate bundle -q --overwrite --version 0.0.1
-INFO[0000] Building annotations.yaml
-INFO[0000] Writing annotations.yaml in /home/user/go/src/github.com/test-org/memcached-operator/bundle/metadata
-INFO[0000] Building Dockerfile
-INFO[0000] Writing bundle.Dockerfile in /home/user/go/src/github.com/test-org/memcached-operator
-operator-sdk bundle validate ./bundle
-INFO[0000] Found annotations file                        bundle-dir=bundle container-tool=docker
-INFO[0000] Could not find optional dependencies file     bundle-dir=bundle container-tool=docker
-INFO[0000] All validation tests have completed successfully
 ```
 
-The above command will have created the following bundle artifacts: a manifests directory
-(`bundle/manifests`) containing a CSV and all CRDs from `config/crds`, [metadata][bundle-metadata]
-directory (`bundle/metadata`), and [`bundle.Dockerfile`][bundle-dockerfile] have been created in
-the Operator project. These files have been statically validated by `operator-sdk bundle validate`
-to ensure the on-disk bundle representation is correct.
+This will prompt you to enter basic information about your operator.
 
-## Deploying an Operator with OLM
+1. Build and push the bundle image
 
-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 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
-
-Before proceeding, make sure you've [Installed OLM](#enabling-olm) onto your
-cluster.
-
-First, we need to build our bundle. To build a memcached-operator bundle, run:
-
-```console
-$ make bundle-build bundle-push BUNDLE_IMG=<some-registry>/memcached-operator-bundle:v0.0.1
+```sh
+$ make bundle-build bundle-push
 ```
 
-Now that the bundle image is present in a registry, [`operator-sdk run bundle`][cli-run-bundle]
-can create a pod to serve that bundle to OLM via a [`Subscription`][install-your-operator],
-along with other OLM objects, ephemerally.
+1. Validate the bundle
 
-```console
-$ operator-sdk run bundle <some-registry>/memcached-operator-bundle:v0.0.1
-INFO[0008] Successfully created registry pod: <some-registry>-memcached-operator-bundle-0-0-1
-INFO[0008] Created CatalogSource: memcached-operator-catalog
-INFO[0008] OperatorGroup "operator-sdk-og" created
-INFO[0008] Created Subscription: memcached-operator-v0-0-1-sub
-INFO[0019] Approved InstallPlan install-krv7q for the Subscription: memcached-operator-v0-0-1-sub
-INFO[0019] Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.1" to reach 'Succeeded' phase
-INFO[0019]   Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.1" to appear
-INFO[0031]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Pending
-INFO[0032]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Installing
-INFO[0040]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Succeeded
-INFO[0040] OLM has successfully installed "memcached-operator.v0.0.1"
+```sh
+$ operator-sdk bundle validate $BUNDLE_IMG
 ```
 
-<!-- TODO(jmccormick2001): add `scorecard` usage here -->
-
-### Upgrading a bundle to a newer version
+1. Install the bundle with OLM
 
-We can use the `operator-sdk run bundle-upgrade` command with a newer version of bundle image to upgrade
-an existing operator bundle deployed on cluster. The command automates the manual orchestration typically required to upgrade an operator
-from one version to another. It extracts the package name from bundle, finds the existing subscription, updates the catalog
-source, deletes the existing registry pod and creates a new registry pod with the version of bundle image provided in the command.
-
-Let's upgrade the previously deployed memcached-operator bundle from version `0.0.1` to `0.0.2`.
-
-```console
-$ operator-sdk run bundle-upgrade <some-registry>/memcached-operator-bundle:v0.0.2
-INFO[0002] Found existing subscription with name memcached-operator-bundle-0-0-1-sub and namespace default
-INFO[0002] Found existing catalog source with name memcached-operator-catalog and namespace default
-INFO[0007] Successfully created registry pod: <some-registry>-memcached-operator-bundle-0-0-2
-INFO[0007] Updated catalog source memcached-operator-catalog with address and annotations
-INFO[0008] Deleted previous registry pod with name "<some-registry>-memcached-operator-bundle-0-0-1"
-INFO[0050] Approved InstallPlan install-c8fkh for the Subscription: memcached-operator-bundle-0-0-1-sub
-INFO[0050] Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.2" to reach 'Succeeded' phase
-INFO[0050]   Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.2" to appear
-INFO[0052]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Pending
-INFO[0057]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: InstallReady
-INFO[0058]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Installing
-INFO[0095]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Succeeded
-INFO[0095] Successfully upgraded to "memcached-operator.v0.0.2"
+```sh
+$ operator-sdk run bundle $BUNDLE_IMG
 ```
 
-#### Upgrading a bundle that was installed traditionally using OLM
-
-An operator bundle can be upgraded even if it was originally deployed using OLM without using the `run bundle` command.
-
-Let's see how to deploy an operator bundle traditionally using OLM and then upgrade the operator bundle to a newer version.
-
-First, create a CatalogSource by building the CatalogSource from a catalog.
-
-```console
-$ oc create -f catalogsource.yaml
-```
-
-```yaml
-# catalogsource.yaml
-apiVersion: operators.coreos.com/v1alpha1
-kind: CatalogSource
-metadata:
-  name: etcdoperator
-  namespace: default
-spec:
-  displayName: Etcd Operators
-  image: <some-registry>/etcd-catalog:latest
-  sourceType: grpc
-```
-
-Next, install the operator bundle by creating a subscription.
-
-```console
-$ oc create -f subscription.yaml
-```
-
-```yaml
-# subscription.yaml
-apiVersion: v1
-items:
-- apiVersion: operators.coreos.com/v1alpha1
-  kind: Subscription
-  metadata:
-    name: etcd
-    namespace: default
-  spec:
-    channel: "stable"
-    installPlanApproval: Manual
-    name: etcd
-    source: etcdoperator
-    sourceNamespace: default
-    startingCSV: etcdoperator.v0.0.1
-```
-
-Once the Operator bundle is deployed, you can use the `run bundle-upgrade` command by specifing the new bundle image that you want to upgrade to.
-
-```console
-$ operator-sdk run bundle-upgrade <some-registry>/etcd-bundle:v0.0.2
-INFO[0000] Found existing subscription with name etcd and namespace default
-INFO[0000] Found existing catalog source with name etcdoperator and namespace default
-INFO[0005] Successfully created registry pod: <some-registry>-etcd-bundle-0-0-2
-INFO[0005] Updated catalog source etcdoperator with address and annotations
-INFO[0005] Deleted previous registry pod with name "<some-registry>-etcd-bundle-0-0-1"
-INFO[0005] Approved InstallPlan install-6vrzh for the Subscription: etcd
-INFO[0005] Waiting for ClusterServiceVersion "default/etcdoperator.v0.0.2" to reach 'Succeeded' phase
-INFO[0005]   Waiting for ClusterServiceVersion "default/etcdoperator.v0.0.2" to appear
-INFO[0007]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Pending
-INFO[0008]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Installing
-INFO[0018]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Succeeded
-INFO[0018] Successfully upgraded to "etcdoperator.v0.0.2"
-```
-
-### Deploying bundles in production
-
-OLM and Operator Registry consumes Operator bundles via a catalog of Operators, implemented as an
-[index image][index-image], which are composed of one or more bundles. To build and push a
-memcached-operator bundle image for version v0.0.1, run:
-
-```console
-$ make bundle-build bundle-push BUNDLE_IMG=<some-registry>/memcached-operator-bundle:v0.0.1
-```
-
-Now you can build and push the catalog by running `catalog-*` Makfile targets, which use
-the Operator package manager tool [`opm`][opm] to [build][doc-index-build] the catalog:
-
-```console
-$ make catalog-build catalog-push CATALOG_IMG=<some-registry>/memcached-operator-catalog:v0.0.1
-```
-
-Assuming `IMAGE_TAG_BASE = <some-registry>/memcached-operator` has the desired tag base, you can inline
-the above two commands to:
-
-```console
-$ make bundle-build bundle-push catalog-build catalog-push
-```
-
-Which will build and push both `<some-registry>/memcached-operator-bundle:v0.0.1`
-and `<some-registry>/memcached-operator-catalog:v0.0.1`.
-
-## Further reading
-
-In-depth discussions of OLM concepts mentioned here:
-- [CatalogSource][catalogsource]
-- [Subscription][subscription]
-- [Install an Operator from a catalog][olm-install]
+## Next Steps
 
+Read the [full tutorial][tutorial-bundle] for a more in-depth look at creating and using a bundle.
 
+[tutorial-bundle]:/docs/olm-integration/tutorial-bundle
 [sdk-user-guide-go]:/docs/building-operators/golang/quickstart
-[sdk-user-guide-ansible]:/docs/building-operators/ansible/quickstart
-[sdk-user-guide-helm]:/docs/building-operators/helm/quickstart
-[quickstart-package-manifests]:/docs/olm-integration/quickstart-package-manifests
 [olm]:https://github.com/operator-framework/operator-lifecycle-manager/
-[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
-[doc-olm-generate]:/docs/olm-integration/generation
-[opm]:https://github.com/operator-framework/operator-registry/blob/master/docs/design/opm-tooling.md
-[index-image]:https://github.com/operator-framework/operator-registry/blob/master/docs/design/opm-tooling.md#index
-[doc-index-build]:https://github.com/operator-framework/operator-registry#building-an-index-of-operators-using-opm
-[install-your-operator]:https://olm.operatorframework.io/docs/tasks/install-operator-with-olm/#install-your-operator
-[catalogsource]:https://olm.operatorframework.io/docs/concepts/crds/catalogsource/
-[subscription]:https://olm.operatorframework.io/docs/concepts/crds/subscription/
-[olm-install]:https://olm.operatorframework.io/docs/tasks/install-operator-with-olm/
diff --git a/website/content/en/docs/olm-integration/tutorial-bundle.md b/website/content/en/docs/olm-integration/tutorial-bundle.md
new file mode 100644
index 00000000..fbdd782d
--- /dev/null
+++ b/website/content/en/docs/olm-integration/tutorial-bundle.md
@@ -0,0 +1,295 @@
+---
+title: OLM Integration Bundle Tutorial
+linkTitle: Bundle Tutorial
+weight: 1
+---
+
+The [Operator Lifecycle Manager (OLM)][olm] is a set of cluster resources that manage the lifecycle of an Operator.
+The Operator SDK supports both creating manifests for OLM deployment, and testing your Operator on an OLM-enabled
+Kubernetes cluster.
+
+This document succinctly walks through getting an Operator OLM-ready with [bundles][bundle], and glosses over
+explanations of certain steps for brevity. The following documents contain more detail on these steps:
+- All operator-framework manifest commands supported by the SDK: [CLI overview][doc-cli-overview].
+- Generating operator-framework manifests: [generation overview][doc-olm-generate].
+
+If you are working with package manifests, see the [package manifests tutorial][tutorial-package-manifests]
+once you have completed the *Setup* section below.
+
+**Important:** this guide assumes your project was scaffolded with `operator-sdk init --project-version=3`.
+These features are unavailable to projects of version `2` or less; this information can be found by inspecting
+your `PROJECT` file's `version` value.
+
+## Setup
+
+Let's first walk through creating an Operator for `memcached`, a distributed key-value store.
+
+Follow one of the user guides to develop the memcached-operator in either [Go][sdk-user-guide-go],
+[Ansible][sdk-user-guide-ansible], or [Helm][sdk-user-guide-helm], depending on which Operator type you are interested in.
+This guide assumes memcached-operator is on version `0.0.1`, which is set in the `Makefile` variable `VERSION`.
+
+### Enabling OLM
+
+Ensure OLM is enabled on your cluster before following this guide. [`operator-sdk olm`][cli-olm]
+has several subcommands that can install, uninstall, and check the status of particular OLM versions in a cluster.
+
+**Note:** Certain cluster types may already have OLM enabled, but under a non-default (`"olm"`) namespace,
+which can be configured by setting `--olm-namespace=[non-default-olm-namespace]` for `operator-sdk olm status|uninstall` subcommands.
+
+You can check if OLM is already installed by running the following command,
+which will detect the installed OLM version automatically (0.15.1 in this example):
+
+```console
+$ operator-sdk olm status
+INFO[0000] Fetching CRDs for version "0.15.1"
+INFO[0002] Fetching resources for version "0.15.1"
+INFO[0002] Successfully got OLM status for version "0.15.1"
+
+NAME                                            NAMESPACE    KIND                        STATUS
+olm                                                          Namespace                   Installed
+operatorgroups.operators.coreos.com                          CustomResourceDefinition    Installed
+catalogsources.operators.coreos.com                          CustomResourceDefinition    Installed
+subscriptions.operators.coreos.com                           CustomResourceDefinition    Installed
+...
+```
+
+All resources listed should have status `Installed`.
+
+If OLM is not already installed, go ahead and install the latest version:
+
+```console
+$ operator-sdk olm install
+INFO[0000] Fetching CRDs for version "latest"
+INFO[0001] Fetching resources for version "latest"
+INFO[0007] Creating CRDs and resources
+INFO[0007]   Creating CustomResourceDefinition "clusterserviceversions.operators.coreos.com"
+INFO[0007]   Creating CustomResourceDefinition "installplans.operators.coreos.com"
+INFO[0007]   Creating CustomResourceDefinition "subscriptions.operators.coreos.com"
+...
+NAME                                            NAMESPACE    KIND                        STATUS
+clusterserviceversions.operators.coreos.com                  CustomResourceDefinition    Installed
+installplans.operators.coreos.com                            CustomResourceDefinition    Installed
+subscriptions.operators.coreos.com                           CustomResourceDefinition    Installed
+catalogsources.operators.coreos.com                          CustomResourceDefinition    Installed
+...
+```
+
+**Note:** By default, `olm status` and `olm uninstall` auto-detect the OLM version installed in your cluster.
+This can fail if the installation is broken in some way, so the version of OLM can be overridden using the
+`--version` flag provided with these commands.
+
+## Creating a bundle
+
+_If working with package manifests, see the [package manifests tutorial][tutorial-package-manifests]._
+
+We will now create bundle manifests by running `make bundle` in the root of the memcached-operator project.
+
+```console
+$ make bundle
+/home/user/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+operator-sdk generate kustomize manifests -q
+kustomize build config/manifests | operator-sdk generate bundle -q --overwrite --version 0.0.1
+INFO[0000] Building annotations.yaml
+INFO[0000] Writing annotations.yaml in /home/user/go/src/github.com/test-org/memcached-operator/bundle/metadata
+INFO[0000] Building Dockerfile
+INFO[0000] Writing bundle.Dockerfile in /home/user/go/src/github.com/test-org/memcached-operator
+operator-sdk bundle validate ./bundle
+INFO[0000] Found annotations file                        bundle-dir=bundle container-tool=docker
+INFO[0000] Could not find optional dependencies file     bundle-dir=bundle container-tool=docker
+INFO[0000] All validation tests have completed successfully
+```
+
+The above command will have created the following bundle artifacts: a manifests directory
+(`bundle/manifests`) containing a CSV and all CRDs from `config/crds`, [metadata][bundle-metadata]
+directory (`bundle/metadata`), and [`bundle.Dockerfile`][bundle-dockerfile] have been created in
+the Operator project. These files have been statically validated by `operator-sdk bundle validate`
+to ensure the on-disk bundle representation is correct.
+
+## Deploying an Operator with OLM
+
+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 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
+
+Before proceeding, make sure you've [Installed OLM](#enabling-olm) onto your
+cluster.
+
+First, we need to build our bundle. To build a memcached-operator bundle, run:
+
+```console
+$ make bundle-build bundle-push BUNDLE_IMG=<some-registry>/memcached-operator-bundle:v0.0.1
+```
+
+Now that the bundle image is present in a registry, [`operator-sdk run bundle`][cli-run-bundle]
+can create a pod to serve that bundle to OLM via a [`Subscription`][install-your-operator],
+along with other OLM objects, ephemerally.
+
+```console
+$ operator-sdk run bundle <some-registry>/memcached-operator-bundle:v0.0.1
+INFO[0008] Successfully created registry pod: <some-registry>-memcached-operator-bundle-0-0-1
+INFO[0008] Created CatalogSource: memcached-operator-catalog
+INFO[0008] OperatorGroup "operator-sdk-og" created
+INFO[0008] Created Subscription: memcached-operator-v0-0-1-sub
+INFO[0019] Approved InstallPlan install-krv7q for the Subscription: memcached-operator-v0-0-1-sub
+INFO[0019] Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.1" to reach 'Succeeded' phase
+INFO[0019]   Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.1" to appear
+INFO[0031]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Pending
+INFO[0032]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Installing
+INFO[0040]   Found ClusterServiceVersion "default/memcached-operator.v0.0.1" phase: Succeeded
+INFO[0040] OLM has successfully installed "memcached-operator.v0.0.1"
+```
+
+<!-- TODO(jmccormick2001): add `scorecard` usage here -->
+
+### Upgrading a bundle to a newer version
+
+We can use the `operator-sdk run bundle-upgrade` command with a newer version of bundle image to upgrade
+an existing operator bundle deployed on cluster. The command automates the manual orchestration typically required to upgrade an operator
+from one version to another. It extracts the package name from bundle, finds the existing subscription, updates the catalog
+source, deletes the existing registry pod and creates a new registry pod with the version of bundle image provided in the command.
+
+Let's upgrade the previously deployed memcached-operator bundle from version `0.0.1` to `0.0.2`.
+
+```console
+$ operator-sdk run bundle-upgrade <some-registry>/memcached-operator-bundle:v0.0.2
+INFO[0002] Found existing subscription with name memcached-operator-bundle-0-0-1-sub and namespace default
+INFO[0002] Found existing catalog source with name memcached-operator-catalog and namespace default
+INFO[0007] Successfully created registry pod: <some-registry>-memcached-operator-bundle-0-0-2
+INFO[0007] Updated catalog source memcached-operator-catalog with address and annotations
+INFO[0008] Deleted previous registry pod with name "<some-registry>-memcached-operator-bundle-0-0-1"
+INFO[0050] Approved InstallPlan install-c8fkh for the Subscription: memcached-operator-bundle-0-0-1-sub
+INFO[0050] Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.2" to reach 'Succeeded' phase
+INFO[0050]   Waiting for ClusterServiceVersion "default/memcached-operator.v0.0.2" to appear
+INFO[0052]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Pending
+INFO[0057]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: InstallReady
+INFO[0058]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Installing
+INFO[0095]   Found ClusterServiceVersion "default/memcached-operator.v0.0.2" phase: Succeeded
+INFO[0095] Successfully upgraded to "memcached-operator.v0.0.2"
+```
+
+#### Upgrading a bundle that was installed traditionally using OLM
+
+An operator bundle can be upgraded even if it was originally deployed using OLM without using the `run bundle` command.
+
+Let's see how to deploy an operator bundle traditionally using OLM and then upgrade the operator bundle to a newer version.
+
+First, create a CatalogSource by building the CatalogSource from a catalog.
+
+```console
+$ oc create -f catalogsource.yaml
+```
+
+```yaml
+# catalogsource.yaml
+apiVersion: operators.coreos.com/v1alpha1
+kind: CatalogSource
+metadata:
+  name: etcdoperator
+  namespace: default
+spec:
+  displayName: Etcd Operators
+  image: <some-registry>/etcd-catalog:latest
+  sourceType: grpc
+```
+
+Next, install the operator bundle by creating a subscription.
+
+```console
+$ oc create -f subscription.yaml
+```
+
+```yaml
+# subscription.yaml
+apiVersion: v1
+items:
+- apiVersion: operators.coreos.com/v1alpha1
+  kind: Subscription
+  metadata:
+    name: etcd
+    namespace: default
+  spec:
+    channel: "stable"
+    installPlanApproval: Manual
+    name: etcd
+    source: etcdoperator
+    sourceNamespace: default
+    startingCSV: etcdoperator.v0.0.1
+```
+
+Once the Operator bundle is deployed, you can use the `run bundle-upgrade` command by specifing the new bundle image that you want to upgrade to.
+
+```console
+$ operator-sdk run bundle-upgrade <some-registry>/etcd-bundle:v0.0.2
+INFO[0000] Found existing subscription with name etcd and namespace default
+INFO[0000] Found existing catalog source with name etcdoperator and namespace default
+INFO[0005] Successfully created registry pod: <some-registry>-etcd-bundle-0-0-2
+INFO[0005] Updated catalog source etcdoperator with address and annotations
+INFO[0005] Deleted previous registry pod with name "<some-registry>-etcd-bundle-0-0-1"
+INFO[0005] Approved InstallPlan install-6vrzh for the Subscription: etcd
+INFO[0005] Waiting for ClusterServiceVersion "default/etcdoperator.v0.0.2" to reach 'Succeeded' phase
+INFO[0005]   Waiting for ClusterServiceVersion "default/etcdoperator.v0.0.2" to appear
+INFO[0007]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Pending
+INFO[0008]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Installing
+INFO[0018]   Found ClusterServiceVersion "default/etcdoperator.v0.0.2" phase: Succeeded
+INFO[0018] Successfully upgraded to "etcdoperator.v0.0.2"
+```
+
+### Deploying bundles in production
+
+OLM and Operator Registry consumes Operator bundles via a catalog of Operators, implemented as an
+[index image][index-image], which are composed of one or more bundles. To build and push a
+memcached-operator bundle image for version v0.0.1, run:
+
+```console
+$ make bundle-build bundle-push BUNDLE_IMG=<some-registry>/memcached-operator-bundle:v0.0.1
+```
+
+Now you can build and push the catalog by running `catalog-*` Makfile targets, which use
+the Operator package manager tool [`opm`][opm] to [build][doc-index-build] the catalog:
+
+```console
+$ make catalog-build catalog-push CATALOG_IMG=<some-registry>/memcached-operator-catalog:v0.0.1
+```
+
+Assuming `IMAGE_TAG_BASE = <some-registry>/memcached-operator` has the desired tag base, you can inline
+the above two commands to:
+
+```console
+$ make bundle-build bundle-push catalog-build catalog-push
+```
+
+Which will build and push both `<some-registry>/memcached-operator-bundle:v0.0.1`
+and `<some-registry>/memcached-operator-catalog:v0.0.1`.
+
+## Further reading
+
+In-depth discussions of OLM concepts mentioned here:
+- [CatalogSource][catalogsource]
+- [Subscription][subscription]
+- [Install an Operator from a catalog][olm-install]
+
+
+[sdk-user-guide-go]:/docs/building-operators/golang/quickstart
+[sdk-user-guide-ansible]:/docs/building-operators/ansible/quickstart
+[sdk-user-guide-helm]:/docs/building-operators/helm/quickstart
+[tutorial-package-manifests]:/docs/olm-integration/tutorial-package-manifests
+[olm]:https://github.com/operator-framework/operator-lifecycle-manager/
+[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
+[doc-olm-generate]:/docs/olm-integration/generation
+[opm]:https://github.com/operator-framework/operator-registry/blob/master/docs/design/opm-tooling.md
+[index-image]:https://github.com/operator-framework/operator-registry/blob/master/docs/design/opm-tooling.md#index
+[doc-index-build]:https://github.com/operator-framework/operator-registry#building-an-index-of-operators-using-opm
+[install-your-operator]:https://olm.operatorframework.io/docs/tasks/install-operator-with-olm/#install-your-operator
+[catalogsource]:https://olm.operatorframework.io/docs/concepts/crds/catalogsource/
+[subscription]:https://olm.operatorframework.io/docs/concepts/crds/subscription/
+[olm-install]:https://olm.operatorframework.io/docs/tasks/install-operator-with-olm/
diff --git a/website/content/en/docs/olm-integration/quickstart-package-manifests.md b/website/content/en/docs/olm-integration/tutorial-package-manifests.md
similarity index 100%
rename from website/content/en/docs/olm-integration/quickstart-package-manifests.md
rename to website/content/en/docs/olm-integration/tutorial-package-manifests.md
diff --git a/website/content/en/docs/overview/cheat-sheet.md b/website/content/en/docs/overview/cheat-sheet.md
index 50498fdd..7b856483 100644
--- a/website/content/en/docs/overview/cheat-sheet.md
+++ b/website/content/en/docs/overview/cheat-sheet.md
@@ -30,7 +30,7 @@ For further information check [Operator SDK Integration with Operator Lifecycle
 
 | Command   | Description  |
 |-------|-----------|
-| `make bundle`          | Create/update the [bundle][bundle] based on the project manifests in the `bundle/` directory. More info [Create a bundle][creating-a-bundle].      |
+| `make bundle`          | Create/update the [bundle][bundle] based on the project manifests in the `bundle/` directory. For more info see [Create a bundle][creating-a-bundle].      |
 | `operator-sdk bundle validate ./bundle`          | To validate your [bundle][bundle] spec definition.      |
 | `operator-sdk bundle validate ./bundle --select-optional suite=operatorframework` | Validate your bundle against [OperatorHub.io][operatorhub-io] criteria. For further information use the flag `--help`. |
 | `operator-sdk olm install` | To install OLM on your cluster for development purposes. |
@@ -66,7 +66,7 @@ make bundle CHANNELS=fast,preview DEFAULT_CHANNEL=stable VERSION=1.0.0 IMG=<some
 **NOTE:** This is not a comprehensive list of make targets or commands. Please see the scaffolded Makefile and `make help` for the full list of targets. Note that you can use `operator-sdk <command> --help` and check the [CLI][cli] section to check all options.
  
 [olm-integration]: /docs/olm-integration/
-[creating-a-bundle]: /docs/olm-integration/quickstart-bundle/#creating-a-bundle
+[creating-a-bundle]: /docs/olm-integration/tutorial-bundle/#creating-a-bundle
 [bundle]:https://github.com/operator-framework/operator-registry/blob/v1.16.1/docs/design/operator-bundle.md
 [operatorhub-io]: https://operatorhub.io/
 [upgrade-project]: /docs/olm-integration/generation/#upgrade-your-operator
diff --git a/website/content/en/docs/upgrading-sdk-version/v1.0.0.md b/website/content/en/docs/upgrading-sdk-version/v1.0.0.md
index ad32facb..fecefa13 100644
--- a/website/content/en/docs/upgrading-sdk-version/v1.0.0.md
+++ b/website/content/en/docs/upgrading-sdk-version/v1.0.0.md
@@ -31,7 +31,7 @@ The following subcommands were removed:
 | `operator-sdk migrate`        | Removed support for hybrid operators, no migration                                                       | [#3385](https://github.com/operator-framework/operator-sdk/pull/3385)
 | `operator-sdk print-deps`     | Removed, no migration                                                                                    | [#3385](https://github.com/operator-framework/operator-sdk/pull/3385)
 | `operator-sdk run local`      | Use `make run`                                                                                           | [#3406](https://github.com/operator-framework/operator-sdk/pull/3406)
-| `operator-sdk test`           | Use controller-runtime's [envtest]https://book.kubebuilder.io/reference/envtest.html) framework          | [#3409](https://github.com/operator-framework/operator-sdk/pull/3409)
+| `operator-sdk test`           | Use controller-runtime's [envtest](https://book.kubebuilder.io/reference/envtest.html) framework          | [#3409](https://github.com/operator-framework/operator-sdk/pull/3409)
 
 ### Library changes
 


Test patch:
diff --git a/internal/ansible/controller/controller_test.go b/internal/ansible/controller/controller_test.go
new file mode 100644
index 00000000..968fdef1
--- /dev/null
+++ b/internal/ansible/controller/controller_test.go
@@ -0,0 +1,39 @@
+// 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 controller
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestFilterPredicate(t *testing.T) {
+	matchLabelPass := make(map[string]string)
+	matchLabelPass["testKey"] = "testValue"
+	selectorPass := metav1.LabelSelector{
+		MatchLabels: matchLabelPass,
+	}
+	noSelector := metav1.LabelSelector{}
+
+	passPredicate, err := parsePredicateSelector(selectorPass)
+	assert.Equal(t, nil, err, "Verify that no error is thrown on a valid populated selector")
+	assert.NotEqual(t, nil, passPredicate, "Verify that a predicate is constructed using a valid selector")
+
+	nilPredicate, err := parsePredicateSelector(noSelector)
+	assert.Equal(t, nil, err, "Verify that no error is thrown on a valid unpopulated selector")
+	assert.Equal(t, nil, nilPredicate, "Verify correct parsing of an unpopulated selector")
+}
diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go
index 55fda9d3..5f20b476 100644
--- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go
+++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go
@@ -379,7 +379,7 @@ var _ = Describe("findMatchingDeploymentAndServiceForWebhook", func() {
 		port1 := new(int32)
 		*port1 = 2311
 		crdToConfigPath := map[string]apiextv1.WebhookConversion{
-			"crd-test-1": apiextv1.WebhookConversion{
+			"crd-test-1": {
 				ClientConfig: &apiextv1.WebhookClientConfig{
 					Service: &apiextv1.ServiceReference{
 						Path: &path1,
@@ -388,7 +388,7 @@ var _ = Describe("findMatchingDeploymentAndServiceForWebhook", func() {
 				},
 			},
 
-			"crd-test-2": apiextv1.WebhookConversion{
+			"crd-test-2": {
 				ClientConfig: &apiextv1.WebhookClientConfig{
 					Service: &apiextv1.ServiceReference{
 						Path: &path1,
diff --git a/internal/generate/collector/clusterserviceversion_test.go b/internal/generate/collector/clusterserviceversion_test.go
index 8604c836..94a476f8 100644
--- a/internal/generate/collector/clusterserviceversion_test.go
+++ b/internal/generate/collector/clusterserviceversion_test.go
@@ -21,6 +21,7 @@ import (
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -160,6 +161,10 @@ var _ = Describe("SplitCSVPermissionsObjects", func() {
 
 		complexTestSetup := func() {
 			c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount(depSA)}
+			c.ServiceAccounts = []corev1.ServiceAccount{
+				newServiceAccount(depSA),
+				newServiceAccount(extraSA),
+			}
 			role1, role2 := newRole(roleName1), newRole(roleName2)
 			c.Roles = []rbacv1.Role{role1, role2}
 			// Use the same names as for Roles to make sure Kind is respected
@@ -198,12 +203,14 @@ var _ = Describe("SplitCSVPermissionsObjects", func() {
 			Expect(getClusterRoleNames(inPerm)).To(Equal([]string{roleName1}))
 			Expect(inCPerm).To(HaveLen(1))
 			Expect(getClusterRoleNames(inCPerm)).To(Equal([]string{roleName1}))
-			Expect(out).To(HaveLen(5))
+			Expect(out).To(HaveLen(6))
 			Expect(getRoleNames(out)).To(Equal([]string{roleName2}))
 			Expect(getClusterRoleNames(out)).To(Equal([]string{roleName2}))
 			Expect(getRoleBindingNames(out)).To(Equal([]string{bindingName1, bindingName2}))
 			Expect(getClusterRoleBindingNames(out)).To(Equal([]string{bindingName2}))
+			Expect(getServiceAccountNames(out)).To(Equal([]string{extraSA}))
 		})
+
 		It("contains a Deployment serviceAccountName and extra ServiceAccount", func() {
 			complexTestSetup()
 			inPerm, inCPerm, out = c.SplitCSVPermissionsObjects([]string{extraSA})
@@ -233,6 +240,10 @@ func getClusterRoleBindingNames(objs []client.Object) []string {
 	return getNamesForKind("ClusterRoleBinding", objs)
 }
 
+func getServiceAccountNames(objs []client.Object) []string {
+	return getNamesForKind("ServiceAccount", objs)
+}
+
 func getNamesForKind(kind string, objs []client.Object) (names []string) {
 	for _, obj := range objs {
 		if obj.GetObjectKind().GroupVersionKind().Kind == kind {
@@ -300,3 +311,9 @@ func newSubject(name, kind string) (s rbacv1.Subject) {
 func newServiceAccountSubject(name string) (s rbacv1.Subject) {
 	return newSubject(name, "ServiceAccount")
 }
+
+func newServiceAccount(name string) (sa corev1.ServiceAccount) {
+	sa.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount"))
+	sa.Name = name
+	return sa
+}
diff --git a/internal/helm/release/manager_test.go b/internal/helm/release/manager_test.go
index 70c5e5b5..466bb693 100644
--- a/internal/helm/release/manager_test.go
+++ b/internal/helm/release/manager_test.go
@@ -15,21 +15,18 @@
 package release
 
 import (
-	"bytes"
-	"encoding/json"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-	cpb "helm.sh/helm/v3/pkg/chart"
-	lpb "helm.sh/helm/v3/pkg/chart/loader"
-	rpb "helm.sh/helm/v3/pkg/release"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	apitypes "k8s.io/apimachinery/pkg/types"
+	"k8s.io/cli-runtime/pkg/resource"
+
 	appsv1 "k8s.io/api/apps/v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/runtime"
-	apitypes "k8s.io/apimachinery/pkg/types"
-	"k8s.io/cli-runtime/pkg/resource"
 )
 
 func newTestUnstructured(containers []interface{}) *unstructured.Unstructured {
@@ -216,85 +213,3 @@ func TestManagerGenerateStrategicMergePatch(t *testing.T) {
 		assert.Equal(t, test.patch, string(diff))
 	}
 }
-
-func TestManagerisUpgrade(t *testing.T) {
-	tests := []struct {
-		name            string
-		releaseName     string
-		releaseNs       string
-		values          map[string]interface{}
-		chart           *cpb.Chart
-		deployedRelease *rpb.Release
-		want            bool
-	}{
-		{
-			name:            "ok",
-			releaseName:     "deployed",
-			releaseNs:       "deployed-ns",
-			values:          map[string]interface{}{"key": "value"},
-			chart:           newTestChart(t, "./testdata/simple"),
-			deployedRelease: newTestRelease(newTestChart(t, "./testdata/simple"), map[string]interface{}{"key": "value"}, "deployed", "deployed-ns"),
-			want:            false,
-		},
-		{
-			name:            "different chart",
-			releaseName:     "deployed",
-			releaseNs:       "deployed-ns",
-			values:          map[string]interface{}{"key": "value"},
-			chart:           newTestChart(t, "./testdata/simple"),
-			deployedRelease: newTestRelease(newTestChart(t, "./testdata/simpledf"), map[string]interface{}{"key": "value"}, "deployed", "deployed-ns"),
-			want:            true,
-		},
-		{
-			name:            "different values",
-			releaseName:     "deployed",
-			releaseNs:       "deployed-ns",
-			values:          map[string]interface{}{"key": "1", "int": int32(1)},
-			chart:           newTestChart(t, "./testdata/simple"),
-			deployedRelease: newTestRelease(newTestChart(t, "./testdata/simple"), map[string]interface{}{"key": "", "int": int64(1)}, "deployed", "deployed-ns"),
-			want:            true,
-		},
-		{
-			name:            "nil values",
-			releaseName:     "deployed",
-			releaseNs:       "deployed-ns",
-			values:          nil,
-			chart:           newTestChart(t, "./testdata/simple"),
-			deployedRelease: newTestRelease(newTestChart(t, "./testdata/simple"), map[string]interface{}{}, "deployed", "deployed-ns"),
-			want:            false,
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			m := manager{
-				releaseName: test.releaseName,
-				namespace:   test.releaseNs,
-				values:      test.values,
-				chart:       test.chart,
-			}
-			isUpgrade, err := m.isUpgrade(test.deployedRelease)
-			assert.Equal(t, test.want, isUpgrade)
-			assert.Equal(t, nil, err)
-		})
-	}
-}
-
-func newTestChart(t *testing.T, path string) *cpb.Chart {
-	chart, err := lpb.Load(path)
-	assert.Nil(t, err)
-	return chart
-}
-
-func newTestRelease(chart *cpb.Chart, values map[string]interface{}, name, namespace string) *rpb.Release { // nolint: unparam
-	release := rpb.Mock(&rpb.MockReleaseOptions{
-		Name:      name,
-		Namespace: namespace,
-		Version:   1,
-	})
-
-	buffer := &bytes.Buffer{}
-	_ = json.NewEncoder(buffer).Encode(chart)
-	_ = json.NewDecoder(buffer).Decode(release.Chart)
-	release.Config = values
-	return release
-}
diff --git a/internal/helm/release/testdata/simple/.helmignore b/internal/helm/release/testdata/simple/.helmignore
deleted file mode 100644
index f0c13194..00000000
--- a/internal/helm/release/testdata/simple/.helmignore
+++ /dev/null
@@ -1,21 +0,0 @@
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
diff --git a/internal/helm/release/testdata/simple/Chart.yaml b/internal/helm/release/testdata/simple/Chart.yaml
deleted file mode 100644
index 786c7721..00000000
--- a/internal/helm/release/testdata/simple/Chart.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: v1
-description: a simple charts
-name: simple
-keywords:
-  - helm
-  - operator-sdk
-version: 0.0.1+master
-appVersion: "master"
diff --git a/internal/helm/release/testdata/simple/templates/NOTES.txt b/internal/helm/release/testdata/simple/templates/NOTES.txt
deleted file mode 100644
index da930300..00000000
--- a/internal/helm/release/testdata/simple/templates/NOTES.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-A simple charts
\ No newline at end of file
diff --git a/internal/helm/release/testdata/simple/templates/_helpers.tpl b/internal/helm/release/testdata/simple/templates/_helpers.tpl
deleted file mode 100644
index e5fe9272..00000000
--- a/internal/helm/release/testdata/simple/templates/_helpers.tpl
+++ /dev/null
@@ -1,7 +0,0 @@
-{{/* vim: set filetype=mustache: */}}
-{{/*
-Expand the name of the chart.
-*/}}
-{{- define "simple.fullname" -}}
-{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
-{{- end -}}
diff --git a/internal/helm/release/testdata/simple/templates/secrets.yaml b/internal/helm/release/testdata/simple/templates/secrets.yaml
deleted file mode 100644
index 4b61fbb7..00000000
--- a/internal/helm/release/testdata/simple/templates/secrets.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: v1
-kind: Secret
-metadata:
-  name: {{ template "simple.fullname" . }}
-  namespace: {{ .Release.Namespace }}
-type: Opaque
-data:
-  password: {{ randAlphaNum 10 | b64enc | quote }}
\ No newline at end of file
diff --git a/internal/helm/release/testdata/simple/values.yaml b/internal/helm/release/testdata/simple/values.yaml
deleted file mode 100644
index ca762273..00000000
--- a/internal/helm/release/testdata/simple/values.yaml
+++ /dev/null
@@ -1,1 +0,0 @@
-# simple values
\ No newline at end of file
diff --git a/internal/helm/release/testdata/simpledf/.helmignore b/internal/helm/release/testdata/simpledf/.helmignore
deleted file mode 100644
index f0c13194..00000000
--- a/internal/helm/release/testdata/simpledf/.helmignore
+++ /dev/null
@@ -1,21 +0,0 @@
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
diff --git a/internal/helm/release/testdata/simpledf/Chart.yaml b/internal/helm/release/testdata/simpledf/Chart.yaml
deleted file mode 100644
index 786c7721..00000000
--- a/internal/helm/release/testdata/simpledf/Chart.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: v1
-description: a simple charts
-name: simple
-keywords:
-  - helm
-  - operator-sdk
-version: 0.0.1+master
-appVersion: "master"
diff --git a/internal/helm/release/testdata/simpledf/templates/NOTES.txt b/internal/helm/release/testdata/simpledf/templates/NOTES.txt
deleted file mode 100644
index da930300..00000000
--- a/internal/helm/release/testdata/simpledf/templates/NOTES.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-A simple charts
\ No newline at end of file
diff --git a/internal/helm/release/testdata/simpledf/templates/_helpers.tpl b/internal/helm/release/testdata/simpledf/templates/_helpers.tpl
deleted file mode 100644
index e5fe9272..00000000
--- a/internal/helm/release/testdata/simpledf/templates/_helpers.tpl
+++ /dev/null
@@ -1,7 +0,0 @@
-{{/* vim: set filetype=mustache: */}}
-{{/*
-Expand the name of the chart.
-*/}}
-{{- define "simple.fullname" -}}
-{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
-{{- end -}}
diff --git a/internal/helm/release/testdata/simpledf/templates/secrets.yaml b/internal/helm/release/testdata/simpledf/templates/secrets.yaml
deleted file mode 100644
index 4b61fbb7..00000000
--- a/internal/helm/release/testdata/simpledf/templates/secrets.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: v1
-kind: Secret
-metadata:
-  name: {{ template "simple.fullname" . }}
-  namespace: {{ .Release.Namespace }}
-type: Opaque
-data:
-  password: {{ randAlphaNum 10 | b64enc | quote }}
\ No newline at end of file
diff --git a/internal/helm/release/testdata/simpledf/values.yaml b/internal/helm/release/testdata/simpledf/values.yaml
deleted file mode 100644
index a99c72dc..00000000
--- a/internal/helm/release/testdata/simpledf/values.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-# simple values
-name: simpledf
\ No newline at end of file
diff --git a/internal/helm/watches/watches_test.go b/internal/helm/watches/watches_test.go
index cdea650c..7f543f39 100644
--- a/internal/helm/watches/watches_test.go
+++ b/internal/helm/watches/watches_test.go
@@ -55,7 +55,7 @@ func TestLoadReader(t *testing.T) {
 			expectErr: false,
 		},
 		{
-			name: "valid with override expansion",
+			name: "valid with override env expansion",
 			data: `---
 - group: mygroup
   version: v1alpha1
@@ -76,6 +76,48 @@ func TestLoadReader(t *testing.T) {
 			},
 			expectErr: false,
 		},
+		{
+			name: "valid with override template expansion",
+			data: `---
+- group: mygroup
+  version: v1alpha1
+  kind: MyKind
+  chart: ../../../internal/plugins/helm/v1/chartutil/testdata/test-chart
+  watchDependentResources: false
+  overrideValues:
+    repo: '{{ ("$MY_IMAGE" | split ":")._0 }}'
+    tag: '{{ ("$MY_IMAGE" | split ":")._1 }}'
+`,
+			env: map[string]string{"MY_IMAGE": "quay.io/operator-framework/helm-operator:latest"},
+			expectWatches: []Watch{
+				{
+					GroupVersionKind:        schema.GroupVersionKind{Group: "mygroup", Version: "v1alpha1", Kind: "MyKind"},
+					ChartDir:                "../../../internal/plugins/helm/v1/chartutil/testdata/test-chart",
+					WatchDependentResources: &falseVal,
+					OverrideValues: map[string]string{
+						"repo": "quay.io/operator-framework/helm-operator",
+						"tag":  "latest",
+					},
+				},
+			},
+			expectErr: false,
+		},
+
+		{
+			name: "invalid with override template expansion",
+			data: `---
+- group: mygroup
+  version: v1alpha1
+  kind: MyKind
+  chart: ../../../internal/plugins/helm/v1/chartutil/testdata/test-chart
+  watchDependentResources: false
+  overrideValues:
+    repo: '{{ ("$MY_IMAGE" | split ":")._0 }}'
+    tag: '{{ ("$MY_IMAGE" | split ":")._1'
+`,
+			env:       map[string]string{"MY_IMAGE": "quay.io/operator-framework/helm-operator:latest"},
+			expectErr: true,
+		},
 		{
 			name: "multiple gvk",
 			data: `---
diff --git a/test/common/scorecard.go b/test/common/scorecard.go
index 3cb8d5f9..57b23820 100644
--- a/test/common/scorecard.go
+++ b/test/common/scorecard.go
@@ -91,5 +91,22 @@ func ScorecardSpec(tc *testutils.TestContext, operatorType string) func() {
 				Expect(results[0].State).To(Equal(expected[results[0].Name]))
 			}
 		})
+
+		It("should configure scorecard storage successfully", func() {
+			cmd = exec.Command(tc.BinaryName, "scorecard", "bundle",
+				"--selector", "suite=basic",
+				"--output", "json",
+				"--test-output", "/testdata",
+				"--wait-time", "4m")
+			outputBytes, err = tc.Run(cmd)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(json.Unmarshal(outputBytes, &output)).To(Succeed())
+
+			Expect(output.Items).To(HaveLen(1))
+			results := output.Items[0].Status.Results
+			Expect(results).To(HaveLen(1))
+			Expect(results[0].Name).To(Equal("basic-check-spec"))
+			Expect(results[0].State).To(Equal(v1alpha3.PassState))
+		})
 	}
 }
diff --git a/testdata/ansible/memcached-operator/Makefile b/testdata/ansible/memcached-operator/Makefile
index 7c1ac731..b071b04c 100644
--- a/testdata/ansible/memcached-operator/Makefile
+++ b/testdata/ansible/memcached-operator/Makefile
@@ -59,7 +59,7 @@ help: ## Display this help.
 ##@ Build
 
 run: ansible-operator ## Run against the configured Kubernetes cluster in ~/.kube/config
-	$(ANSIBLE_OPERATOR) run
+	ANSIBLE_ROLES_PATH="$(ANSIBLE_ROLES_PATH):$(shell pwd)/roles" $(ANSIBLE_OPERATOR) run
 
 docker-build: ## Build docker image with the manager.
 	docker build -t ${IMG} .
diff --git a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml b/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
index 157a0cef..79085e5a 100644
--- a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
+++ b/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml b/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
deleted file mode 100644
index adc7cf0c..00000000
--- a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  creationTimestamp: null
-  name: memcached-operator-controller-manager
diff --git a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml b/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
index 985dcef3..e81ccabb 100644
--- a/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
+++ b/testdata/ansible/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
@@ -115,6 +115,7 @@ spec:
                 ports:
                 - containerPort: 8443
                   name: https
+                  protocol: TCP
                 resources: {}
               - args:
                 - --health-probe-bind-address=:6789
diff --git a/testdata/ansible/memcached-operator/config/default/manager_auth_proxy_patch.yaml b/testdata/ansible/memcached-operator/config/default/manager_auth_proxy_patch.yaml
index fabc2065..8fbf836e 100644
--- a/testdata/ansible/memcached-operator/config/default/manager_auth_proxy_patch.yaml
+++ b/testdata/ansible/memcached-operator/config/default/manager_auth_proxy_patch.yaml
@@ -18,6 +18,7 @@ spec:
         - "--v=10"
         ports:
         - containerPort: 8443
+          protocol: TCP
           name: https
       - name: manager
         args:
diff --git a/testdata/ansible/memcached-operator/config/rbac/auth_proxy_service.yaml b/testdata/ansible/memcached-operator/config/rbac/auth_proxy_service.yaml
index 6cf656be..71f17972 100644
--- a/testdata/ansible/memcached-operator/config/rbac/auth_proxy_service.yaml
+++ b/testdata/ansible/memcached-operator/config/rbac/auth_proxy_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/testdata/go/v3/memcached-operator/Makefile b/testdata/go/v3/memcached-operator/Makefile
index c8744e94..2cf8fbab 100644
--- a/testdata/go/v3/memcached-operator/Makefile
+++ b/testdata/go/v3/memcached-operator/Makefile
@@ -39,6 +39,8 @@ BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
 IMG ?= controller:latest
 # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
 CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
+# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+ENVTEST_K8S_VERSION = 1.21
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -86,7 +88,7 @@ vet: ## Run go vet against code.
 	go vet ./...
 
 test: manifests generate fmt vet envtest ## Run tests.
-	go test ./... -coverprofile cover.out
+	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
 
 ##@ Build
 
@@ -129,7 +131,6 @@ kustomize: ## Download kustomize locally if necessary.
 ENVTEST = $(shell pwd)/bin/setup-envtest
 envtest: ## Download envtest-setup locally if necessary.
 	$(call go-get-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
-	$(ENVTEST) use 1.21
 
 # go-get-tool will 'go get' any package $2 and install it to $1.
 PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
diff --git a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
index 157a0cef..79085e5a 100644
--- a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
+++ b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
deleted file mode 100644
index adc7cf0c..00000000
--- a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  creationTimestamp: null
-  name: memcached-operator-controller-manager
diff --git a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-webhook-service_v1_service.yaml b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-webhook-service_v1_service.yaml
index 4c9ef443..3bb3965d 100644
--- a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-webhook-service_v1_service.yaml
+++ b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator-webhook-service_v1_service.yaml
@@ -6,6 +6,7 @@ metadata:
 spec:
   ports:
   - port: 443
+    protocol: TCP
     targetPort: 9443
   selector:
     control-plane: controller-manager
diff --git a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
index 8d9dd284..b8bf2a34 100644
--- a/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
+++ b/testdata/go/v3/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
@@ -119,6 +119,7 @@ spec:
                 ports:
                 - containerPort: 8443
                   name: https
+                  protocol: TCP
                 resources: {}
               - args:
                 - --health-probe-bind-address=:8081
@@ -146,8 +147,8 @@ spec:
                   periodSeconds: 10
                 resources:
                   limits:
-                    cpu: 100m
-                    memory: 30Mi
+                    cpu: 200m
+                    memory: 100Mi
                   requests:
                     cpu: 100m
                     memory: 20Mi
diff --git a/testdata/go/v3/memcached-operator/config/default/manager_auth_proxy_patch.yaml b/testdata/go/v3/memcached-operator/config/default/manager_auth_proxy_patch.yaml
index a224be19..4e2232fa 100644
--- a/testdata/go/v3/memcached-operator/config/default/manager_auth_proxy_patch.yaml
+++ b/testdata/go/v3/memcached-operator/config/default/manager_auth_proxy_patch.yaml
@@ -18,6 +18,7 @@ spec:
         - "--v=10"
         ports:
         - containerPort: 8443
+          protocol: TCP
           name: https
       - name: manager
         args:
diff --git a/testdata/go/v3/memcached-operator/config/manager/manager.yaml b/testdata/go/v3/memcached-operator/config/manager/manager.yaml
index 79adfe72..20262442 100644
--- a/testdata/go/v3/memcached-operator/config/manager/manager.yaml
+++ b/testdata/go/v3/memcached-operator/config/manager/manager.yaml
@@ -47,8 +47,8 @@ spec:
           periodSeconds: 10
         resources:
           limits:
-            cpu: 100m
-            memory: 30Mi
+            cpu: 200m
+            memory: 100Mi
           requests:
             cpu: 100m
             memory: 20Mi
diff --git a/testdata/go/v3/memcached-operator/config/rbac/auth_proxy_service.yaml b/testdata/go/v3/memcached-operator/config/rbac/auth_proxy_service.yaml
index 6cf656be..71f17972 100644
--- a/testdata/go/v3/memcached-operator/config/rbac/auth_proxy_service.yaml
+++ b/testdata/go/v3/memcached-operator/config/rbac/auth_proxy_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/testdata/go/v3/memcached-operator/config/webhook/service.yaml b/testdata/go/v3/memcached-operator/config/webhook/service.yaml
index 31e0f829..3f638bd9 100644
--- a/testdata/go/v3/memcached-operator/config/webhook/service.yaml
+++ b/testdata/go/v3/memcached-operator/config/webhook/service.yaml
@@ -7,6 +7,7 @@ metadata:
 spec:
   ports:
     - port: 443
+      protocol: TCP
       targetPort: 9443
   selector:
     control-plane: controller-manager
diff --git a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml b/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
index 157a0cef..79085e5a 100644
--- a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
+++ b/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager-metrics-service_v1_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml b/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
deleted file mode 100644
index adc7cf0c..00000000
--- a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator-controller-manager_v1_serviceaccount.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  creationTimestamp: null
-  name: memcached-operator-controller-manager
diff --git a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml b/testdata/helm/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
index 7d025a25..6016a7cf 100644
--- a/testdata/helm/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
+++ b/testdata/helm/memcached-operator/bundle/manifests/memcached-operator.clusterserviceversion.yaml
@@ -203,6 +203,7 @@ spec:
                 ports:
                 - containerPort: 8443
                   name: https
+                  protocol: TCP
                 resources: {}
               - args:
                 - --health-probe-bind-address=:8081
@@ -225,8 +226,8 @@ spec:
                   periodSeconds: 10
                 resources:
                   limits:
-                    cpu: 100m
-                    memory: 90Mi
+                    cpu: 200m
+                    memory: 100Mi
                   requests:
                     cpu: 100m
                     memory: 60Mi
diff --git a/testdata/helm/memcached-operator/config/default/manager_auth_proxy_patch.yaml b/testdata/helm/memcached-operator/config/default/manager_auth_proxy_patch.yaml
index 33886eb0..92bc8851 100644
--- a/testdata/helm/memcached-operator/config/default/manager_auth_proxy_patch.yaml
+++ b/testdata/helm/memcached-operator/config/default/manager_auth_proxy_patch.yaml
@@ -18,6 +18,7 @@ spec:
         - "--v=10"
         ports:
         - containerPort: 8443
+          protocol: TCP
           name: https
       - name: manager
         args:
diff --git a/testdata/helm/memcached-operator/config/manager/manager.yaml b/testdata/helm/memcached-operator/config/manager/manager.yaml
index 6dde7ee4..cb2ea1ba 100644
--- a/testdata/helm/memcached-operator/config/manager/manager.yaml
+++ b/testdata/helm/memcached-operator/config/manager/manager.yaml
@@ -46,8 +46,8 @@ spec:
           periodSeconds: 10
         resources:
           limits:
-            cpu: 100m
-            memory: 90Mi
+            cpu: 200m
+            memory: 100Mi
           requests:
             cpu: 100m
             memory: 60Mi
diff --git a/testdata/helm/memcached-operator/config/rbac/auth_proxy_service.yaml b/testdata/helm/memcached-operator/config/rbac/auth_proxy_service.yaml
index 6cf656be..71f17972 100644
--- a/testdata/helm/memcached-operator/config/rbac/auth_proxy_service.yaml
+++ b/testdata/helm/memcached-operator/config/rbac/auth_proxy_service.yaml
@@ -9,6 +9,7 @@ spec:
   ports:
   - name: https
     port: 8443
+    protocol: TCP
     targetPort: https
   selector:
     control-plane: controller-manager
diff --git a/website/content/en/docs/olm-integration/testing-deployment.md b/website/content/en/docs/olm-integration/testing-deployment.md
index d69eab15..83141766 100644
--- a/website/content/en/docs/olm-integration/testing-deployment.md
+++ b/website/content/en/docs/olm-integration/testing-deployment.md
@@ -174,6 +174,6 @@ Let's look at the anatomy of the `cleanup` configuration model:
 [csv-install-modes]:https://github.com/operator-framework/operator-lifecycle-manager/blob/master/doc/design/building-your-csv.md#operator-metadata
 [cli-olm-install]:/docs/cli/operator-sdk_olm_install
 [cli-olm-status]:/docs/cli/operator-sdk_olm_status
-[creating-bundles]:/docs/olm-integration/quickstart-bundle/#creating-a-bundle
+[creating-bundles]:/docs/olm-integration/tutorial-bundle/#creating-a-bundle
 [add-sa-secret]:https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
 [image-reg-config]:/docs/olm-integration/cli-overview#private-bundle-and-catalog-image-registries
diff --git a/website/content/en/docs/testing-operators/scorecard/custom-tests.md b/website/content/en/docs/testing-operators/scorecard/custom-tests.md
index 0a72542f..76e60ca4 100644
--- a/website/content/en/docs/testing-operators/scorecard/custom-tests.md
+++ b/website/content/en/docs/testing-operators/scorecard/custom-tests.md
@@ -242,6 +242,16 @@ The `--skip-cleanup` flag can be used when executing the `operator-sdk scorecard
 This is useful when debugging or writing new tests so that you can view
 the test logs or the pod manifests.
 
+### Storing scorecard test output
+
+The `--test-output` flag can be used when executing the `operator-sdk scorecard` command with a config specifying [output persistence][storage] to store the output of the scorecard tests in a specific directory. Any persistent volume data will be stored in the specified local directory upon completion of the scorecard tests.
+
+```console
+$ operator-sdk scorecard ./bundle --test-output=/mytestoutput
+```
+
+**Note**: By default, the gathered test output will be stored in `$(pwd)/test-output`.
+
 ### Scorecard initContainer
 
 The scorecard inserts an `initContainer` into the test pods it creates. The
@@ -301,3 +311,4 @@ connection to invoke the Kube API.
 [sample_makefile]: https://github.com/operator-framework/operator-sdk/blob/09c3aa14625965af9f22f513cd5c891471dbded2/Makefile
 [kustomize-patchJson6902]: https://kubernetes-sigs.github.io/kustomize/api-reference/kustomization/patchesjson6902/
 [testresults]:https://github.com/operator-framework/api/blob/333d064/pkg/apis/scorecard/v1alpha3/test_types.go#L35
+[storage]:https://pkg.go.dev/github.com/operator-framework/api@v0.10.4/pkg/apis/scorecard/v1alpha3#Storage