Repository: kubernetes-sigs/security-profiles-operator
Pull request #849 (2022-03-20 21:48:19)

Bundle packaging for SPO
- api: Add a dummy spec to secprofnodestatus to work around operator-SDK bug
- make: Add PROJECT, CSV and other tooling to create bundles
- make: install and use opm to create catalogs
- examples: OLM manifests
- hack: add the SPO profile configMap
- manager: create the webhook CR if it doesn't exist.

<!--  Thanks for sending a pull request!  Here are some tips for you:

- If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
- Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
- If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
- If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?
/kind feature

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
Adds a set of Makefile targets, various workarounds and fixups to create OLM
bundles and catalogs for SPO. This would allow SPO to be distributed via
operatorhub.io and distributions which rely on OLM such as OpenShift.

#### Which issue(s) this PR fixes:
None although it is a prerequisite to 779.

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->

#### Does this PR have test?
No. I'm not sure how to add them. Especially the catalogsource
creation requires that the images are pulled into the catalog
from a public Internet location. Maybe just running bundle create,
verify and such could be added?

I'm open to suggestions.

<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:
Please see the patch "examples: OLM manifests" to see how to test
the OLM integration. You'll note that the version installed is
the latest built upstream. Edit the CSV and set the image to a
version that includes the patch "manager: create the webhook CR if it doesn't exist."
or alternatively create the MutatingWebhookConfiguration yourself
from a manifest.

#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
NONE
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/849#" title="Author self-approved">jhrozek</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/main/OWNERS)~~ [jhrozek]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 1adb08e9bbdefc69bd933ad20993b14467f1f559 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1505662550568603648) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
codecov-commenter: # [Codecov](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/849?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#849](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/849?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (1adb08e) into [main](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/commit/32a49abd3e3d73fde39c5e6739bec6e2722fe5c1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (32a49ab) will **not change** coverage.
> The diff coverage is `n/a`.


```diff
@@           Coverage Diff           @@
##             main     #849   +/-   ##
=======================================
  Coverage   52.60%   52.60%           
=======================================
  Files          42       42           
  Lines        4268     4268           
=======================================
  Hits         2245     2245           
  Misses       1952     1952           
  Partials       71       71           
```

JAORMX: > No. I'm not sure how to add them. Especially the catalogsource
creation requires that the images are pulled into the catalog
from a public Internet location. Maybe just running bundle create,
verify and such could be added?

Does it really have to be a public internet location? Can't we spawn up a registry in a local cluster and push the images there?
jhrozek: > > No. I'm not sure how to add them. Especially the catalogsource
> > creation requires that the images are pulled into the catalog
> > from a public Internet location. Maybe just running bundle create,
> > verify and such could be added?
> 
> Does it really have to be a public internet location? Can't we spawn up a registry in a local cluster and push the images there?

I guess it mean to say it only supports talking to a repo, yes, not a local container storage. I know you can run podman as a repo, ideas for other repos are welcome.
Also, would this be a standalone test where you make sure the bundle installs SPO and then run some subset of e2e tests to make sure everything (mainly webhooks I guess..) works?
saschagrunert: > > > No. I'm not sure how to add them. Especially the catalogsource
> > > creation requires that the images are pulled into the catalog
> > > from a public Internet location. Maybe just running bundle create,
> > > verify and such could be added?
> > 
> > 
> > Does it really have to be a public internet location? Can't we spawn up a registry in a local cluster and push the images there?
> 
> I guess it mean to say it only supports talking to a reigistry using, not a local container storage. I know you can run podman as a registry, ideas for other registries are welcome. Also, would this be a standalone test where you make sure the bundle installs SPO and then run some subset of e2e tests to make sure everything (mainly webhooks I guess..) works?

Can we use the GitHub container registry for that? https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 7bbfb45a8eb469fb3eeb56fd64386c0435554968 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1507103209255079936) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jhrozek: > Great work, thank you for digging into this feature! pray
> 
> > > > No. I'm not sure how to add them. Especially the catalogsource
> > > > creation requires that the images are pulled into the catalog
> > > > from a public Internet location. Maybe just running bundle create,
> > > > verify and such could be added?
> > > 
> > > 
> > > Does it really have to be a public internet location? Can't we spawn up a registry in a local cluster and push the images there?
> > 
> > 
> > I guess it mean to say it only supports talking to a reigistry using, not a local container storage. I know you can run podman as a registry, ideas for other registries are welcome. Also, would this be a standalone test where you make sure the bundle installs SPO and then run some subset of e2e tests to make sure everything (mainly webhooks I guess..) works?
> 
> Can we use the GitHub container registry for that? https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry

I ended up using Ozz's suggestion and just used the same approach as the gatekeeper operator during the e2e test.
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 45338fec2d9cec3361cb6270baf4d26f2cd5f6a2 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1507105815314567168) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
jhrozek: @JAORMX @saschagrunert I ended up writing test for OLM backage based on gatekeeper's tests. So far the tests only cover installing the bundle and making sure that SPO is running. I'd like to also add some functional tests (a subset, not all e2e tests) but so far our e2e tests insist on deploying the operator and cert-manager. So I'll continue working on the tests, but wanted to get feedback on the progress.
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 759161836f5922b56007d02d5b18b405d0528d91 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1507106808412508160) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 69af9098d7d3a93805dabdbd85277d34acffcc2f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1507108516777693184) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 46d633a5684a00b71a84a692bcfb030497119737 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1508179118024298496) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @jhrozek: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-security-profiles-operator-verify | 226fca28ddafd8438a0777e8f58d603842a2df6a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/849/pull-security-profiles-operator-verify/1508182448393949184) | true | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=849). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ajhrozek). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/849#" title="Author self-approved">jhrozek</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/849#pullrequestreview-922719700" title="Approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/main/OWNERS)~~ [jhrozek,saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/.gitignore b/.gitignore
index 6e159c045..ada249886 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ Vagrantfile
 build
 image.tar
 result
+bundle
+bundle.Dockerfile
diff --git a/Makefile b/Makefile
index 5599b42fd..c592ab630 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,8 @@ GOLANGCI_LINT_VERSION = v1.45.0
 REPO_INFRA_VERSION = v0.2.5
 KUSTOMIZE_VERSION = 4.5.3
 KUBERNETES_SPLIT_YAML_VERSION = 0.3.0
