Repository: rancher/fleet
Pull request #3692 (2025-05-15 06:23:03)

Use secrets for storing OCI registry details
This PR implements the changes described in https://github.com/rancher/fleet/issues/3688.

Secrets are used to store all the details that define a connection to an OCI registry. Two levels of secrets are defined: one specified by the user (referenced in the ociReferenceSecret field of GitRepo), and a default one that can be created for a given namespace.

Fleet uses the user-defined secret, and if no user-defined secret is specified, falls back to the default one.

A specific type is enforced for any OCI reference secret.

If either of the two secrets exists, a deployment is executed, storing the Bundle resources in the referenced OCI registry. Otherwise, etcd is used.

Two new fields are introduced in the OCI secrets to perform read operations from the OCI registry (these are used by the agents).

A SHA verification of the contents of the manifest downloaded from the OCI registry is added. If the calculated SHA does not match the expected one, the content is rejected and an error is reported.

This PR also includes refactoring of the e2e tests to simplify how cluster resources are read, using a controller-runtime client instead of the kubectl wrapper.

Refers to: https://github.com/rancher/fleet/issues/3688

- [ ] <!-- If applicable,--> I have updated the documentation via a pull request in the
[fleet-docs](https://github.com/rancher/fleet-docs) repository.



Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ba7089e2..1f3a48e7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -43,12 +43,12 @@ jobs:
             secret/data/github/repo/${{ github.repository }}/fossa/credential token | FOSSA_API_KEY
 
       - name: Run FOSSA scan
-        uses: fossas/fossa-action@c0a7d013f84c8ee5e910593186598625513cc1e4 # v1.6.0
+        uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0
         with:
           api-key: ${{ env.FOSSA_API_KEY }}
 
       - name: Run FOSSA tests
-        uses: fossas/fossa-action@c0a7d013f84c8ee5e910593186598625513cc1e4 # v1.6.0
+        uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0
         with:
           api-key: ${{ env.FOSSA_API_KEY }}
           run-tests: false
diff --git a/README.md b/README.md
index ed7a98ce..f0b98bd3 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Install the Fleet Helm charts (there's two because we separate out CRDs for ulti
 
 ```shell
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet-crd https://github.com/rancher/fleet/releases/download/v0.12.2/fleet-crd-0.12.2.tgz
+    fleet-crd https://github.com/rancher/fleet/releases/download/v0.12.3/fleet-crd-0.12.3.tgz
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet https://github.com/rancher/fleet/releases/download/v0.12.2/fleet-0.12.2.tgz
+    fleet https://github.com/rancher/fleet/releases/download/v0.12.3/fleet-0.12.3.tgz
 ```
 
 ## Add a Git Repo to watch
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 2be27f3a..88c4af35 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -6453,28 +6453,10 @@ spec:
                   description: KeepResources specifies if the resources created must
                     be kept after deleting the GitRepo.
                   type: boolean
-                ociRegistry:
-                  description: OCIRegistry specifies the OCI registry related parameters
-                  properties:
-                    authSecretName:
-                      description: AuthSecretName contains the auth secret where the
-                        OCI registry credentials are stored.
-                      nullable: true
-                      type: string
-                    basicHTTP:
-                      description: BasicHTTP uses HTTP connections to the OCI registry
-                        when enabled.
-                      nullable: true
-                      type: boolean
-                    insecureSkipTLS:
-                      description: InsecureSkipTLS allows connections to OCI registry
-                        without certs when enabled.
-                      nullable: true
-                      type: boolean
-                    reference:
-                      description: Reference of the OCI Registry
-                      type: string
-                  type: object
+                ociRegistrySecret:
+                  description: OCIRegistrySecret contains the name of the secret to
+                    be used for retrieving the OCI registry connection details.
+                  type: string
                 paths:
                   description: 'Paths is the directories relative to the git repo
                     root that contain resources to be applied.
diff --git a/charts/fleet/templates/configmap_known_hosts.yaml b/charts/fleet/templates/configmap_known_hosts.yaml
index 6f46d3ad..bf4b48e1 100644
--- a/charts/fleet/templates/configmap_known_hosts.yaml
+++ b/charts/fleet/templates/configmap_known_hosts.yaml
@@ -4,15 +4,14 @@ metadata:
   name: known-hosts
 data:
   known_hosts: |
-    bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M=
     bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE=
     bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO
-    github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
+    bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M=
     github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
+    github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
     github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
+    gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
     gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
     gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
-    gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
-
     ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
     vs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
diff --git a/dev/setup-fleet b/dev/setup-fleet
index ac9919ab..f84ac8d0 100755
--- a/dev/setup-fleet
+++ b/dev/setup-fleet
@@ -41,6 +41,9 @@ helm -n cattle-fleet-system upgrade --install --create-namespace --wait --reset-
   --set apiServerURL="$server" \
   $shards_settings \
   --set bootstrap.agentNamespace=cattle-fleet-local-system \
+  --set agent.leaderElection.leaseDuration=10s \
+  --set agent.leaderElection.retryPeriod=1s \
+  --set agent.leaderElection.renewDeadline=5s \
   --set-string extraEnv[0].name=EXPERIMENTAL_OCI_STORAGE \
   --set-string extraEnv[0].value=true \
   --set-string extraEnv[1].name=EXPERIMENTAL_HELM_OPS \
diff --git a/dev/update-agent-k3d b/dev/update-agent-k3d
index b201c551..f3a34da0 100755
--- a/dev/update-agent-k3d
+++ b/dev/update-agent-k3d
@@ -18,3 +18,15 @@ docker build -f package/Dockerfile.agent -t rancher/fleet-agent:dev --build-arg=
 fleet_ctx=$(kubectl config current-context)
 k3d image import rancher/fleet-agent:dev -m direct -c "${fleet_ctx#k3d-}"
 kubectl delete pod -l app=fleet-agent -n cattle-fleet-local-system
+
+upstream_ctx="${FLEET_E2E_CLUSTER-k3d-upstream}"
+downstream_ctx="${FLEET_E2E_CLUSTER_DOWNSTREAM-k3d-downstream1}"
+downstream_keyword="${downstream_ctx#k3d-}"
+downstream_keyword="${downstream_keyword%[0-9]*}"
+if [ "$upstream_ctx" != "$downstream_ctx" ]; then
+  for cluster in $(k3d cluster list -o json | \
+      jq -r ".[].name | select(. | contains(\"${downstream_keyword}\"))"); do
+    k3d image import rancher/fleet-agent:dev -m direct -c "${cluster}"
+    kubectl --context "k3d-$cluster" delete pod -l app=fleet-agent -n cattle-fleet-system
+  done
+fi
diff --git a/go.mod b/go.mod
index ff661aac..92575900 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/gobwas/glob v0.2.3
 	github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
 	github.com/google/go-cmp v0.7.0
-	github.com/google/go-containerregistry v0.20.3
+	github.com/google/go-containerregistry v0.20.5
 	github.com/gorilla/mux v1.8.1
 	github.com/hashicorp/go-getter v1.7.8
 	github.com/jpillora/backoff v1.0.0
@@ -35,10 +35,10 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.2
-	github.com/prometheus/common v0.63.0
-	github.com/rancher/fleet/pkg/apis v0.12.1
+	github.com/prometheus/common v0.64.0
+	github.com/rancher/fleet/pkg/apis v0.12.3
 	github.com/rancher/lasso v0.2.2
-	github.com/rancher/wrangler/v3 v3.2.0
+	github.com/rancher/wrangler/v3 v3.2.1
 	github.com/reugn/go-quartz v0.13.0
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.9.1
@@ -52,14 +52,14 @@ require (
 	gopkg.in/go-playground/webhooks.v5 v5.17.0
 	gotest.tools v2.2.0+incompatible
 	helm.sh/helm/v3 v3.17.3
-	k8s.io/api v0.32.4
-	k8s.io/apiextensions-apiserver v0.32.4
-	k8s.io/apimachinery v0.32.4
-	k8s.io/cli-runtime v0.32.4
-	k8s.io/client-go v0.32.4
+	k8s.io/api v0.32.5
+	k8s.io/apiextensions-apiserver v0.32.5
+	k8s.io/apimachinery v0.32.5
+	k8s.io/cli-runtime v0.32.5
+	k8s.io/client-go v0.32.5
 	k8s.io/klog/v2 v2.130.1
 	k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
-	k8s.io/kubectl v0.32.4
+	k8s.io/kubectl v0.32.5
 	k8s.io/kubernetes v1.32.2
 	k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
 	oras.land/oras-go/v2 v2.5.0
@@ -100,12 +100,12 @@ require (
 	github.com/containerd/platforms v0.2.1 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
 	github.com/cpuguy83/dockercfg v0.3.2 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
 	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/cli v27.5.0+incompatible // indirect
+	github.com/docker/cli v28.1.1+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.8.2 // indirect
+	github.com/docker/docker-credential-helpers v0.9.3 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
@@ -180,7 +180,6 @@ require (
 	github.com/moby/locker v1.0.1 // indirect
 	github.com/moby/patternmatcher v0.6.0 // indirect
 	github.com/moby/spdystream v0.5.0 // indirect
-	github.com/moby/sys/atomicwriter v0.1.0 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
 	github.com/moby/sys/user v0.4.0 // indirect
 	github.com/moby/sys/userns v0.1.0 // indirect
@@ -209,7 +208,7 @@ require (
 	github.com/tklauser/go-sysconf v0.3.12 // indirect
 	github.com/tklauser/numcpus v0.6.1 // indirect
 	github.com/ulikunitz/xz v0.5.10 // indirect
-	github.com/vbatts/tar-split v0.11.6 // indirect
+	github.com/vbatts/tar-split v0.12.1 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -220,20 +219,20 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
 	go.opentelemetry.io/otel v1.35.0 // indirect
 	go.opentelemetry.io/otel/metric v1.35.0 // indirect
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.39.0 // indirect
-	golang.org/x/oauth2 v0.25.0 // indirect
+	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/oauth2 v0.30.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
-	golang.org/x/tools v0.31.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/api v0.155.0 // indirect
 	google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
@@ -246,10 +245,10 @@ require (
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiserver v0.32.4 // indirect
-	k8s.io/code-generator v0.32.4 // indirect
-	k8s.io/component-base v0.32.4 // indirect
-	k8s.io/component-helpers v0.32.4 // indirect
+	k8s.io/apiserver v0.32.5 // indirect
+	k8s.io/code-generator v0.32.5 // indirect
+	k8s.io/component-base v0.32.5 // indirect
+	k8s.io/component-helpers v0.32.5 // indirect
 	k8s.io/controller-manager v0.32.1 // indirect
 	k8s.io/gengo v0.0.0-20250130153323-76c5745d3511 // indirect
 	k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect
diff --git a/go.sum b/go.sum
index 5fb4d7c0..ff4d27c7 100644
--- a/go.sum
+++ b/go.sum
@@ -725,8 +725,9 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRcc
 github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
 github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
 github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
-github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
+github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -740,14 +741,14 @@ github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aB
 github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
 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 v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
-github.com/docker/cli v27.5.0+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.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
 github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
-github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -936,8 +937,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
-github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
+github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
+github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -1223,8 +1224,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
-github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
-github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
+github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
+github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
@@ -1232,8 +1233,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rancher/lasso v0.2.2 h1:oKP5d4+eSupwHftLMTWo6QS1tYYZ5XK+ZjP+VNhwmk8=
 github.com/rancher/lasso v0.2.2/go.mod h1:KSV3jBXfdXqdCuMm2uC8kKB9q/wuDYb3h0eHZoRjShM=
-github.com/rancher/wrangler/v3 v3.2.0 h1:fZmhSOczW+pxAhyOaGG+9xbEwETPGA5gbS0x0Im2zWs=
-github.com/rancher/wrangler/v3 v3.2.0/go.mod h1:0C5QyvSrQOff8gQQzpB/L/FF03EQycjR3unSJcKCHno=
+github.com/rancher/wrangler/v3 v3.2.1 h1:V51PnoGb8bZ5jJdxFlqKQApzWdSp4sEy5OPGuEGqVbI=
+github.com/rancher/wrangler/v3 v3.2.1/go.mod h1:RV8kkv5br5HaxXWamIbr95pOjvVeoC5CeBldcdw5Fv0=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/reugn/go-quartz v0.13.0 h1:0eMxvj28Qu1npIDdN9Mzg9hwyksGH6XJt4Cz0QB8EUk=
 github.com/reugn/go-quartz v0.13.0/go.mod h1:0ghKksELp8MJ4h84T203aTHRF3Kug5BrxEW3ErBvhzY=
@@ -1300,8 +1301,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
 github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
 github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
-github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
+github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
+github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -1345,8 +1346,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
 go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
 go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
@@ -1355,8 +1356,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfg
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
 go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
 go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
-go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
 go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
 go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
@@ -1518,8 +1521,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
-golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1549,8 +1552,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
 golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
-golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
-golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1778,8 +1781,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2110,24 +2113,24 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
-k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/apiserver v0.32.4 h1:Yf7sd/y+GOQKH1Qf6wUeayZrYXe2SKZ17Bcq7VQM5HQ=
-k8s.io/apiserver v0.32.4/go.mod h1:JFUMNtE2M5yqLZpIsgCb06SkVSW1YcxW1oyLSTfjXR8=
-k8s.io/cli-runtime v0.32.4 h1:5O9eC50+yFFODAan3QXeTJHtoe69ie/A9vWBITIn+KM=
-k8s.io/cli-runtime v0.32.4/go.mod h1:Zn7nvBY625sEEYGtTMMPS619nrmVxabGssHyAKFK7RA=
-k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
-k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
-k8s.io/code-generator v0.32.4 h1:d4dm/43RD6xhPBX22JgJw9JUpwTKzVR6tAxJD7pz83o=
-k8s.io/code-generator v0.32.4/go.mod h1:R0bKdIg1smtvsKvj9q7SxTeKq5X9ko6PuICCGt4yqxg=
-k8s.io/component-base v0.32.4 h1:HuF+2JVLbFS5GODLIfPCb1Td6b+G2HszJoArcWOSr5I=
-k8s.io/component-base v0.32.4/go.mod h1:10KloJEYw1keU/Xmjfy9TKJqUq7J2mYdiD1VDXoco4o=
-k8s.io/component-helpers v0.32.4 h1:pZ7LYWpyqqg4j5cWqpGSgenXYWF2JXe6dgdy7MZufqU=
-k8s.io/component-helpers v0.32.4/go.mod h1:p9iLmy15AWoh3TM+B0g7rRuNk68ZSByLxtgm3uYI84I=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apiextensions-apiserver v0.32.5 h1:o0aKvmzIIs8Uk54pidk32pxET+Pg2ULnh9WI1PuKTwE=
+k8s.io/apiextensions-apiserver v0.32.5/go.mod h1:5fpedJa3HJJFBukAZ6ur91DEDye5gYuXISPbOiNLYpU=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/apiserver v0.32.5 h1:phmm2EOUVFI+cLiq8Grtuh166fTt/qgvkGPkpgzp5uY=
+k8s.io/apiserver v0.32.5/go.mod h1:5bfueS1tgARVWVXRJBMI5mHoCmev0jOvbxebai/kiqc=
+k8s.io/cli-runtime v0.32.5 h1:cyf6pJLpOFzxT4PbOKIXFyNbQV2IFP53jGADXtrd6tw=
+k8s.io/cli-runtime v0.32.5/go.mod h1:AcqQUyDDFwc4ymBlPpUXVOkyFVjKi9dnDQn3unv1C7E=
+k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0=
+k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA=
+k8s.io/code-generator v0.32.5 h1:dvoXgaWTDPLsg0txUzWj5xPV8UwHOsBhmm4JC9Gd1Qo=
+k8s.io/code-generator v0.32.5/go.mod h1:7S6jUv4ZAnI2yDUJUQUEuc3gv6+qFhnkB5Fhs9Eb0d8=
+k8s.io/component-base v0.32.5 h1:2HiX+m3s9Iz5CMqdCVDH2V942UqzQvjuhcXb4W+KCsg=
+k8s.io/component-base v0.32.5/go.mod h1:jDsPNFFElv9m27TcYxlpEX7TZ3vdgx2g4PaqMUHpV/Y=
+k8s.io/component-helpers v0.32.5 h1:CfUO2BUlAUwddmPw4T7vcqdO992a8WGQW53Ldl5K9vg=
+k8s.io/component-helpers v0.32.5/go.mod h1:YRmZMXae6PC7ywPMhrnfPDQUMOaqE8re98CnyTqUcwE=
 k8s.io/controller-manager v0.32.1 h1:z3oQp1O5l0cSzM/MKf8V4olhJ9TmnELoJRPcV/v1s+Y=
 k8s.io/controller-manager v0.32.1/go.mod h1:dVA1UZPbqHH4hEhrrnLvQ4d5qVQCklNB8GEzYV59v/4=
 k8s.io/gengo v0.0.0-20250130153323-76c5745d3511 h1:4eL6zr5VCj71nu2nOuQ6j6m/kqh5WueXBN8daZkNe90=
@@ -2141,8 +2144,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
-k8s.io/kubectl v0.32.4 h1:PhiS4JgnJE6/JcksIJHAQI41F4hwJ270UopbcDsutF0=
-k8s.io/kubectl v0.32.4/go.mod h1:XYFDrC1l3cpjWRj7UfS0LDAMlK+6eTema28NeUYFKD8=
+k8s.io/kubectl v0.32.5 h1:LUHZhYkIu9Cji6x1V7s4uyCGxJlS461VSYj2GyuQJtM=
+k8s.io/kubectl v0.32.5/go.mod h1:YA7mZP44lVEn9qXRinM9THMNvVWJ6edwyHZSVMTVQbo=
 k8s.io/kubernetes v1.32.2 h1:mShetlA102UpjRVSGzB+5vjJwy8oPy8FMWrkTH5f37o=
 k8s.io/kubernetes v1.32.2/go.mod h1:tiIKO63GcdPRBHW2WiUFm3C0eoLczl3f7qi56Dm1W8I=
 k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=
diff --git a/internal/cmd/agent/clusterstatus.go b/internal/cmd/agent/clusterstatus.go
index 1f276b45..92812536 100644
--- a/internal/cmd/agent/clusterstatus.go
+++ b/internal/cmd/agent/clusterstatus.go
@@ -2,12 +2,9 @@ package agent
 
 import (
 	"context"
-	"fmt"
-	"os"
 	"time"
 
 	"k8s.io/client-go/rest"
-	"k8s.io/client-go/tools/clientcmd"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/log"
@@ -17,54 +14,41 @@ import (
 	"github.com/rancher/fleet/internal/cmd/agent/register"
 )
 
-type ClusterStatus struct {
-	UpstreamOptions
-	CheckinInterval string `usage:"How often to post cluster status" env:"CHECKIN_INTERVAL"`
-	AgentInfo       *register.AgentInfo
+type ClusterStatusRunnable struct {
+	config          *rest.Config
+	namespace       string
+	checkinInterval string
+	agentInfo       *register.AgentInfo
 }
 
-func (cs *ClusterStatus) Start(ctx context.Context) error {
+func (cs *ClusterStatusRunnable) Start(ctx context.Context) error {
 	ctrl.SetLogger(zap.New(zap.UseFlagOptions(zopts)))
 	ctx = log.IntoContext(ctx, ctrl.Log)
 
 	var err error
 	var checkinInterval time.Duration
-	if cs.CheckinInterval != "" {
-		checkinInterval, err = time.ParseDuration(cs.CheckinInterval)
+	if cs.checkinInterval != "" {
+		checkinInterval, err = time.ParseDuration(cs.checkinInterval)
 		if err != nil {
 			return err
 		}
 	}
 
-	localConfig, err := clientcmd.BuildConfigFromFlags("", cs.Kubeconfig)
-	if err != nil {
-		return fmt.Errorf("failed to load kubeconfig: %w", err)
-	}
-
-	// without rate limiting
-	localConfig = rest.CopyConfig(localConfig)
-	localConfig.QPS = -1
-	localConfig.RateLimiter = nil
+	setupLog.Info("Starting cluster status ticker", "checkin interval", checkinInterval.String(), "cluster namespace", cs.agentInfo.ClusterNamespace, "cluster name", cs.agentInfo.ClusterName)
 
-	setupLog.Info("Fetching kubeconfig for upstream cluster from registration", "namespace", cs.Namespace)
-
-	clnt, err := client.New(localConfig, client.Options{
-		Scheme: scheme,
-	})
+	// use a separate client for the cluster status ticker, that does not use a cache
+	client, err := client.New(cs.config, client.Options{Scheme: scheme})
 	if err != nil {
-		fmt.Println("failed to create client")
-		os.Exit(1)
+		return err
 	}
 
-	setupLog.Info("Starting cluster status ticker", "checkin interval", checkinInterval.String(), "cluster namespace", cs.AgentInfo.ClusterNamespace, "cluster name", cs.AgentInfo.ClusterName)
-
 	go func() {
 		clusterstatus.Ticker(
 			ctx,
-			clnt,
-			cs.Namespace,
-			cs.AgentInfo.ClusterNamespace,
-			cs.AgentInfo.ClusterName,
+			client,
+			cs.namespace,
+			cs.agentInfo.ClusterNamespace,
+			cs.agentInfo.ClusterName,
 			checkinInterval,
 		)
 
diff --git a/internal/cmd/agent/clusterstatus/ticker.go b/internal/cmd/agent/clusterstatus/ticker.go
index 88c39746..81459fd3 100644
--- a/internal/cmd/agent/clusterstatus/ticker.go
+++ b/internal/cmd/agent/clusterstatus/ticker.go
@@ -5,8 +5,6 @@ import (
 	"context"
 	"time"
 
-	"github.com/sirupsen/logrus"
-
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/durations"
 
@@ -41,7 +39,7 @@ func Ticker(ctx context.Context, client client.Client, agentNamespace string, cl
 		time.Sleep(durations.ClusterRegisterDelay)
 		logger.V(1).Info("Reporting cluster status once")
 		if err := h.Update(ctx); err != nil {
-			logrus.Errorf("failed to report cluster status: %v", err)
+			logger.Error(err, "failed to report initial cluster status")
 		}
 	}()
 	go func() {
@@ -51,7 +49,7 @@ func Ticker(ctx context.Context, client client.Client, agentNamespace string, cl
 		for range ticker.Context(ctx, checkinInterval) {
 			logger.V(1).Info("Reporting cluster status")
 			if err := h.Update(ctx); err != nil {
-				logrus.Errorf("failed to report cluster status: %v", err)
+				logger.Error(err, "failed to report cluster status")
 			}
 		}
 	}()
@@ -68,21 +66,21 @@ func (h *handler) Update(ctx context.Context) error {
 		return nil
 	}
 
-	cluster := &fleet.Cluster{}
-	err := h.client.Get(context.Background(), types.NamespacedName{
-		Namespace: h.clusterNamespace,
-		Name:      h.clusterName,
-	}, cluster)
-	if err != nil {
-		return err
+	cluster := &fleet.Cluster{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      h.clusterName,
+			Namespace: h.clusterNamespace,
+		},
 	}
 
-	// Create a patch with the updated status
-	patch := client.MergeFrom(cluster.DeepCopy())
-	cluster.Status.Agent = agentStatus
+	// Create a patch with the updated status, we avoid Get as that would
+	// need additional RBAC
+	patch := `[{"op":"add","path":"/status/agent","value":{"lastSeen":"` +
+		agentStatus.LastSeen.Format(time.RFC3339) +
+		`","namespace":"` + agentStatus.Namespace +
+		`"}}]`
 
-	// Apply the patch
-	err = h.client.Status().Patch(ctx, cluster, patch)
+	err := h.client.Status().Patch(ctx, cluster, client.RawPatch(types.JSONPatchType, []byte(patch)))
 	if err != nil {
 		return err
 	}
diff --git a/internal/cmd/agent/deployer/deployer.go b/internal/cmd/agent/deployer/deployer.go
index 34200e8e..44eab82e 100644
--- a/internal/cmd/agent/deployer/deployer.go
+++ b/internal/cmd/agent/deployer/deployer.go
@@ -5,13 +5,12 @@ import (
 	"fmt"
 	"reflect"
 	"regexp"
-	"strconv"
 	"strings"
 
 	"github.com/rancher/fleet/internal/bundlereader"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	"github.com/rancher/fleet/internal/manifest"
-	"github.com/rancher/fleet/internal/ociwrapper"
+	"github.com/rancher/fleet/internal/ocistorage"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
 	"github.com/rancher/wrangler/v3/pkg/condition"
@@ -114,38 +113,26 @@ func (d *Deployer) helmdeploy(ctx context.Context, logger logr.Logger, bd *fleet
 		err error
 	)
 	if bd.Spec.OCIContents {
-		// First we need to access the secret where the OCI registry reference and credentials are located
-		var secret corev1.Secret
+		oci := ocistorage.NewOCIWrapper()
 		secretID := types.NamespacedName{Name: manifestID, Namespace: bd.Namespace}
-		if err := d.upstreamClient.Get(ctx, secretID, &secret); err != nil {
-			return "", err
-		}
-		ref, ok := secret.Data[ociwrapper.OCISecretReference]
-		if !ok {
-			return "", fmt.Errorf("expected data [reference] not found in secret: %s", manifestID)
-		}
-		username := string(secret.Data[ociwrapper.OCISecretUsername])
-		password := string(secret.Data[ociwrapper.OCISecretPassword])
-		basicHttp, err := strconv.ParseBool(string(secret.Data[ociwrapper.OCISecretBasicHTTP]))
+		opts, err := ocistorage.ReadOptsFromSecret(ctx, d.upstreamClient, secretID)
 		if err != nil {
-			return "", fmt.Errorf("value for [ociRegistry.http] in secret %s cannot be parsed as boolean", manifestID)
+			return "", err
 		}
-		insecure, err := strconv.ParseBool(string(secret.Data[ociwrapper.OCISecretInsecure]))
+		m, err = oci.PullManifest(ctx, opts, manifestID)
 		if err != nil {
-			return "", fmt.Errorf("value for [ociRegistry.insecure] in secret %s cannot be parsed as boolean", manifestID)
-		}
-		ociOpts := ociwrapper.OCIOpts{
-			Reference:       string(ref),
-			Username:        username,
-			Password:        password,
-			BasicHTTP:       basicHttp,
-			InsecureSkipTLS: insecure,
+			return "", err
 		}
-		oci := ociwrapper.NewOCIWrapper()
-		m, err = oci.PullManifest(ctx, ociOpts, manifestID)
+		// Verify that the calculated manifestID for the manifest
+		// we just downloaded matches the expected one.
+		// Otherwise, the manifest will be considered incorrect or corrupted.
+		actualID, err := m.ID()
 		if err != nil {
 			return "", err
 		}
+		if actualID != manifestID {
+			return "", fmt.Errorf("invalid or corrupt manifest. Expecting id: %q, got %q", manifestID, actualID)
+		}
 	} else if bd.Spec.HelmChartOptions != nil {
 		m, err = bundlereader.GetManifestFromHelmChart(ctx, d.client, bd)
 		if err != nil {
diff --git a/internal/cmd/agent/operator.go b/internal/cmd/agent/operator.go
index f4198ebe..b963fd92 100644
--- a/internal/cmd/agent/operator.go
+++ b/internal/cmd/agent/operator.go
@@ -59,10 +59,10 @@ func init() {
 func start(
 	ctx context.Context,
 	localConfig *rest.Config,
-	systemNamespace,
+	systemNamespace string,
 	agentScope string,
+	checkinInterval string,
 	workersOpts AgentReconcilerWorkers,
-	clusterStatus *ClusterStatus,
 	agentInfo *register.AgentInfo,
 ) error {
 	upstreamConfig, err := agentInfo.ClientConfig.ClientConfig()
@@ -91,7 +91,7 @@ func start(
 		Metrics:                metricsserver.Options{BindAddress: metricsAddr},
 		HealthProbeBindAddress: probeAddr,
 		LeaderElection:         false,
-		// only watch resources in the fleet namespace
+		// only watch resources in the cluster namespace
 		Cache: cache.Options{
 			DefaultNamespaces: map[string]cache.Config{fleetNamespace: {}},
 		},
@@ -148,6 +148,12 @@ func start(
 		return err
 	}
 
+	clusterStatus := &ClusterStatusRunnable{
+		agentInfo:       agentInfo,
+		config:          upstreamConfig,
+		checkinInterval: checkinInterval,
+		namespace:       systemNamespace,
+	}
 	if err := mgr.Add(clusterStatus); err != nil {
 		setupLog.Error(err, "unable to add cluster status controller")
 		return err
diff --git a/internal/cmd/agent/root.go b/internal/cmd/agent/root.go
index 316af315..ed0b9196 100644
--- a/internal/cmd/agent/root.go
+++ b/internal/cmd/agent/root.go
@@ -25,7 +25,7 @@ import (
 )
 
 type UpstreamOptions struct {
-	Kubeconfig string `usage:"kubeconfig file for agent's cluster"`
+	Kubeconfig string `usage:"kubeconfig file for upstream cluster"`
 	Namespace  string `usage:"system namespace is the namespace, the agent runs in, e.g. cattle-fleet-system" env:"NAMESPACE"`
 }
 
@@ -156,16 +156,7 @@ func (a *FleetAgent) Run(cmd *cobra.Command, args []string) error {
 					workersOpts.Drift = w
 				}
 
-				clusterStatus := &ClusterStatus{
-					UpstreamOptions: UpstreamOptions{
-						Kubeconfig: a.Kubeconfig,
-						Namespace:  a.Namespace,
-					},
-					CheckinInterval: a.CheckinInterval,
-					AgentInfo:       agentInfo,
-				}
-
-				if err := start(ctx, localConfig, a.Namespace, a.AgentScope, workersOpts, clusterStatus, agentInfo); err != nil {
+				if err := start(ctx, localConfig, a.Namespace, a.AgentScope, a.CheckinInterval, workersOpts, agentInfo); err != nil {
 					setupLog.Error(err, "failed to start agent")
 				}
 			},
diff --git a/internal/cmd/cli/apply.go b/internal/cmd/cli/apply.go
index 7157b63f..386367fe 100644
--- a/internal/cmd/cli/apply.go
+++ b/internal/cmd/cli/apply.go
@@ -67,6 +67,7 @@ type Apply struct {
 	OCIPasswordFile             string            `usage:"Path of file containing basic auth password for OCI registry" name:"oci-password-file"`
 	OCIBasicHTTP                bool              `usage:"Use HTTP to access the OCI regustry" name:"oci-basic-http"`
 	OCIInsecure                 bool              `usage:"Allow connections to OCI registry without certs" name:"oci-insecure"`
+	OCIRegistrySecret           string            `usage:"OCI storage registry secret name" name:"oci-registry-secret"`
 	DrivenScan                  bool              `usage:"Use driven scan. Bundles are defined by the user" name:"driven-scan"`
 	DrivenScanSeparator         string            `usage:"Separator to use for bundle folder and options file" name:"driven-scan-sep" default:":"`
 }
@@ -129,6 +130,7 @@ func (a *Apply) run(cmd *cobra.Command, args []string) error {
 		CorrectDriftKeepFailHistory: a.CorrectDriftKeepFailHistory,
 		DrivenScan:                  a.DrivenScan,
 		DrivenScanSeparator:         a.DrivenScanSeparator,
+		OCIRegistrySecret:           a.OCIRegistrySecret,
 	}
 
 	knownHostsPath, err := writeTmpKnownHosts()
@@ -141,9 +143,6 @@ func (a *Apply) run(cmd *cobra.Command, args []string) error {
 	if err := a.addAuthToOpts(&opts, os.ReadFile); err != nil {
 		return fmt.Errorf("adding auth to opts: %w", err)
 	}
-	if err := a.addOCISpecToOpts(&opts, os.ReadFile); err != nil {
-		return fmt.Errorf("adding oci spec to opts: %w", err)
-	}
 
 	if a.File == "-" {
 		opts.BundleReader = os.Stdin
@@ -234,29 +233,6 @@ func (a *Apply) addAuthToOpts(opts *apply.Options, readFile readFile) error {
 	return nil
 }
 
-// addOCISpecToOpts adds the OCI registry specs (with auth if provided)
-func (a *Apply) addOCISpecToOpts(opts *apply.Options, readFile readFile) error {
-	// returning if the OCI registry reference is not defined
-	if a.OCIReference == "" {
-		return nil
-	}
-	opts.OCIRegistry.Reference = a.OCIReference
-
-	if a.OCIUsername != "" && a.OCIPasswordFile != "" {
-		password, err := readFile(a.OCIPasswordFile)
-		if err != nil {
-			return err
-		}
-
-		opts.OCIRegistry.Username = a.OCIUsername
-		opts.OCIRegistry.Password = string(password)
-	}
-	opts.OCIRegistry.BasicHTTP = a.OCIBasicHTTP
-	opts.OCIRegistry.InsecureSkipTLS = a.OCIInsecure
-
-	return nil
-}
-
 func currentCommit() string {
 	cmd := exec.Command("git", "rev-parse", "HEAD")
 	buf := &bytes.Buffer{}
diff --git a/internal/cmd/cli/apply/apply.go b/internal/cmd/cli/apply/apply.go
index 0cf7be2c..a00c24c4 100644
--- a/internal/cmd/cli/apply/apply.go
+++ b/internal/cmd/cli/apply/apply.go
@@ -19,7 +19,7 @@ import (
 	"github.com/rancher/fleet/internal/helmvalues"
 	"github.com/rancher/fleet/internal/manifest"
 	"github.com/rancher/fleet/internal/names"
-	"github.com/rancher/fleet/internal/ociwrapper"
+	"github.com/rancher/fleet/internal/ocistorage"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
 	"github.com/rancher/wrangler/v3/pkg/yaml"
@@ -31,6 +31,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/utils/ptr"
 )
 
@@ -72,6 +73,7 @@ type Options struct {
 	CorrectDriftForce           bool
 	CorrectDriftKeepFailHistory bool
 	OCIRegistry                 OCIRegistrySpec
+	OCIRegistrySecret           string
 	DrivenScan                  bool
 	DrivenScanSeparator         string
 }
@@ -305,6 +307,7 @@ func Dir(ctx context.Context, client client.Client, name, baseDir string, opts *
 	if len(bundle.Spec.Resources) == 0 {
 		return ErrNoResources
 	}
+
 	gitRepoBundlesMap[bundle.Name] = true
 
 	objects := []runtime.Object{bundle}
@@ -333,12 +336,18 @@ func Dir(ctx context.Context, client client.Client, name, baseDir string, opts *
 		}
 		bundle.Spec.ValuesHash = h
 
-		if opts.OCIRegistry.Reference == "" {
-			if bundle, err = save(ctx, client, bundle); err != nil {
+		var ociOpts ocistorage.OCIOpts
+		secretOCIRegistryID := types.NamespacedName{Name: opts.OCIRegistrySecret, Namespace: bundle.Namespace}
+		useOCIRegistry, err := shouldStoreInOCIRegistry(ctx, client, secretOCIRegistryID, &ociOpts)
+		if err != nil {
+			return err
+		}
+		if useOCIRegistry {
+			if bundle, err = saveOCIBundle(ctx, client, bundle, ociOpts); err != nil {
 				return err
 			}
 		} else {
-			if bundle, err = saveOCIBundle(ctx, client, bundle, opts); err != nil {
+			if bundle, err = save(ctx, client, bundle); err != nil {
 				return err
 			}
 		}
@@ -370,21 +379,38 @@ func Dir(ctx context.Context, client client.Client, name, baseDir string, opts *
 	return err
 }
 
-func pushOCIManifest(ctx context.Context, bundle *fleet.Bundle, opts *Options) (string, error) {
+func shouldStoreInOCIRegistry(ctx context.Context, c client.Reader, ociSecretKey types.NamespacedName, ociOpts *ocistorage.OCIOpts) (bool, error) {
+	if !ocistorage.ExperimentalOCIIsEnabled() {
+		return false, nil
+	}
+
+	opts, err := ocistorage.ReadOptsFromSecret(ctx, c, ociSecretKey)
+	if err != nil {
+		if apierrors.IsNotFound(err) && ociSecretKey.Name == "" {
+			// don't return not found errors when no secret name was specified by the user
+			return false, nil
+		}
+		return false, err
+	}
+	ociOpts.Reference = opts.Reference
+	ociOpts.Username = opts.Username
+	ociOpts.Password = opts.Password
+	ociOpts.AgentUsername = opts.AgentUsername
+	ociOpts.AgentPassword = opts.AgentPassword
+	ociOpts.BasicHTTP = opts.BasicHTTP
+	ociOpts.InsecureSkipTLS = opts.InsecureSkipTLS
+
+	return true, nil
+}
+
+func pushOCIManifest(ctx context.Context, bundle *fleet.Bundle, opts ocistorage.OCIOpts) (string, error) {
 	manifest := manifest.FromBundle(bundle)
 	manifestID, err := manifest.ID()
 	if err != nil {
 		return "", err
 	}
-	ociOpts := ociwrapper.OCIOpts{
-		Reference:       opts.OCIRegistry.Reference,
-		Username:        opts.OCIRegistry.Username,
-		Password:        opts.OCIRegistry.Password,
-		BasicHTTP:       opts.OCIRegistry.BasicHTTP,
-		InsecureSkipTLS: opts.OCIRegistry.InsecureSkipTLS,
-	}
-	oci := ociwrapper.NewOCIWrapper()
-	err = oci.PushManifest(ctx, ociOpts, manifestID, manifest)
+	oci := ocistorage.NewOCIWrapper()
+	err = oci.PushManifest(ctx, opts, manifestID, manifest)
 	if err != nil {
 		return "", err
 	}
@@ -394,6 +420,10 @@ func pushOCIManifest(ctx context.Context, bundle *fleet.Bundle, opts *Options) (
 func save(ctx context.Context, c client.Client, bundle *fleet.Bundle) (*fleet.Bundle, error) {
 	updated := bundle.DeepCopy()
 	result, err := controllerutil.CreateOrUpdate(ctx, c, bundle, func() error {
+		if bundle != nil && bundle.Spec.HelmOpOptions != nil {
+			return fmt.Errorf("a helmOps bundle with name %q already exists", bundle.Name)
+		}
+
 		bundle.Spec = updated.Spec
 		bundle.Annotations = updated.Annotations
 		bundle.Labels = updated.Labels
@@ -426,7 +456,7 @@ func saveImageScans(ctx context.Context, c client.Client, bundle *fleet.Bundle,
 	return nil
 }
 
-func saveOCIBundle(ctx context.Context, c client.Client, bundle *fleet.Bundle, opts *Options) (*fleet.Bundle, error) {
+func saveOCIBundle(ctx context.Context, c client.Client, bundle *fleet.Bundle, opts ocistorage.OCIOpts) (*fleet.Bundle, error) {
 	manifestID, err := pushOCIManifest(ctx, bundle, opts)
 	if err != nil {
 		return bundle, err
@@ -435,6 +465,10 @@ func saveOCIBundle(ctx context.Context, c client.Client, bundle *fleet.Bundle, o
 
 	updated := bundle.DeepCopy()
 	_, err = controllerutil.CreateOrUpdate(ctx, c, bundle, func() error {
+		if bundle != nil && bundle.Spec.HelmOpOptions != nil {
+			return fmt.Errorf("a helmOps bundle with name %q already exists", bundle.Name)
+		}
+
 		bundle.Spec = updated.Spec
 		bundle.Annotations = updated.Annotations
 		bundle.Labels = updated.Labels
@@ -467,7 +501,7 @@ func saveOCIBundle(ctx context.Context, c client.Client, bundle *fleet.Bundle, o
 // In this case we need to create a secret to store the
 // OCI registry reference and credentials so the fleet controller is
 // able to access.
-func newOCISecret(manifestID string, bundle *fleet.Bundle, opts *Options) *corev1.Secret {
+func newOCISecret(manifestID string, bundle *fleet.Bundle, opts ocistorage.OCIOpts) *corev1.Secret {
 	return &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      manifestID,
@@ -484,12 +518,15 @@ func newOCISecret(manifestID string, bundle *fleet.Bundle, opts *Options) *corev
 			},
 		},
 		Data: map[string][]byte{
-			ociwrapper.OCISecretReference: []byte(opts.OCIRegistry.Reference),
-			ociwrapper.OCISecretUsername:  []byte(opts.OCIRegistry.Username),
-			ociwrapper.OCISecretPassword:  []byte(opts.OCIRegistry.Password),
-			ociwrapper.OCISecretBasicHTTP: []byte(strconv.FormatBool(opts.OCIRegistry.BasicHTTP)),
-			ociwrapper.OCISecretInsecure:  []byte(strconv.FormatBool(opts.OCIRegistry.InsecureSkipTLS)),
+			ocistorage.OCISecretReference:     []byte(opts.Reference),
+			ocistorage.OCISecretUsername:      []byte(opts.Username),
+			ocistorage.OCISecretPassword:      []byte(opts.Password),
+			ocistorage.OCISecretAgentUsername: []byte(opts.AgentUsername),
+			ocistorage.OCISecretAgentPassword: []byte(opts.AgentPassword),
+			ocistorage.OCISecretBasicHTTP:     []byte(strconv.FormatBool(opts.BasicHTTP)),
+			ocistorage.OCISecretInsecure:      []byte(strconv.FormatBool(opts.InsecureSkipTLS)),
 		},
+		Type: fleet.SecretTypeOCIStorage,
 	}
 }
 
diff --git a/internal/cmd/controller/gitops/reconciler/gitjob.go b/internal/cmd/controller/gitops/reconciler/gitjob.go
index f7a1ef57..e4cc9c2d 100644
--- a/internal/cmd/controller/gitops/reconciler/gitjob.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob.go
@@ -14,7 +14,7 @@ import (
 	fleetcli "github.com/rancher/fleet/internal/cmd/cli"
 	"github.com/rancher/fleet/internal/config"
 	"github.com/rancher/fleet/internal/names"
-	"github.com/rancher/fleet/internal/ociwrapper"
+	"github.com/rancher/fleet/internal/ocistorage"
 	ssh "github.com/rancher/fleet/internal/ssh"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/cert"
@@ -386,20 +386,6 @@ func (r *GitJobReconciler) newJobSpec(ctx context.Context, gitrepo *v1alpha1.Git
 		}
 	}
 
-	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
-		vol, volMnt, err := ociVolumeFromSecret(ctx, r.Client,
-			gitrepo.Namespace,
-			gitrepo.Spec.OCIRegistry.AuthSecretName,
-			ociRegistryAuthVolumeName,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		volumes = append(volumes, vol)
-		volumeMounts = append(volumeMounts, volMnt)
-	}
-
 	shardID := gitrepo.Labels[sharding.ShardingRefLabel]
 
 	nodeSelector := map[string]string{"kubernetes.io/os": "linux"}
@@ -714,31 +700,15 @@ func argsAndEnvs(
 		env = append(env, gitSSHCommandEnvVar(knownHosts.IsStrict()))
 	}
 
-	if ociwrapper.ExperimentalOCIIsEnabled() && gitrepo.Spec.OCIRegistry != nil && gitrepo.Spec.OCIRegistry.Reference != "" {
-		args = append(args, "--oci-reference", gitrepo.Spec.OCIRegistry.Reference)
-		if gitrepo.Spec.OCIRegistry.AuthSecretName != "" {
-			args = append(args, "--oci-password-file", "/etc/fleet/oci/password")
-			env = append(env,
-				corev1.EnvVar{
-					Name: "OCI_USERNAME",
-					ValueFrom: &corev1.EnvVarSource{
-						SecretKeyRef: &corev1.SecretKeySelector{
-							Optional: &[]bool{true}[0],
-							Key:      "username",
-							LocalObjectReference: corev1.LocalObjectReference{
-								Name: gitrepo.Spec.OCIRegistry.AuthSecretName,
-							},
-						},
-					},
-				})
-		}
-		if gitrepo.Spec.OCIRegistry.BasicHTTP {
-			args = append(args, "--oci-basic-http")
-		}
-		if gitrepo.Spec.OCIRegistry.InsecureSkipTLS {
-			args = append(args, "--oci-insecure")
-		}
+	if ocistorage.ExperimentalOCIIsEnabled() {
+		env = append(env,
+			corev1.EnvVar{
+				Name:  "EXPERIMENTAL_OCI_STORAGE",
+				Value: "true",
+			})
+		args = append(args, "--oci-registry-secret", gitrepo.Spec.OCIRegistrySecret)
 	}
+
 	if len(gitrepo.Spec.Bundles) > 0 {
 		args = append(args, "--driven-scan")
 		if drivenScanSeparator != "" {
@@ -800,34 +770,6 @@ func volumes(targetsConfigName string) ([]corev1.Volume, []corev1.VolumeMount) {
 	return volumes, volumeMounts
 }
 
-// ociVolumeFromSecret generates a volume and volume mount from a basic-auth secret.
-func ociVolumeFromSecret(
-	ctx context.Context,
-	c client.Client,
-	namespace, secretName, volumeName string,
-) (corev1.Volume, corev1.VolumeMount, error) {
-	var secret corev1.Secret
-	if err := c.Get(ctx, types.NamespacedName{
-		Namespace: namespace,
-		Name:      secretName,
-	}, &secret); err != nil {
-		return corev1.Volume{}, corev1.VolumeMount{}, err
-	}
-	volume := corev1.Volume{
-		Name: volumeName,
-		VolumeSource: corev1.VolumeSource{
-			Secret: &corev1.SecretVolumeSource{
-				SecretName: secretName,
-			},
-		},
-	}
-	volumeMount := corev1.VolumeMount{
-		Name:      volumeName,
-		MountPath: "/etc/fleet/oci",
-	}
-	return volume, volumeMount, nil
-}
-
 // volumesFromSecret generates volumes and volume mounts from a Helm secret, assuming that that secret exists.
 // If the secret has a cacerts key, it will be mounted into /etc/ssl/certs, too.
 func volumesFromSecret(
diff --git a/internal/cmd/controller/helmops/reconciler/helmop_controller.go b/internal/cmd/controller/helmops/reconciler/helmop_controller.go
index bcb2d261..074fff18 100644
--- a/internal/cmd/controller/helmops/reconciler/helmop_controller.go
+++ b/internal/cmd/controller/helmops/reconciler/helmop_controller.go
@@ -159,6 +159,11 @@ func (r *HelmOpReconciler) createUpdateBundle(ctx context.Context, helmop *fleet
 		return nil, err
 	}
 
+	if err == nil && b.Spec.HelmOpOptions == nil {
+		// A gitOps bundle with the same name exists; abort.
+		return nil, fmt.Errorf("a non-helmops bundle already exists with name %s; aborting", helmop.Name)
+	}
+
 	// calculate the new representation of the helmop resource
 	bundle := r.calculateBundle(helmop)
 
diff --git a/internal/cmd/controller/reconciler/bundle_controller.go b/internal/cmd/controller/reconciler/bundle_controller.go
index 94d19586..ca8c3d7f 100644
--- a/internal/cmd/controller/reconciler/bundle_controller.go
+++ b/internal/cmd/controller/reconciler/bundle_controller.go
@@ -17,7 +17,7 @@ import (
 	"github.com/rancher/fleet/internal/helmvalues"
 	"github.com/rancher/fleet/internal/manifest"
 	"github.com/rancher/fleet/internal/metrics"
-	"github.com/rancher/fleet/internal/ociwrapper"
+	"github.com/rancher/fleet/internal/ocistorage"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/sharding"
 	"github.com/rancher/wrangler/v3/pkg/genericcondition"
@@ -181,7 +181,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 	if bundle.Spec.HelmOpOptions != nil && !experimentalHelmOpsEnabled() {
 		return ctrl.Result{}, fmt.Errorf("bundle contains data used by helm ops but env variable EXPERIMENTAL_HELM_OPS is not set to true")
 	}
-	contentsInOCI := bundle.Spec.ContentsID != "" && ociwrapper.ExperimentalOCIIsEnabled()
+	contentsInOCI := bundle.Spec.ContentsID != "" && ocistorage.ExperimentalOCIIsEnabled()
 	contentsInHelmChart := bundle.Spec.HelmOpOptions != nil
 	manifestID := bundle.Spec.ContentsID
 	var resourcesManifest *manifest.Manifest
@@ -505,7 +505,7 @@ func (r *BundleReconciler) getOCIReference(ctx context.Context, bundle *fleet.Bu
 	if err := r.Get(ctx, namespacedName, &ociSecret); err != nil {
 		return "", err
 	}
-	ref, ok := ociSecret.Data[ociwrapper.OCISecretReference]
+	ref, ok := ociSecret.Data[ocistorage.OCISecretReference]
 	if !ok {
 		return "", fmt.Errorf("expected data [reference] not found in secret: %s", bundle.Spec.ContentsID)
 	}
@@ -557,7 +557,7 @@ func (r *BundleReconciler) cloneSecret(
 }
 
 func (r *BundleReconciler) handleContentAccessSecrets(ctx context.Context, bundle *fleet.Bundle, bd *fleet.BundleDeployment) error {
-	contentsInOCI := bundle.Spec.ContentsID != "" && ociwrapper.ExperimentalOCIIsEnabled()
+	contentsInOCI := bundle.Spec.ContentsID != "" && ocistorage.ExperimentalOCIIsEnabled()
 	contentsInHelmChart := bundle.Spec.HelmOpOptions != nil && experimentalHelmOpsEnabled()
 
 	if contentsInOCI {
diff --git a/internal/ociwrapper/ociwrapper.go b/internal/ocistorage/ociwrapper.go
similarity index 94%
rename from internal/ociwrapper/ociwrapper.go
rename to internal/ocistorage/ociwrapper.go
index ec73f98a..58f58993 100644
--- a/internal/ociwrapper/ociwrapper.go
+++ b/internal/ocistorage/ociwrapper.go
@@ -1,4 +1,4 @@
-package ociwrapper
+package ocistorage
 
 import (
 	"bytes"
@@ -28,17 +28,15 @@ const (
 	fileType     = "application/fleet.file"
 	artifactType = "application/fleet.manifest"
 
-	OCISecretUsername  = "username"
-	OCISecretPassword  = "password"
-	OCISecretReference = "reference"
-	OCISecretBasicHTTP = "http"
-	OCISecretInsecure  = "insecure"
+	OCIStorageExperimentalFlag = "EXPERIMENTAL_OCI_STORAGE"
 )
 
 type OCIOpts struct {
 	Reference       string
 	Username        string
 	Password        string
+	AgentUsername   string
+	AgentPassword   string
 	BasicHTTP       bool
 	InsecureSkipTLS bool
 }
@@ -182,6 +180,14 @@ func (o *OCIWrapper) pushFile(ctx context.Context, opts OCIOpts, reader io.Reade
 func (o *OCIWrapper) pullFile(ctx context.Context, opts OCIOpts, id string) ([]byte, error) {
 	s := o.oci.NewStore()
 
+	// use the agent credentials (read only) if present
+	if opts.AgentUsername != "" {
+		opts.Username = opts.AgentUsername
+	}
+	if opts.AgentPassword != "" {
+		opts.Password = opts.AgentPassword
+	}
+
 	// copy from remote OCI registry to local memory store
 	repo, err := newOCIRepository(id, opts)
 	if err != nil {
@@ -261,6 +267,6 @@ func (o *OCIWrapper) PullManifest(ctx context.Context, opts OCIOpts, id string)
 // ExperimentalOCIIsEnabled returns true if the EXPERIMENTAL_OCI_STORAGE env variable is set to true
 // returns false otherwise
 func ExperimentalOCIIsEnabled() bool {
-	value, err := strconv.ParseBool(os.Getenv("EXPERIMENTAL_OCI_STORAGE"))
+	value, err := strconv.ParseBool(os.Getenv(OCIStorageExperimentalFlag))
 	return err == nil && value
 }
diff --git a/internal/ocistorage/secret.go b/internal/ocistorage/secret.go
new file mode 100644
index 00000000..f42db7c3
--- /dev/null
+++ b/internal/ocistorage/secret.go
@@ -0,0 +1,112 @@
+package ocistorage
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+)
+
+const (
+	OCISecretUsername      = "username"
+	OCISecretPassword      = "password"
+	OCISecretAgentUsername = "agentUsername"
+	OCISecretAgentPassword = "agentPassword"
+	OCISecretReference     = "reference"
+	OCISecretBasicHTTP     = "basicHTTP"
+	OCISecretInsecure      = "insecure"
+
+	OCIStorageDefaultSecretName = "ocistorage"
+)
+
+// ReadOptsFromSecret reads the secret identified by the given NamespacedName and
+// returns an OCIOpts structure filled with the information obtained from that secret.
+func ReadOptsFromSecret(ctx context.Context, c client.Reader, ns types.NamespacedName) (OCIOpts, error) {
+	// if no secret was specified, fallback to the default one
+	if ns.Name == "" {
+		ns.Name = OCIStorageDefaultSecretName
+	}
+
+	opts := OCIOpts{}
+	var secret corev1.Secret
+	err := c.Get(ctx, ns, &secret)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	if secret.Type != fleet.SecretTypeOCIStorage {
+		return OCIOpts{}, fmt.Errorf("unexpected secret type: got %q, want %q", secret.Type, fleet.SecretTypeOCIStorage)
+	}
+
+	// Fill the values from the secret.
+	// Only Reference is strictly required.
+	opts.Reference, err = getStringValueFromSecret(secret.Data, OCISecretReference, true)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.Username, err = getStringValueFromSecret(secret.Data, OCISecretUsername, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.Password, err = getStringValueFromSecret(secret.Data, OCISecretPassword, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.AgentUsername, err = getStringValueFromSecret(secret.Data, OCISecretAgentUsername, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.AgentPassword, err = getStringValueFromSecret(secret.Data, OCISecretAgentPassword, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.BasicHTTP, err = getBoolValueFromSecret(secret.Data, OCISecretBasicHTTP, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	opts.InsecureSkipTLS, err = getBoolValueFromSecret(secret.Data, OCISecretInsecure, false)
+	if err != nil {
+		return OCIOpts{}, err
+	}
+
+	return opts, nil
+}
+
+func getStringValueFromSecret(data map[string][]byte, key string, required bool) (string, error) {
+	value, ok := data[key]
+	if !ok {
+		if !required {
+			return "", nil
+		}
+		return "", fmt.Errorf("key %q not found in secret", key)
+	}
+	return string(value), nil
+}
+
+func getBoolValueFromSecret(data map[string][]byte, key string, required bool) (bool, error) {
+	value, ok := data[key]
+	if !ok {
+		if !required {
+			return false, nil
+		}
+		return false, fmt.Errorf("key %q not found in secret", key)
+	}
+	valueStr := string(value)
+	boolValue, err := strconv.ParseBool(valueStr)
+	if err != nil {
+		return false, fmt.Errorf("failed to parse %q as bool: %w", valueStr, err)
+	}
+
+	return boolValue, nil
+}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
index 23d86f05..4b707847 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
@@ -136,8 +136,8 @@ type GitRepoSpec struct {
 	// Disables git polling. When enabled only webhooks will be used.
 	DisablePolling bool `json:"disablePolling,omitempty"`
 
-	// OCIRegistry specifies the OCI registry related parameters
-	OCIRegistry *OCIRegistrySpec `json:"ociRegistry,omitempty"`
+	// OCIRegistrySecret contains the name of the secret to be used for retrieving the OCI registry connection details.
+	OCIRegistrySecret string `json:"ociRegistrySecret,omitempty"`
 
 	// WebhookSecret contains the name of the secret to use for webhook parsing
 	WebhookSecret string `json:"webhookSecret,omitempty"`
@@ -231,22 +231,3 @@ type CorrectDrift struct {
 	// KeepFailHistory keeps track of failed rollbacks in the helm history.
 	KeepFailHistory bool `json:"keepFailHistory,omitempty"`
 }
-
-type OCIRegistrySpec struct {
-	// Reference of the OCI Registry
-	Reference string `json:"reference,omitempty"`
-
-	// AuthSecretName contains the auth secret where the OCI registry credentials are stored.
-	// +nullable
-	AuthSecretName string `json:"authSecretName,omitempty"`
-
-	// BasicHTTP uses HTTP connections to the OCI registry when enabled.
-	// +optional
-	// +nullable
-	BasicHTTP bool `json:"basicHTTP,omitempty"`
-
-	// InsecureSkipTLS allows connections to OCI registry without certs when enabled.
-	// +optional
-	// +nullable
-	InsecureSkipTLS bool `json:"insecureSkipTLS,omitempty"`
-}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
index 28f38986..75e5b2bd 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -1571,11 +1571,6 @@ func (in *GitRepoSpec) DeepCopyInto(out *GitRepoSpec) {
 		*out = new(CorrectDrift)
 		**out = **in
 	}
-	if in.OCIRegistry != nil {
-		in, out := &in.OCIRegistry, &out.OCIRegistry
-		*out = new(OCIRegistrySpec)
-		**out = **in
-	}
 	if in.Bundles != nil {
 		in, out := &in.Bundles, &out.Bundles
 		*out = make([]BundlePath, len(*in))
@@ -2027,21 +2022,6 @@ func (in *NonReadyStatus) DeepCopy() *NonReadyStatus {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *OCIRegistrySpec) DeepCopyInto(out *OCIRegistrySpec) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRegistrySpec.
-func (in *OCIRegistrySpec) DeepCopy() *OCIRegistrySpec {
-	if in == nil {
-		return nil
-	}
-	out := new(OCIRegistrySpec)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Operation) DeepCopyInto(out *Operation) {
 	*out = *in
diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod
index f54b8409..251b2fd0 100644
--- a/pkg/apis/go.mod
+++ b/pkg/apis/go.mod
@@ -5,9 +5,9 @@ go 1.24.0
 toolchain go1.24.3
 
 require (
-	github.com/rancher/wrangler/v3 v3.2.0
-	k8s.io/api v0.32.4
-	k8s.io/apimachinery v0.32.4
+	github.com/rancher/wrangler/v3 v3.2.1
+	k8s.io/api v0.32.5
+	k8s.io/apimachinery v0.32.5
 )
 
 require (
diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum
index 3447c87a..41cf935b 100644
--- a/pkg/apis/go.sum
+++ b/pkg/apis/go.sum
@@ -31,8 +31,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rancher/wrangler/v3 v3.2.0 h1:fZmhSOczW+pxAhyOaGG+9xbEwETPGA5gbS0x0Im2zWs=
-github.com/rancher/wrangler/v3 v3.2.0/go.mod h1:0C5QyvSrQOff8gQQzpB/L/FF03EQycjR3unSJcKCHno=
+github.com/rancher/wrangler/v3 v3.2.1 h1:V51PnoGb8bZ5jJdxFlqKQApzWdSp4sEy5OPGuEGqVbI=
+github.com/rancher/wrangler/v3 v3.2.1/go.mod h1:RV8kkv5br5HaxXWamIbr95pOjvVeoC5CeBldcdw5Fv0=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -81,10 +81,10 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
-k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
-k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
-k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
+k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
+k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
+k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
diff --git a/updatecli/updatecli.d/installation.yaml b/updatecli/updatecli.d/installation.yaml
index a6ad532b..5fdcd0e7 100644
--- a/updatecli/updatecli.d/installation.yaml
+++ b/updatecli/updatecli.d/installation.yaml
@@ -4,13 +4,13 @@ scms:
   fleet:
     kind: github
     spec:
-      user: '{{ .scm.default.user }}'
-      email: '{{ .scm.default.email }}'
-      owner: '{{ .scm.default.owner }}'
-      repository: '{{ .scm.default.repository }}'
+      user: '{{ .scms.main.user }}'
+      email: '{{ .scms.main.email }}'
+      owner: '{{ .scms.main.owner }}'
+      repository: '{{ .scms.main.repository }}'
       token: '{{ requiredEnv "UPDATECLI_GITHUB_TOKEN" }}'
       username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}'
-      branch: '{{ .scm.default.branch }}'
+      branch: '{{ .scms.main.branch }}'
 
 sources:
   fleet:
diff --git a/updatecli/updatecli.d/known-hosts.yaml b/updatecli/updatecli.d/known-hosts.yaml
index b9fd1be2..36f9e30a 100644
--- a/updatecli/updatecli.d/known-hosts.yaml
+++ b/updatecli/updatecli.d/known-hosts.yaml
@@ -1,20 +1,20 @@
 name: Update known_hosts config map
 
+{{ range $id, $scm := .scms }}
+
+---
+name: 'Synchronise known-hosts for {{ $id }}'
 scms:
   fleet:
     kind: github
     spec:
-      user: '{{ .scm.default.user }}'
-      email: '{{ .scm.default.email }}'
-      owner: '{{ .scm.default.owner }}'
-      repository: '{{ .scm.default.repository }}'
+      user: '{{ $scm.user }}'
+      email: '{{ $scm.email }}'
+      owner: '{{ $scm.owner }}'
+      repository: '{{ $scm.repository }}'
       token: '{{ requiredEnv "UPDATECLI_GITHUB_TOKEN" }}'
       username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}'
-      branch: '{{ .scm.default.branch }}'
-
-      # XXX: could we get this to work with minimal duplication and maintenance effort for _all_ maintained branches
-      # containing the file?
-      # e.g. currently `main`, `release/v0.12`, `release/v0.11`, `release/v0.10`
+      branch: '{{ $scm.branch }}'
 
 targets:
   configMapWithUpdatedEntries:
@@ -28,11 +28,11 @@ targets:
         spec:
           files:
             - charts/fleet/templates/configmap_known_hosts.yaml
-      command: bash .github/scripts/update_known_hosts_configmap.sh
+      command: bash <(git show main:.github/scripts/update_known_hosts_configmap.sh)
 
 actions:
   default:
-    name: '[updatecli] Update known-hosts config map with new entries'
+    name: "[{{ $id }}][updatecli] Update known-hosts config map with new entries"
     kind: github/pullrequest
     scmid: fleet
     spec:
@@ -40,3 +40,5 @@ actions:
       mergemethod: squash
       labels:
         - kind/known-hosts # /!\ label must exist in the repo!
+
+{{ end }}
diff --git a/updatecli/values.d/scm.yaml b/updatecli/values.d/scm.yaml
index 57e97747..57d56e6d 100644
--- a/updatecli/values.d/scm.yaml
+++ b/updatecli/values.d/scm.yaml
@@ -1,8 +1,17 @@
-scm:
-  enabled: true
-  default:
+scms:
+  main: &main
+    enabled: true
     user: 'fleet-bot'
     email: 'fleet@suse.de'
     owner: rancher
     repository: fleet
     branch: main
+  v12:
+    <<: *main
+    branch: 'release/v0.12'
+  v11:
+    <<: *main
+    branch: 'release/v0.11'
+  v10:
+    <<: *main
+    branch: 'release/v0.10'


Test patch:
diff --git a/.github/workflows/e2e-rancher-upgrade-fleet-to-head-ci.yml b/.github/workflows/e2e-rancher-upgrade-fleet-to-head-ci.yml
index 1db7a5fc..85e3acc8 100644
--- a/.github/workflows/e2e-rancher-upgrade-fleet-to-head-ci.yml
+++ b/.github/workflows/e2e-rancher-upgrade-fleet-to-head-ci.yml
@@ -114,7 +114,7 @@ jobs:
             ttl.sh/rancher/fleet-${{ steps.uuid.outputs.uuid }}
           tags: type=raw,value=1h
       -
-        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
+        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6
         with:
           context: .
           file: package/Dockerfile
@@ -131,7 +131,7 @@ jobs:
             ttl.sh/rancher/fleet-agent-${{ steps.uuid.outputs.uuid }}
           tags: type=raw,value=1h
       -
-        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
+        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6
         with:
           context: .
           file: package/Dockerfile.agent
diff --git a/.github/workflows/release-against-test-charts.yml b/.github/workflows/release-against-test-charts.yml
index b3818ad3..a40fd9bc 100644
--- a/.github/workflows/release-against-test-charts.yml
+++ b/.github/workflows/release-against-test-charts.yml
@@ -72,7 +72,7 @@ jobs:
           tags: type=raw,value=1h
 
       - name: Build and push Fleet controller image
-        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
+        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6
         with:
           context: ./fleet
           file: ./fleet/package/Dockerfile
@@ -91,7 +91,7 @@ jobs:
           tags: type=raw,value=1h
 
       - name: Build and push Fleet agent image
-        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
+        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6
         with:
           context: ./fleet
           file: ./fleet/package/Dockerfile.agent
diff --git a/e2e/assets/single-cluster/test-oci.yaml b/e2e/assets/single-cluster/test-oci.yaml
index ce25b4dd..b42bc037 100644
--- a/e2e/assets/single-cluster/test-oci.yaml
+++ b/e2e/assets/single-cluster/test-oci.yaml
@@ -8,7 +8,4 @@ spec:
   branch: master
   paths:
   - simple-chart-oci
-  ociRegistry:
-    reference: {{.OCIReference}}
-    authSecretName: oci-secret
-    insecureSkipTLS: {{.OCIInsecureSkipTLS}}
+  ociRegistrySecret: {{.OCIRegistrySecret}}
diff --git a/e2e/single-cluster/oci_registry_test.go b/e2e/single-cluster/oci_registry_test.go
index 9d1862ae..5aefb4bb 100644
--- a/e2e/single-cluster/oci_registry_test.go
+++ b/e2e/single-cluster/oci_registry_test.go
@@ -1,36 +1,80 @@
 package singlecluster_test
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"os"
 	"path"
-	"regexp"
 	"strconv"
 	"strings"
 	"time"
 
 	"github.com/rancher/fleet/e2e/testenv"
 	"github.com/rancher/fleet/e2e/testenv/infra/cmd"
+	"github.com/rancher/fleet/e2e/testenv/k8sclient"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
+	"github.com/rancher/fleet/internal/manifest"
+	"github.com/rancher/fleet/internal/ocistorage"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 )
 
-const experimentalEnvVar = "EXPERIMENTAL_OCI_STORAGE"
-
-func getFailedPodNames(k kubectl.Command, namespace string) ([]string, error) {
-	return getPodNamesByFieldSelector(k, namespace, "status.phase=Failed")
+func createOCIRegistrySecret(
+	secretName,
+	namespace,
+	reference,
+	username,
+	password,
+	agentUsername,
+	agentPassword string,
+	insecure,
+	basicHTTP bool) {
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: namespace,
+		},
+		Data: map[string][]byte{
+			ocistorage.OCISecretReference:     []byte(reference),
+			ocistorage.OCISecretUsername:      []byte(username),
+			ocistorage.OCISecretPassword:      []byte(password),
+			ocistorage.OCISecretAgentUsername: []byte(agentUsername),
+			ocistorage.OCISecretAgentPassword: []byte(agentPassword),
+			ocistorage.OCISecretInsecure:      []byte(strconv.FormatBool(insecure)),
+			ocistorage.OCISecretBasicHTTP:     []byte(strconv.FormatBool(basicHTTP)),
+		},
+		Type: corev1.SecretType(fleet.SecretTypeOCIStorage),
+	}
+	k8sclient.CreateObjectShouldSucceed(clientUpstream, secret)
 }
 
-func getPodNamesByFieldSelector(k kubectl.Command, namespace, selector string) ([]string, error) {
-	strSelector := fmt.Sprintf("--field-selector=%s", selector)
-	out, err := k.Namespace(namespace).Get("pods", strSelector, "-o", "custom-columns=NAME:metadata.name", "--no-headers")
-	if err != nil {
-		return []string{}, err
-	}
-	return strings.Split(out, "\n"), nil
+func createDefaultOCIRegistrySecret(
+	namespace,
+	reference,
+	username,
+	password,
+	agentUsername,
+	agentPassword string,
+	insecure,
+	basicHTTP bool) {
+	createOCIRegistrySecret(
+		ocistorage.OCIStorageDefaultSecretName,
+		namespace,
+		reference,
+		username,
+		password,
+		agentUsername,
+		agentPassword,
+		insecure,
+		basicHTTP)
 }
 
 // getActualEnvVariable returns the value of the given env variable
@@ -69,7 +113,7 @@ func checkEnvVariable(k kubectl.Command, component string, env string) (bool, er
 // fleet-controller and gitjob deployments to set the given one.
 // It waits until the pods related to both deployments are restarted.
 func updateExperimentalFlagValue(k kubectl.Command, value bool) {
-	actualEnvVal := getActualEnvVariable(k, experimentalEnvVar)
+	actualEnvVal := getActualEnvVariable(k, ocistorage.OCIStorageExperimentalFlag)
 	GinkgoWriter.Printf("Env variable value to be used in this test: %t\n", value)
 	if actualEnvVal == value {
 		return
@@ -93,7 +137,7 @@ func updateExperimentalFlagValue(k kubectl.Command, value bool) {
 	}).WithTimeout(time.Second * 60).Should(ContainSubstring("pod/gitjob-"))
 
 	// set the experimental env value to the deployments
-	strEnvValue := fmt.Sprintf("%s=%t", experimentalEnvVar, value)
+	strEnvValue := fmt.Sprintf("%s=%t", ocistorage.OCIStorageExperimentalFlag, value)
 	_, _ = k.Namespace(ns).Run("set", "env", "deployment/fleet-controller", strEnvValue)
 	_, _ = k.Namespace(ns).Run("set", "env", "deployment/gitjob", strEnvValue)
 
@@ -102,7 +146,7 @@ func updateExperimentalFlagValue(k kubectl.Command, value bool) {
 		out, err := k.Namespace(ns).Get(
 			"pod",
 			"-l", "app=fleet-controller,fleet.cattle.io/shard-default=true",
-			"-o", fmt.Sprintf(`jsonpath={range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].env[?(@.name=="%s")].value}{end}`, experimentalEnvVar),
+			"-o", fmt.Sprintf(`jsonpath={range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].env[?(@.name=="%s")].value}{end}`, ocistorage.OCIStorageExperimentalFlag),
 		)
 		g.Expect(err).ToNot(HaveOccurred())
 
@@ -119,7 +163,7 @@ func updateExperimentalFlagValue(k kubectl.Command, value bool) {
 		out, err := k.Namespace(ns).Get(
 			"pod",
 			"-l", "app=gitjob,fleet.cattle.io/shard-default=true",
-			"-o", fmt.Sprintf(`jsonpath={range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].env[?(@.name=="%s")].value}{end}`, experimentalEnvVar),
+			"-o", fmt.Sprintf(`jsonpath={range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].env[?(@.name=="%s")].value}{end}`, ocistorage.OCIStorageExperimentalFlag),
 		)
 		g.Expect(err).ToNot(HaveOccurred())
 
@@ -133,32 +177,74 @@ func updateExperimentalFlagValue(k kubectl.Command, value bool) {
 	}).WithTimeout(time.Second * 30).Should(Succeed())
 }
 
+func pushFakeOCIContents(secret, manifestID string) {
+	fakeManifest := &manifest.Manifest{
+		Commit: "some-bad-commit",
+		Resources: []fleet.BundleResource{
+			{
+				Name:    "test-resource",
+				Content: "some-test-content",
+			},
+		},
+	}
+	secretKey := types.NamespacedName{Name: secret, Namespace: env.Namespace}
+	opts, err := ocistorage.ReadOptsFromSecret(context.TODO(), clientUpstream, secretKey)
+	Expect(err).ToNot(HaveOccurred())
+	err = ocistorage.NewOCIWrapper().PushManifest(context.TODO(), opts, manifestID, fakeManifest)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func verifySecretsAreEqual(originalName, originalNamespace, name, namespace string) {
+	var originalSecret, secret corev1.Secret
+	k8sclient.GetObjectShouldSucceed(clientUpstream, originalName, originalNamespace, &originalSecret)
+	k8sclient.GetObjectShouldSucceed(clientUpstream, name, namespace, &secret)
+
+	Expect(originalSecret.Data).To(Equal(secret.Data))
+	Expect(originalSecret.Type).To(Equal(secret.Type))
+}
+
+func getOCIRegistryExternalIP(k kubectl.Command) string {
+	if v := os.Getenv("external_ip"); v != "" {
+		return v
+	}
+
+	externalIP, err := k.Namespace(cmd.InfraNamespace).Get("service", "zot-service", "-o", "jsonpath={.status.loadBalancer.ingress[0].ip}")
+	Expect(err).ToNot(HaveOccurred(), externalIP)
+	return externalIP
+}
+
 var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-registry", "infra-setup"), func() {
 	var (
 		asset                  = "single-cluster/test-oci.yaml"
-		insecureSkipTLS        bool
-		forceGitRepoURL        string
 		k                      kubectl.Command
 		tempDir                string
 		contentsID             string
 		downstreamNamespace    string
+		defaultOCIRegistry     string
 		ociRegistry            string
 		experimentalFlagBefore bool
-		experimentalValue      bool
 		contentIDToPurge       string
+
+		// user-defined variables for defining the test behaviour
+		experimentalValue         bool
+		insecureSkipTLS           bool
+		deployDefaultSecret       bool
+		deploySpecificSecretName  string
+		forceDefaultReference     string
+		forceUserDefinedReference string
 	)
 
 	BeforeEach(func() {
 		k = env.Kubectl.Namespace(env.Namespace)
 		tempDir = GinkgoT().TempDir()
-		externalIP, err := k.Namespace(cmd.InfraNamespace).Get("service", "zot-service", "-o", "jsonpath={.status.loadBalancer.ingress[0].ip}")
-		Expect(err).ToNot(HaveOccurred(), externalIP)
+		externalIP := getOCIRegistryExternalIP(k)
 		Expect(net.ParseIP(externalIP)).ShouldNot(BeNil())
-		ociRegistry = fmt.Sprintf("%s:8082", externalIP)
+		defaultOCIRegistry = fmt.Sprintf("%s:8082", externalIP)
+		ociRegistry = defaultOCIRegistry
 
 		// store the actual value of the experimental env value
 		// we'll restore in the AfterEach statement if needed
-		experimentalFlagBefore = getActualEnvVariable(k, experimentalEnvVar)
+		experimentalFlagBefore = getActualEnvVariable(k, ocistorage.OCIStorageExperimentalFlag)
 
 		// reset the value of the contents resource to purge
 		contentIDToPurge = ""
@@ -168,40 +254,70 @@ var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-reg
 		updateExperimentalFlagValue(k, experimentalValue)
 		Expect(os.Getenv("CI_OCI_USERNAME")).NotTo(BeEmpty())
 		Expect(os.Getenv("CI_OCI_PASSWORD")).NotTo(BeEmpty())
-		out, err := k.Create(
-			"secret", "generic", "oci-secret",
-			"--from-literal=username="+os.Getenv("CI_OCI_USERNAME"),
-			"--from-literal=password="+os.Getenv("CI_OCI_PASSWORD"),
-		)
-		Expect(err).ToNot(HaveOccurred(), out)
 
 		gitrepo := path.Join(tempDir, "gitrepo.yaml")
-		if forceGitRepoURL != "" {
-			ociRegistry = forceGitRepoURL
+
+		if forceDefaultReference != "" {
+			defaultOCIRegistry = forceDefaultReference
+		}
+		if forceUserDefinedReference != "" {
+			ociRegistry = forceUserDefinedReference
+		}
+
+		if deployDefaultSecret {
+			createDefaultOCIRegistrySecret(
+				env.Namespace,
+				defaultOCIRegistry,
+				os.Getenv("CI_OCI_USERNAME"),
+				os.Getenv("CI_OCI_PASSWORD"),
+				"",
+				"",
+				insecureSkipTLS,
+				false)
+		}
+
+		if deploySpecificSecretName != "" {
+			createOCIRegistrySecret(
+				deploySpecificSecretName,
+				env.Namespace,
+				ociRegistry,
+				os.Getenv("CI_OCI_USERNAME"),
+				os.Getenv("CI_OCI_PASSWORD"),
+				"",
+				"",
+				insecureSkipTLS,
+				false)
 		}
 
-		err = testenv.Template(gitrepo, testenv.AssetPath(asset), struct {
-			OCIReference       string
-			OCIInsecureSkipTLS bool
+		err := testenv.Template(gitrepo, testenv.AssetPath(asset), struct {
+			OCIRegistrySecret string
 		}{
-			ociRegistry,
-			insecureSkipTLS,
+			deploySpecificSecretName,
 		})
 		Expect(err).ToNot(HaveOccurred())
 
-		out, err = k.Apply("-f", gitrepo)
+		out, err := k.Apply("-f", gitrepo)
 		Expect(err).ToNot(HaveOccurred(), out)
 
-		downstreamNamespace, err = k.Namespace("fleet-local").Get("cluster", "local", `-o=jsonpath={.status.namespace}`)
-		Expect(err).ToNot(HaveOccurred(), out)
+		var cluster fleet.Cluster
+		k8sclient.GetObjectShouldSucceed(clientUpstream, "local", env.Namespace, &cluster)
+		downstreamNamespace = cluster.Status.Namespace
 	})
 
 	AfterEach(func() {
-		out, err := k.Delete("secret", "oci-secret")
-		Expect(err).ToNot(HaveOccurred(), out)
+		if deployDefaultSecret {
+			var secret corev1.Secret
+			k8sclient.GetObjectShouldSucceed(clientUpstream, ocistorage.OCIStorageDefaultSecretName, env.Namespace, &secret)
+			k8sclient.DeleteObjectShouldSucceed(clientUpstream, &secret)
+		}
+		if deploySpecificSecretName != "" {
+			var secret corev1.Secret
+			k8sclient.GetObjectShouldSucceed(clientUpstream, deploySpecificSecretName, env.Namespace, &secret)
+			k8sclient.DeleteObjectShouldSucceed(clientUpstream, &secret)
+		}
 
 		gitrepo := path.Join(tempDir, "gitrepo.yaml")
-		out, err = k.Delete("-f", gitrepo)
+		out, err := k.Delete("-f", gitrepo)
 		Expect(err).ToNot(HaveOccurred(), out)
 
 		// secrets for bundle and bundledeployment should be gone
@@ -224,123 +340,155 @@ var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-reg
 
 		// check that contents have been purged when deleting the gitrepo
 		if contentIDToPurge != "" {
-			out, err := k.Delete("contents", contentIDToPurge)
-			Expect(out).To(ContainSubstring("not found"))
-			Expect(err).To(HaveOccurred())
+			k8sclient.ObjectShouldNotExist(clientUpstream, contentIDToPurge, "", &fleet.Content{}, true)
 		}
 	})
 
-	When("creating a gitrepo resource with ociRegistry info", func() {
+	When("applying a gitrepo with the default ociSecret also deployed", func() {
 		Context("containing a valid helm chart", func() {
 			BeforeEach(func() {
-				insecureSkipTLS = true
-				forceGitRepoURL = ""
 				experimentalValue = true
+				insecureSkipTLS = true
+				deployDefaultSecret = true
+				deploySpecificSecretName = ""
+				forceDefaultReference = ""
+				forceUserDefinedReference = ""
 			})
 
 			It("deploys the bundle", func() {
 				By("creating the bundle", func() {
-					Eventually(func() string {
-						out, _ := k.Namespace("fleet-local").Get("bundles")
-						return out
-					}).Should(ContainSubstring("sample-simple-chart-oci"))
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
 				})
 				By("setting the ContentsID field in the bundle", func() {
-					Eventually(func() string {
-						contentsID, _ = k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.spec.contentsId}`)
-						return contentsID
-					}).Should(ContainSubstring("s-"))
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+					Expect(bundle.Spec.ContentsID).To(ContainSubstring("s-"))
+					contentsID = bundle.Spec.ContentsID
 				})
 
 				By("setting the OCI reference status field key to the OCI path of the manifest", func() {
-					Eventually(func() bool {
-						out, _ := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.status.ociReference}`)
-						return out == fmt.Sprintf("oci://%s/%s:latest", ociRegistry, contentsID)
-					}).Should(BeTrue())
-				})
-				By("creating a bundle secret", func() {
-					Eventually(func() string {
-						out, err := k.Namespace("fleet-local").Get("secret", contentsID)
-						if err != nil {
-							// return nothing in case of error
-							// This avoids false positives when kubectl returns "secrets "XXXXX" not found
-							return ""
-						}
-						return out
-					}).Should(ContainSubstring(contentsID))
+					Eventually(func(g Gomega) {
+						var bundle fleet.Bundle
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+						g.Expect(bundle.Status.OCIReference).To(Equal(fmt.Sprintf("oci://%s/%s:latest", ociRegistry, contentsID)))
+					}).Should(Succeed())
+				})
+				By("creating a bundle secret with the expected contents", func() {
+					verifySecretsAreEqual(ocistorage.OCIStorageDefaultSecretName, env.Namespace, contentsID, env.Namespace)
 				})
 				By("creating a bundledeployment secret", func() {
-					Eventually(func() string {
-						out, err := k.Namespace(downstreamNamespace).Get("secret", contentsID)
-						if err != nil {
-							// return nothing in case of error
-							// This avoids false positives when kubectl returns "secrets "XXXXX" not found
-							return ""
-						}
-						GinkgoWriter.Printf("BundleDeployment secret: %s\n", out)
-						return out
-					}).Should(ContainSubstring(contentsID))
+					verifySecretsAreEqual(ocistorage.OCIStorageDefaultSecretName, env.Namespace, contentsID, downstreamNamespace)
 				})
 				By("not creating a contents resource for this chart", func() {
-					out, _ := k.Get("contents")
-					Expect(out).NotTo(ContainSubstring(contentsID))
+					var content fleet.Content
+					k8sclient.ObjectShouldNotExist(clientUpstream, contentsID, "", &content, false)
 				})
 				By("deploying the helm chart", func() {
-					Eventually(func() string {
-						out, _ := k.Namespace("fleet-local").Get("configmaps")
-						return out
-					}).Should(ContainSubstring("sample-config"))
+					var cm corev1.ConfigMap
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "test-simple-chart-config", "default", &cm)
 				})
 			})
 		})
 	})
 
-	When("creating a gitrepo resource with invalid ociRegistry info and experimental flag is false", func() {
-		Context("containing a public oci based helm chart", func() {
+	When("applying a gitrepo with the default ociSecret with wrong reference and the user-defined secret also deployed with correct values", func() {
+		Context("containing a valid helm chart", func() {
 			BeforeEach(func() {
+				experimentalValue = true
 				insecureSkipTLS = true
-				forceGitRepoURL = "not-valid-oci-registry.com"
-				experimentalValue = false
+				deployDefaultSecret = true
+				deploySpecificSecretName = "test-secret"
+				forceDefaultReference = "not-valid-oci-registry.com"
+				forceUserDefinedReference = ""
 			})
 
-			It("creates the bundle with no OCI storage", func() {
-				Eventually(func(g Gomega) {
-					// check for the bundle
-					out, _ := k.Namespace("fleet-local").Get("bundles")
-					g.Expect(out).To(ContainSubstring("sample-simple-chart-oci"))
-
-					// check for contentsID
-					contentsID, err := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.spec.contentsId}`)
-					g.Expect(err).ToNot(HaveOccurred())
-					g.Expect(contentsID).To(BeEmpty())
-
-					GinkgoWriter.Printf("ContentsID: %s\n", contentsID)
-
-					// bundle secret should not be created
-					secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
-					g.Expect(secrets).ToNot(ContainSubstring("s-"))
-
-					// gets the bundles sha256 for the resources
-					// We'll use that to identify the contents resource for this bundle
-					resourcesSha256, _ := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.status.resourcesSha256Sum}`)
-					g.Expect(resourcesSha256).ToNot(BeEmpty())
+			It("deploys the bundle", func() {
+				By("creating the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+				})
+				By("setting the ContentsID field in the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+					Expect(bundle.Spec.ContentsID).To(ContainSubstring("s-"))
+					contentsID = bundle.Spec.ContentsID
+				})
 
-					// delete the last 3 chars from the sha256 so it matches the contents ID
-					resourcesSha256 = resourcesSha256[:len(resourcesSha256)-3]
+				By("setting the OCI reference status field key to the OCI path of the manifest", func() {
+					Eventually(func(g Gomega) {
+						var bundle fleet.Bundle
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+						g.Expect(bundle.Status.OCIReference).To(Equal(fmt.Sprintf("oci://%s/%s:latest", ociRegistry, contentsID)))
+					}).Should(Succeed())
+				})
+				By("creating a bundle secret with the expected contents from the user-defined secret", func() {
+					verifySecretsAreEqual(deploySpecificSecretName, env.Namespace, contentsID, env.Namespace)
+				})
+				By("creating a bundledeployment secret", func() {
+					verifySecretsAreEqual(deploySpecificSecretName, env.Namespace, contentsID, downstreamNamespace)
+				})
+				By("not creating a contents resource for this chart", func() {
+					var content fleet.Content
+					k8sclient.ObjectShouldNotExist(clientUpstream, contentsID, "", &content, false)
+				})
+				By("deploying the helm chart", func() {
+					var cm corev1.ConfigMap
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "test-simple-chart-config", "default", &cm)
+				})
+			})
+		})
+	})
 
-					// check that it created a content resource with the above sha256
-					contents, _ := k.Get("contents")
-					g.Expect(contents).To(ContainSubstring(resourcesSha256))
+	When("applying a gitrepo with the default ociSecret also deployed with incorrect reference and experimental flag is not set", func() {
+		Context("containing a public oci based helm chart", func() {
+			BeforeEach(func() {
+				experimentalValue = false
+				insecureSkipTLS = true
+				deployDefaultSecret = true
+				deploySpecificSecretName = ""
+				forceDefaultReference = "not-valid-oci-registry.com"
+				forceUserDefinedReference = ""
+			})
 
+			It("creates the bundle with no OCI storage", func() {
+				By("creating the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+				})
+				By("not setting the ContentsID field in the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+					Expect(bundle.Spec.ContentsID).To(BeEmpty())
+				})
+				By("creating a bundle deployment", func() {
+					var bd fleet.BundleDeployment
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", downstreamNamespace, &bd)
+					Expect(bd.Spec.DeploymentID).ToNot(BeEmpty())
+					tokens := strings.Split(bd.Spec.DeploymentID, ":")
+					Expect(tokens).To(HaveLen(2))
+					contentsID = tokens[0]
 					// save the contents id to purge after the test
 					// this will prevent interference with the previous test
 					// as the resources sha256 is the same
-					contentIDToPurge = "s-" + resourcesSha256
-
-					// finally check that the helm chart was deployed
-					configmaps, _ := k.Namespace("fleet-local").Get("configmaps")
-					g.Expect(configmaps).To(ContainSubstring("sample-config"))
-				}).Should(Succeed())
+					contentIDToPurge = contentsID
+				})
+				By("creating a content resource with the expected ID", func() {
+					var content fleet.Content
+					k8sclient.GetObjectShouldSucceed(clientUpstream, contentsID, "", &content)
+				})
+				By("not creating a bundle secret", func() {
+					var secret corev1.Secret
+					k8sclient.ObjectShouldNotExist(clientUpstream, contentsID, env.Namespace, &secret, false)
+				})
+				By("not creating a bundle deployment secret", func() {
+					var secret corev1.Secret
+					k8sclient.ObjectShouldNotExist(clientUpstream, contentsID, downstreamNamespace, &secret, false)
+				})
+				By("deploying the helm chart", func() {
+					var cm corev1.ConfigMap
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "test-simple-chart-config", "default", &cm)
+				})
 			})
 		})
 	})
@@ -348,43 +496,44 @@ var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-reg
 	When("creating a gitrepo resource with invalid ociRegistry info", func() {
 		Context("containing a public oci based helm chart", func() {
 			BeforeEach(func() {
-				insecureSkipTLS = true
-				forceGitRepoURL = "not-valid-oci-registry.com"
 				experimentalValue = true
+				insecureSkipTLS = true
+				deployDefaultSecret = true
+				deploySpecificSecretName = ""
+				forceDefaultReference = "not-valid-oci-registry.com"
+				forceUserDefinedReference = ""
 			})
 
 			It("does not create the bundle", func() {
-				// look for failed pods with the name sample-xxxx-xxxx
-				r, _ := regexp.Compile("sample-([a-z0-9]+)-([a-z0-9]+)")
-				var failedJob string
-				Eventually(func(g Gomega) {
-					failedPods, err := getFailedPodNames(k, "fleet-local")
-					failedJob = ""
-
-					g.Expect(err).ToNot(HaveOccurred())
-					for _, pod := range failedPods {
-						if r.MatchString(pod) {
-							failedJob = pod
-							break
+				By("setting the right error message in the GitRepo", func() {
+					Eventually(func(g Gomega) {
+						var repo fleet.GitRepo
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample", env.Namespace, &repo)
+						stalledFound := false
+						stalledMessage := ""
+						for _, cond := range repo.Status.Conditions {
+							if cond.Type == "Stalled" {
+								stalledMessage = cond.Message
+								stalledFound = true
+								break
+							}
 						}
+						g.Expect(stalledFound).To(BeTrue())
+						g.Expect(stalledMessage).To(ContainSubstring("no such host"))
+					}).Should(Succeed())
+				})
+				By("not creating the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.ObjectShouldNotExist(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle, true)
+				})
+				By("not creating the bundle secret", func() {
+					var secrets corev1.SecretList
+					err := clientUpstream.List(context.TODO(), &secrets, &client.ListOptions{Namespace: env.Namespace})
+					Expect(err).ToNot(HaveOccurred())
+					for _, s := range secrets.Items {
+						Expect(s.Name).ToNot(HavePrefix("s-"))
 					}
-					g.Expect(failedJob).ToNot(BeEmpty())
-				}).Should(Succeed())
-
-				Expect(failedJob).ShouldNot(BeEmpty())
-				// check that the logs of the job reflect the bad OCI registry
-				logs, err := k.Namespace("fleet-local").Logs(failedJob)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(logs).Should(ContainSubstring("no such host"))
-
-				// we don't fallback to content resources, so the bundle should not be created
-				// at this point
-				bundles, _ := k.Namespace("fleet-local").Get("bundles")
-				Expect(bundles).ShouldNot(ContainSubstring("sample-simple-chart"))
-
-				// bundle secret should not be created either
-				secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
-				Expect(secrets).ShouldNot(ContainSubstring("s-"))
+				})
 			})
 		})
 	})
@@ -392,41 +541,116 @@ var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-reg
 	When("creating a gitrepo resource with valid ociRegistry but not ignoring certs", func() {
 		Context("containing a public oci based helm chart", func() {
 			BeforeEach(func() {
-				insecureSkipTLS = false
-				forceGitRepoURL = ""
 				experimentalValue = true
+				insecureSkipTLS = false
+				deployDefaultSecret = true
+				deploySpecificSecretName = ""
+				forceDefaultReference = ""
+				forceUserDefinedReference = ""
 			})
 
 			It("does not create the bundle", func() {
-				// look for failed pods with the name sample-xxxx-xxxx
-				r, _ := regexp.Compile("sample-([a-z0-9]+)-([a-z0-9]+)")
-				var failedJob string
-				Eventually(func() bool {
-					failedPods, err := getFailedPodNames(k, "fleet-local")
-					Expect(err).ToNot(HaveOccurred())
-					for _, pod := range failedPods {
-						if r.MatchString(pod) {
-							failedJob = pod
-							return true
+				By("setting the right error message in the GitRepo", func() {
+					Eventually(func(g Gomega) {
+						var repo fleet.GitRepo
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample", env.Namespace, &repo)
+						stalledFound := false
+						stalledMessage := ""
+						for _, cond := range repo.Status.Conditions {
+							if cond.Type == "Stalled" {
+								stalledMessage = cond.Message
+								stalledFound = true
+								break
+							}
 						}
+						g.Expect(stalledFound).To(BeTrue())
+						g.Expect(stalledMessage).To(ContainSubstring("tls: failed to verify certificate: x509"))
+					}).Should(Succeed())
+				})
+				By("not creating the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.ObjectShouldNotExist(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle, true)
+				})
+				By("not creating the bundle secret", func() {
+					var secrets corev1.SecretList
+					err := clientUpstream.List(context.TODO(), &secrets, &client.ListOptions{Namespace: env.Namespace})
+					Expect(err).ToNot(HaveOccurred())
+					for _, s := range secrets.Items {
+						Expect(s.Name).ToNot(HavePrefix("s-"))
 					}
-					return false
-				}).Should(BeTrue())
-
-				Expect(failedJob).ShouldNot(BeEmpty())
-				// check that the logs of the job reflect the bad OCI registry
-				logs, err := k.Namespace("fleet-local").Logs(failedJob)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(logs).Should(ContainSubstring("tls: failed to verify certificate: x509"))
-
-				// we don't fallback to content resources, so the bundle should not be created
-				// at this point
-				bundles, _ := k.Namespace("fleet-local").Get("bundles")
-				Expect(bundles).ShouldNot(ContainSubstring("sample-simple-chart"))
-
-				// bundle secret should not be created either
-				secrets, _ := k.Namespace("fleet-local").Get("secrets", "-o", "custom-columns=NAME:metadata.name", "--no-headers")
-				Expect(secrets).ShouldNot(ContainSubstring("s-"))
+				})
+			})
+		})
+	})
+
+	When("applying a gitrepo and the OCI artifact is changed with invalid contents", func() {
+		Context("containing a valid helm chart", func() {
+			BeforeEach(func() {
+				experimentalValue = true
+				insecureSkipTLS = true
+				deployDefaultSecret = true
+				deploySpecificSecretName = ""
+				forceDefaultReference = ""
+				forceUserDefinedReference = ""
+			})
+
+			It("deploys the bundle and rejects the content as it's different from the expected", func() {
+				By("creating the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+				})
+				By("setting the ContentsID field in the bundle", func() {
+					var bundle fleet.Bundle
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+					Expect(bundle.Spec.ContentsID).To(ContainSubstring("s-"))
+					contentsID = bundle.Spec.ContentsID
+				})
+
+				By("setting the OCI reference status field key to the OCI path of the manifest", func() {
+					Eventually(func(g Gomega) {
+						var bundle fleet.Bundle
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", env.Namespace, &bundle)
+						g.Expect(bundle.Status.OCIReference).To(Equal(fmt.Sprintf("oci://%s/%s:latest", ociRegistry, contentsID)))
+					}).Should(Succeed())
+				})
+				By("creating a bundle secret with the expected contents", func() {
+					verifySecretsAreEqual(ocistorage.OCIStorageDefaultSecretName, env.Namespace, contentsID, env.Namespace)
+				})
+				By("creating a bundledeployment secret", func() {
+					verifySecretsAreEqual(ocistorage.OCIStorageDefaultSecretName, env.Namespace, contentsID, downstreamNamespace)
+				})
+				By("not creating a contents resource for this chart", func() {
+					var content fleet.Content
+					k8sclient.ObjectShouldNotExist(clientUpstream, contentsID, "", &content, false)
+				})
+				By("deploying the helm chart", func() {
+					var cm corev1.ConfigMap
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "test-simple-chart-config", "default", &cm)
+				})
+				By("changing the contents of the oci artifact", func() {
+					pushFakeOCIContents(ocistorage.OCIStorageDefaultSecretName, contentsID)
+				})
+				By("forcing the bundle deployment to re-deploy and reject the oci contents", func() {
+					var bd fleet.BundleDeployment
+					k8sclient.GetObjectShouldSucceed(clientUpstream, "sample-simple-chart-oci", downstreamNamespace, &bd)
+					// force the bundledeployment to re-deploy by deleting it
+					k8sclient.DeleteObjectShouldSucceed(clientUpstream, &bd)
+					Eventually(func(g Gomega) {
+						var repo fleet.GitRepo
+						k8sclient.GetObjectShouldSucceed(clientUpstream, "sample", env.Namespace, &repo)
+						conditionFound := false
+						message := ""
+						for _, cond := range repo.Status.Conditions {
+							if cond.Type == "Ready" {
+								message = cond.Message
+								conditionFound = true
+								break
+							}
+						}
+						g.Expect(conditionFound).To(BeTrue())
+						g.Expect(message).To(ContainSubstring("invalid or corrupt manifest"))
+					}).Should(Succeed())
+				})
 			})
 		})
 	})
diff --git a/e2e/single-cluster/suite_test.go b/e2e/single-cluster/suite_test.go
index 58d6fb24..3a0600aa 100644
--- a/e2e/single-cluster/suite_test.go
+++ b/e2e/single-cluster/suite_test.go
@@ -7,8 +7,15 @@ import (
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/tools/clientcmd"
 
 	"github.com/rancher/fleet/e2e/testenv"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 func TestE2E(t *testing.T) {
@@ -21,7 +28,8 @@ const (
 )
 
 var (
-	env *testenv.Env
+	env            *testenv.Env
+	clientUpstream client.Client
 )
 
 var _ = BeforeSuite(func() {
@@ -30,6 +38,32 @@ var _ = BeforeSuite(func() {
 	testenv.SetRoot("../..")
 
 	env = testenv.New()
+	clientUpstream = getClientForContext(env.Upstream)
 
 	Expect(env.Namespace).To(Equal("fleet-local"), "The single-cluster test assets target the default clustergroup and only work in fleet-local")
 })
+
+func getClientForContext(contextName string) client.Client {
+	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+	configOverrides := &clientcmd.ConfigOverrides{
+		CurrentContext: contextName,
+	}
+
+	clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
+
+	restConfig, err := clientConfig.ClientConfig()
+	Expect(err).ToNot(HaveOccurred())
+
+	// Set up scheme
+	schema := runtime.NewScheme()
+	Expect(corev1.AddToScheme(schema)).ToNot(HaveOccurred())
+	Expect(appsv1.AddToScheme(schema)).ToNot(HaveOccurred())
+	Expect(clientgoscheme.AddToScheme(schema)).ToNot(HaveOccurred())
+	Expect(fleet.AddToScheme(schema)).ToNot(HaveOccurred())
+
+	// Create controller-runtime client
+	c, err := client.New(restConfig, client.Options{Scheme: schema})
+	Expect(err).ToNot(HaveOccurred())
+
+	return c
+}
diff --git a/e2e/testenv/k8sclient/k8sclient.go b/e2e/testenv/k8sclient/k8sclient.go
new file mode 100644
index 00000000..f3adb5f5
--- /dev/null
+++ b/e2e/testenv/k8sclient/k8sclient.go
@@ -0,0 +1,51 @@
+package k8sclient
+
+import (
+	"context"
+	"time"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	. "github.com/onsi/gomega"
+)
+
+// GetObjectShouldSucceed gets the object identified by name and namespace and ensures it succeeds
+func GetObjectShouldSucceed(c client.Client, name, namespace string, obj client.Object) {
+	Eventually(func(g Gomega) {
+		err := c.Get(context.TODO(), client.ObjectKey{Name: name, Namespace: namespace}, obj)
+		g.Expect(err).ToNot(HaveOccurred())
+	}).Should(Succeed())
+}
+
+// DeleteObjectShouldSucceed deletes the given object and ensures it succeeds
+func DeleteObjectShouldSucceed(c client.Client, obj client.Object) {
+	err := c.Delete(context.TODO(), obj)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+// CreateObjectShouldSucceed creates the given object and ensures it succeeds
+func CreateObjectShouldSucceed(c client.Client, obj client.Object) {
+	err := c.Create(context.TODO(), obj)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+// UpdateObjectShouldSucceed updates the given object and ensures it succeeds
+func UpdateObjectShouldSucceed(c client.Client, obj client.Object) {
+	err := c.Update(context.TODO(), obj)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+// ObjectShouldNotExist checks the object identified with name and namespace does not exist.
+// If checkConsistently is set to true it checks consistently for 5 seconds, otherwise it runs a single check
+func ObjectShouldNotExist(c client.Client, name, namespace string, obj client.Object, checkConsistently bool) {
+	if checkConsistently {
+		Consistently(func(g Gomega) {
+			err := c.Get(context.TODO(), client.ObjectKey{Name: name, Namespace: namespace}, obj)
+			g.Expect(errors.IsNotFound(err)).To(BeTrue())
+		}, 5*time.Second, 1*time.Second).Should(Succeed())
+	} else {
+		err := c.Get(context.TODO(), client.ObjectKey{Name: name, Namespace: namespace}, obj)
+		Expect(errors.IsNotFound(err)).To(BeTrue())
+	}
+}
diff --git a/integrationtests/cli/apply/apply_online_test.go b/integrationtests/cli/apply/apply_online_test.go
index 51b1e72f..fc836691 100644
--- a/integrationtests/cli/apply/apply_online_test.go
+++ b/integrationtests/cli/apply/apply_online_test.go
@@ -42,6 +42,7 @@ var _ = Describe("Fleet apply online", Label("online"), func() {
 				bundle.ObjectMeta = oldBundle.ObjectMeta
 				bundle.Name = ns.Name
 				bundle.Namespace = ns.Namespace
+				bundle.Spec = oldBundle.Spec
 				return nil
 			},
 		)
@@ -109,4 +110,34 @@ data:
 			Expect(err).NotTo(HaveOccurred())
 		})
 	})
+
+	When("A HelmOps bundle already exists in the same namespace", func() {
+		BeforeEach(func() {
+			name = "labels_update"
+			dirs = []string{cli.AssetsPath + "labels_update"}
+			//bundle in the cluster
+			oldBundle = &fleet.Bundle{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "foo",
+					Name:      "labels_update",
+				},
+				Spec: fleet.BundleSpec{
+					HelmOpOptions: &fleet.BundleHelmOptions{
+						// values themselves do not matter, as long as Helm options are non-null and the bundle is therefore detected as
+						// a HelmOps bundle.
+						SecretName: "foo",
+					},
+				},
+			}
+		})
+
+		It("detects the existing bundle and fails to create the new bundle", func() {
+			// No update expected here, as checks for existence of an existing bundle should reveal that a
+			// HelmOps bundle with the same name already exists.
+
+			err := fleetApplyOnline(clientMock, name, dirs, options)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("already exists"))
+		})
+	})
 })
diff --git a/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go b/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
index f37ccc6d..113ec60f 100644
--- a/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
+++ b/internal/cmd/controller/helmops/reconciler/helmop_controller_test.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strings"
 	"testing"
 
 	"go.uber.org/mock/gomock"
@@ -151,6 +152,89 @@ func TestReconcile_ErrorCreatingBundleIsShownInStatus(t *testing.T) {
 	}
 }
 
+// Validates that the HelmOps reconciler will not create a bundle if another bundle exists with the same name, for
+// instance a gitOps bundle.
+func TestReconcile_ErrorCreatingBundleIfBundleWithSameNameExists(t *testing.T) {
+	os.Setenv("EXPERIMENTAL_HELM_OPS", "true")
+	mockCtrl := gomock.NewController(t)
+	defer mockCtrl.Finish()
+	scheme := runtime.NewScheme()
+	utilruntime.Must(batchv1.AddToScheme(scheme))
+	helmop := fleet.HelmOp{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-workload",
+			Namespace: "default",
+		},
+	}
+	namespacedName := types.NamespacedName{Name: helmop.Name, Namespace: helmop.Namespace}
+	client := mocks.NewMockClient(mockCtrl)
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+		func(ctx context.Context, req types.NamespacedName, fh *fleet.HelmOp, opts ...interface{}) error {
+			fh.Name = helmop.Name
+			fh.Namespace = helmop.Namespace
+			fh.Spec.Helm = &fleet.HelmOptions{
+				Chart: "chart",
+			}
+			controllerutil.AddFinalizer(fh, finalize.HelmOpFinalizer)
+			return nil
+		},
+	)
+
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), fleet.Bundle{}).AnyTimes().DoAndReturn(
+		func(ctx context.Context, req types.NamespacedName, bundle *fleet.Bundle, opts ...interface{}) error {
+			bundle.ObjectMeta = metav1.ObjectMeta{
+				Name:      "my-workload",
+				Namespace: "default",
+			}
+
+			return nil
+		},
+	)
+
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), fleet.HelmOp{}).AnyTimes().DoAndReturn(
+		func(ctx context.Context, req types.NamespacedName, bundle *fleet.HelmOp, opts ...interface{}) error {
+			return nil
+		},
+	)
+
+	expectedErrorMsg := "non-helmops bundle already exists"
+	statusClient := mocks.NewMockSubResourceWriter(mockCtrl)
+	client.EXPECT().Status().Return(statusClient).Times(1)
+	statusClient.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Do(
+		func(ctx context.Context, helmop *fleet.HelmOp, opts ...interface{}) {
+			c, found := getCondition(helmop, fleet.HelmOpAcceptedCondition)
+			if !found {
+				t.Errorf("expecting to find the %s condition and could not find it.", fleet.HelmOpAcceptedCondition)
+			}
+			if !strings.Contains(c.Message, expectedErrorMsg) {
+				t.Errorf("expecting message [%s] in condition, got [%s]", expectedErrorMsg, c.Message)
+			}
+		},
+	).Times(1)
+
+	r := HelmOpReconciler{
+		Client: client,
+		Scheme: scheme,
+	}
+
+	ctx := context.TODO()
+
+	res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName})
+	if err == nil {
+		t.Errorf("expecting error, got nil")
+	}
+
+	if err != nil && !strings.Contains(err.Error(), expectedErrorMsg) {
+		t.Errorf("expecting error: [%s], got %v", expectedErrorMsg, err.Error())
+	}
+
+	if res.Requeue {
+		t.Errorf("expecting Requeue set to false, it was true")
+	}
+}
+
 func TestReconcile_CreatesBundleAndUpdatesStatus(t *testing.T) {
 	os.Setenv("EXPERIMENTAL_HELM_OPS", "true")
 	mockCtrl := gomock.NewController(t)
diff --git a/internal/ociwrapper/ociwrapper_test.go b/internal/ocistorage/ociwrapper_test.go
similarity index 96%
rename from internal/ociwrapper/ociwrapper_test.go
rename to internal/ocistorage/ociwrapper_test.go
index aa7c08cb..deb25419 100644
--- a/internal/ociwrapper/ociwrapper_test.go
+++ b/internal/ocistorage/ociwrapper_test.go
@@ -1,4 +1,4 @@
-package ociwrapper
+package ocistorage
 
 import (
 	"bytes"
@@ -317,27 +317,26 @@ var _ = Describe("OCIUtils tests", func() {
 
 var _ = Describe("OCIUtils Experimental flag tests", func() {
 	var envBeforeTest string
-	const experimentalEnv = "EXPERIMENTAL_OCI_STORAGE"
 
 	BeforeEach(func() {
-		envBeforeTest = os.Getenv(experimentalEnv)
+		envBeforeTest = os.Getenv(OCIStorageExperimentalFlag)
 	})
 
 	AfterEach(func() {
 		if envBeforeTest != "" {
 			// set the value it had before the test
-			Expect(os.Setenv(experimentalEnv, envBeforeTest)).ToNot(HaveOccurred())
+			Expect(os.Setenv(OCIStorageExperimentalFlag, envBeforeTest)).ToNot(HaveOccurred())
 		} else {
-			Expect(os.Unsetenv(experimentalEnv)).ToNot(HaveOccurred())
+			Expect(os.Unsetenv(OCIStorageExperimentalFlag)).ToNot(HaveOccurred())
 		}
 	})
 
 	DescribeTable("Check value returned is the expected one",
 		func(value string, expected bool) {
 			if value == "unset" {
-				Expect(os.Unsetenv(experimentalEnv)).ToNot(HaveOccurred())
+				Expect(os.Unsetenv(OCIStorageExperimentalFlag)).ToNot(HaveOccurred())
 			} else {
-				Expect(os.Setenv(experimentalEnv, value)).ToNot(HaveOccurred())
+				Expect(os.Setenv(OCIStorageExperimentalFlag, value)).ToNot(HaveOccurred())
 			}
 			result := ExperimentalOCIIsEnabled()
 			Expect(result).To(Equal(expected))
diff --git a/internal/ocistorage/secret_test.go b/internal/ocistorage/secret_test.go
new file mode 100644
index 00000000..1df1bcba
--- /dev/null
+++ b/internal/ocistorage/secret_test.go
@@ -0,0 +1,239 @@
+package ocistorage
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/rancher/fleet/internal/mocks"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	"go.uber.org/mock/gomock"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("OCIOpts loaded from secret", func() {
+	var (
+		ctrl                   *gomock.Controller
+		secretGetErrorMessage  string
+		secretGetNotFoundError bool
+		mockClient             *mocks.MockClient
+
+		secretName string
+		secretData map[string][]byte
+		secretType string
+	)
+
+	JustBeforeEach(func() {
+		ctrl = gomock.NewController(GinkgoT())
+		mockClient = mocks.NewMockClient(ctrl)
+		ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+		getSecretFromMockClient(
+			mockClient,
+			ns,
+			secretData,
+			secretType,
+			secretGetNotFoundError,
+			secretGetErrorMessage,
+		)
+	})
+
+	When("the given oci storage secret exists with all fields set", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretData = map[string][]byte{
+				OCISecretUsername:      []byte("username"),
+				OCISecretPassword:      []byte("password"),
+				OCISecretAgentUsername: []byte("agentUsername"),
+				OCISecretAgentPassword: []byte("agentPassword"),
+				OCISecretReference:     []byte("reference"),
+				OCISecretBasicHTTP:     []byte("true"),
+				OCISecretInsecure:      []byte("true"),
+			}
+			secretType = fleet.SecretTypeOCIStorage
+			secretGetErrorMessage = ""
+			secretGetNotFoundError = false
+		})
+		It("returns the expected OCIOpts from the data in the secret", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			opts, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(opts.Reference).To(Equal(string(secretData[OCISecretReference])))
+			Expect(opts.Username).To(Equal(string(secretData[OCISecretUsername])))
+			Expect(opts.Password).To(Equal(string(secretData[OCISecretPassword])))
+			Expect(opts.AgentUsername).To(Equal(string(secretData[OCISecretAgentUsername])))
+			Expect(opts.AgentPassword).To(Equal(string(secretData[OCISecretAgentPassword])))
+			Expect(opts.BasicHTTP).To(BeTrue())
+			Expect(opts.InsecureSkipTLS).To(BeTrue())
+		})
+	})
+
+	When("the secret name is not set, but a default secret exists", func() {
+		BeforeEach(func() {
+			secretName = ""
+			secretData = map[string][]byte{
+				OCISecretUsername:      []byte("username"),
+				OCISecretPassword:      []byte("password"),
+				OCISecretAgentUsername: []byte("agentUsername"),
+				OCISecretAgentPassword: []byte("agentPassword"),
+				OCISecretReference:     []byte("reference"),
+				OCISecretBasicHTTP:     []byte("true"),
+				OCISecretInsecure:      []byte("true"),
+			}
+			secretType = fleet.SecretTypeOCIStorage
+			secretGetErrorMessage = ""
+			secretGetNotFoundError = false
+		})
+		It("returns the expected OCIOpts from the data in the secret", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			opts, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(opts.Reference).To(Equal(string(secretData[OCISecretReference])))
+			Expect(opts.Username).To(Equal(string(secretData[OCISecretUsername])))
+			Expect(opts.Password).To(Equal(string(secretData[OCISecretPassword])))
+			Expect(opts.AgentUsername).To(Equal(string(secretData[OCISecretAgentUsername])))
+			Expect(opts.AgentPassword).To(Equal(string(secretData[OCISecretAgentPassword])))
+			Expect(opts.BasicHTTP).To(BeTrue())
+			Expect(opts.InsecureSkipTLS).To(BeTrue())
+		})
+	})
+
+	When("the given oci storage secret exists with all the non-required fields are not set", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretData = map[string][]byte{
+				OCISecretReference: []byte("reference"),
+			}
+
+			secretType = fleet.SecretTypeOCIStorage
+			secretGetErrorMessage = ""
+			secretGetNotFoundError = false
+		})
+		It("returns the expected OCIOpts from the data in the secret", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			opts, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(opts.Reference).To(Equal(string(secretData[OCISecretReference])))
+			Expect(opts.Username).To(BeEmpty())
+			Expect(opts.Password).To(BeEmpty())
+			Expect(opts.AgentUsername).To(BeEmpty())
+			Expect(opts.AgentPassword).To(BeEmpty())
+			Expect(opts.BasicHTTP).To(BeFalse())
+			Expect(opts.InsecureSkipTLS).To(BeFalse())
+		})
+	})
+
+	When("the given oci storage secret exists and reference is not set", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretData = map[string][]byte{
+				OCISecretUsername:      []byte("username"),
+				OCISecretPassword:      []byte("password"),
+				OCISecretAgentUsername: []byte("agentUsername"),
+				OCISecretAgentPassword: []byte("agentPassword"),
+				OCISecretBasicHTTP:     []byte("true"),
+				OCISecretInsecure:      []byte("true"),
+			}
+
+			secretType = fleet.SecretTypeOCIStorage
+			secretGetErrorMessage = ""
+			secretGetNotFoundError = false
+		})
+		It("returns an error complaining about reference not being set", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			_, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(Equal("key \"reference\" not found in secret"))
+		})
+	})
+
+	When("the given oci storage secret does not exist", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretGetNotFoundError = true
+			secretGetErrorMessage = ""
+		})
+		It("returns an error complaining about a secret not being found", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			_, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).To(HaveOccurred())
+			Expect(apierrors.IsNotFound(err)).To(BeTrue())
+		})
+	})
+
+	When("the given oci storage secret exists but the type is not the expected one", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretType = "party-like-its-1999"
+			secretGetNotFoundError = false
+			secretGetErrorMessage = ""
+		})
+		It("returns an error complaining about wrong type", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			_, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(Equal(fmt.Sprintf("unexpected secret type: got %q, want %q", secretType, fleet.SecretTypeOCIStorage)))
+		})
+	})
+
+	When("there is an error when getting the secret", func() {
+		BeforeEach(func() {
+			secretName = "test"
+			secretGetNotFoundError = false
+			secretGetErrorMessage = "SOME ERROR"
+		})
+		It("returns the error", func() {
+			ns := types.NamespacedName{Name: secretName, Namespace: "test"}
+			_, err := ReadOptsFromSecret(context.TODO(), mockClient, ns)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(Equal(secretGetErrorMessage))
+		})
+	})
+})
+
+func getSecretFromMockClient(
+	mockClient *mocks.MockClient,
+	ns types.NamespacedName,
+	data map[string][]byte,
+	secretType string,
+	wantNotFound bool,
+	wantErrorMessage string) {
+	if wantErrorMessage != "" {
+		mockClient.EXPECT().Get(gomock.Any(), ns, gomock.Any()).DoAndReturn(
+			func(_ context.Context, _ types.NamespacedName, secret *corev1.Secret, _ ...interface{}) error {
+				return errors.New(wantErrorMessage)
+			},
+		)
+	} else if wantNotFound {
+		mockClient.EXPECT().Get(gomock.Any(), ns, gomock.Any()).DoAndReturn(
+			func(_ context.Context, _ types.NamespacedName, secret *corev1.Secret, _ ...interface{}) error {
+				return apierrors.NewNotFound(schema.GroupResource{}, "TEST ERROR")
+			},
+		)
+	} else if ns.Name == "" {
+		// verify that when the name is not set it uses the default secret name.
+		mockClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
+			func(_ context.Context, key types.NamespacedName, secret *corev1.Secret, _ ...interface{}) error {
+				Expect(key.Name).To(Equal(OCIStorageDefaultSecretName))
+				secret.Data = data
+				secret.Type = corev1.SecretType(secretType)
+				return nil
+			},
+		)
+	} else if ns.Name != "" {
+		mockClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
+			func(_ context.Context, key types.NamespacedName, secret *corev1.Secret, _ ...interface{}) error {
+				Expect(ns.Name).To(Equal(key.Name))
+				secret.Data = data
+				secret.Type = corev1.SecretType(secretType)
+				return nil
+			},
+		)
+	}
+}
diff --git a/internal/ociwrapper/suite_test.go b/internal/ocistorage/suite_test.go
similarity index 92%
rename from internal/ociwrapper/suite_test.go
rename to internal/ocistorage/suite_test.go
index d7c84821..7840cd33 100644
--- a/internal/ociwrapper/suite_test.go
+++ b/internal/ocistorage/suite_test.go
@@ -1,4 +1,4 @@
-package ociwrapper_test
+package ocistorage_test
 
 import (
 	"testing"