Repository: operator-framework/operator-sdk
Pull request #5096 (2021-07-28 18:06:24)

Add Testing for Scorecard Storage Output Feature
The scorecard storage feature has merged and now needs automated testing within our repo. Opening as a draft in order to test/sample various testing strategies.


Patch:
diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD
index d3c390a6..abfd8fa0 100644
--- a/CONTRIBUTING.MD
+++ b/CONTRIBUTING.MD
@@ -64,8 +64,13 @@ The first line is the subject and should be no longer than 70 characters, the se
 
 Most contributions involve some sort of documentation. See our [doc contribution guidelines][doc-contrib] for details.
 
+## Changelog
+
+The operator-sdk maintains a changelog and most contributions should involve a changelog entry. See our [changelog contribution guidelines][changelog-contrib] for details.
+
 [operator_framework]: https://groups.google.com/forum/#!forum/operator-framework
 [developer-guide]: https://sdk.operatorframework.io/docs/contribution-guidelines/developer-guide/
 [reporting-issues]: https://sdk.operatorframework.io/docs/contribution-guidelines/reporting-issues/
 [golang-style-doc]: https://github.com/golang/go/wiki/CodeReviewComments
-[doc-contrib]:https://sdk.operatorframework.io/docs/contribution-guidelines/documentation
+[doc-contrib]: https://sdk.operatorframework.io/docs/contribution-guidelines/documentation
+[changelog-contrib]:https://master.sdk.operatorframework.io/docs/contribution-guidelines/changelog
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-labelselector.yaml b/changelog/fragments/helm-labelselector.yaml
new file mode 100644
index 00000000..1e2d1049
--- /dev/null
+++ b/changelog/fragments/helm-labelselector.yaml
@@ -0,0 +1,7 @@
+entries:
+  - description: >
+      Added a predicate for helm operators that filters resources based on selectors specified in watches.yaml.
+      Only the resources that contain the labels specified by selectors will be reconciled.
+
+    kind: addition
+    breaking: false
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/cmd/helm-operator/run/cmd.go b/internal/cmd/helm-operator/run/cmd.go
index ef367713..e909d27c 100644
--- a/internal/cmd/helm-operator/run/cmd.go
+++ b/internal/cmd/helm-operator/run/cmd.go
@@ -202,6 +202,7 @@ func run(cmd *cobra.Command, f *flags.Flags) {
 			WatchDependentResources: *w.WatchDependentResources,
 			OverrideValues:          w.OverrideValues,
 			MaxConcurrentReconciles: f.MaxConcurrentReconciles,
+			Selector:                w.Selector,
 		})
 		if err != nil {
 			log.Error(err, "Failed to add manager factory to controller.")
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/controller/controller.go b/internal/helm/controller/controller.go
index 3870fa78..7b4e44ba 100644
--- a/internal/helm/controller/controller.go
+++ b/internal/helm/controller/controller.go
@@ -16,6 +16,7 @@ package controller
 
 import (
 	"fmt"
+	"reflect"
 	"strings"
 	"sync"
 	"time"
@@ -30,6 +31,7 @@ import (
 	crthandler "sigs.k8s.io/controller-runtime/pkg/handler"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
+	ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 	"sigs.k8s.io/yaml"
 
@@ -51,6 +53,7 @@ type WatchOptions struct {
 	WatchDependentResources bool
 	OverrideValues          map[string]string
 	MaxConcurrentReconciles int
+	Selector                metav1.LabelSelector
 }
 
 // Add creates a new helm operator controller and adds it to the manager
@@ -80,7 +83,19 @@ func Add(mgr manager.Manager, options WatchOptions) error {
 
 	o := &unstructured.Unstructured{}
 	o.SetGroupVersionKind(options.GVK)
-	if err := c.Watch(&source.Kind{Type: o}, &libhandler.InstrumentedEnqueueRequestForObject{}); err != nil {
+
+	var preds []ctrlpredicate.Predicate
+	p, err := parsePredicateSelector(options.Selector)
+
+	if err != nil {
+		return err
+	}
+
+	if p != nil {
+		preds = append(preds, p)
+	}
+
+	if err := c.Watch(&source.Kind{Type: o}, &libhandler.InstrumentedEnqueueRequestForObject{}, preds...); err != nil {
 		return err
 	}
 
@@ -93,6 +108,20 @@ func Add(mgr manager.Manager, options WatchOptions) error {
 	return nil
 }
 
+// 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
+}
+
 // watchDependentResources adds a release hook function to the HelmOperatorReconciler
 // that adds watches for resources in released Helm charts.
 func watchDependentResources(mgr manager.Manager, r *HelmOperatorReconciler, c controller.Controller) {
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 c02413ab..e2f43a1d 100644
--- a/internal/helm/watches/watches.go
+++ b/internal/helm/watches/watches.go
@@ -15,13 +15,17 @@
 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"
 	"sigs.k8s.io/yaml"
 )
@@ -32,9 +36,10 @@ const WatchesFile = "watches.yaml"
 // custom resource.
 type Watch struct {
 	schema.GroupVersionKind `json:",inline"`
-	ChartDir                string            `json:"chart"`
-	WatchDependentResources *bool             `json:"watchDependentResources,omitempty"`
-	OverrideValues          map[string]string `json:"overrideValues,omitempty"`
+	ChartDir                string               `json:"chart"`
+	WatchDependentResources *bool                `json:"watchDependentResources,omitempty"`
+	OverrideValues          map[string]string    `json:"overrideValues,omitempty"`
+	Selector                metav1.LabelSelector `json:"selector"`
 }
 
 // UnmarshalYAML unmarshals an individual watch from the Helm watches.yaml file
@@ -110,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/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/reference/watches.md b/website/content/en/docs/building-operators/helm/reference/watches.md
index de423dfd..f8447b68 100644
--- a/website/content/en/docs/building-operators/helm/reference/watches.md
+++ b/website/content/en/docs/building-operators/helm/reference/watches.md
@@ -19,6 +19,7 @@ The follow tables describes the fields in an entry in `watches.yaml`:
 | chart                   | The path to the helm chart to use when reconciling this GVK.  |
 | watchDependentResources | Enable watching resources that are created by helm (default: `true`). |
 | overrideValues          | Values to be used for overriding Helm chart's defaults. For additional information see the [reference doc][override-values]. |
+| selector                | The conditions that a resource's labels must satsify in order to get reconciled. For additional information see [labels and selectors documentation][label-selector-doc]. |
 
 
 For reference, here is an example of a simple `watches.yaml` file:
@@ -31,7 +32,11 @@ For reference, here is an example of a simple `watches.yaml` file:
   chart: helm-charts/foo
   overrideValues:
     image.repository: quay.io/mycustomrepo
-  watchDependentResources: false   
+  watchDependentResources: false
+  selector:
+    matchExpressions:
+     - {key: testLabel, operator: Exists, values: []}
 ```
 
 [override-values]: /docs/building-operators/helm/reference/advanced_features/override_values/
+[label-selector-doc]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
diff --git a/website/content/en/docs/contribution-guidelines/changelog.md b/website/content/en/docs/contribution-guidelines/changelog.md
new file mode 100644
index 00000000..01c4f008
--- /dev/null
+++ b/website/content/en/docs/contribution-guidelines/changelog.md
@@ -0,0 +1,18 @@
+---
+title: Changelog
+weight: 25
+---
+
+The Operator SDK project tracks changes across releases by maintaining a live changelog. This changelog is compiled via fragments located in `changelog/fragments`
+
+Contributors are asked to add these changelog fragments when creating pull requests by following this [template][changelog-template].
+
+The changlog fragments document the following information:
+
+* Description of Changes
+* Types of Changes
+  * One of: addition, change, deprecation, removal, bugfix
+* Migration Guide for Changes
+  * These are required for breaking changes, and essential (but not required) for scaffolding-related changes.
+
+[changelog-template]: https://github.com/operator-framework/operator-sdk/blob/master/changelog/fragments/00-template.yaml
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/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/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/controller/controller_test.go b/internal/helm/controller/controller_test.go
new file mode 100644
index 00000000..968fdef1
--- /dev/null
+++ b/internal/helm/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/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/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