+OPERATOR_SDK_VERSION ?= v1.17.0
+OPM_VERSION ?= v1.19.1
 
 CONTROLLER_GEN_CMD := $(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen
 
@@ -397,6 +399,133 @@ generate:
 	$(CONTROLLER_GEN_CMD) rbac:roleName=spod paths="./internal/pkg/daemon/..." output:rbac:stdout >> deploy/base/role.yaml
 	$(CONTROLLER_GEN_CMD) rbac:roleName=spo-webhook paths="./internal/pkg/webhooks/..." output:rbac:stdout >> deploy/base/role.yaml
 
+## Bundle packaging begins here
+## read more at https://sdk.operatorframework.io/docs/olm-integration/tutorial-bundle/
+
+.PHONY: operator-sdk
+OPERATOR_SDK = $(BUILD_DIR)/operator-sdk
+operator-sdk: $(BUILD_DIR) ## Download sdk locally if necessary.
+ifeq (,$(wildcard $(OPERATOR_SDK)))
+ifeq (,$(shell which operator-sdk 2>/dev/null))
+	@{ \
+	set -e ;\
+	mkdir -p $(dir $(OPERATOR_SDK)) ;\
+	OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
+	curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/${OPERATOR_SDK_VERSION}/operator-sdk_$${OS}_$${ARCH} ;\
+	chmod +x $(OPERATOR_SDK) ;\
+	}
+else
+OPERATOR_SDK = $(shell which operator-sdk)
+endif
+endif
+
+
+# CHANNELS define the bundle channels used in the bundle.
+# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
+# To re-generate a bundle for other specific channels without changing the standard setup, you can:
+# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
+# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
+CHANNELS="stable"
+ifneq ($(origin CHANNELS), undefined)
+BUNDLE_CHANNELS := --channels=$(CHANNELS)
+endif
+
+# DEFAULT_CHANNEL defines the default channel used in the bundle.
+# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
+# To re-generate a bundle for any other default channel without changing the default setup, you can:
+# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
+# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
+DEFAULT_CHANNEL="stable"
+ifneq ($(origin DEFAULT_CHANNEL), undefined)
+BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
+endif
+BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
+
+# BUNDLE_IMG defines the image:tag used for the bundle.
+# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
+BUNDLE_IMG ?= $(PROJECT)-bundle:v$(VERSION)
+
+# The operator manifest to include in the CSV. Defaults to the cluster-scoped
+# operator. Can be only one
+BUNDLE_OPERATOR_MANIFEST ?= deploy/operator.yaml
+
+# These examples are added to the alm-examples annotation and subsequently
+# displayed in the UI
+OLM_EXAMPLES := \
+	examples/apparmorprofile.yaml \
+	examples/config.yaml \
+	examples/profilerecording-seccomp-bpf.yaml \
+	examples/profilebinding.yaml \
+	examples/rawselinuxprofile.yaml \
+	examples/seccompprofile.yaml \
+	examples/selinuxprofile.yaml
+
+.PHONY: bundle
+bundle: operator-sdk deployments ## Generate bundle manifests and metadata, then validate generated files.
+	sed -i "s/\(olm.skipRange: '>=.*\)<.*'/\1<$(VERSION)'/" deploy/base/clusterserviceversion.yaml
+	cat $(OLM_EXAMPLES) $(BUNDLE_OPERATOR_MANIFEST) deploy/base/clusterserviceversion.yaml | $(OPERATOR_SDK) generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
+	git restore deploy/base/clusterserviceversion.yaml
+	mkdir -p ./bundle/tests/scorecard
+	cp deploy/bundle-test-config.yaml ./bundle/tests/scorecard/config.yaml
+	# For some reason, the profile CM is not added automatically to the
+	# bundle, but in general bundles can contain CMs, so let's add it
+	# manually
+	python hack/extract-cm.py $(BUNDLE_OPERATOR_MANIFEST) configMap security-profiles-operator-profile > ./bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml
+	$(OPERATOR_SDK) bundle validate ./bundle
+
+.PHONY: bundle-build
+bundle-build: ## Build the bundle image.
+	$(CONTAINER_RUNTIME) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
+
+.PHONY: bundle-push
+bundle-push: ## Push the bundle image.
+	$(CONTAINER_RUNTIME) push $(BUNDLE_IMG)
+
+.PHONY: verify-bundle
+verify-bundle: bundle ## Verify the bundle doesn't alter the state of the tree
+	hack/tree-status
+
+.PHONY: opm
+OPM = $(BUILD_DIR)/opm
+opm: $(BUILD_DIR) ## Download opm locally if necessary.
+ifeq (,$(wildcard $(OPM)))
+ifeq (,$(shell which opm 2>/dev/null))
+	@{ \
+	set -e ;\
+	mkdir -p $(dir $(OPM)) ;\
+	OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \
+	curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/$(OPM_VERSION)/$${OS}-$${ARCH}-opm ;\
+	chmod +x $(OPM) ;\
+	}
+else
+OPM = $(shell which opm)
+endif
+endif
+
+# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
+# These images MUST exist in a registry and be pull-able.
+BUNDLE_IMGS ?= $(BUNDLE_IMG)
+
+# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
+CATALOG_IMG ?= $(PROJECT)-catalog:v$(VERSION)
+
+# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
+ifneq ($(origin CATALOG_BASE_IMG), undefined)
+FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
+endif
+
+# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
+# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
+# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
+.PHONY: catalog-build
+catalog-build: opm ## Build a catalog image.
+	$(OPM) $(OPM_EXTRA_ARGS) index add --container-tool $(CONTAINER_RUNTIME) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
+
+# Push the catalog image.
+.PHONY: catalog-push
+catalog-push: ## Push a catalog image.
+	$(CONTAINER_RUNTIME) push $(CATALOG_IMG)
+
 ## OpenShift-only
 ## These targets are meant to make development in OpenShift easier.
 
diff --git a/PROJECT b/PROJECT
new file mode 100644
index 000000000..a21aa3107
--- /dev/null
+++ b/PROJECT
@@ -0,0 +1,66 @@
+domain: security-profiles-operator.x-k8s.io
+layout:
+- go.kubebuilder.io/v3
+plugins:
+  manifests.sdk.operatorframework.io/v2: {}
+  scorecard.sdk.operatorframework.io/v2: {}
+projectName: security-profiles-operator
+repo: github.com/kubernetes-sigs/security-profiles-operator
+resources:
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: SecurityProfileNodeStatus
+  path: sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1
+  version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: SeccompProfile
+  path: sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1
+  version: v1beta1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: SecurityProfilesOperatorDaemon
+  path: sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1
+  version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: SelinuxProfile
+  path: sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha2
+  version: v1alpha2
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: AppArmorProfile
+  path: sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1
+  version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: ProfileBinding
+  path: sigs.k8s.io/security-profiles-operator/api/profilebinding/v1alpha1
+  version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: security-profiles-operator.x-k8s.io
+  kind: ProfileRecording
+  path: sigs.k8s.io/security-profiles-operator/api/profilerecording/v1alpha1
+  version: v1alpha1
+version: "3"
diff --git a/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go b/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
index 19a5515ae..dfb47b35f 100644
--- a/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
+++ b/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
@@ -84,10 +84,14 @@ type SecurityProfileNodeStatus struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
+	Spec SecurityProfileNodeStatusSpec `json:"spec,omitempty"`
+
 	NodeName string       `json:"nodeName"`
 	Status   ProfileState `json:"status,omitempty"`
 }
 
