Repository: operator-framework/operator-controller
Pull request #1942 (2025-04-29 13:30:19)

✨ add NetworkPolicy objects for catalogd and operator-controller
# Description

This PR adds `NetworkPolicy` objects, 1 each for operator-controller and catalogd.

Both network policies allow:
- all egress traffic (for communication with arbitrary image registries)
- ingress traffic to the respective metrics ports

In addition, catalogd's network policy also allows
- ingress traffic to the mutating webhook server
- ingress traffic to the catalogd http server

<!--
Thank you for your contribution!

Please provide a summary of the changes and the motivation behind the change.
-->

## Reviewer Checklist

- [ ] API Go Documentation
- [ ] Tests: Unit Tests (and E2E Tests, if appropriate)
- [ ] Comprehensive Commit Messages
- [ ] Links to related GitHub Issue(s)


joelanford: I did verify that the NetworkPolicy objects have an effect in `kind` clusters. For example, I:
- changed the egress policy to be empty (which means allow no egress). This broke both catalogd and operator-controller.
- changed the ingress policy of catalogd such that the catalogd http server was denied. This broke operator-controller.
joelanford: Installing a separate CNI is no longer necessary. See:
- https://github.com/operator-framework/operator-controller/pull/1942#issuecomment-2838949442
- https://github.com/kubernetes-sigs/kind/pull/3612
bentito: Should this have a new e2e specific to validating the reduced network access are working as intended?
openshift-ci[bot]: New changes are detected. LGTM label has been removed.
joelanford: > Should this have a new e2e specific to validating the reduced network access are working as intended?

Brett and I discussed this a bit offline and couldn't immediately figure out a concrete test to add. 
- What we _can_ say is that the current NetworkPolicy objects are working because the e2e's are still passing.
- But, we _cannot_ say that the current NetworkPolicy objects are the minimum necessary for the e2e's to pass.

It would be nice to be able to introduce a higher barrier for changing network policy because it is a security-sensitive change. We would not want to accidentally open up network access unnecessarily. 

From a technical perspective, I'm not sure tests that probing blocked ports would work all that well because we may need to have something actually listening on the blocked port and matched by the network policy selector in order to ever see something other than a network failure (would the client see a difference between "NetworkPolicy blocked the traffic" and "nothing is listening there"?)

Perhaps we could have a meta-test like "make sure the NetworkPolicy looks like $this", where "$this" is some sort of annotated list of rules where a justification is provided. If we had that, then:
- PR authors would have to change "$this" if they wanted to change the NetworkPolicy.
- Reviewers would be confronted with the change and would have a chance to see the justification for the change, which would theoretically provide more context to a review than an opaque two-line YAML change in the NetworkPolicy definition.

Patch:
diff --git a/config/base/catalogd/manager/kustomization.yaml b/config/base/catalogd/manager/kustomization.yaml
index 4ca2781..2c10750 100644
--- a/config/base/catalogd/manager/kustomization.yaml
+++ b/config/base/catalogd/manager/kustomization.yaml
@@ -1,6 +1,7 @@
 resources:
 - manager.yaml
-- catalogd_service.yaml
+- service.yaml
+- network_policy.yaml
 - webhook/manifests.yaml
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
diff --git a/config/base/catalogd/manager/network_policy.yaml b/config/base/catalogd/manager/network_policy.yaml
new file mode 100644
index 0000000..853b54a
--- /dev/null
+++ b/config/base/catalogd/manager/network_policy.yaml
@@ -0,0 +1,22 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: controller-manager
+  namespace: system
+spec:
+  podSelector:
+    matchLabels:
+      control-plane: catalogd-controller-manager
+  policyTypes:
+    - Ingress
+    - Egress
+  ingress:
+    - ports:
+        - protocol: TCP
+          port: 7443 # metrics
+        - protocol: TCP
+          port: 8443 # catalogd http server
+        - protocol: TCP
+          port: 9443 # webhook
+  egress:
+    - {}  # Allows all egress traffic (needed to pull catalog images from arbitrary image registries)
diff --git a/config/base/catalogd/manager/catalogd_service.yaml b/config/base/catalogd/manager/service.yaml
similarity index 100%
rename from config/base/catalogd/manager/catalogd_service.yaml
rename to config/base/catalogd/manager/service.yaml
diff --git a/config/base/operator-controller/manager/kustomization.yaml b/config/base/operator-controller/manager/kustomization.yaml
index c4dc211..259f17c 100644
--- a/config/base/operator-controller/manager/kustomization.yaml
+++ b/config/base/operator-controller/manager/kustomization.yaml
@@ -4,6 +4,7 @@ kind: Kustomization
 resources:
 - manager.yaml
 - service.yaml
