Repository: operator-framework/operator-sdk
Pull request #4555 (2021-02-19 01:10:20)

ci: add ansible-operator-base image and manual build workflow
**Description of the change:**
- images/ansible-operator/base.Dockerfile: Dockerfile for the ansible-operator-base image
- .github/workflows/deploy-manual.yml: start workflows manually, namely the ansible-operator-base image deployment job

**Motivation for the change:** the ansible-operator image takes awhile to build, so it would be nice to build a base image containing the long-running dependency installation steps every so often, ex. when dependencies are updated.

/area dependency

**Checklist**

If the pull request includes user-facing changes, extra documentation is required:
- [ ] Add a new changelog fragment in `changelog/fragments` (see [`changelog/fragments/00-template.yaml`](https://github.com/operator-framework/operator-sdk/tree/master/changelog/fragments/00-template.yaml))
- [ ] Add or update relevant sections of the docs website in [`website/content/en/docs`](https://github.com/operator-framework/operator-sdk/tree/master/website/content/en/docs)


estroz: /cc @jmrodri @theishshah 
estroz: The default tag would look like
```
quay.io/operator-framework/ansible-operator-base:v1.4.0-24-g0c75f7af1801373a3c85e3fb728dd2dccfd6a851
```
Which would then be pinned in `images/ansible-operator/Dockerfile` like
```dockerfile
FROM quay.io/operator-framework/ansible-operator-base:v1.4.0-24-g0c75f7af1801373a3c85e3fb728dd2dccfd6a851

ARG BIN=bin/ansible-operator
COPY $BIN /usr/local/bin/ansible-operator

CMD ["/usr/local/bin/ansible-operator", "run", "--watches-file=./watches.yaml"]
```
estroz: /retest

Patch:
diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml
new file mode 100644
index 00000000..09ec2190
--- /dev/null
+++ b/.github/workflows/deploy-manual.yml
@@ -0,0 +1,53 @@
+name: deploy-manual
+
+on:
+  workflow_dispatch:
+    inputs:
+      tag:
+        description: ansible-operator-base image tag, ex. "v1.2.3-10-g6e1b47e6ca7c507b8ecf197a8edcd412dd64d85d"
+        required: false
+
+jobs:
+  # Build the ansible-operator-base image.
+  ansible-operator-base:
+    runs-on: ubuntu-18.04
+    environment: deploy
+    steps:
+
+    - name: set up qemu
+      uses: docker/setup-qemu-action@v1
+
+    - name: set up buildx
+      uses: docker/setup-buildx-action@v1
+
+    - name: quay.io login
+      uses: docker/login-action@v1
+      with:
+        username: ${{ secrets.QUAY_USERNAME }}
+        password: ${{ secrets.QUAY_PASSWORD }}
+        registry: quay.io
+
+    - name: create tag
+      id: tag
+      run: |
+        set -e
+        IMG=quay.io/operator-framework/ansible-operator-base
+        TAG="${{ github.event.inputs.tag }}"
+        if [[ "$TAG" == "" ]]; then
+          TAG=$(git describe --tags --always --dirty --long --abbrev=100)
+        fi
+        echo ::set-output name=tag::${IMG}:${TAG}
+
+    - name: checkout
+      uses: actions/checkout@v2
+      with:
+        fetch-depth: 1
+
+    - name: build and push
+      uses: docker/build-push-action@v2
+      with:
+        file: ./images/ansible-operator/base.Dockerfile
+        context: ./images/ansible-operator
+        platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x
+        push: true
+        tags: ${{ steps.tag.outputs.tag }}
diff --git a/changelog/fragments/bugfix-4575.yaml b/changelog/fragments/bugfix-4575.yaml
new file mode 100644
index 00000000..119e6f1f
--- /dev/null
+++ b/changelog/fragments/bugfix-4575.yaml
@@ -0,0 +1,5 @@
+entries:
+  - description: >
+      Fixed samples kustomization.yaml generation on `operator-sdk init --plugins=helm --helm-chart=<chart>`,
+      caused by out-of-order operations in plugin code.
+    kind: bugfix
diff --git a/hack/generate/samples/internal/helm/memcached.go b/hack/generate/samples/internal/helm/memcached.go
index 83f81e1b..b9cfc086 100644
--- a/hack/generate/samples/internal/helm/memcached.go
+++ b/hack/generate/samples/internal/helm/memcached.go
@@ -65,21 +65,16 @@ func (mh *MemcachedHelm) Run() {
 	// role and cause sanity testing to fail.
 	os.Setenv("KUBECONFIG", "broken_so_we_generate_static_default_rules")
 
-	log.Infof("creating the project")
+	helmChartPath := "../../../hack/generate/samples/internal/helm/testdata/memcached-0.0.1.tgz"
+	log.Infof("creating the project using the helm chart in: (%v)", helmChartPath)
 	err := mh.ctx.Init(
 		"--plugins", "helm",
-		"--domain", mh.ctx.Domain)
-	pkg.CheckError("creating the project", err)
-
-	helmChartPath := "../../../hack/generate/samples/internal/helm/testdata/memcached-0.0.1.tgz"
-	log.Infof("using the helm chart in: (%v)", helmChartPath)
-
-	err = mh.ctx.CreateAPI(
+		"--domain", mh.ctx.Domain,
 		"--group", mh.ctx.Group,
 		"--version", mh.ctx.Version,
 		"--kind", mh.ctx.Kind,
 		"--helm-chart", helmChartPath)
-	pkg.CheckError("scaffolding apis", err)
+	pkg.CheckError("creating the project", err)
 
 	log.Infof("customizing the sample")
 	err = testutils.ReplaceInFile(
diff --git a/images/ansible-operator/Dockerfile b/images/ansible-operator/Dockerfile
index 81ae0546..02ac66fb 100644
--- a/images/ansible-operator/Dockerfile
+++ b/images/ansible-operator/Dockerfile
@@ -17,6 +17,8 @@ COPY . .
 RUN GOOS=linux GOARCH=$TARGETARCH make build/ansible-operator
 
 # Final image.
+# TODO(estroz): replace ubi image in FROM with the following base image once a build has occurred:
+# FROM quay.io/operator-framework/ansible-operator-base:<tag>-<commit-ish>
 FROM registry.access.redhat.com/ubi8/ubi:8.3-227
 ARG TARGETARCH
 
diff --git a/images/ansible-operator/base.Dockerfile b/images/ansible-operator/base.Dockerfile
new file mode 100644
index 00000000..74850020
--- /dev/null
+++ b/images/ansible-operator/base.Dockerfile
@@ -0,0 +1,52 @@
+# This Dockerfile defines the base image for the ansible-operator image.
+# It is built with dependencies that take a while to download, thus speeding
+# up ansible deploy jobs.
+
+FROM registry.access.redhat.com/ubi8/ubi:8.3-227
+ARG TARGETARCH
+
+RUN mkdir -p /etc/ansible \
+  && echo "localhost ansible_connection=local" > /etc/ansible/hosts \
+  && echo '\n\
+[defaults]\n\
+roles_path = /opt/ansible/roles\n\
+library = /usr/share/ansible/openshift\n'\
+> /etc/ansible/ansible.cfg
+
+ENV HOME=/opt/ansible \
+    USER_NAME=ansible \
+    USER_UID=1001
+
+# Install python dependencies
+# Ensure fresh metadata rather than cached metadata in the base by running
+# yum clean all && rm -rf /var/yum/cache/* first
+RUN yum clean all && rm -rf /var/cache/yum/* \
+  && yum -y update \
+  && yum install -y libffi-devel openssl-devel python38-devel gcc python38-pip python38-setuptools \
+  && pip3 install --no-cache-dir \
+    cryptography==3.3.2 \
+    ansible-runner==1.3.4 \
+    ansible-runner-http==1.0.0 \
+    ipaddress==1.0.23 \
+    kubernetes==10.1.0 \
+    openshift==0.10.3 \
+    ansible==2.9.15 \
+    jmespath==0.10.0 \
+  && yum remove -y gcc libffi-devel openssl-devel python38-devel \
+  && yum clean all \
+  && rm -rf /var/cache/yum
+
+# Ensure directory permissions are properly set
+RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd \
+  && mkdir -p ${HOME}/.ansible/tmp \
+  && chown -R ${USER_UID}:0 ${HOME} \
+  && chmod -R ug+rwx ${HOME}
+
+ENV TINI_VERSION=v0.19.0
+RUN curl -L -o /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} \
+  && chmod +x /tini && /tini --version
+
+WORKDIR ${HOME}
+USER ${USER_UID}
+
+ENTRYPOINT ["/tini", "--"]
diff --git a/internal/plugins/helm/v1/api.go b/internal/plugins/helm/v1/api.go
index 3463692e..b006de58 100644
--- a/internal/plugins/helm/v1/api.go
+++ b/internal/plugins/helm/v1/api.go
@@ -15,10 +15,12 @@
 package v1
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 
 	"github.com/spf13/pflag"
+	"helm.sh/helm/v3/pkg/chart"
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
@@ -31,8 +33,11 @@ import (
 )
 
 type createAPISubcommand struct {
-	config        config.Config
-	createOptions chartutil.CreateOptions
+	config  config.Config
+	options chartutil.CreateOptions
+
+	resource *resource.Resource
+	chrt     *chart.Chart
 }
 
 var (
@@ -87,32 +92,26 @@ func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) {
 }
 
 const (
-	groupFlag            = "group"
-	versionFlag          = "version"
-	kindFlag             = "kind"
 	helmChartFlag        = "helm-chart"
 	helmChartRepoFlag    = "helm-chart-repo"
 	helmChartVersionFlag = "helm-chart-version"
-	crdVersionFlag       = "crd-version"
 
-	crdVersionV1      = "v1"
-	crdVersionV1beta1 = "v1beta1"
+	defaultCRDVersion = "v1"
 )
 
 // BindFlags will set the flags for the plugin
 func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
-	p.createOptions = chartutil.CreateOptions{}
 	fs.SortFlags = false
 
-	fs.StringVar(&p.createOptions.GVK.Group, groupFlag, "", "resource group")
-	fs.StringVar(&p.createOptions.GVK.Version, versionFlag, "", "resource version")
-	fs.StringVar(&p.createOptions.GVK.Kind, kindFlag, "", "resource kind")
+	fs.StringVar(&p.options.GVK.Group, "group", "", "resource group")
+	fs.StringVar(&p.options.GVK.Version, "version", "", "resource version")
+	fs.StringVar(&p.options.GVK.Kind, "kind", "", "resource kind")
 
-	fs.StringVar(&p.createOptions.Chart, helmChartFlag, "", "helm chart")
-	fs.StringVar(&p.createOptions.Repo, helmChartRepoFlag, "", "helm chart repository")
-	fs.StringVar(&p.createOptions.Version, helmChartVersionFlag, "", "helm chart version (default: latest)")
+	fs.StringVar(&p.options.Chart, helmChartFlag, "", "helm chart")
+	fs.StringVar(&p.options.Repo, helmChartRepoFlag, "", "helm chart repository")
+	fs.StringVar(&p.options.Version, helmChartVersionFlag, "", "helm chart version (default: latest)")
 
-	fs.StringVar(&p.createOptions.CRDVersion, crdVersionFlag, crdVersionV1, "crd version to generate")
+	fs.StringVar(&p.options.CRDVersion, "crd-version", defaultCRDVersion, "crd version to generate")
 }
 
 // InjectConfig will inject the PROJECT file/config in the plugin
@@ -136,19 +135,19 @@ func (p *createAPISubcommand) Run() error {
 
 // SDK phase 2 plugins.
 func (p *createAPISubcommand) runPhase2() error {
-	ogvk := p.createOptions.GVK
-	gvk := resource.GVK{Group: ogvk.Group, Version: ogvk.Version, Kind: ogvk.Kind}
-
+	if p.resource == nil {
+		return errors.New("resource must not be nil")
+	}
 	// Initially the helm/v1 plugin was written to not create a "plugins" config entry
 	// for any phase 2 plugin because they did not have their own keys. Now there are phase 2
 	// plugin keys, so those plugins should be run if keys exist. Otherwise, enact old behavior.
 
 	if manifestsv2.HasPluginConfig(p.config) {
-		if err := manifestsv2.RunCreateAPI(p.config, gvk); err != nil {
+		if err := manifestsv2.RunCreateAPI(p.config, p.resource.GVK); err != nil {
 			return err
 		}
 	} else {
-		if err := manifests.RunCreateAPI(p.config, gvk); err != nil {
+		if err := manifests.RunCreateAPI(p.config, p.resource.GVK); err != nil {
 			return err
 		}
 	}
@@ -157,36 +156,49 @@ func (p *createAPISubcommand) runPhase2() error {
 }
 
 // Validate perform the required validations for this plugin
-func (p *createAPISubcommand) Validate() error {
-	if p.createOptions.CRDVersion != crdVersionV1 && p.createOptions.CRDVersion != crdVersionV1beta1 {
-		return fmt.Errorf("value of --%s must be either %q or %q", crdVersionFlag, crdVersionV1, crdVersionV1beta1)
-	}
-
-	if len(strings.TrimSpace(p.createOptions.Chart)) == 0 {
-		if len(strings.TrimSpace(p.createOptions.Repo)) != 0 {
+func (p *createAPISubcommand) Validate() (err error) {
+	if len(strings.TrimSpace(p.options.Chart)) == 0 {
+		if len(strings.TrimSpace(p.options.Repo)) != 0 {
 			return fmt.Errorf("value of --%s can only be used with --%s", helmChartRepoFlag, helmChartFlag)
-		} else if len(p.createOptions.Version) != 0 {
+		} else if len(p.options.Version) != 0 {
 			return fmt.Errorf("value of --%s can only be used with --%s", helmChartVersionFlag, helmChartFlag)
 		}
 	}
 
-	if len(strings.TrimSpace(p.createOptions.Chart)) == 0 {
-		if len(strings.TrimSpace(p.createOptions.GVK.Group)) == 0 {
-			return fmt.Errorf("value of --%s must not have empty value", groupFlag)
+	if len(strings.TrimSpace(p.options.Chart)) == 0 {
+		if len(strings.TrimSpace(p.options.GVK.Group)) == 0 {
+			return errors.New("value of --group must not have empty value")
 		}
-		if len(strings.TrimSpace(p.createOptions.GVK.Version)) == 0 {
-			return fmt.Errorf("value of --%s must not have empty value", versionFlag)
+		if len(strings.TrimSpace(p.options.GVK.Version)) == 0 {
+			return errors.New("value of --version must not have empty value")
 		}
-		if len(strings.TrimSpace(p.createOptions.GVK.Kind)) == 0 {
-			return fmt.Errorf("value of --%s must not have empty value", kindFlag)
+		if len(strings.TrimSpace(p.options.GVK.Kind)) == 0 {
+			return errors.New("value of --kind must not have empty value")
 		}
+	}
 
-		// Validate the resource.
-		ogvk := p.createOptions.GVK
-		gvk := resource.GVK{Group: ogvk.Group, Version: ogvk.Version, Kind: ogvk.Kind}
-		if err := gvk.Validate(); err != nil {
-			return err
-		}
+	// Create and validate the resource and chart from CreateOptions.
+	p.resource, p.chrt, err = chartutil.CreateChart(p.config, p.options)
+	if err != nil {
+		return err
+	}
+	if err := p.resource.Validate(); err != nil {
+		return err
+	}
+
+	// Check that resource doesn't exist
+	if p.config.HasResource(p.resource.GVK) {
+		return errors.New("the API resource already exists")
+	}
+
+	// Check that the provided group can be added to the project
+	if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.GVK.Group) {
+		return errors.New("multiple groups are not allowed by default, to enable multi-group set 'multigroup: true' in your PROJECT file")
+	}
+
+	// Check CRDVersion against all other CRDVersions in p.config for compatibility.
+	if !p.config.IsCRDVersionCompatible(p.resource.API.CRDVersion) {
+		return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q", p.resource.API.CRDVersion)
 	}
 
 	return nil
@@ -194,7 +206,7 @@ func (p *createAPISubcommand) Validate() error {
 
 // GetScaffolder returns cmdutil.Scaffolder which will be executed due the RunOptions interface implementation
 func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	return scaffolds.NewAPIScaffolder(p.config, p.createOptions), nil
+	return scaffolds.NewAPIScaffolder(p.config, p.resource, p.chrt), nil
 }
 
 // PostScaffold runs all actions that should be executed after the default plugin scaffold
diff --git a/internal/plugins/helm/v1/chartutil/chart.go b/internal/plugins/helm/v1/chartutil/chart.go
index 671a1cce..c1dc5248 100644
--- a/internal/plugins/helm/v1/chartutil/chart.go
+++ b/internal/plugins/helm/v1/chartutil/chart.go
@@ -32,6 +32,8 @@ import (
 	"helm.sh/helm/v3/pkg/getter"
 	"helm.sh/helm/v3/pkg/repo"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/kubebuilder/v3/pkg/config"
+	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
 )
 
@@ -71,8 +73,7 @@ type CreateOptions struct {
 	Domain string
 }
 
-// CreateChart scaffolds a new helm chart for the project rooted in projectDir
-// based on the passed opts.
+// CreateChart creates a new helm chart based on the passed opts.
 //
 // It returns a scaffold.Resource that can be used by the caller to create
 // other related files. opts.ResourceAPIVersion and opts.ResourceKind are
@@ -82,17 +83,16 @@ type CreateOptions struct {
 // left unset: opts.ResourceAPIVersion defaults to "charts.helm.k8s.io/v1alpha1"
 // and opts.ResourceKind is deduced from the specified opts.Chart.
 //
-// CreateChart also returns a chart.Chart that references the newly created
-// chart.
+// CreateChart also returns the newly created chart.Chart.
 //
-// If opts.Chart is empty, CreateChart scaffolds the default chart from helm's
+// If opts.Chart is empty, CreateChart creates the default chart from helm's
 // default template.
 //
 // If opts.Chart is a local file, CreateChart verifies that it is a valid helm
-// chart archive and unpacks it into the project's helm charts directory.
+// chart archive and returns its chart.Chart representation.
 //
 // If opts.Chart is a local directory, CreateChart verifies that it is a valid
-// helm chart directory and copies it into the project's helm charts directory.
+// helm chart directory and returns its chart.Chart representation.
 //
 // For any other value of opts.Chart, CreateChart attempts to fetch the helm chart
 // from a remote repository.
@@ -115,36 +115,34 @@ type CreateOptions struct {
 // opts.Version is not used when opts.Chart itself refers to a specific version, for
 // example when it is a local path or a URL.
 //
-// CreateChart returns an error if an error occurs creating the scaffold.Resource or
-// creating the chart.
-func CreateChart(projectDir string, opts CreateOptions) (*golang.Options, *chart.Chart, error) {
-	chartsDir := filepath.Join(projectDir, HelmChartsDir)
-	err := os.MkdirAll(chartsDir, 0755)
+// CreateChart returns an error if an error occurs creating the resource.Resource or
+// loading the chart.Chart.
+func CreateChart(cfg config.Config, opts CreateOptions) (r *resource.Resource, c *chart.Chart, err error) {
+	tmpDir, err := ioutil.TempDir("", "osdk-helm-chart")
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to create helm-charts directory: %v", err)
+		return nil, nil, err
 	}
-
-	var (
-		r *golang.Options
-		c *chart.Chart
-	)
+	defer func() {
+		if err := os.RemoveAll(tmpDir); err != nil {
+			log.Errorf("Failed to remove temporary chart directory %s: %s", tmpDir, err)
+		}
+	}()
 
 	// If we don't have a helm chart reference, scaffold the default chart
 	// from Helm's default template. Otherwise, fetch it.
 	if len(opts.Chart) == 0 {
-		r, c, err = scaffoldChart(chartsDir, opts)
+		r, c, err = scaffoldChart(cfg, tmpDir, opts)
 		if err != nil {
 			return nil, nil, fmt.Errorf("failed to scaffold default chart: %v", err)
 		}
 	} else {
-		r, c, err = fetchChart(chartsDir, opts)
+		r, c, err = fetchChart(cfg, tmpDir, opts)
 		if err != nil {
 			return nil, nil, fmt.Errorf("failed to fetch chart: %v", err)
 		}
 	}
 
-	relChartPath := filepath.Join(HelmChartsDir, c.Name())
-	absChartPath := filepath.Join(projectDir, relChartPath)
+	absChartPath := filepath.Join(tmpDir, c.Name())
 	if err := fetchChartDependencies(absChartPath); err != nil {
 		return nil, nil, fmt.Errorf("failed to fetch chart dependencies: %v", err)
 	}
@@ -155,36 +153,23 @@ func CreateChart(projectDir string, opts CreateOptions) (*golang.Options, *chart
 		return nil, nil, fmt.Errorf("failed to load chart: %v", err)
 	}
 
-	fmt.Printf("Created %s\n", relChartPath)
 	return r, c, nil
 }
 
-func scaffoldChart(destDir string, opts CreateOptions) (*golang.Options, *chart.Chart, error) {
-	r := &golang.Options{}
-	r.Namespaced = true
-	r.Domain = opts.Domain
-	r.Group = opts.GVK.Group
-	r.Version = opts.GVK.Version
-	r.Kind = opts.GVK.Kind
-
-	chartPath, err := chartutil.Create(strings.ToLower(r.Kind), destDir)
+func scaffoldChart(cfg config.Config, destDir string, opts CreateOptions) (*resource.Resource, *chart.Chart, error) {
+	chartPath, err := chartutil.Create(strings.ToLower(opts.GVK.Kind), destDir)
 	if err != nil {
 		return nil, nil, err
 	}
-
 	chart, err := loader.Load(chartPath)
 	if err != nil {
 		return nil, nil, err
 	}
-	return r, chart, nil
-}
 
-func fetchChart(destDir string, opts CreateOptions) (*golang.Options, *chart.Chart, error) {
-	var (
-		chart *chart.Chart
-		err   error
-	)
+	return opts.NewResource(cfg), chart, nil
+}
 
+func fetchChart(cfg config.Config, destDir string, opts CreateOptions) (_ *resource.Resource, chart *chart.Chart, err error) {
 	if _, err = os.Stat(opts.Chart); err == nil {
 		chart, err = createChartFromDisk(destDir, opts.Chart)
 	} else {
@@ -205,14 +190,24 @@ func fetchChart(destDir string, opts CreateOptions) (*golang.Options, *chart.Cha
 		opts.GVK.Kind = strcase.ToCamel(chartName)
 	}
 
-	r := &golang.Options{}
-	r.Namespaced = true
-	r.Domain = opts.Domain
-	r.Group = opts.GVK.Group
-	r.Version = opts.GVK.Version
-	r.Kind = opts.GVK.Kind
+	return opts.NewResource(cfg), chart, nil
+}
 
-	return r, chart, nil
+func (opts CreateOptions) NewResource(cfg config.Config) *resource.Resource {
+	ro := &golang.Options{}
+	ro.DoAPI = true
+	ro.Namespaced = true
+	ro.Domain = opts.Domain
+	ro.CRDVersion = opts.CRDVersion
+	ro.Group = opts.GVK.Group
+	ro.Version = opts.GVK.Version
+	ro.Kind = opts.GVK.Kind
+
+	r := ro.NewResource(cfg)
+	r.Domain = cfg.GetDomain()
+	// remove the path since is not a Golang project
+	r.Path = ""
+	return &r
 }
 
 func createChartFromDisk(destDir, source string) (*chart.Chart, error) {
@@ -221,7 +216,7 @@ func createChartFromDisk(destDir, source string) (*chart.Chart, error) {
 		return nil, err
 	}
 
-	// Save it into our project's helm-charts directory.
+	// Save it into destDir.
 	if err := chartutil.SaveDir(chart, destDir); err != nil {
 		return nil, err
 	}
@@ -246,17 +241,7 @@ func createChartFromRemote(destDir string, opts CreateOptions) (*chart.Chart, er
 		opts.Chart = chartURL
 	}
 
-	tmpDir, err := ioutil.TempDir("", "osdk-helm-chart")
-	if err != nil {
-		return nil, err
-	}
-	defer func() {
-		if err := os.RemoveAll(tmpDir); err != nil {
-			log.Errorf("Failed to remove temporary directory %s: %s", tmpDir, err)
-		}
-	}()
-
-	chartArchive, _, err := c.DownloadTo(opts.Chart, opts.Version, tmpDir)
+	chartArchive, _, err := c.DownloadTo(opts.Chart, opts.Version, destDir)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/plugins/helm/v1/init.go b/internal/plugins/helm/v1/init.go
index 9b75c74f..5b3570f6 100644
--- a/internal/plugins/helm/v1/init.go
+++ b/internal/plugins/helm/v1/init.go
@@ -24,7 +24,6 @@ import (
 	"k8s.io/apimachinery/pkg/util/validation"
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
 
 	"github.com/operator-framework/operator-sdk/internal/kubebuilder/cmdutil"
 	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil"
@@ -34,14 +33,14 @@ import (
 )
 
 type initSubcommand struct {
-	config    config.Config
-	options   *golang.Options
-	apiPlugin createAPISubcommand
+	config  config.Config
+	apiSubc createAPISubcommand
 
 	// For help text.
 	commandName string
 
 	// Flags
+	domain      string
 	projectName string
 }
 
@@ -119,11 +118,9 @@ Writes the following files:
 
 // BindFlags will set the flags for the plugin
 func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
-	fs.SortFlags = false
-	p.options = &golang.Options{}
-	fs.StringVar(&p.options.Domain, "domain", "my.domain", "domain for groups")
+	fs.StringVar(&p.domain, "domain", "my.domain", "domain for groups")
 	fs.StringVar(&p.projectName, "project-name", "", "name of this project, the default being directory name")
-	p.apiPlugin.BindFlags(fs)
+	p.apiSubc.BindFlags(fs)
 }
 
 // InjectConfig will inject the PROJECT file/config in the plugin
@@ -131,11 +128,33 @@ func (p *initSubcommand) InjectConfig(c config.Config) {
 	// v3 project configs get a 'layout' value.
 	_ = c.SetLayout(pluginKey)
 	p.config = c
-	p.apiPlugin.config = p.config
+	p.apiSubc.config = p.config
 }
 
+// CreateOptions with defaults set, for comparison in case GVK/chart inputs were set.
+var emptyCreateOptions = chartutil.CreateOptions{CRDVersion: "v1"}
+
 // Run will call the plugin actions
 func (p *initSubcommand) Run() error {
+	// Set values in the config
+	if err := p.config.SetProjectName(p.projectName); err != nil {
+		return err
+	}
+	if err := p.config.SetDomain(p.domain); err != nil {
+		return err
+	}
+
+	// Check if the project name is a valid k8s namespace (DNS 1123 label).
+	if p.config.GetProjectName() == "" {
+		dir, err := os.Getwd()
+		if err != nil {
+			return fmt.Errorf("error getting current directory: %v", err)
+		}
+		if err = p.config.SetProjectName(strings.ToLower(filepath.Base(dir))); err != nil {
+			return err
+		}
+	}
+
 	if err := cmdutil.Run(p); err != nil {
 		return err
 	}
@@ -145,6 +164,14 @@ func (p *initSubcommand) Run() error {
 		return err
 	}
 
+	// If API creation is configured, run the 'create api' subcommand.
+	if p.apiSubc.options != emptyCreateOptions {
+		p.apiSubc.options.Domain = p.domain
+		if err := p.apiSubc.Run(); err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
 
@@ -157,71 +184,27 @@ func (p *initSubcommand) runPhase2() error {
 		return err
 	}
 
-	if p.options.DoAPI {
-		if err := p.apiPlugin.runPhase2(); err != nil {
-			return err
-		}
-	}
-
 	return nil
 }
 
 // Validate perform the required validations for this plugin
 func (p *initSubcommand) Validate() error {
-	// Set values in the config
-	if err := p.config.SetProjectName(p.projectName); err != nil {
-		return err
-	}
-	if err := p.config.SetDomain(p.options.Domain); err != nil {
-		return err
-	}
-
-	// Check if the project name is a valid k8s namespace (DNS 1123 label).
-	if p.config.GetProjectName() == "" {
-		dir, err := os.Getwd()
-		if err != nil {
-			return fmt.Errorf("error getting current directory: %v", err)
-		}
-		if err = p.config.SetProjectName(strings.ToLower(filepath.Base(dir))); err != nil {
-			return err
-		}
-	}
-
 	if err := validation.IsDNS1123Label(p.config.GetProjectName()); err != nil {
 		return fmt.Errorf("project name (%s) is invalid: %v", p.config.GetProjectName(), err)
 	}
 
-	defaultOpts := chartutil.CreateOptions{CRDVersion: "v1"}
-	if !p.apiPlugin.createOptions.GVK.Empty() || p.apiPlugin.createOptions != defaultOpts {
-		p.options.DoAPI = true
-		return p.apiPlugin.Validate()
-	}
-
 	return nil
 }
 
 // GetScaffolder returns cmdutil.Scaffolder which will be executed due the RunOptions interface implementation
 func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	var (
-		apiScaffolder cmdutil.Scaffolder
-		err           error
-	)
-	if p.options.DoAPI {
-		apiScaffolder, err = p.apiPlugin.GetScaffolder()
-		if err != nil {
-			return nil, err
-		}
-	}
-	return scaffolds.NewInitScaffolder(p.config, apiScaffolder), nil
+	return scaffolds.NewInitScaffolder(p.config), nil
 }
 
 // PostScaffold will run the required actions after the default plugin scaffold
 func (p *initSubcommand) PostScaffold() error {
-
-	if p.options.DoAPI {
-		return p.apiPlugin.PostScaffold()
+	if p.apiSubc.options == emptyCreateOptions {
+		fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
 	}
-
-	fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
 	return nil
 }
diff --git a/internal/plugins/helm/v1/scaffolds/api.go b/internal/plugins/helm/v1/scaffolds/api.go
index 29c7db31..6323c036 100644
--- a/internal/plugins/helm/v1/scaffolds/api.go
+++ b/internal/plugins/helm/v1/scaffolds/api.go
@@ -23,13 +23,15 @@ import (
 	"os"
 	"path/filepath"
 
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chartutil"
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	"sigs.k8s.io/kubebuilder/v3/pkg/model"
 	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
 
 	"github.com/operator-framework/operator-sdk/internal/kubebuilder/cmdutil"
 	"github.com/operator-framework/operator-sdk/internal/kubebuilder/machinery"
-	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil"
+	internalchartutil "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil"
 	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/internal/templates"
 	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/internal/templates/config/crd"
 	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/internal/templates/config/rbac"
@@ -41,15 +43,17 @@ var _ cmdutil.Scaffolder = &apiScaffolder{}
 // apiScaffolder contains configuration for generating scaffolding for Go type
 // representing the API and controller that implements the behavior for the API.
 type apiScaffolder struct {
-	config config.Config
-	opts   chartutil.CreateOptions
+	config   config.Config
+	resource *resource.Resource
+	chrt     *chart.Chart
 }
 
 // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
-func NewAPIScaffolder(config config.Config, opts chartutil.CreateOptions) cmdutil.Scaffolder {
+func NewAPIScaffolder(config config.Config, res *resource.Resource, chrt *chart.Chart) cmdutil.Scaffolder {
 	return &apiScaffolder{
-		config: config,
-		opts:   opts,
+		config:   config,
+		resource: res,
+		chrt:     chrt,
 	}
 }
 
@@ -66,59 +70,36 @@ func (s *apiScaffolder) newUniverse(r *resource.Resource) *model.Universe {
 }
 
 func (s *apiScaffolder) scaffold() error {
-	projectDir, err := os.Getwd()
-	if err != nil {
-		return err
-	}
-	resourceOptions, chrt, err := chartutil.CreateChart(projectDir, s.opts)
-	if err != nil {
-		return err
+	if s.resource == nil {
+		return errors.New("resource must not be nil")
 	}
 
-	resourceOptions.DoAPI = true
-	//todo(camilamacedo86): replace the options by kubernetes-sigs/kubebuilder#1974
-	if err := resourceOptions.Validate(); err != nil {
+	if err := s.config.UpdateResource(*s.resource); err != nil {
 		return err
 	}
+	// Path for file builders.
+	chartPath := filepath.Join(internalchartutil.HelmChartsDir, s.chrt.Name())
 
-	// Check that resource doesn't exist
-	if s.config.HasResource(resourceOptions.GVK()) {
-		return errors.New("the API resource already exists")
-	}
-
-	// Check that the provided group can be added to the project
-	if !s.config.IsMultiGroup() && s.config.ResourcesLength() != 0 && !s.config.HasGroup(resourceOptions.Group) {
-		return errors.New("multiple groups are not allowed by default, to enable multi-group set 'multigroup: true' in your PROJECT file")
+	// Write the chart to disk.
+	projectDir, err := os.Getwd()
+	if err != nil {
+		return err
 	}
-
-	resource := resourceOptions.NewResource(s.config)
-
-	resource.Domain = s.config.GetDomain()
-
-	// remove the path since is not a Golang project
-	resource.Path = ""
-
-	// add the resource API info to complain with project-version=3
-	// todo: ensure that this information is properly returned from
-	// resource.newResource in upstream ( see kubernetes-sigs/kubebuilder#1974)
-	// and then, remove it.
-	resource.API.Namespaced = true
-	resource.API.CRDVersion = s.opts.CRDVersion
-
-	if err := s.config.UpdateResource(resource); err != nil {
+	absChartDir := filepath.Join(projectDir, internalchartutil.HelmChartsDir)
+	if err := chartutil.SaveDir(s.chrt, absChartDir); err != nil {
 		return err
 	}
+	fmt.Println("Created", chartPath)
 
-	chartPath := filepath.Join(chartutil.HelmChartsDir, chrt.Metadata.Name)
 	if err := machinery.NewScaffold().Execute(
-		s.newUniverse(&resource),
+		s.newUniverse(s.resource),
 		&templates.WatchesUpdater{ChartPath: chartPath},
-		&crd.CRD{CRDVersion: s.opts.CRDVersion},
+		&crd.CRD{CRDVersion: s.resource.API.CRDVersion},
 		&crd.Kustomization{},
 		&rbac.CRDEditorRole{},
 		&rbac.CRDViewerRole{},
-		&rbac.ManagerRoleUpdater{Chart: chrt},
-		&samples.CRDSample{ChartPath: chartPath, Chart: chrt},
+		&rbac.ManagerRoleUpdater{Chart: s.chrt},
+		&samples.CRDSample{ChartPath: chartPath, Chart: s.chrt},
 	); err != nil {
 		return fmt.Errorf("error scaffolding APIs: %v", err)
 	}
diff --git a/internal/plugins/helm/v1/scaffolds/init.go b/internal/plugins/helm/v1/scaffolds/init.go
index a696f681..9a9906bf 100644
--- a/internal/plugins/helm/v1/scaffolds/init.go
+++ b/internal/plugins/helm/v1/scaffolds/init.go
@@ -47,15 +47,13 @@ var helmOperatorVersion = version.ImageVersion
 var _ cmdutil.Scaffolder = &initScaffolder{}
 
 type initScaffolder struct {
-	config        config.Config
-	apiScaffolder cmdutil.Scaffolder
+	config config.Config
 }
 
 // NewInitScaffolder returns a new Scaffolder for project initialization operations
-func NewInitScaffolder(config config.Config, apiScaffolder cmdutil.Scaffolder) cmdutil.Scaffolder {
+func NewInitScaffolder(config config.Config) cmdutil.Scaffolder {
 	return &initScaffolder{
-		config:        config,
-		apiScaffolder: apiScaffolder,
+		config: config,
 	}
 }
 
@@ -67,13 +65,7 @@ func (s *initScaffolder) newUniverse() *model.Universe {
 
 // Scaffold implements Scaffolder
 func (s *initScaffolder) Scaffold() error {
-	if err := s.scaffold(); err != nil {
-		return err
-	}
-	if s.apiScaffolder != nil {
-		return s.apiScaffolder.Scaffold()
-	}
-	return nil
+	return s.scaffold()
 }
 
 func (s *initScaffolder) scaffold() error {


Test patch:
diff --git a/internal/plugins/helm/v1/chartutil/chart_test.go b/internal/plugins/helm/v1/chartutil/chart_test.go
index bd12af23..e848b6e8 100644
--- a/internal/plugins/helm/v1/chartutil/chart_test.go
+++ b/internal/plugins/helm/v1/chartutil/chart_test.go
@@ -21,10 +21,10 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/repo/repotest"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
+	cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
+	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
 
 	"github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil"
 )
@@ -202,27 +202,21 @@ type createChartTestCase struct {
 	helmChartVersion string
 	helmChartRepo    string
 
-	expectResource     *golang.Options
+	expectResource     *resource.Resource
 	expectChartName    string
 	expectChartVersion string
 	expectErr          bool
 }
 
-func mustNewResource(group, version, kind string) *golang.Options {
-	r := &golang.Options{
-		Namespaced: true,
-		Group:      group,
-		Version:    version,
-		Kind:       kind,
-	}
-	return r
+func mustNewResource(group, version, kind string) *resource.Resource {
+	opts := chartutil.CreateOptions{}
+	opts.GVK.Group = group
+	opts.GVK.Version = version
+	opts.GVK.Kind = kind
+	return opts.NewResource(cfgv3.New())
 }
 
 func runTestCase(t *testing.T, testDir string, tc createChartTestCase) {
-	outputDir := filepath.Join(testDir, "output")
-	assert.NoError(t, os.Mkdir(outputDir, 0755))
-	defer os.RemoveAll(outputDir)
-
 	os.Setenv("XDG_CONFIG_HOME", filepath.Join(testDir, ".config"))
 	os.Setenv("XDG_CACHE_HOME", filepath.Join(testDir, ".cache"))
 	os.Setenv("HELM_REPOSITORY_CONFIG", filepath.Join(testDir, "repositories.yaml"))
@@ -242,7 +236,7 @@ func runTestCase(t *testing.T, testDir string, tc createChartTestCase) {
 		Version: tc.helmChartVersion,
 		Repo:    tc.helmChartRepo,
 	}
-	resource, chrt, err := chartutil.CreateChart(outputDir, opts)
+	resource, chrt, err := chartutil.CreateChart(cfgv3.New(), opts)
 	if tc.expectErr {
 		assert.Error(t, err)
 		return
@@ -254,11 +248,4 @@ func runTestCase(t *testing.T, testDir string, tc createChartTestCase) {
 	assert.Equal(t, tc.expectResource, resource)
 	assert.Equal(t, tc.expectChartName, chrt.Name())
 	assert.Equal(t, tc.expectChartVersion, chrt.Metadata.Version)
-
-	loadedChart, err := loader.Load(filepath.Join(outputDir, chartutil.HelmChartsDir, chrt.Name()))
-	if err != nil {
-		t.Fatalf("Could not load chart from expected location: %s", err)
-	}
-
-	assert.Equal(t, loadedChart, chrt)
 }
diff --git a/test/common/scorecard.go b/test/common/scorecard.go
new file mode 100644
index 00000000..3cb8d5f9
--- /dev/null
+++ b/test/common/scorecard.go
@@ -0,0 +1,95 @@
+// 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 common
+
+import (
+	"encoding/json"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
+	"github.com/operator-framework/operator-sdk/internal/testutils"
+)
+
+// ScorecardSpec runs a set of scorecard tests common to all operator types.
+func ScorecardSpec(tc *testutils.TestContext, operatorType string) func() {
+	return func() {
+		var (
+			err         error
+			cmd         *exec.Cmd
+			outputBytes []byte
+			output      v1alpha3.TestList
+		)
+
+		It("should run a single scorecard test successfully", func() {
+			cmd = exec.Command(tc.BinaryName, "scorecard", "bundle",
+				"--selector", "suite=basic",
+				"--output", "json",
+				"--wait-time", "2m")
+			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))
+		})
+
+		It("should run all enabled scorecard tests successfully", func() {
+			cmd = exec.Command(tc.BinaryName, "scorecard", "bundle",
+				"--output", "json",
+				"--wait-time", "4m")
+			outputBytes, err = tc.Run(cmd)
+			// Some tests are expected to fail, which results in scorecard exiting 1.
+			Expect(err).To(HaveOccurred())
+			Expect(json.Unmarshal(outputBytes, &output)).To(Succeed())
+
+			expected := map[string]v1alpha3.State{
+				// Basic suite.
+				"basic-check-spec": v1alpha3.PassState,
+				// OLM suite.
+				"olm-bundle-validation":    v1alpha3.PassState,
+				"olm-crds-have-validation": v1alpha3.FailState,
+				"olm-crds-have-resources":  v1alpha3.FailState,
+				"olm-spec-descriptors":     v1alpha3.FailState,
+				"olm-status-descriptors":   v1alpha3.FailState,
+			}
+			if strings.ToLower(operatorType) == "go" {
+				// Go projects have generated CRD validation.
+				expected["olm-crds-have-validation"] = v1alpha3.PassState
+				// The Go sample project tests a custom suite.
+				expected["customtest1"] = v1alpha3.PassState
+				expected["customtest2"] = v1alpha3.PassState
+			}
+
+			Expect(output.Items).To(HaveLen(len(expected)))
+			for i := 0; i < len(output.Items); i++ {
+				results := output.Items[i].Status.Results
+				Expect(results).To(HaveLen(1))
+				Expect(results[0].Name).NotTo(BeEmpty())
+				fmt.Fprintln(GinkgoWriter, "    - Name: ", results[0].Name)
+				fmt.Fprintln(GinkgoWriter, "      Expected: ", expected[results[0].Name])
+				fmt.Fprintln(GinkgoWriter, "      Output: ", results[0].State)
+				Expect(results[0].State).To(Equal(expected[results[0].Name]))
+			}
+		})
+	}
+}
diff --git a/test/e2e/ansible/cluster_test.go b/test/e2e/ansible/cluster_test.go
index 2920a924..2798fff8 100644
--- a/test/e2e/ansible/cluster_test.go
+++ b/test/e2e/ansible/cluster_test.go
@@ -70,7 +70,7 @@ var _ = Describe("Running ansible projects", func() {
 		It("should run correctly in a cluster", func() {
 			By("checking if the Operator project Pod is running")
 			verifyControllerUp := func() error {
-				By("getting the controller-manager pod name")
+				// Get the controller-manager pod name
 				podOutput, err := tc.Kubectl.Get(
 					true,
 					"pods", "-l", "control-plane=controller-manager",
@@ -79,8 +79,6 @@ var _ = Describe("Running ansible projects", func() {
 				if err != nil {
 					return fmt.Errorf("could not get pods: %v", err)
 				}
-
-				By("ensuring the created controller-manager Pod")
 				podNames := kbtestutils.GetNonEmptyLines(podOutput)
 				if len(podNames) != 1 {
 					return fmt.Errorf("expecting 1 pod, have %d", len(podNames))
@@ -90,12 +88,12 @@ var _ = Describe("Running ansible projects", func() {
 					return fmt.Errorf("expecting pod name %q to contain %q", controllerPodName, "controller-manager")
 				}
 
-				By("checking the controller-manager Pod is running")
+				// Ensure the controller-manager Pod is running.
 				status, err := tc.Kubectl.Get(
 					true,
 					"pods", controllerPodName, "-o", "jsonpath={.status.phase}")
 				if err != nil {
-					return fmt.Errorf("failed to get pod stauts for %q: %v", controllerPodName, err)
+					return fmt.Errorf("failed to get pod status for %q: %v", controllerPodName, err)
 				}
 				if status != "Running" {
 					return fmt.Errorf("controller pod in %s status", status)
diff --git a/test/e2e/ansible/scorecard_test.go b/test/e2e/ansible/scorecard_test.go
index 0afddd4a..aaae1001 100644
--- a/test/e2e/ansible/scorecard_test.go
+++ b/test/e2e/ansible/scorecard_test.go
@@ -15,63 +15,9 @@
 package e2e_ansible_test
 
 import (
-	"encoding/json"
-	"fmt"
-	"os/exec"
-
 	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-	"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
-)
-
-var _ = Describe("Testing Ansible Projects with Scorecard", func() {
-	Context("with operator-sdk", func() {
-		const (
-			OLMBundleValidationTest   = "olm-bundle-validation"
-			OLMCRDsHaveValidationTest = "olm-crds-have-validation"
-			OLMCRDsHaveResourcesTest  = "olm-crds-have-resources"
-			OLMSpecDescriptorsTest    = "olm-spec-descriptors"
-			OLMStatusDescriptorsTest  = "olm-status-descriptors"
-		)
 
-		It("should work successfully with scorecard", func() {
-			By("running basic scorecard tests")
-			var scorecardOutput v1alpha3.TestList
-			runScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=basic",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err := tc.Run(runScorecardCmd)
-			Expect(err).NotTo(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(scorecardOutput.Items).To(HaveLen(1))
-			Expect(scorecardOutput.Items[0].Status.Results[0].State).To(Equal(v1alpha3.PassState))
-
-			By("running olm scorecard tests")
-			runOLMScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=olm",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err = tc.Run(runOLMScorecardCmd)
-			Expect(err).To(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-
-			expected := make(map[string]v1alpha3.State)
-			expected[OLMBundleValidationTest] = v1alpha3.PassState
-			expected[OLMCRDsHaveResourcesTest] = v1alpha3.FailState
-			expected[OLMCRDsHaveValidationTest] = v1alpha3.FailState
-			expected[OLMSpecDescriptorsTest] = v1alpha3.FailState
-			expected[OLMStatusDescriptorsTest] = v1alpha3.FailState
+	"github.com/operator-framework/operator-sdk/test/common"
+)
 
-			Expect(len(scorecardOutput.Items)).To(Equal(len(expected)))
-			for a := 0; a < len(scorecardOutput.Items); a++ {
-				fmt.Println("    - Name: ", scorecardOutput.Items[a].Status.Results[0].Name)
-				fmt.Println("      Expected: ", expected[scorecardOutput.Items[a].Status.Results[0].Name])
-				fmt.Println("      Output: ", scorecardOutput.Items[a].Status.Results[0].State)
-				Expect(scorecardOutput.Items[a].Status.Results[0].State).To(Equal(expected[scorecardOutput.Items[a].Status.Results[0].Name]))
-			}
-		})
-	})
-})
+var _ = Describe("scorecard", common.ScorecardSpec(&tc, "ansible"))
diff --git a/test/e2e/ansible/suite_test.go b/test/e2e/ansible/suite_test.go
index 7b398d23..7b439ca9 100644
--- a/test/e2e/ansible/suite_test.go
+++ b/test/e2e/ansible/suite_test.go
@@ -126,9 +126,8 @@ var _ = BeforeSuite(func() {
 		Expect(tc.LoadImageToKindClusterWithName("quay.io/operator-framework/scorecard-test:dev")).To(Succeed())
 	}
 
-	By("creating bundle image")
-	err = tc.GenerateBundle()
-	Expect(err).NotTo(HaveOccurred())
+	By("generating bundle")
+	Expect(tc.GenerateBundle()).To(Succeed())
 })
 
 // AfterSuite run after all the specs have run, regardless of whether any tests have failed to ensures that
diff --git a/test/e2e/go/cluster_test.go b/test/e2e/go/cluster_test.go
index 58c441a8..d91d04d8 100644
--- a/test/e2e/go/cluster_test.go
+++ b/test/e2e/go/cluster_test.go
@@ -62,7 +62,7 @@ var _ = Describe("operator-sdk", func() {
 		It("should run correctly in a cluster", func() {
 			By("checking if the Operator project Pod is running")
 			verifyControllerUp := func() error {
-				By("getting the controller-manager pod name")
+				// Get the controller-manager pod name
 				podOutput, err := tc.Kubectl.Get(
 					true,
 					"pods", "-l", "control-plane=controller-manager",
@@ -71,8 +71,6 @@ var _ = Describe("operator-sdk", func() {
 				if err != nil {
 					return fmt.Errorf("could not get pods: %v", err)
 				}
-
-				By("ensuring the created controller-manager Pod")
 				podNames := kbtestutils.GetNonEmptyLines(podOutput)
 				if len(podNames) != 1 {
 					return fmt.Errorf("expecting 1 pod, have %d", len(podNames))
@@ -82,7 +80,7 @@ var _ = Describe("operator-sdk", func() {
 					return fmt.Errorf("expecting pod name %q to contain %q", controllerPodName, "controller-manager")
 				}
 
-				By("checking the controller-manager Pod is running")
+				// Ensure the controller-manager Pod is running.
 				status, err := tc.Kubectl.Get(
 					true,
 					"pods", controllerPodName, "-o", "jsonpath={.status.phase}")
diff --git a/test/e2e/go/scorecard_test.go b/test/e2e/go/scorecard_test.go
index f028c6f1..a3974c94 100644
--- a/test/e2e/go/scorecard_test.go
+++ b/test/e2e/go/scorecard_test.go
@@ -15,71 +15,9 @@
 package e2e_go_test
 
 import (
-	"encoding/json"
-	"os/exec"
-
 	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
 
-	"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
+	"github.com/operator-framework/operator-sdk/test/common"
 )
 
-var _ = Describe("Testing Go Projects with Scorecard", func() {
-	Context("with operator-sdk", func() {
-		const (
-			OLMBundleValidationTest   = "olm-bundle-validation"
-			OLMCRDsHaveValidationTest = "olm-crds-have-validation"
-			OLMCRDsHaveResourcesTest  = "olm-crds-have-resources"
-			OLMSpecDescriptorsTest    = "olm-spec-descriptors"
-			OLMStatusDescriptorsTest  = "olm-status-descriptors"
-		)
-
-		It("should work successfully with scorecard", func() {
-			By("running basic scorecard tests")
-			var scorecardOutput v1alpha3.TestList
-			runScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=basic",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err := tc.Run(runScorecardCmd)
-			Expect(err).NotTo(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(scorecardOutput.Items).To(HaveLen(1))
-			Expect(scorecardOutput.Items[0].Status.Results[0].State).To(Equal(v1alpha3.PassState))
-
-			By("running custom scorecard tests")
-			runScorecardCmd = exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=custom",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err = tc.Run(runScorecardCmd)
-			Expect(err).NotTo(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(scorecardOutput.Items).To(HaveLen(2))
-
-			By("running olm scorecard tests")
-			runOLMScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=olm",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err = tc.Run(runOLMScorecardCmd)
-			Expect(err).To(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-
-			resultTable := make(map[string]v1alpha3.State)
-			resultTable[OLMStatusDescriptorsTest] = v1alpha3.FailState
-			resultTable[OLMCRDsHaveResourcesTest] = v1alpha3.FailState
-			resultTable[OLMBundleValidationTest] = v1alpha3.PassState
-			resultTable[OLMSpecDescriptorsTest] = v1alpha3.FailState
-			resultTable[OLMCRDsHaveValidationTest] = v1alpha3.PassState
-
-			Expect(len(scorecardOutput.Items)).To(Equal(len(resultTable)))
-			for a := 0; a < len(scorecardOutput.Items); a++ {
-				Expect(scorecardOutput.Items[a].Status.Results[0].State).To(Equal(resultTable[scorecardOutput.Items[a].Status.Results[0].Name]))
-			}
-		})
-	})
-})
+var _ = Describe("scorecard", common.ScorecardSpec(&tc, "go"))
diff --git a/test/e2e/go/suite_test.go b/test/e2e/go/suite_test.go
index 22ec2aec..4184582f 100644
--- a/test/e2e/go/suite_test.go
+++ b/test/e2e/go/suite_test.go
@@ -83,9 +83,8 @@ var _ = BeforeSuite(func() {
 		Expect(tc.LoadImageToKindClusterWithName("quay.io/operator-framework/custom-scorecard-tests:dev")).To(Succeed())
 	}
 
-	By("creating bundle image")
-	err = tc.GenerateBundle()
-	Expect(err).NotTo(HaveOccurred())
+	By("generating bundle")
+	Expect(tc.GenerateBundle()).To(Succeed())
 
 	By("installing cert manager bundle")
 	Expect(tc.InstallCertManager(false)).To(Succeed())
diff --git a/test/e2e/helm/cluster_test.go b/test/e2e/helm/cluster_test.go
index 4524dce6..8baccd4f 100644
--- a/test/e2e/helm/cluster_test.go
+++ b/test/e2e/helm/cluster_test.go
@@ -64,7 +64,7 @@ var _ = Describe("Running Helm projects", func() {
 		It("should run correctly in a cluster", func() {
 			By("checking if the Operator project Pod is running")
 			verifyControllerUp := func() error {
-				By("getting the controller-manager pod name")
+				// Get the controller-manager pod name
 				podOutput, err := tc.Kubectl.Get(
 					true,
 					"pods", "-l", "control-plane=controller-manager",
@@ -73,8 +73,6 @@ var _ = Describe("Running Helm projects", func() {
 				if err != nil {
 					return fmt.Errorf("could not get pods: %v", err)
 				}
-
-				By("ensuring the created controller-manager Pod")
 				podNames := kbtestutils.GetNonEmptyLines(podOutput)
 				if len(podNames) != 1 {
 					return fmt.Errorf("expecting 1 pod, have %d", len(podNames))
@@ -84,12 +82,12 @@ var _ = Describe("Running Helm projects", func() {
 					return fmt.Errorf("expecting pod name %q to contain %q", controllerPodName, "controller-manager")
 				}
 
-				By("checking the controller-manager Pod is running")
+				// Ensure the controller-manager Pod is running.
 				status, err := tc.Kubectl.Get(
 					true,
 					"pods", controllerPodName, "-o", "jsonpath={.status.phase}")
 				if err != nil {
-					return fmt.Errorf("failed to get pod stauts for %q: %v", controllerPodName, err)
+					return fmt.Errorf("failed to get pod status for %q: %v", controllerPodName, err)
 				}
 				if status != "Running" {
 					return fmt.Errorf("controller pod in %s status", status)
diff --git a/test/e2e/helm/scorecard_test.go b/test/e2e/helm/scorecard_test.go
index 8412288c..44d53c3b 100644
--- a/test/e2e/helm/scorecard_test.go
+++ b/test/e2e/helm/scorecard_test.go
@@ -15,64 +15,9 @@
 package e2e_helm_test
 
 import (
-	"encoding/json"
-	"fmt"
-	"os/exec"
-
 	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
 
-	"github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3"
+	"github.com/operator-framework/operator-sdk/test/common"
 )
 
-var _ = Describe("Testing Helm Projects with Scorecard", func() {
-	Context("with operator-sdk", func() {
-		const (
-			OLMBundleValidationTest   = "olm-bundle-validation"
-			OLMCRDsHaveValidationTest = "olm-crds-have-validation"
-			OLMCRDsHaveResourcesTest  = "olm-crds-have-resources"
-			OLMSpecDescriptorsTest    = "olm-spec-descriptors"
-			OLMStatusDescriptorsTest  = "olm-status-descriptors"
-		)
-
-		It("should work successfully with scorecard", func() {
-			By("running basic scorecard tests")
-			var scorecardOutput v1alpha3.TestList
-			runScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=basic",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err := tc.Run(runScorecardCmd)
-			Expect(err).NotTo(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(scorecardOutput.Items).To(HaveLen(1))
-			Expect(scorecardOutput.Items[0].Status.Results[0].State).To(Equal(v1alpha3.PassState))
-
-			By("running olm scorecard tests")
-			runOLMScorecardCmd := exec.Command(tc.BinaryName, "scorecard", "bundle",
-				"--selector=suite=olm",
-				"--output=json",
-				"--wait-time=120s")
-			scorecardOutputBytes, err = tc.Run(runOLMScorecardCmd)
-			Expect(err).To(HaveOccurred())
-			err = json.Unmarshal(scorecardOutputBytes, &scorecardOutput)
-			Expect(err).NotTo(HaveOccurred())
-
-			expected := make(map[string]v1alpha3.State)
-			expected[OLMBundleValidationTest] = v1alpha3.PassState
-			expected[OLMCRDsHaveResourcesTest] = v1alpha3.FailState
-			expected[OLMCRDsHaveValidationTest] = v1alpha3.FailState
-			expected[OLMSpecDescriptorsTest] = v1alpha3.FailState
-			expected[OLMStatusDescriptorsTest] = v1alpha3.FailState
-
-			Expect(len(scorecardOutput.Items)).To(Equal(len(expected)))
-			for a := 0; a < len(scorecardOutput.Items); a++ {
-				fmt.Println("    - Name: ", scorecardOutput.Items[a].Status.Results[0].Name)
-				fmt.Println("      Expected: ", expected[scorecardOutput.Items[a].Status.Results[0].Name])
-				fmt.Println("      Output: ", scorecardOutput.Items[a].Status.Results[0].State)
-				Expect(scorecardOutput.Items[a].Status.Results[0].State).To(Equal(expected[scorecardOutput.Items[a].Status.Results[0].Name]))
-			}
-		})
-	})
-})
+var _ = Describe("scorecard", common.ScorecardSpec(&tc, "helm"))
diff --git a/test/e2e/helm/suite_test.go b/test/e2e/helm/suite_test.go
index 1c19b149..91b693d8 100644
--- a/test/e2e/helm/suite_test.go
+++ b/test/e2e/helm/suite_test.go
@@ -87,9 +87,8 @@ var _ = BeforeSuite(func() {
 		Expect(tc.LoadImageToKindClusterWithName("quay.io/operator-framework/scorecard-test:dev")).To(Succeed())
 	}
 
-	By("creating bundle image")
-	err = tc.GenerateBundle()
-	Expect(err).NotTo(HaveOccurred())
+	By("generating bundle")
+	Expect(tc.GenerateBundle()).To(Succeed())
 })
 
 // AfterSuite run after all the specs have run, regardless of whether any tests have failed to ensures that