+type SecurityProfileNodeStatusSpec struct{}
+
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
 // SecurityProfileNodeStatusList contains a list of SecurityProfileNodeStatus.
diff --git a/api/secprofnodestatus/v1alpha1/zz_generated.deepcopy.go b/api/secprofnodestatus/v1alpha1/zz_generated.deepcopy.go
index 0e8ee32cc..dae71557b 100644
--- a/api/secprofnodestatus/v1alpha1/zz_generated.deepcopy.go
+++ b/api/secprofnodestatus/v1alpha1/zz_generated.deepcopy.go
@@ -30,6 +30,7 @@ func (in *SecurityProfileNodeStatus) DeepCopyInto(out *SecurityProfileNodeStatus
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityProfileNodeStatus.
@@ -81,3 +82,18 @@ func (in *SecurityProfileNodeStatusList) DeepCopyObject() runtime.Object {
 	}
 	return nil
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecurityProfileNodeStatusSpec) DeepCopyInto(out *SecurityProfileNodeStatusSpec) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityProfileNodeStatusSpec.
+func (in *SecurityProfileNodeStatusSpec) DeepCopy() *SecurityProfileNodeStatusSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(SecurityProfileNodeStatusSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/dependencies.yaml b/dependencies.yaml
index 36468bbd3..c0d004fa7 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -6,6 +6,8 @@ dependencies:
       match: GO_VERSION
     - path: .github/workflows/test.yml
       match: GO_VERSION
+    - path: .github/workflows/olm_tests.yaml
+      match: GO_VERSION
     - path: hack/ci/Vagrantfile-fedora
       match: GO_VERSION
     - path: hack/ci/Vagrantfile-ubuntu
@@ -29,6 +31,24 @@ dependencies:
     - path: Makefile
       match: KUSTOMIZE_VERSION
 
+  - name: operator-sdk
+    version: v1.17.0
+    refPaths:
+    - path: Makefile
+      match: OPERATOR_SDK_VERSION
+
+  - name: opm
+    version: v1.19.1
+    refPaths:
+    - path: Makefile
+      match: OPM_VERSION
+
+  - name: olm
+    version: v0.18.2
+    refPaths:
+    - path: hack/ci/e2e-olm.sh
+      match: OLM_VERSION
+
   - name: cert-manager
     version: 1.7.2
     refPaths:
@@ -36,6 +56,8 @@ dependencies:
       match: jetstack/cert-manager
     - path: installation-usage.md
       match: jetstack/cert-manager
+    - path: hack/ci/e2e-olm.sh
+      match: jetstack/cert-manager
 
   - name: kind
     version: 0.12.0
@@ -48,6 +70,8 @@ dependencies:
     refPaths:
     - path: test/suite_test.go
       match: kindImage
+    - path: .github/workflows/olm_tests.yaml
+      match: KIND_IMG_TAG
 
   - name: qemu
     version: 6.1.0-8
diff --git a/deploy/base/clusterserviceversion.yaml b/deploy/base/clusterserviceversion.yaml
new file mode 100644
index 000000000..f7e70bd83
--- /dev/null
+++ b/deploy/base/clusterserviceversion.yaml
@@ -0,0 +1,87 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+  annotations:
+    alm-examples: '[]'
+    capabilities: Basic Install
+    olm.skipRange: '>=0.4.1 <0.4.2-dev'
+    operatorframework.io/suggested-namespace: security-profiles-operator`
+  name: security-profiles-operator.v0.0.0
+  namespace: placeholder
+spec:
+  apiservicedefinitions: {}
+  customresourcedefinitions:
+    owned:
+    - description: AppArmorProfile is a cluster level specification for an AppArmor
+        profile.
+      displayName: App Armor Profile
+      kind: AppArmorProfile
+      name: apparmorprofiles.security-profiles-operator.x-k8s.io
+      version: v1alpha1
+    - description: ProfileBinding is the Schema for the profilebindings API.
+      displayName: Profile Binding
+      kind: ProfileBinding
+      name: profilebindings.security-profiles-operator.x-k8s.io
+      version: v1alpha1
+    - description: ProfileRecording is the Schema for the profilerecordings API.
+      displayName: Profile Recording
+      kind: ProfileRecording
+      name: profilerecordings.security-profiles-operator.x-k8s.io
+      version: v1alpha1
+    - description: SeccompProfile is a cluster level specification for a seccomp profile.
+        See https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#seccomp
+      displayName: Seccomp Profile
+      kind: SeccompProfile
+      name: seccompprofiles.security-profiles-operator.x-k8s.io
+      version: v1beta1
+    - description: SecurityProfileNodeStatus is a per-node status of a security profile
+      displayName: Security Profile Node Status
+      kind: SecurityProfileNodeStatus
+      name: securityprofilenodestatuses.security-profiles-operator.x-k8s.io
+      version: v1alpha1
+    - description: SecurityProfilesOperatorDaemon is the Schema to configure the spod
+        deployment.
+      displayName: Security Profiles Operator Daemon
+      kind: SecurityProfilesOperatorDaemon
+      name: securityprofilesoperatordaemons.security-profiles-operator.x-k8s.io
+      version: v1alpha1
+    - description: SelinuxProfile is the Schema for the selinuxprofiles API.
+      displayName: Selinux Profile
+      kind: SelinuxProfile
+      name: selinuxprofiles.security-profiles-operator.x-k8s.io
+      version: v1alpha2
+  description: SPO is an operator which aims to make it easier for users to use SELinux, seccomp and AppArmor in Kubernetes clusters
+  displayName: Security Profiles Operator
+  icon:
+  - base64data: ""
+    mediatype: ""
+  install:
+    spec:
+      deployments: null
+    strategy: ""
+  installModes:
+  - supported: true
+    type: OwnNamespace
+  - supported: true
+    type: SingleNamespace
+  - supported: false
+    type: MultiNamespace
+  - supported: false
+    type: AllNamespaces
+  keywords:
+  - security
+  - selinux
+  - seccomp
+  - apparmor
+  - ebpf
+  links:
+  - name: Security Profiles Operator
+    url: https://github.com/kubernetes-sigs/security-profiles-operator
+  maintainers:
+  - email: dev@kubernetes.io
+    name: Kubernetes upstream
+  maturity: alpha
+  provider:
+    name: Kubernetes SIGs
+    url: https://github.com/kubernetes-sigs
+  version: 0.0.0
diff --git a/deploy/base/crds/securityprofilenodestatus.yaml b/deploy/base/crds/securityprofilenodestatus.yaml
index 92247afcc..a6f68c0e5 100644
--- a/deploy/base/crds/securityprofilenodestatus.yaml
+++ b/deploy/base/crds/securityprofilenodestatus.yaml
@@ -47,6 +47,8 @@ spec:
             type: object
           nodeName:
             type: string
+          spec:
+            type: object
           status:
             description: ProfileState defines the state that the profile is in. A
               profile in this context refers to a SeccompProfile or a SELinux profile,
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index cf5fea966..d583070b4 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -517,6 +517,8 @@ spec:
             type: object
           nodeName:
             type: string
+          spec:
+            type: object
           status:
             description: ProfileState defines the state that the profile is in. A
               profile in this context refers to a SeccompProfile or a SELinux profile,
diff --git a/deploy/openshift-dev.yaml b/deploy/openshift-dev.yaml
index ada3034c9..5e2e27f74 100644
--- a/deploy/openshift-dev.yaml
+++ b/deploy/openshift-dev.yaml
@@ -517,6 +517,8 @@ spec:
             type: object
           nodeName:
             type: string
+          spec:
+            type: object
           status:
             description: ProfileState defines the state that the profile is in. A
               profile in this context refers to a SeccompProfile or a SELinux profile,
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index 92f0c7bb3..a3faaebc0 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -517,6 +517,8 @@ spec:
             type: object
           nodeName:
             type: string
+          spec:
+            type: object
           status:
             description: ProfileState defines the state that the profile is in. A
               profile in this context refers to a SeccompProfile or a SELinux profile,
diff --git a/examples/config.yaml b/examples/config.yaml
index c4aa5c1dc..13f050d10 100644
--- a/examples/config.yaml
+++ b/examples/config.yaml
@@ -7,4 +7,5 @@ metadata:
 spec:
   enableSelinux: false
   enableLogEnricher: false
-  enableAppArmor: false
\ No newline at end of file
+  enableAppArmor: false
+
diff --git a/examples/olm/install-resources.yaml b/examples/olm/install-resources.yaml
new file mode 100644
index 000000000..f1c035ff8
--- /dev/null
+++ b/examples/olm/install-resources.yaml
@@ -0,0 +1,36 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+    name: security-profiles-operator
+---
+apiVersion: operators.coreos.com/v1alpha1
+kind: CatalogSource
+metadata:
+  name: security-profiles-operator
+  # namespace: openshift-marketplace on OCP
+  namespace: olm
+spec:
+  sourceType: grpc
+  image: quay.io/security-profiles-operator/security-profiles-operator-catalog:latest
+---
+apiVersion: operators.coreos.com/v1
+kind: OperatorGroup
+metadata:
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+spec:
+  targetNamespaces:
+  - security-profiles-operator
+---
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+  name: security-profiles-operator-sub
+  namespace: security-profiles-operator
+spec:
+  channel: stable
+  name: security-profiles-operator
+  source: security-profiles-operator
+  #sourceNamespace: openshift-marketplace on OCP
+  sourceNamespace: olm
diff --git a/hack/extract-cm.py b/hack/extract-cm.py
new file mode 100644
index 000000000..33bb27c24
--- /dev/null
+++ b/hack/extract-cm.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+# Copyright 2022 The Kubernetes 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.
+
+import sys
+import yaml
+
+def filter_manifest(manifest, kind, name):
+    with open(manifest, "r") as mstream:
+        data = yaml.load_all(mstream, Loader=yaml.FullLoader)
+        for resource in data:
+            if resource["kind"].lower() != kind.lower():
+                continue
+            if resource["metadata"]["name"] != name:
+                continue
+
+            del resource["metadata"]["namespace"]
+            yaml.dump(resource, sys.stdout)
+            break
+
+if __name__ == "__main__":
+    if len(sys.argv) < 4:
+        sys.exit(1)
+
+    manifest = sys.argv[1]
+    kind = sys.argv[2]
+    name = sys.argv[3]
+
+    filter_manifest(manifest, kind, name)
diff --git a/hack/kind-with-registry.sh b/hack/kind-with-registry.sh
new file mode 100755
index 000000000..9b5c1958d
--- /dev/null
+++ b/hack/kind-with-registry.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+# Copyright 2022 The Kubernetes 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.
+
+set -o errexit
+
+# desired cluster name; default is "kind"
+KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}"
+KIND_IMG_TAG="${KIND_IMG_TAG:-}"
+
+# create registry container unless it already exists
+reg_name='kind-registry'
+reg_port='5000'
+running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)"
+if [ "${running}" != 'true' ]; then
+  docker run \
+    -d --restart=always -p "${reg_port}:5000" --name "${reg_name}" \
+    registry:2
+fi
+
+kind version
+
+KIND_CMD=
+if [[ -z "${KIND_IMG_TAG}" ]]; then
+  KIND_CMD="kind create cluster ${IMG} --name ${KIND_CLUSTER_NAME} --wait=5m --config=-"
+else
+  KIND_CMD="kind create cluster --image kindest/node:${KIND_IMG_TAG} --name ${KIND_CLUSTER_NAME} --wait=5m --config=-"
+fi
+
+# create a cluster with the local registry enabled in containerd
+cat <<EOF | ${KIND_CMD}
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+containerdConfigPatches:
+- |-
+  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${reg_port}"]
+    endpoint = ["http://${reg_name}:${reg_port}"]
+EOF
+
+# connect the registry to the cluster network
+docker network connect "kind" "${reg_name}" || true
+
+# Document the local registry
+# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
+cat <<EOF | kubectl apply -f -
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: local-registry-hosting
+  namespace: kube-public
+data:
+  localRegistryHosting.v1: |
+    host: "localhost:${reg_port}"
+    help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
+EOF
diff --git a/internal/pkg/manager/spod/bindata/webhook.go b/internal/pkg/manager/spod/bindata/webhook.go
index bca8fb2df..d7136c735 100644
--- a/internal/pkg/manager/spod/bindata/webhook.go
+++ b/internal/pkg/manager/spod/bindata/webhook.go
@@ -127,20 +127,21 @@ func GetWebhook(
 
 func (w *Webhook) Create(ctx context.Context, c client.Client) error {
 	for k, o := range w.objectMap() {
-		if k == "config" {
-			// The config already exists because it's a global resource we have to remove later on
-			if err := c.Patch(ctx, o, client.Merge); err != nil {
-				return errors.Wrapf(err, "updating %s", k)
-			}
-		} else {
-			if err := c.Create(ctx, o); err != nil {
-				if kerrors.IsAlreadyExists(err) {
-					continue
+		if err := c.Create(ctx, o); err != nil {
+			if kerrors.IsAlreadyExists(err) {
+				if k == "config" {
+					// The config already exists because it's a global resource we have to remove later on
+					if err := c.Patch(ctx, o, client.Merge); err != nil {
+						return errors.Wrapf(err, "updating %s", k)
+					}
 				}
+				continue
+			} else {
 				return errors.Wrapf(err, "creating %s", k)
 			}
 		}
 	}
+
 	return nil
 }
 


Test patch:
diff --git a/.github/workflows/olm_tests.yaml b/.github/workflows/olm_tests.yaml
new file mode 100644
index 000000000..c9eb9eebc
--- /dev/null
+++ b/.github/workflows/olm_tests.yaml
@@ -0,0 +1,56 @@
+name: OLM-Tests
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+env:
+  GO_VERSION: '1.18'
+  KIND_IMG_TAG: v1.23.4
+permissions:
+  actions: none
+  checks: none
+  contents: none
+  deployments: none
+  id-token: none
+  issues: none
+  discussions: none
+  packages: none
+  pull-requests: none
+  repository-projects: none
+  security-events: none
+  statuses: none
+jobs:
+  main:
+    name: Run tests
+    runs-on: ubuntu-20.04
+
+    steps:
+    - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
+
+    - name: Download binaries
+      run: |
+        mkdir -p ${GITHUB_WORKSPACE}/build
+        echo "${GITHUB_WORKSPACE}/build" >> ${GITHUB_PATH}
+        make operator-sdk
+        make opm
+
+    - uses: actions/setup-go@331ce1d993939866bb63c32c6cbbfd48fa76fc57 # v2.1.4
+      with:
+        go-version: ${{ env.GO_VERSION }}
+    - name: Install dependencies
+      run: |
+        sudo apt update
+        sudo apt install -y libseccomp-dev libelf-dev libapparmor-dev
+        sudo hack/install-libbpf.sh
+
+    - name: Create and verify bundle
+      run: |
+        make verify-bundle
+
+    - name: Create and set up K8s KinD Cluster
+      run: |
+        ./hack/kind-with-registry.sh
+
+    - name: Run the tests
+      run: ./hack/ci/e2e-olm.sh
\ No newline at end of file
diff --git a/deploy/bundle-test-config.yaml b/deploy/bundle-test-config.yaml
new file mode 100644
index 000000000..ce8e8edea
--- /dev/null
+++ b/deploy/bundle-test-config.yaml
@@ -0,0 +1,70 @@
+apiVersion: scorecard.operatorframework.io/v1alpha3
+kind: Configuration
+metadata:
+  name: config
+stages:
+- parallel: true
+  tests:
+  - entrypoint:
+    - scorecard-test
+    - basic-check-spec
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: basic
+      test: basic-check-spec-test
+    storage:
+      spec:
+        mountPath: {}
+  - entrypoint:
+    - scorecard-test
+    - olm-bundle-validation
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: olm
+      test: olm-bundle-validation-test
+    storage:
+      spec:
+        mountPath: {}
+  - entrypoint:
+    - scorecard-test
+    - olm-crds-have-validation
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: olm
+      test: olm-crds-have-validation-test
+    storage:
+      spec:
+        mountPath: {}
+  - entrypoint:
+    - scorecard-test
+    - olm-crds-have-resources
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: olm
+      test: olm-crds-have-resources-test
+    storage:
+      spec:
+        mountPath: {}
+  - entrypoint:
+    - scorecard-test
+    - olm-spec-descriptors
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: olm
+      test: olm-spec-descriptors-test
+    storage:
+      spec:
+        mountPath: {}
+  - entrypoint:
+    - scorecard-test
+    - olm-status-descriptors
+    image: quay.io/operator-framework/scorecard-test:v1.17.0
+    labels:
+      suite: olm
+      test: olm-status-descriptors-test
+    storage:
+      spec:
+        mountPath: {}
+storage:
+  spec:
+    mountPath: {}
diff --git a/hack/ci/e2e-olm.sh b/hack/ci/e2e-olm.sh
new file mode 100755
index 000000000..ae85ba0da
--- /dev/null
+++ b/hack/ci/e2e-olm.sh
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+# Copyright 2021 The Kubernetes 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.
+
+OLM_VERSION=v0.18.2
+
+REPO=localhost:5000
+IMG=${REPO}/security-profiles-operator:${GITHUB_SHA}
+BUNDLE_IMG=${REPO}/security-profiles-operator-bundle:v${GITHUB_SHA}
+CATALOG_IMG=${REPO}/security-profiles-operator-catalog:v${GITHUB_SHA}
+
+function build_and_push_spo() {
+        make image IMAGE=${IMG}
+        podman push --tls-verify=false ${IMG}
+}
+
+function build_and_push_packages() {
+        OPERATOR_MANIFEST=deploy/operator-ci.yaml
+
+        # Create a manifest with local image
+        pushd deploy/base
+        kustomize edit set image security-profiles-operator=$IMG
+        popd
+        kustomize build --reorder=none deploy/overlays/cluster -o ${OPERATOR_MANIFEST}
+
+        # this is a kludge, we need to make sure kustomize can be overwritten
+        rm -f build/kustomize
+
+        # create bundle, bundle image, push bundle using our manifest created earlier
+        make bundle BUNDLE_OPERATOR_MANIFEST=${OPERATOR_MANIFEST} IMG=${IMG}
+        # GH CI workers have pretty limited CPU and won't be able to run SPO, OLM and cert-manager at the same time
+        sed -i '/cpu\:/d' bundle/manifests/security-profiles-operator.clusterserviceversion.yaml
+        make bundle-build BUNDLE_IMG=${BUNDLE_IMG}
+        podman push --tls-verify=false ${BUNDLE_IMG}
+
+        # create catalog image, push catalog
+        make catalog-build OPM_EXTRA_ARGS=" --skip-tls" BUNDLE_IMGS=${BUNDLE_IMG} CATALOG_IMG=${CATALOG_IMG}
+        podman push --tls-verify=false ${CATALOG_IMG}
+}
+
+function deploy_olm() {
+        operator-sdk olm install --version ${OLM_VERSION} --timeout 6m
+}
+
+function deploy_spo() {
+        # cert-manager first. This should be done using dependencies in the
+        # future
+        kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml
+        kubectl -ncert-manager wait --for condition=ready pod -l app.kubernetes.io/instance=cert-manager
+
+        # let's roll..
+        sed -i "s#quay.io/security-profiles-operator/security-profiles-operator-catalog:latest#${CATALOG_IMG}#g" examples/olm/install-resources.yaml
+        kubectl create -f examples/olm/install-resources.yaml
+}
+
+function check_spo_is_running() {
+        # Useful in case the CatalogSource is fubar. We retry several times
+        # because on transient errors (which are for some reason common even
+        # if the catalog is local) the pod gets restarted
+        for i in $(seq 1 5); do
+            kubectl -nolm wait --for=condition=ready pods -lolm.catalogSource=security-profiles-operator
+            catalog_logs=$(kubectl -nolm logs $(kubectl -nolm get pods --no-headers -lolm.catalogSource=security-profiles-operator | awk '{print $1}') 2>/dev/null)
+            if "x"$logs != "x"; then
+                echo $catalog_logs
+                break
+            fi
+        done
+
+        # wait a bit for CSV to appear
+        # (jhrozek): I didn't find a useful condition or status to wait for..
+        # ..if only there was a way to check if ANY installedCSV is set..
+        sleep 30
+        CSV=$(kubectl -nsecurity-profiles-operator get sub security-profiles-operator-sub -ojsonpath='{.status.installedCSV}')
+        # wait for the CSV to be actually installed
+        kubectl -nsecurity-profiles-operator wait --for=jsonpath='{.status.phase}'=Succeeded csv $CSV
+
+        # wait for the operator to be ready
+        kubectl -nsecurity-profiles-operator wait --for=condition=ready pod -lname=security-profiles-operator
+        kubectl -nsecurity-profiles-operator wait --for=condition=ready pod -lname=security-profiles-operator-webhook
+        kubectl -nsecurity-profiles-operator wait --for=condition=ready pod -lname=spod
+}
+
+# The actual script begins here
+build_and_push_spo
+build_and_push_packages
+deploy_olm
+deploy_spo
+check_spo_is_running