+- network_policy.yaml
 
 images:
 - name: controller
diff --git a/config/base/operator-controller/manager/network_policy.yaml b/config/base/operator-controller/manager/network_policy.yaml
new file mode 100644
index 0000000..2e68bea
--- /dev/null
+++ b/config/base/operator-controller/manager/network_policy.yaml
@@ -0,0 +1,18 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: controller-manager
+  namespace: system
+spec:
+  podSelector:
+    matchLabels:
+      control-plane: operator-controller-controller-manager
+  policyTypes:
+    - Ingress
+    - Egress
+  ingress:
+    - ports:
+        - protocol: TCP
+          port: 8443 # metrics
+  egress:
+    - {}  # Allows all egress traffic (needed to pull bundle images from arbitrary image registries)
diff --git a/go.mod b/go.mod
index d1e22cc..757bbbe 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11
 	github.com/operator-framework/api v0.31.0
 	github.com/operator-framework/helm-operator-plugins v0.8.0
-	github.com/operator-framework/operator-registry v1.51.0
+	github.com/operator-framework/operator-registry v1.53.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
@@ -49,7 +49,7 @@ require (
 )
 
 require (
-	cel.dev/expr v0.19.1 // indirect
+	cel.dev/expr v0.20.0 // indirect
 	dario.cat/mergo v1.0.1 // indirect
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
@@ -77,7 +77,7 @@ require (
 	github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
 	github.com/containerd/ttrpc v1.2.7 // indirect
 	github.com/containerd/typeurl/v2 v2.2.3 // indirect
-	github.com/containers/common v0.62.0 // indirect
+	github.com/containers/common v0.63.0 // indirect
 	github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
 	github.com/containers/ocicrypt v1.2.1 // indirect
 	github.com/containers/storage v1.58.0 // indirect
@@ -85,7 +85,7 @@ require (
 	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/cli v28.0.4+incompatible // indirect
+	github.com/docker/cli v28.1.1+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
 	github.com/docker/docker v28.0.4+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.9.3 // indirect
@@ -121,9 +121,10 @@ require (
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.3 // indirect
-	github.com/google/cel-go v0.22.1 // indirect
+	github.com/google/cel-go v0.23.0 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/mux v1.8.1 // indirect
@@ -152,7 +153,7 @@ require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
-	github.com/mattn/go-sqlite3 v1.14.27 // indirect
+	github.com/mattn/go-sqlite3 v1.14.28 // indirect
 	github.com/miekg/pkcs11 v1.1.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -173,7 +174,7 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
-	github.com/onsi/gomega v1.36.2 // indirect
+	github.com/onsi/gomega v1.37.0 // indirect
 	github.com/opencontainers/runtime-spec v1.2.1 // indirect
 	github.com/operator-framework/operator-lib v0.17.0 // indirect
 	github.com/otiai10/copy v1.14.1 // indirect
@@ -232,8 +233,8 @@ require (
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
-	google.golang.org/grpc v1.71.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/grpc v1.72.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
@@ -249,6 +250,11 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
 )
 
+// cel-go v0.23.0 upgrade causes errors raised from the vendor source which lead to think in
+// incompatibilities scenarios. After upgrade to use the latest versions of k8s/api v0.33+
+// we should try to see if we could fix this one and remove this replace
+replace github.com/google/cel-go => github.com/google/cel-go v0.22.1
+
 replace k8s.io/api => k8s.io/api v0.32.3
 
 replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.3
diff --git a/go.sum b/go.sum
index 723301e..1f5e6cf 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
-cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
+cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
@@ -79,8 +79,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
 github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
 github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
 github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
-github.com/containers/common v0.62.0 h1:Sl9WE5h7Y/F3bejrMAA4teP1EcY9ygqJmW4iwSloZ10=
-github.com/containers/common v0.62.0/go.mod h1:Yec+z8mrSq4rydHofrnDCBqAcNA/BGrSg1kfFUL6F6s=
+github.com/containers/common v0.63.0 h1:ox6vgUYX5TSvt4W+bE36sYBVz/aXMAfRGVAgvknSjBg=
+github.com/containers/common v0.63.0/go.mod h1:+3GCotSqNdIqM3sPs152VvW7m5+Mg8Kk+PExT3G9hZw=
 github.com/containers/image/v5 v5.35.0 h1:T1OeyWp3GjObt47bchwD9cqiaAm/u4O4R9hIWdrdrP8=
 github.com/containers/image/v5 v5.35.0/go.mod h1:8vTsgb+1gKcBL7cnjyNOInhJQfTUQjJoO2WWkKDoebM=
 github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
@@ -110,8 +110,8 @@ github.com/distribution/distribution/v3 v3.0.0-rc.3 h1:JRJso9IVLoooKX76oWR+DWCCd
 github.com/distribution/distribution/v3 v3.0.0-rc.3/go.mod h1:offoOgrnYs+CFwis8nE0hyzYZqRCZj5EFc5kgfszwiE=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
-github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
+github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
@@ -249,8 +249,8 @@ github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lw
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
-github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
+github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -337,8 +337,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
-github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
@@ -386,10 +386,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
-github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
-github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
-github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
+github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
+github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
+github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
+github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -404,8 +404,8 @@ github.com/operator-framework/helm-operator-plugins v0.8.0 h1:0f6HOQC5likkf0b/Ov
 github.com/operator-framework/helm-operator-plugins v0.8.0/go.mod h1:Sc+8bE38xTCgCChBUvtq/PxatEg9fAypr7S5iAw8nlA=
 github.com/operator-framework/operator-lib v0.17.0 h1:cbz51wZ9+GpWR1ZYP4CSKSSBxDlWxmmnseaHVZZjZt4=
 github.com/operator-framework/operator-lib v0.17.0/go.mod h1:TGopBxIE8L6E/Cojzo26R3NFp1eNlqhQNmzqhOblaLw=
-github.com/operator-framework/operator-registry v1.51.0 h1:3T1H2W0wYvJx82x+Ue6nooFsn859ceJf1yH6MdRdjMQ=
-github.com/operator-framework/operator-registry v1.51.0/go.mod h1:dJadFTSvsgpeiqhTMK7+zXrhU0LIlx4Y/aDz0efq5oQ=
+github.com/operator-framework/operator-registry v1.53.0 h1:TjJvNxWgd0R+Xv+2ZTluL3Ik0VDv2aHhO/aAPPg5vtA=
+github.com/operator-framework/operator-registry v1.53.0/go.mod h1:ypPDmJOl3x6jo4qEbi0/PNri82u0JIzWlPtIwHlyTSE=
 github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
 github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
 github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
@@ -586,6 +586,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC
 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
 go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -731,15 +733,15 @@ google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2Z
 google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
 google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
 google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
-google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -803,6 +805,8 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ
 k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
 oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
+oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
+oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
 sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
diff --git a/internal/operator-controller/rukpak/convert/registryv1.go b/internal/operator-controller/rukpak/convert/registryv1.go
index 68244d3..1417239 100644
--- a/internal/operator-controller/rukpak/convert/registryv1.go
+++ b/internal/operator-controller/rukpak/convert/registryv1.go
@@ -261,7 +261,7 @@ func (c Converter) Convert(rv1 render.RegistryV1, installNamespace string, targe
 		return nil, fmt.Errorf("webhookDefinitions are not supported")
 	}
 
-	objs, err := c.BundleRenderer.Render(rv1, installNamespace, targetNamespaces)
+	objs, err := c.BundleRenderer.Render(rv1, installNamespace, render.WithTargetNamespaces(targetNamespaces...))
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/operator-controller/rukpak/render/render.go b/internal/operator-controller/rukpak/render/render.go
index af904ae..279320a 100644
--- a/internal/operator-controller/rukpak/render/render.go
+++ b/internal/operator-controller/rukpak/render/render.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -69,19 +70,33 @@ type Options struct {
 
 func (o *Options) apply(opts ...Option) *Options {
 	for _, opt := range opts {
-		opt(o)
+		if opt != nil {
+			opt(o)
+		}
 	}
 	return o
 }
 
 type Option func(*Options)
 
+func WithTargetNamespaces(namespaces ...string) Option {
+	return func(o *Options) {
+		o.TargetNamespaces = namespaces
+	}
+}
+
+func WithUniqueNameGenerator(generator UniqueNameGenerator) Option {
+	return func(o *Options) {
+		o.UniqueNameGenerator = generator
+	}
+}
+
 type BundleRenderer struct {
 	BundleValidator    BundleValidator
 	ResourceGenerators []ResourceGenerator
 }
 
-func (r BundleRenderer) Render(rv1 RegistryV1, installNamespace string, watchNamespaces []string, opts ...Option) ([]client.Object, error) {
+func (r BundleRenderer) Render(rv1 RegistryV1, installNamespace string, opts ...Option) ([]client.Object, error) {
 	// validate bundle
 	if err := r.BundleValidator.Validate(&rv1); err != nil {
 		return nil, err
@@ -89,7 +104,7 @@ func (r BundleRenderer) Render(rv1 RegistryV1, installNamespace string, watchNam
 
 	genOpts := (&Options{
 		InstallNamespace:    installNamespace,
-		TargetNamespaces:    watchNamespaces,
+		TargetNamespaces:    []string{metav1.NamespaceAll},
 		UniqueNameGenerator: DefaultUniqueNameGenerator,
 	}).apply(opts...)
 


Test patch:
diff --git a/internal/operator-controller/rukpak/render/render_test.go b/internal/operator-controller/rukpak/render/render_test.go
index ede57ff..510a629 100644
--- a/internal/operator-controller/rukpak/render/render_test.go
+++ b/internal/operator-controller/rukpak/render/render_test.go
@@ -29,7 +29,7 @@ func Test_BundleRenderer_ValidatesBundle(t *testing.T) {
 			},
 		},
 	}
-	objs, err := renderer.Render(render.RegistryV1{}, "", nil)
+	objs, err := renderer.Render(render.RegistryV1{}, "")
 	require.Nil(t, objs)
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "this bundle is invalid")
@@ -37,7 +37,7 @@ func Test_BundleRenderer_ValidatesBundle(t *testing.T) {
 
 func Test_BundleRenderer_CreatesCorrectDefaultOptions(t *testing.T) {
 	expectedInstallNamespace := "install-namespace"
-	expectedTargetNamespaces := []string{"ns-one", "ns-two"}
+	expectedTargetNamespaces := []string{""}
 	expectedUniqueNameGenerator := render.DefaultUniqueNameGenerator
 
 	renderer := render.BundleRenderer{
@@ -51,7 +51,34 @@ func Test_BundleRenderer_CreatesCorrectDefaultOptions(t *testing.T) {
 		},
 	}
 
-	_, _ = renderer.Render(render.RegistryV1{}, expectedInstallNamespace, expectedTargetNamespaces)
+	_, _ = renderer.Render(render.RegistryV1{}, expectedInstallNamespace)
+}
+
+func Test_BundleRenderer_AppliesUserOptions(t *testing.T) {
+	isOptionApplied := false
+	_, _ = render.BundleRenderer{}.Render(render.RegistryV1{}, "install-namespace", func(options *render.Options) {
+		isOptionApplied = true
+	})
+	require.True(t, isOptionApplied)
+}
+
+func Test_WithTargetNamespaces(t *testing.T) {
+	opts := &render.Options{
+		TargetNamespaces: []string{"target-namespace"},
+	}
+	render.WithTargetNamespaces("a", "b", "c")(opts)
+	require.Equal(t, []string{"a", "b", "c"}, opts.TargetNamespaces)
+}
+
+func Test_WithUniqueNameGenerator(t *testing.T) {
+	opts := &render.Options{
+		UniqueNameGenerator: render.DefaultUniqueNameGenerator,
+	}
+	render.WithUniqueNameGenerator(func(s string, i interface{}) (string, error) {
+		return "a man needs a name", nil
+	})(opts)
+	generatedName, _ := opts.UniqueNameGenerator("", nil)
+	require.Equal(t, "a man needs a name", generatedName)
 }
 
 func Test_BundleRenderer_CallsResourceGenerators(t *testing.T) {
@@ -65,7 +92,7 @@ func Test_BundleRenderer_CallsResourceGenerators(t *testing.T) {
 			},
 		},
 	}
-	objs, err := renderer.Render(render.RegistryV1{}, "", nil)
+	objs, err := renderer.Render(render.RegistryV1{}, "")
 	require.NoError(t, err)
 	require.Equal(t, []client.Object{&corev1.Namespace{}, &corev1.Service{}, &appsv1.Deployment{}}, objs)
 }
@@ -81,7 +108,7 @@ func Test_BundleRenderer_ReturnsResourceGeneratorErrors(t *testing.T) {
 			},
 		},
 	}
-	objs, err := renderer.Render(render.RegistryV1{}, "", nil)
+	objs, err := renderer.Render(render.RegistryV1{}, "")
 	require.Nil(t, objs)
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "generator error")
diff --git a/test/e2e/metrics_test.go b/test/e2e/metrics_test.go
index a1f6c4a..4a88c3d 100644
--- a/test/e2e/metrics_test.go
+++ b/test/e2e/metrics_test.go
@@ -19,11 +19,11 @@ import (
 	"fmt"
 	"io"
 	"os/exec"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/util/rand"
 
 	"github.com/operator-framework/operator-controller/test/utils"
 )
@@ -31,38 +31,45 @@ import (
 // TestOperatorControllerMetricsExportedEndpoint verifies that the metrics endpoint for the operator controller
 func TestOperatorControllerMetricsExportedEndpoint(t *testing.T) {
 	client := utils.FindK8sClient(t)
+	curlNamespace := createRandomNamespace(t, client)
+	componentNamespace := getComponentNamespace(t, client, "control-plane=operator-controller-controller-manager")
+	metricsURL := fmt.Sprintf("https://operator-controller-service.%s.svc.cluster.local:8443/metrics", componentNamespace)
+
 	config := NewMetricsTestConfig(
-		t, client,
-		"control-plane=operator-controller-controller-manager",
+		client,
+		curlNamespace,
 		"operator-controller-metrics-reader",
 		"operator-controller-metrics-binding",
-		"operator-controller-controller-manager",
+		"operator-controller-metrics-reader",
 		"oper-curl-metrics",
-		"https://operator-controller-service.NAMESPACE.svc.cluster.local:8443/metrics",
+		metricsURL,
 	)
 
-	config.run()
+	config.run(t)
 }
 
 // TestCatalogdMetricsExportedEndpoint verifies that the metrics endpoint for catalogd
 func TestCatalogdMetricsExportedEndpoint(t *testing.T) {
 	client := utils.FindK8sClient(t)
+	curlNamespace := createRandomNamespace(t, client)
+	componentNamespace := getComponentNamespace(t, client, "control-plane=catalogd-controller-manager")
+	metricsURL := fmt.Sprintf("https://catalogd-service.%s.svc.cluster.local:7443/metrics", componentNamespace)
+
 	config := NewMetricsTestConfig(
-		t, client,
-		"control-plane=catalogd-controller-manager",
+		client,
+		curlNamespace,
 		"catalogd-metrics-reader",
 		"catalogd-metrics-binding",
-		"catalogd-controller-manager",
+		"catalogd-metrics-reader",
 		"catalogd-curl-metrics",
-		"https://catalogd-service.NAMESPACE.svc.cluster.local:7443/metrics",
+		metricsURL,
 	)
 
-	config.run()
+	config.run(t)
 }
 
 // MetricsTestConfig holds the necessary configurations for testing metrics endpoints.
 type MetricsTestConfig struct {
-	t              *testing.T
 	client         string
 	namespace      string
 	clusterRole    string
@@ -73,12 +80,8 @@ type MetricsTestConfig struct {
 }
 
 // NewMetricsTestConfig initializes a new MetricsTestConfig.
-func NewMetricsTestConfig(t *testing.T, client, selector, clusterRole, clusterBinding, serviceAccount, curlPodName, metricsURL string) *MetricsTestConfig {
-	namespace := getComponentNamespace(t, client, selector)
-	metricsURL = strings.ReplaceAll(metricsURL, "NAMESPACE", namespace)
-
+func NewMetricsTestConfig(client, namespace, clusterRole, clusterBinding, serviceAccount, curlPodName, metricsURL string) *MetricsTestConfig {
 	return &MetricsTestConfig{
-		t:              t,
 		client:         client,
 		namespace:      namespace,
 		clusterRole:    clusterRole,
@@ -90,38 +93,44 @@ func NewMetricsTestConfig(t *testing.T, client, selector, clusterRole, clusterBi
 }
 
 // run will execute all steps of those tests
-func (c *MetricsTestConfig) run() {
-	c.createMetricsClusterRoleBinding()
-	token := c.getServiceAccountToken()
-	c.createCurlMetricsPod()
-	c.validate(token)
-	defer c.cleanup()
+func (c *MetricsTestConfig) run(t *testing.T) {
+	defer c.cleanup(t)
+
+	c.createMetricsClusterRoleBinding(t)
+	token := c.getServiceAccountToken(t)
+	c.createCurlMetricsPod(t)
+	c.validate(t, token)
 }
 
 // createMetricsClusterRoleBinding to binding and expose the metrics
-func (c *MetricsTestConfig) createMetricsClusterRoleBinding() {
-	c.t.Logf("Creating ClusterRoleBinding %s in namespace %s", c.clusterBinding, c.namespace)
+func (c *MetricsTestConfig) createMetricsClusterRoleBinding(t *testing.T) {
+	t.Logf("Creating ClusterRoleBinding %s for %s in namespace %s", c.clusterBinding, c.serviceAccount, c.namespace)
 	cmd := exec.Command(c.client, "create", "clusterrolebinding", c.clusterBinding,
 		"--clusterrole="+c.clusterRole,
 		"--serviceaccount="+c.namespace+":"+c.serviceAccount)
 	output, err := cmd.CombinedOutput()
-	require.NoError(c.t, err, "Error creating ClusterRoleBinding: %s", string(output))
+	require.NoError(t, err, "Error creating ClusterRoleBinding: %s", string(output))
 }
 
 // getServiceAccountToken return the token requires to have access to the metrics
-func (c *MetricsTestConfig) getServiceAccountToken() string {
-	c.t.Logf("Generating ServiceAccount token at namespace %s", c.namespace)
-	cmd := exec.Command(c.client, "create", "token", c.serviceAccount, "-n", c.namespace)
+func (c *MetricsTestConfig) getServiceAccountToken(t *testing.T) string {
+	t.Logf("Creating ServiceAccount %q in namespace %q", c.serviceAccount, c.namespace)
+	output, err := exec.Command(c.client, "create", "serviceaccount", c.serviceAccount, "--namespace="+c.namespace).CombinedOutput()
+	require.NoError(t, err, "Error creating service account: %v", string(output))
+
+	t.Logf("Generating ServiceAccount token for %q in namespace %q", c.serviceAccount, c.namespace)
+	cmd := exec.Command(c.client, "create", "token", c.serviceAccount, "--namespace", c.namespace)
 	tokenOutput, tokenCombinedOutput, err := stdoutAndCombined(cmd)
-	require.NoError(c.t, err, "Error creating token: %s", string(tokenCombinedOutput))
+	require.NoError(t, err, "Error creating token: %s", string(tokenCombinedOutput))
 	return string(bytes.TrimSpace(tokenOutput))
 }
 
 // createCurlMetricsPod creates the Pod with curl image to allow check if the metrics are working
-func (c *MetricsTestConfig) createCurlMetricsPod() {
-	c.t.Logf("Creating curl pod (%s/%s) to validate the metrics endpoint", c.namespace, c.curlPodName)
+func (c *MetricsTestConfig) createCurlMetricsPod(t *testing.T) {
+	t.Logf("Creating curl pod (%s/%s) to validate the metrics endpoint", c.namespace, c.curlPodName)
 	cmd := exec.Command(c.client, "run", c.curlPodName,
-		"--image=curlimages/curl", "-n", c.namespace,
+		"--image=curlimages/curl",
+		"--namespace", c.namespace,
 		"--restart=Never",
 		"--overrides", `{
 			"spec": {
@@ -142,55 +151,66 @@ func (c *MetricsTestConfig) createCurlMetricsPod() {
 			}
 		}`)
 	output, err := cmd.CombinedOutput()
-	require.NoError(c.t, err, "Error creating curl pod: %s", string(output))
+	require.NoError(t, err, "Error creating curl pod: %s", string(output))
 }
 
 // validate verifies if is possible to access the metrics
-func (c *MetricsTestConfig) validate(token string) {
-	c.t.Log("Waiting for the curl pod to be ready")
-	waitCmd := exec.Command(c.client, "wait", "--for=condition=Ready", "pod", c.curlPodName, "-n", c.namespace, "--timeout=60s")
+func (c *MetricsTestConfig) validate(t *testing.T, token string) {
+	t.Log("Waiting for the curl pod to be ready")
+	waitCmd := exec.Command(c.client, "wait", "--for=condition=Ready", "pod", c.curlPodName, "--namespace", c.namespace, "--timeout=60s")
 	waitOutput, waitErr := waitCmd.CombinedOutput()
-	require.NoError(c.t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput))
+	require.NoError(t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput))
 
-	c.t.Log("Validating the metrics endpoint")
-	curlCmd := exec.Command(c.client, "exec", c.curlPodName, "-n", c.namespace, "--",
+	t.Log("Validating the metrics endpoint")
+	curlCmd := exec.Command(c.client, "exec", c.curlPodName, "--namespace", c.namespace, "--",
 		"curl", "-v", "-k", "-H", "Authorization: Bearer "+token, c.metricsURL)
 	output, err := curlCmd.CombinedOutput()
-	require.NoError(c.t, err, "Error calling metrics endpoint: %s", string(output))
-	require.Contains(c.t, string(output), "200 OK", "Metrics endpoint did not return 200 OK")
+	require.NoError(t, err, "Error calling metrics endpoint: %s", string(output))
+	require.Contains(t, string(output), "200 OK", "Metrics endpoint did not return 200 OK")
 }
 
 // cleanup removes the created resources. Uses a context with timeout to prevent hangs.
-func (c *MetricsTestConfig) cleanup() {
-	c.t.Log("Cleaning up resources")
-	_ = exec.Command(c.client, "delete", "clusterrolebinding", c.clusterBinding, "--ignore-not-found=true", "--force").Run()
-	_ = exec.Command(c.client, "delete", "pod", c.curlPodName, "-n", c.namespace, "--ignore-not-found=true", "--force").Run()
+func (c *MetricsTestConfig) cleanup(t *testing.T) {
+	type objDesc struct {
+		resourceName string
+		name         string
+		namespace    string
+	}
+	objects := []objDesc{
+		{"clusterrolebinding", c.clusterBinding, ""},
+		{"pod", c.curlPodName, c.namespace},
+		{"serviceaccount", c.serviceAccount, c.namespace},
+		{"namespace", c.namespace, ""},
+	}
+
+	t.Log("Cleaning up resources")
+	for _, obj := range objects {
+		args := []string{"delete", obj.resourceName, obj.name, "--ignore-not-found=true", "--force"}
+		if obj.namespace != "" {
+			args = append(args, "--namespace", obj.namespace)
+		}
+		output, err := exec.Command(c.client, args...).CombinedOutput()
+		require.NoError(t, err, "Error deleting %q %q in namespace %q: %v", obj.resourceName, obj.name, obj.namespace, string(output))
+	}
 
 	// Create a context with a 60-second timeout.
 	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
 	defer cancel()
 
-	// Wait for the ClusterRoleBinding to be deleted.
-	if err := waitForDeletion(ctx, c.client, "clusterrolebinding", c.clusterBinding); err != nil {
-		c.t.Logf("Error waiting for clusterrolebinding deletion: %v", err)
-	} else {
-		c.t.Log("ClusterRoleBinding deleted")
-	}
-
-	// Wait for the Pod to be deleted.
-	if err := waitForDeletion(ctx, c.client, "pod", c.curlPodName, "-n", c.namespace); err != nil {
-		c.t.Logf("Error waiting for pod deletion: %v", err)
-	} else {
-		c.t.Log("Pod deleted")
+	for _, obj := range objects {
+		err := waitForDeletion(ctx, c.client, obj.resourceName, obj.name, obj.namespace)
+		require.NoError(t, err, "Error deleting %q %q in namespace %q", obj.resourceName, obj.name, obj.namespace)
+		t.Logf("Successfully deleted %q %q in namespace %q", obj.resourceName, obj.name, obj.namespace)
 	}
 }
 
 // waitForDeletion uses "kubectl wait" to block until the specified resource is deleted
 // or until the 60-second timeout is reached.
-func waitForDeletion(ctx context.Context, client, resourceType, resourceName string, extraArgs ...string) error {
-	args := []string{"wait", "--for=delete", resourceType, resourceName}
-	args = append(args, extraArgs...)
-	args = append(args, "--timeout=60s")
+func waitForDeletion(ctx context.Context, client, resourceType, resourceName, resourceNamespace string) error {
+	args := []string{"wait", "--for=delete", "--timeout=60s", resourceType, resourceName}
+	if resourceNamespace != "" {
+		args = append(args, "--namespace", resourceNamespace)
+	}
 	cmd := exec.CommandContext(ctx, client, args...)
 	output, err := cmd.CombinedOutput()
 	if err != nil {
@@ -199,6 +219,17 @@ func waitForDeletion(ctx context.Context, client, resourceType, resourceName str
 	return nil
 }
 
+// createRandomNamespace creates a random namespace
+func createRandomNamespace(t *testing.T, client string) string {
+	nsName := fmt.Sprintf("testns-%s", rand.String(8))
+
+	cmd := exec.Command(client, "create", "namespace", nsName)
+	output, err := cmd.CombinedOutput()
+	require.NoError(t, err, "Error creating namespace: %s", string(output))
+
+	return nsName
+}
+
 // getComponentNamespace returns the namespace where operator-controller or catalogd is running
 func getComponentNamespace(t *testing.T, client, selector string) string {
 	cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector="+selector, "--output=jsonpath={.items[0].metadata.namespace}")