Repository: rancher/fleet
Pull request #1905 (2023-10-26 16:18:27)

Split agent
Refers to #1772 

To prepare the agent for controller-runtime, split the code into three processes:
* registration
* cluster status updater
* agent controller, watches bundledeployments

Only the last part is an actual controller. Cluster status updates work on a timer and registration is done when the agent starts up. A new function "registration.Get" was added, to just return an existing registration to the other processes.

This PR switches the agent to a statefulset, to avoid leader election. I don't expect the agent to scale horizontally, as a controller. 
Updating the cluster status with information about the cluster nodes seems unrelated to Fleet's purpose and we might discuss this in the future.
The code of the handlers in the bundledeployment controller is currently spread around the "handler", a deploy "manager" and the helm deployer. The PR takes first steps to consolidate the functionality into the deployer package, making it more obvious which client is used when during reconciliation.

"Trigger" was renamed to "driftdetect" as it uses the helm history to detect drift for deployments. Which is similar to the "monitor", which updates the bundle deployments status.


Patch:
diff --git a/.github/scripts/deploy-fleet.sh b/.github/scripts/deploy-fleet.sh
index 37982117..97fe5da7 100755
--- a/.github/scripts/deploy-fleet.sh
+++ b/.github/scripts/deploy-fleet.sh
@@ -39,7 +39,7 @@ eventually helm upgrade --install fleet charts/fleet \
 # wait for controller and agent rollout
 kubectl -n cattle-fleet-system rollout status deploy/fleet-controller
 { grep -E -q -m 1 "fleet-agent-local.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-local -w)
-kubectl -n cattle-fleet-system rollout status deploy/fleet-agent
+kubectl -n cattle-fleet-system rollout status statefulset/fleet-agent
 
 # label local cluster
 kubectl patch clusters.fleet.cattle.io -n fleet-local local --type=json -p '[{"op": "add", "path": "/metadata/labels/management.cattle.io~1cluster-display-name", "value": "local" }]'
diff --git a/.github/scripts/setup-rancher.sh b/.github/scripts/setup-rancher.sh
index bec8cb0e..a7742fe2 100755
--- a/.github/scripts/setup-rancher.sh
+++ b/.github/scripts/setup-rancher.sh
@@ -35,3 +35,4 @@ kubectl -n cattle-system rollout status deploy/rancher
 # wait for rancher to create fleet namespace, deployment and controller
 { grep -q -m 1 "fleet"; kill $!; } < <(kubectl get deployments -n "$fleetns" -w)
 kubectl -n "$fleetns" rollout status deploy/fleet-controller
+{ grep -E -q -m 1 "fleet-agent-local.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-local -w)
diff --git a/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh b/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
index 18b36d2a..bdb70dd8 100755
--- a/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
+++ b/.github/scripts/upgrade-rancher-fleet-to-dev-fleet.sh
@@ -45,6 +45,6 @@ sleep 5
 { grep -E -q -m 1 "fleet-agent-c.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-default -w)
 
 kubectl config use-context k3d-downstream
-kubectl -n cattle-fleet-system rollout status deploy/fleet-agent
+kubectl -n cattle-fleet-system rollout status statefulset/fleet-agent
 
 helm list -A
diff --git a/charts/fleet-agent/templates/deployment.yaml b/charts/fleet-agent/templates/deployment.yaml
index 582eed60..d9348424 100644
--- a/charts/fleet-agent/templates/deployment.yaml
+++ b/charts/fleet-agent/templates/deployment.yaml
@@ -1,5 +1,5 @@
 apiVersion: apps/v1
-kind: Deployment
+kind: StatefulSet
 metadata:
   name: fleet-agent
 spec:
@@ -11,6 +11,30 @@ spec:
       labels:
         app: fleet-agent
     spec:
+      initContainers:
+      - env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        image: '{{ template "system_default_registry" . }}{{.Values.image.repository}}:{{.Values.image.tag}}'
+        name: fleet-agent-register
+        command:
+        - fleetagent
+        - register
+        {{- if .Values.debug }}
+        - --debug
+        - --debug-level
+        - {{ quote .Values.debugLevel }}
+        {{- else }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          privileged: false
+          capabilities:
+            drop:
+            - ALL
+        {{- end }}
       containers:
       - env:
         - name: NAMESPACE
@@ -34,6 +58,29 @@ spec:
             drop:
             - ALL
         {{- end }}
+      - env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        image: '{{ template "system_default_registry" . }}{{.Values.image.repository}}:{{.Values.image.tag}}'
+        name: fleet-agent-clusterstatus
+        command:
+        - fleetagent
+        - clusterstatus
+        {{- if .Values.debug }}
+        - --debug
+        - --debug-level
+        - {{ quote .Values.debugLevel }}
+        {{- else }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          privileged: false
+          capabilities:
+            drop:
+            - ALL
+        {{- end }}
       serviceAccountName: fleet-agent
       nodeSelector: {{ include "linux-node-selector" . | nindent 8 }}
 {{- if .Values.fleetAgent.nodeSelector }}
diff --git a/dev/setup-fleet b/dev/setup-fleet
index 38b2832d..89b28f0b 100755
--- a/dev/setup-fleet
+++ b/dev/setup-fleet
@@ -21,9 +21,9 @@ helm -n cattle-fleet-system upgrade --install --create-namespace --wait --reset-
   --set debug=true --set debugLevel=1 fleet charts/fleet
 
 # wait for controller and agent rollout
-kubectl -n cattle-fleet-system rollout status deploy/fleet-controller
+kubectl -n cattle-fleet-system rollout status deployment/fleet-controller
 { grep -E -q -m 1 "fleet-agent-local.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-local -w)
-kubectl -n cattle-fleet-system rollout status deploy/fleet-agent
+kubectl -n cattle-fleet-system rollout status statefulset/fleet-agent
 
 # label local cluster
 kubectl patch clusters.fleet.cattle.io -n fleet-local local --type=json -p '[{"op": "add", "path": "/metadata/labels/management.cattle.io~1cluster-display-name", "value": "local" }]'
diff --git a/dev/setup-rancher-clusters b/dev/setup-rancher-clusters
index 07cca7ce..fc1db5d0 100755
--- a/dev/setup-rancher-clusters
+++ b/dev/setup-rancher-clusters
@@ -45,6 +45,7 @@ kubectl -n cattle-system rollout status deploy/rancher
 # wait for rancher to create fleet namespace, deployment and controller
 { grep -q -m 1 "fleet"; kill $!; } < <(kubectl get deployments -n cattle-fleet-system -w)
 kubectl -n cattle-fleet-system rollout status deploy/fleet-controller
+{ grep -E -q -m 1 "fleet-agent-local.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-local -w)
 
 ./.github/scripts/wait-for-loadbalancer.sh
 
diff --git a/go.mod b/go.mod
index f92a11af..bc4a306e 100644
--- a/go.mod
+++ b/go.mod
@@ -6,33 +6,33 @@ replace (
 	github.com/rancher/fleet/pkg/apis => ./pkg/apis
 
 	helm.sh/helm/v3 => github.com/rancher/helm/v3 v3.12.3-fleet1
-	k8s.io/api => k8s.io/api v0.28.0
-	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.0
-	k8s.io/apimachinery => k8s.io/apimachinery v0.28.0 // indirect
-	k8s.io/apiserver => k8s.io/apiserver v0.28.0
-	k8s.io/cli-runtime => k8s.io/cli-runtime v0.28.0
-	k8s.io/client-go => k8s.io/client-go v0.28.0
-	k8s.io/cloud-provider => k8s.io/cloud-provider v0.28.0
-	k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.28.0
-	k8s.io/code-generator => k8s.io/code-generator v0.28.0
-	k8s.io/component-base => k8s.io/component-base v0.28.0
-	k8s.io/component-helpers => k8s.io/component-helpers v0.28.0
-	k8s.io/controller-manager => k8s.io/controller-manager v0.28.0
-	k8s.io/cri-api => k8s.io/cri-api v0.28.0
-	k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.28.0
-	k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.28.0
-	k8s.io/endpointslice => k8s.io/endpointslice v0.28.0
-	k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.28.0
-	k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.28.0
-	k8s.io/kube-proxy => k8s.io/kube-proxy v0.28.0
-	k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.28.0
-	k8s.io/kubectl => k8s.io/kubectl v0.28.0
-	k8s.io/kubelet => k8s.io/kubelet v0.28.0
-	k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.28.0
-	k8s.io/metrics => k8s.io/metrics v0.28.0
-	k8s.io/mount-utils => k8s.io/mount-utils v0.28.0
-	k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.28.0
-	k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.28.0
+	k8s.io/api => k8s.io/api v0.28.3
+	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.3
+	k8s.io/apimachinery => k8s.io/apimachinery v0.28.3 // indirect
+	k8s.io/apiserver => k8s.io/apiserver v0.28.3
+	k8s.io/cli-runtime => k8s.io/cli-runtime v0.28.3
+	k8s.io/client-go => k8s.io/client-go v0.28.3
+	k8s.io/cloud-provider => k8s.io/cloud-provider v0.28.3
+	k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.28.3
+	k8s.io/code-generator => k8s.io/code-generator v0.28.3
+	k8s.io/component-base => k8s.io/component-base v0.28.3
+	k8s.io/component-helpers => k8s.io/component-helpers v0.28.3
+	k8s.io/controller-manager => k8s.io/controller-manager v0.28.3
+	k8s.io/cri-api => k8s.io/cri-api v0.28.3
+	k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.28.3
+	k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.28.3
+	k8s.io/endpointslice => k8s.io/endpointslice v0.28.3
+	k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.28.3
+	k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.28.3
+	k8s.io/kube-proxy => k8s.io/kube-proxy v0.28.3
+	k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.28.3
+	k8s.io/kubectl => k8s.io/kubectl v0.28.3
+	k8s.io/kubelet => k8s.io/kubelet v0.28.3
+	k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.28.3
+	k8s.io/metrics => k8s.io/metrics v0.28.3
+	k8s.io/mount-utils => k8s.io/mount-utils v0.28.3
+	k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.28.3
+	k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.28.3
 )
 
 require (
@@ -64,20 +64,19 @@ require (
 	golang.org/x/sync v0.4.0
 	gopkg.in/yaml.v2 v2.4.0
 	helm.sh/helm/v3 v3.12.3
-	k8s.io/api v0.28.0
-	k8s.io/apiextensions-apiserver v0.28.0
-	k8s.io/apimachinery v0.28.0
-	k8s.io/cli-runtime v0.28.0
+	k8s.io/api v0.28.3
+	k8s.io/apimachinery v0.28.3
+	k8s.io/cli-runtime v0.28.3
 	k8s.io/client-go v12.0.0+incompatible
 	k8s.io/klog/v2 v2.100.1
-	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9
-	k8s.io/kubernetes v1.28.0
+	k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00
+	k8s.io/kubernetes v1.28.3
 	sigs.k8s.io/cli-utils v0.34.0
-	sigs.k8s.io/controller-runtime v0.13.0
+	sigs.k8s.io/controller-runtime v0.16.1
 	sigs.k8s.io/controller-tools v0.12.0
 	sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3
 	sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3
-	sigs.k8s.io/yaml v1.3.0
+	sigs.k8s.io/yaml v1.4.0
 )
 
 require (
@@ -115,19 +114,21 @@ require (
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
-	github.com/emicklei/go-restful/v3 v3.10.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.7.0 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fatih/color v1.15.0 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.5 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
-	github.com/go-openapi/jsonpointer v0.19.6 // indirect
+	github.com/go-logr/zapr v1.2.4 // indirect
+	github.com/go-openapi/jsonpointer v0.20.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
-	github.com/go-openapi/swag v0.22.3 // indirect
+	github.com/go-openapi/swag v0.22.4 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/gobuffalo/flect v1.0.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
@@ -137,10 +138,10 @@ require (
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
+	github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect
 	github.com/google/s2a-go v0.1.4 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/google/uuid v1.3.0 // indirect
+	github.com/google/uuid v1.3.1 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.11.0 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
@@ -152,7 +153,7 @@ require (
 	github.com/hashicorp/go-safetemp v1.0.0 // indirect
 	github.com/hashicorp/go-version v1.6.0 // indirect
 	github.com/huandu/xstrings v1.4.0 // indirect
-	github.com/imdario/mergo v0.3.15 // indirect
+	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -169,7 +170,7 @@ require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
-	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
@@ -188,10 +189,10 @@ require (
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_golang v1.16.0 // indirect
-	github.com/prometheus/client_model v0.4.0 // indirect
-	github.com/prometheus/common v0.44.0 // indirect
-	github.com/prometheus/procfs v0.10.1 // indirect
+	github.com/prometheus/client_golang v1.17.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.45.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rubenv/sql-migrate v1.3.1 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -211,18 +212,22 @@ require (
 	go.opentelemetry.io/otel v1.14.0 // indirect
 	go.opentelemetry.io/otel/trace v1.14.0 // indirect
 	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.25.0 // indirect
 	golang.org/x/crypto v0.14.0 // indirect
-	golang.org/x/mod v0.12.0 // indirect
+	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
+	golang.org/x/mod v0.13.0 // indirect
 	golang.org/x/net v0.17.0 // indirect
-	golang.org/x/oauth2 v0.10.0 // indirect
+	golang.org/x/oauth2 v0.13.0 // indirect
 	golang.org/x/sys v0.13.0 // indirect
 	golang.org/x/term v0.13.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
-	golang.org/x/tools v0.13.0 // indirect
+	golang.org/x/tools v0.14.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/api v0.126.0 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/appengine v1.6.8 // indirect
 	google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
@@ -231,15 +236,16 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiserver v0.28.0 // indirect
-	k8s.io/code-generator v0.28.0 // indirect
-	k8s.io/component-base v0.28.0 // indirect
-	k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4 // indirect
+	k8s.io/apiextensions-apiserver v0.28.3 // indirect
+	k8s.io/apiserver v0.28.3 // indirect
+	k8s.io/code-generator v0.28.3 // indirect
+	k8s.io/component-base v0.28.3 // indirect
+	k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect
 	k8s.io/kubectl v0.27.3 // indirect
-	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
+	k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
 	oras.land/oras-go v1.2.3 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
 )
 
 replace github.com/json-iterator/go v1.1.10 => github.com/json-iterator/go v1.1.12
diff --git a/go.sum b/go.sum
index 52268f9f..8d38b6d3 100644
--- a/go.sum
+++ b/go.sum
@@ -240,6 +240,9 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl
 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo=
 github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -335,8 +338,8 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arX
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
-github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
-github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -351,8 +354,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
 github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
-github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
+github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -368,8 +371,8 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3
 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
@@ -400,14 +403,16 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
-github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
+github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
+github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
 github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
 github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
 github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
+github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -518,8 +523,9 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0=
+github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
 github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
@@ -527,8 +533,9 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@@ -604,14 +611,13 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -702,8 +708,8 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
 github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
 github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
@@ -792,25 +798,25 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
-github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
+github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 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.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
-github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
-github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rancher/gitjob v0.1.103 h1:s8HB5d1cVMl+XcUMnbGfDrDf1C2P0NJOsVO4ZiebxVM=
 github.com/rancher/gitjob v0.1.103/go.mod h1:ySp7tcP5DOpoTnxFoyCoadiBdXd8rR+urWf1s/0xC10=
@@ -946,16 +952,18 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93V
 go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
-go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
+go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -986,6 +994,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1013,8 +1023,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1102,8 +1112,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri
 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
-golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
-golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
 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=
@@ -1300,8 +1310,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
+golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
 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=
@@ -1311,8 +1321,8 @@ golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNq
 golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
-gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -1370,8 +1380,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1575,35 +1586,35 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 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=
-k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM=
-k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY=
-k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E=
-k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE=
-k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA=
-k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
-k8s.io/apiserver v0.28.0 h1:wVh7bK6Xj7hq+5ntInysTeQRAOqqFoKGUOW2yj8DXrY=
-k8s.io/apiserver v0.28.0/go.mod h1:MvLmtxhQ0Tb1SZk4hfJBjs8iqr5nhYeaFSaoEcz7Lk4=
-k8s.io/cli-runtime v0.28.0 h1:Tcz1nnccXZDNIzoH6EwjCs+7ezkUGhorzCweEvlVOFg=
-k8s.io/cli-runtime v0.28.0/go.mod h1:U+ySmOKBm/JUCmebhmecXeTwNN1RzI7DW4+OM8Oryas=
-k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM=
-k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc=
-k8s.io/code-generator v0.28.0 h1:msdkRVJNVFgdiIJ8REl/d3cZsMB9HByFcWMmn13NyuE=
-k8s.io/code-generator v0.28.0/go.mod h1:ueeSJZJ61NHBa0ccWLey6mwawum25vX61nRZ6WOzN9A=
-k8s.io/component-base v0.28.0 h1:HQKy1enJrOeJlTlN4a6dU09wtmXaUvThC0irImfqyxI=
-k8s.io/component-base v0.28.0/go.mod h1:Yyf3+ZypLfMydVzuLBqJ5V7Kx6WwDr/5cN+dFjw1FNk=
-k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4 h1:aClvVG6GbX10ISHcc24J+tqbr0S7fEe1MWkFJ7cWWCI=
-k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
+k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w=
+k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
+k8s.io/cli-runtime v0.28.3 h1:lvuJYVkwCqHEvpS6KuTZsUVwPePFjBfSGvuaLl2SxzA=
+k8s.io/cli-runtime v0.28.3/go.mod h1:jeX37ZPjIcENVuXDDTskG3+FnVuZms5D9omDXS/2Jjc=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
+k8s.io/code-generator v0.28.3 h1:I847QvdpYx7xKiG2KVQeCSyNF/xU9TowaDAg601mvlw=
+k8s.io/code-generator v0.28.3/go.mod h1:A2EAHTRYvCvBrb/MM2zZBNipeCk3f8NtpdNIKawC43M=
+k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
+k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
+k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks=
+k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
-k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
-k8s.io/kubectl v0.28.0 h1:qhfju0OaU+JGeBlToPeeIg2UJUWP++QwTkpio6nlPKg=
-k8s.io/kubectl v0.28.0/go.mod h1:1We+E5nSX3/TVoSQ6y5Bzld5OhTBHZHlKEYl7g/NaTk=
-k8s.io/kubernetes v1.28.0 h1:p8qq/VoNHnBWinLEi5LO2IvCfzFouN7Jhdz8+L++V+U=
-k8s.io/kubernetes v1.28.0/go.mod h1:rBQpjGYlLBV0KuOLw8EG45N5EBCskWiPpi0xy5liHMI=
-k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
-k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
+k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
+k8s.io/kubectl v0.28.3 h1:H1Peu1O3EbN9zHkJCcvhiJ4NUj6lb88sGPO5wrWIM6k=
+k8s.io/kubectl v0.28.3/go.mod h1:RDAudrth/2wQ3Sg46fbKKl4/g+XImzvbsSRZdP2RiyE=
+k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY=
+k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 oras.land/oras-go v1.2.3 h1:v8PJl+gEAntI1pJ/LCrDgsuk+1PKVavVEPsYIHFE5uY=
 oras.land/oras-go v1.2.3/go.mod h1:M/uaPdYklze0Vf3AakfarnpoEckvw0ESbRdN8Z1vdJg=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@@ -1611,8 +1622,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/cli-utils v0.34.0 h1:zCUitt54f0/MYj/ajVFnG6XSXMhpZ72O/3RewIchW8w=
 sigs.k8s.io/cli-utils v0.34.0/go.mod h1:EXyMwPMu9OL+LRnj0JEMsGG/fRvbgFadcVlSnE8RhFs=
-sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ=
-sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI=
+sigs.k8s.io/controller-runtime v0.16.1 h1:+15lzrmHsE0s2kNl0Dl8cTchI5Cs8qofo5PGcPrV9z0=
+sigs.k8s.io/controller-runtime v0.16.1/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU=
 sigs.k8s.io/controller-tools v0.12.0 h1:TY6CGE6+6hzO7hhJFte65ud3cFmmZW947jajXkuDfBw=
 sigs.k8s.io/controller-tools v0.12.0/go.mod h1:rXlpTfFHZMpZA8aGq9ejArgZiieHd+fkk/fTatY8A2M=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
@@ -1621,8 +1632,8 @@ sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKU
 sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
 sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
 sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/cmd/agent/clusterstatus.go b/internal/cmd/agent/clusterstatus.go
new file mode 100644
index 00000000..cdfd42b6
--- /dev/null
+++ b/internal/cmd/agent/clusterstatus.go
@@ -0,0 +1,155 @@
+package agent
+
+import (
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/util/workqueue"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	command "github.com/rancher/fleet/internal/cmd"
+	"github.com/rancher/fleet/internal/cmd/agent/clusterstatus"
+	"github.com/rancher/fleet/internal/cmd/agent/register"
+	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/durations"
+	"github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io"
+
+	cache2 "github.com/rancher/lasso/pkg/cache"
+	"github.com/rancher/lasso/pkg/client"
+	"github.com/rancher/lasso/pkg/controller"
+	"github.com/rancher/wrangler/v2/pkg/generated/controllers/core"
+	"github.com/rancher/wrangler/v2/pkg/kubeconfig"
+	"github.com/rancher/wrangler/v2/pkg/ratelimit"
+)
+
+func NewClusterStatus() *cobra.Command {
+	cmd := command.Command(&ClusterStatus{}, cobra.Command{
+		Use:   "clusterstatus [flags]",
+		Short: "Continuously report resource status to the upstream cluster",
+	})
+	return cmd
+}
+
+type ClusterStatus struct {
+	UpstreamOptions
+	CheckinInterval string `usage:"How often to post cluster status" env:"CHECKIN_INTERVAL"`
+}
+
+func (cs *ClusterStatus) Run(cmd *cobra.Command, args []string) error {
+	// provide a logger in the context to be compatible with controller-runtime
+	zopts := zap.Options{
+		Development: true,
+	}
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zopts)))
+	ctx := log.IntoContext(cmd.Context(), ctrl.Log)
+
+	var err error
+	var checkinInterval time.Duration
+	if cs.CheckinInterval != "" {
+		checkinInterval, err = time.ParseDuration(cs.CheckinInterval)
+		if err != nil {
+			return err
+		}
+	}
+
+	clientConfig := kubeconfig.GetNonInteractiveClientConfig(cs.Kubeconfig)
+	kc, err := clientConfig.ClientConfig()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	// without rate limiting
+	localConfig := rest.CopyConfig(kc)
+	localConfig.RateLimiter = ratelimit.None
+
+	// cannot start without kubeconfig for upstream cluster
+	setupLog.Info("Fetching kubeconfig for upstream cluster from registration", "namespace", cs.Namespace)
+	agentInfo, err := register.Get(ctx, cs.Namespace, localConfig)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	// set up factory for upstream cluster
+	fleetNamespace, _, err := agentInfo.ClientConfig.Namespace()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetRESTConfig, err := agentInfo.ClientConfig.ClientConfig()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	//  now we have both configs
+	fleetMapper, mapper, _, err := newMappers(ctx, fleetRESTConfig, clientConfig)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetSharedFactory, err := newSharedControllerFactory(fleetRESTConfig, fleetMapper, fleetNamespace)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetFactory, err := fleet.NewFactoryFromConfigWithOptions(fleetRESTConfig, &fleet.FactoryOptions{
+		SharedControllerFactory: fleetSharedFactory,
+	})
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	// set up factory for local cluster
+	localFactory, err := newSharedControllerFactory(localConfig, mapper, "")
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	coreFactory, err := core.NewFactoryFromConfigWithOptions(localConfig, &core.FactoryOptions{
+		SharedControllerFactory: localFactory,
+	})
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	setupLog.Info("Starting cluster status ticker", "checkin interval", checkinInterval.String(), "cluster namespace", agentInfo.ClusterNamespace, "cluster name", agentInfo.ClusterName)
+
+	clusterstatus.Ticker(ctx,
+		cs.Namespace,
+		agentInfo.ClusterNamespace,
+		agentInfo.ClusterName,
+		checkinInterval,
+		coreFactory.Core().V1().Node(),
+		fleetFactory.Fleet().V1alpha1().Cluster(),
+	)
+
+	<-cmd.Context().Done()
+
+	return nil
+}
+
+func newSharedControllerFactory(config *rest.Config, mapper meta.RESTMapper, namespace string) (controller.SharedControllerFactory, error) {
+	cf, err := client.NewSharedClientFactory(config, &client.SharedClientFactoryOptions{
+		Mapper: mapper,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	cacheFactory := cache2.NewSharedCachedFactory(cf, &cache2.SharedCacheFactoryOptions{
+		DefaultNamespace: namespace,
+		DefaultResync:    durations.DefaultResyncAgent,
+	})
+	slowRateLimiter := workqueue.NewItemExponentialFailureRateLimiter(durations.SlowFailureRateLimiterBase, durations.SlowFailureRateLimiterMax)
+
+	return controller.NewSharedControllerFactory(cacheFactory, &controller.SharedControllerFactoryOptions{
+		KindRateLimiter: map[schema.GroupVersionKind]workqueue.RateLimiter{
+			v1alpha1.SchemeGroupVersion.WithKind("BundleDeployment"): slowRateLimiter,
+		},
+	}), nil
+}
diff --git a/internal/cmd/agent/controllers/cluster/cluster.go b/internal/cmd/agent/clusterstatus/ticker.go
similarity index 82%
rename from internal/cmd/agent/controllers/cluster/cluster.go
rename to internal/cmd/agent/clusterstatus/ticker.go
index ab69fbf1..d93e6dc6 100644
--- a/internal/cmd/agent/controllers/cluster/cluster.go
+++ b/internal/cmd/agent/clusterstatus/ticker.go
@@ -1,5 +1,5 @@
-// Package cluster updates the cluster.fleet.cattle.io status in the upstream cluster with the current node status.
-package cluster
+// Package clusterstatus updates the cluster.fleet.cattle.io status in the upstream cluster with the current node status.
+package clusterstatus
 
 import (
 	"context"
@@ -19,27 +19,29 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/equality"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
 type handler struct {
 	agentNamespace   string
 	clusterName      string
 	clusterNamespace string
-	nodes            corecontrollers.NodeCache
+	nodes            corecontrollers.NodeClient
 	clusters         fleetcontrollers.ClusterClient
 	reported         fleet.AgentStatus
 }
 
-func Register(ctx context.Context,
+func Ticker(ctx context.Context,
 	agentNamespace string,
 	clusterNamespace string,
 	clusterName string,
 	checkinInterval time.Duration,
-	nodes corecontrollers.NodeCache,
+	nodes corecontrollers.NodeClient,
 	clusters fleetcontrollers.ClusterClient) {
 
+	logger := log.FromContext(ctx).WithName("clusterstatus").WithValues("cluster", clusterName, "interval", checkinInterval)
+
 	h := handler{
 		agentNamespace:   agentNamespace,
 		clusterName:      clusterName,
@@ -50,6 +52,7 @@ func Register(ctx context.Context,
 
 	go func() {
 		time.Sleep(durations.ClusterRegisterDelay)
+		logger.V(1).Info("Reporting cluster node status once")
 		if err := h.Update(); err != nil {
 			logrus.Errorf("failed to report cluster node status: %v", err)
 		}
@@ -59,6 +62,7 @@ func Register(ctx context.Context,
 			checkinInterval = durations.DefaultClusterCheckInterval
 		}
 		for range ticker.Context(ctx, checkinInterval) {
+			logger.V(1).Info("Reporting cluster node status")
 			if err := h.Update(); err != nil {
 				logrus.Errorf("failed to report cluster node status: %v", err)
 			}
@@ -68,12 +72,12 @@ func Register(ctx context.Context,
 
 // Update the cluster.fleet.cattle.io status in the upstream cluster with the current node status
 func (h *handler) Update() error {
-	nodes, err := h.nodes.List(labels.Everything())
+	nodes, err := h.nodes.List(metav1.ListOptions{})
 	if err != nil {
 		return err
 	}
 
-	ready, nonReady := sortReadyUnready(nodes)
+	ready, nonReady := sortReadyUnready(nodes.Items)
 
 	agentStatus := fleet.AgentStatus{
 		LastSeen:      metav1.Now(),
@@ -114,7 +118,7 @@ func (h *handler) Update() error {
 	return nil
 }
 
-func sortReadyUnready(nodes []*corev1.Node) (ready []string, nonReady []string) {
+func sortReadyUnready(nodes []corev1.Node) (ready []string, nonReady []string) {
 	var (
 		masterNodeNames         []string
 		nonReadyMasterNodeNames []string
diff --git a/internal/cmd/agent/controller.go b/internal/cmd/agent/controller.go
new file mode 100644
index 00000000..7c57e2f9
--- /dev/null
+++ b/internal/cmd/agent/controller.go
@@ -0,0 +1,105 @@
+package agent
+
+import (
+	"context"
+	"time"
+
+	"github.com/rancher/fleet/internal/cmd/agent/controllers"
+	"github.com/rancher/fleet/internal/cmd/agent/register"
+
+	"github.com/sirupsen/logrus"
+
+	"github.com/rancher/lasso/pkg/mapper"
+	"github.com/rancher/wrangler/v2/pkg/kubeconfig"
+	"github.com/rancher/wrangler/v2/pkg/ratelimit"
+	"github.com/rancher/wrangler/v2/pkg/ticker"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/discovery/cached/memory"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/clientcmd"
+)
+
+// defaultNamespace is the namespace to use for resources that don't specify a namespace, e.g. "default"
+const defaultNamespace = "default"
+
+// start the fleet agent
+func start(ctx context.Context, kubeConfig, namespace, agentScope string) error {
+	clientConfig := kubeconfig.GetNonInteractiveClientConfig(kubeConfig)
+	kc, err := clientConfig.ClientConfig()
+	if err != nil {
+		return err
+	}
+
+	agentInfo, err := register.Get(ctx, namespace, kc)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetNamespace, _, err := agentInfo.ClientConfig.Namespace()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetRESTConfig, err := agentInfo.ClientConfig.ClientConfig()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	fleetMapper, mapper, discovery, err := newMappers(ctx, fleetRESTConfig, clientConfig)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	appCtx, err := controllers.NewAppContext(
+		fleetNamespace, namespace, agentInfo.ClusterNamespace, agentInfo.ClusterName,
+		fleetRESTConfig, clientConfig, fleetMapper, mapper, discovery)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	err = controllers.Register(ctx,
+		appCtx,
+		fleetNamespace, defaultNamespace,
+		agentScope)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	if err := appCtx.Start(ctx); err != nil {
+		logrus.Fatal(err)
+	}
+
+	return nil
+}
+
+func newMappers(ctx context.Context, fleetRESTConfig *rest.Config, clientconfig clientcmd.ClientConfig) (meta.RESTMapper, meta.RESTMapper, discovery.CachedDiscoveryInterface, error) {
+	fleetMapper, err := mapper.New(fleetRESTConfig)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	client, err := clientconfig.ClientConfig()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	client.RateLimiter = ratelimit.None
+
+	d, err := discovery.NewDiscoveryClientForConfig(client)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	discovery := memory.NewMemCacheClient(d)
+	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discovery)
+
+	go func() {
+		for range ticker.Context(ctx, 30*time.Second) {
+			discovery.Invalidate()
+			mapper.Reset()
+		}
+	}()
+
+	return fleetMapper, mapper, discovery, nil
+}
diff --git a/internal/cmd/agent/controllers/bundledeployment/controller.go b/internal/cmd/agent/controllers/bundledeployment/controller.go
index 92d3768b..9ad64ddc 100644
--- a/internal/cmd/agent/controllers/bundledeployment/controller.go
+++ b/internal/cmd/agent/controllers/bundledeployment/controller.go
@@ -14,6 +14,9 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/rancher/fleet/internal/cmd/agent/deployer"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/cleanup"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/driftdetect"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/monitor"
 	"github.com/rancher/fleet/internal/cmd/agent/trigger"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
@@ -38,28 +41,39 @@ var nsResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource:
 type handler struct {
 	cleanupOnce sync.Once
 
-	ctx           context.Context
-	trigger       *trigger.Trigger
-	deployManager *deployer.Manager
-	bdController  fleetcontrollers.BundleDeploymentController
-	restMapper    meta.RESTMapper
-	dynamic       dynamic.Interface
+	ctx     context.Context
+	trigger *trigger.Trigger
+
+	Deployer    *deployer.Deployer
+	Monitor     *monitor.Monitor
+	DriftDetect *driftdetect.DriftDetect
+	Cleanup     *cleanup.Cleanup
+
+	bdController fleetcontrollers.BundleDeploymentController
+	restMapper   meta.RESTMapper
+	dynamic      dynamic.Interface
 }
 
 func Register(ctx context.Context,
 	trigger *trigger.Trigger,
 	restMapper meta.RESTMapper,
 	dynamic dynamic.Interface,
-	deployManager *deployer.Manager,
+	deployer *deployer.Deployer,
+	cleanup *cleanup.Cleanup,
+	monitor *monitor.Monitor,
+	driftDetect *driftdetect.DriftDetect,
 	bdController fleetcontrollers.BundleDeploymentController) {
 
 	h := &handler{
-		ctx:           ctx,
-		trigger:       trigger,
-		deployManager: deployManager,
-		bdController:  bdController,
-		restMapper:    restMapper,
-		dynamic:       dynamic,
+		ctx:          ctx,
+		trigger:      trigger,
+		Deployer:     deployer,
+		Cleanup:      cleanup,
+		Monitor:      monitor,
+		DriftDetect:  driftDetect,
+		bdController: bdController,
+		restMapper:   restMapper,
+		dynamic:      dynamic,
 	}
 
 	fleetcontrollers.RegisterBundleDeploymentStatusHandler(ctx,
@@ -75,12 +89,12 @@ func Register(ctx context.Context,
 		h.MonitorBundle)
 
 	bdController.OnChange(ctx, "bundle-trigger", h.Trigger)
-	bdController.OnChange(ctx, "bundle-cleanup", h.Cleanup)
+	bdController.OnChange(ctx, "bundle-cleanup", h.CleanupReleases)
 }
 
 func (h *handler) garbageCollect() {
 	for {
-		if err := h.deployManager.Cleanup(); err != nil {
+		if err := h.Cleanup.Cleanup(); err != nil {
 			logrus.Errorf("failed to cleanup orphaned releases: %v", err)
 		}
 		select {
@@ -91,7 +105,7 @@ func (h *handler) garbageCollect() {
 	}
 }
 
-func (h *handler) Cleanup(key string, bd *fleet.BundleDeployment) (*fleet.BundleDeployment, error) {
+func (h *handler) CleanupReleases(key string, bd *fleet.BundleDeployment) (*fleet.BundleDeployment, error) {
 	h.cleanupOnce.Do(func() {
 		go h.garbageCollect()
 	})
@@ -99,7 +113,7 @@ func (h *handler) Cleanup(key string, bd *fleet.BundleDeployment) (*fleet.Bundle
 	if bd != nil {
 		return bd, nil
 	}
-	return nil, h.deployManager.Delete(key)
+	return nil, h.Deployer.Delete(key)
 }
 
 func (h *handler) DeployBundle(bd *fleet.BundleDeployment, status fleet.BundleDeploymentStatus) (fleet.BundleDeploymentStatus, error) {
@@ -115,7 +129,7 @@ func (h *handler) DeployBundle(bd *fleet.BundleDeployment, status fleet.BundleDe
 	}
 
 	logrus.Infof("Deploying bundle %s/%s", bd.Namespace, bd.Name)
-	release, err := h.deployManager.Deploy(bd)
+	release, err := h.Deployer.Deploy(bd)
 	if err != nil {
 		// When an error from DeployBundle is returned it causes DeployBundle
 		// to requeue and keep trying to deploy on a loop. If there is something
@@ -356,7 +370,7 @@ func (h *handler) Trigger(key string, bd *fleet.BundleDeployment) (*fleet.Bundle
 
 	logrus.Debugf("Triggering for bundledeployment '%s'", key)
 
-	resources, err := h.deployManager.AllResources(bd)
+	resources, err := h.DriftDetect.AllResources(bd)
 	if err != nil {
 		return bd, err
 	}
@@ -436,7 +450,7 @@ func (h *handler) MonitorBundle(bd *fleet.BundleDeployment, status fleet.BundleD
 		return status, nil
 	}
 
-	err := h.deployManager.UpdateBundleDeploymentStatus(h.restMapper, bd)
+	err := h.Monitor.UpdateBundleDeploymentStatus(h.restMapper, bd)
 	if err != nil {
 
 		// Returning an error will cause MonitorBundle to requeue in a loop.
diff --git a/internal/cmd/agent/controllers/controllers.go b/internal/cmd/agent/controllers/controllers.go
index f921495f..7d2d5eb3 100644
--- a/internal/cmd/agent/controllers/controllers.go
+++ b/internal/cmd/agent/controllers/controllers.go
@@ -3,15 +3,16 @@ package controllers
 
 import (
 	"context"
-	"time"
 
 	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/util/workqueue"
 
 	"github.com/rancher/fleet/internal/cmd/agent/controllers/bundledeployment"
-	"github.com/rancher/fleet/internal/cmd/agent/controllers/cluster"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/cleanup"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/driftdetect"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/monitor"
 	"github.com/rancher/fleet/internal/cmd/agent/trigger"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	"github.com/rancher/fleet/internal/manifest"
@@ -75,8 +76,7 @@ func (a *AppContext) Start(ctx context.Context) error {
 
 func Register(ctx context.Context,
 	appCtx *AppContext,
-	fleetNamespace, defaultNamespace, agentScope string,
-	checkinInterval time.Duration) error {
+	fleetNamespace, defaultNamespace, agentScope string) error {
 
 	labelPrefix := "fleet"
 	if defaultNamespace != "" {
@@ -93,26 +93,29 @@ func Register(ctx context.Context,
 		trigger.New(ctx, appCtx.restMapper, appCtx.Dynamic),
 		appCtx.restMapper,
 		appCtx.Dynamic,
-		deployer.NewManager(
+		deployer.New(
+			manifest.NewLookup(appCtx.Fleet.Content()),
+			helmDeployer),
+		cleanup.New(
 			fleetNamespace,
 			defaultNamespace,
-			labelPrefix,
-			agentScope,
 			appCtx.Fleet.BundleDeployment().Cache(),
 			appCtx.Fleet.BundleDeployment(),
-			manifest.NewLookup(appCtx.Fleet.Content()),
+			helmDeployer),
+		monitor.New(
+			defaultNamespace,
+			labelPrefix,
+			agentScope,
+			helmDeployer,
+			appCtx.Apply),
+		driftdetect.New(
+			defaultNamespace,
+			labelPrefix,
+			agentScope,
 			helmDeployer,
 			appCtx.Apply),
 		appCtx.Fleet.BundleDeployment())
 
-	cluster.Register(ctx,
-		appCtx.AgentNamespace,
-		appCtx.ClusterNamespace,
-		appCtx.ClusterName,
-		checkinInterval,
-		appCtx.Core.Node().Cache(),
-		appCtx.Fleet.Cluster())
-
 	return nil
 }
 
diff --git a/internal/cmd/agent/deployer/cleanup/cleanup.go b/internal/cmd/agent/deployer/cleanup/cleanup.go
new file mode 100644
index 00000000..c9944ffa
--- /dev/null
+++ b/internal/cmd/agent/deployer/cleanup/cleanup.go
@@ -0,0 +1,81 @@
+package cleanup
+
+import (
+	"github.com/rancher/fleet/internal/helmdeployer"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	fleetcontrollers "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
+
+	"github.com/sirupsen/logrus"
+
+	apierror "k8s.io/apimachinery/pkg/api/errors"
+)
+
+type Cleanup struct {
+	fleetNamespace             string
+	defaultNamespace           string
+	bundleDeploymentCache      fleetcontrollers.BundleDeploymentCache
+	deployer                   *helmdeployer.Helm
+	bundleDeploymentController fleetcontrollers.BundleDeploymentController
+}
+
+func New(fleetNamespace string,
+	defaultNamespace string,
+	bundleDeploymentCache fleetcontrollers.BundleDeploymentCache,
+	bundleDeploymentController fleetcontrollers.BundleDeploymentController,
+	deployer *helmdeployer.Helm) *Cleanup {
+	return &Cleanup{
+		fleetNamespace:             fleetNamespace,
+		defaultNamespace:           defaultNamespace,
+		bundleDeploymentCache:      bundleDeploymentCache,
+		deployer:                   deployer,
+		bundleDeploymentController: bundleDeploymentController,
+	}
+}
+
+func (m *Cleanup) Cleanup() error {
+	deployed, err := m.deployer.ListDeployments()
+	if err != nil {
+		return err
+	}
+
+	for _, deployed := range deployed {
+		bundleDeployment, err := m.bundleDeploymentCache.Get(m.fleetNamespace, deployed.BundleID)
+		if apierror.IsNotFound(err) {
+			// found a helm secret, but no bundle deployment, so uninstall the release
+			logrus.Infof("Deleting orphan bundle ID %s, release %s", deployed.BundleID, deployed.ReleaseName)
+			if err := m.deployer.Delete(deployed.BundleID, deployed.ReleaseName); err != nil {
+				return err
+			}
+
+			return nil
+		} else if err != nil {
+			return err
+		}
+
+		key := m.releaseKey(bundleDeployment)
+		if key != deployed.ReleaseName {
+			// found helm secret and bundle deployment for BundleID, but release name doesn't match, so delete the release
+			logrus.Infof("Deleting unknown bundle ID %s, release %s, expecting release %s", deployed.BundleID, deployed.ReleaseName, key)
+			if err := m.deployer.Delete(deployed.BundleID, deployed.ReleaseName); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// releaseKey returns a deploymentKey from namespace+releaseName
+func (m *Cleanup) releaseKey(bd *fleet.BundleDeployment) string {
+	ns := m.defaultNamespace
+	if bd.Spec.Options.TargetNamespace != "" {
+		ns = bd.Spec.Options.TargetNamespace
+	} else if bd.Spec.Options.DefaultNamespace != "" {
+		ns = bd.Spec.Options.DefaultNamespace
+	}
+
+	if bd.Spec.Options.Helm == nil || bd.Spec.Options.Helm.ReleaseName == "" {
+		return ns + "/" + bd.Name
+	}
+	return ns + "/" + bd.Spec.Options.Helm.ReleaseName
+}
diff --git a/internal/cmd/agent/deployer/deployer.go b/internal/cmd/agent/deployer/deployer.go
new file mode 100644
index 00000000..14af2166
--- /dev/null
+++ b/internal/cmd/agent/deployer/deployer.go
@@ -0,0 +1,51 @@
+package deployer
+
+import (
+	"github.com/rancher/fleet/internal/helmdeployer"
+	"github.com/rancher/fleet/internal/manifest"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	"github.com/rancher/wrangler/v2/pkg/kv"
+)
+
+type Deployer struct {
+	lookup   manifest.Lookup
+	deployer *helmdeployer.Helm
+}
+
+func New(lookup manifest.Lookup, deployer *helmdeployer.Helm) *Deployer {
+	return &Deployer{
+		lookup:   lookup,
+		deployer: deployer,
+	}
+}
+
+func (m *Deployer) Delete(bundleDeploymentKey string) error {
+	_, name := kv.RSplit(bundleDeploymentKey, "/")
+	return m.deployer.Delete(name, "")
+}
+
+// Deploy the bundle deployment, i.e. with helmdeployer.
+// This loads the manifest and the contents from the upstream cluster.
+func (m *Deployer) Deploy(bd *fleet.BundleDeployment) (string, error) {
+	if bd.Spec.DeploymentID == bd.Status.AppliedDeploymentID {
+		if ok, err := m.deployer.EnsureInstalled(bd.Name, bd.Status.Release); err != nil {
+			return "", err
+		} else if ok {
+			return bd.Status.Release, nil
+		}
+	}
+	manifestID, _ := kv.Split(bd.Spec.DeploymentID, ":")
+	manifest, err := m.lookup.Get(manifestID)
+	if err != nil {
+		return "", err
+	}
+
+	manifest.Commit = bd.Labels["fleet.cattle.io/commit"]
+	resource, err := m.deployer.Deploy(bd.Name, manifest, bd.Spec.Options)
+	if err != nil {
+		return "", err
+	}
+
+	return resource.ID, nil
+}
diff --git a/internal/cmd/agent/deployer/driftdetect/driftdetect.go b/internal/cmd/agent/deployer/driftdetect/driftdetect.go
new file mode 100644
index 00000000..1e19c242
--- /dev/null
+++ b/internal/cmd/agent/deployer/driftdetect/driftdetect.go
@@ -0,0 +1,70 @@
+package driftdetect
+
+import (
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/plan"
+	"github.com/rancher/fleet/internal/helmdeployer"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	"github.com/rancher/wrangler/v2/pkg/apply"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+type DriftDetect struct {
+	defaultNamespace string
+	deployer         *helmdeployer.Helm
+	apply            apply.Apply
+	labelPrefix      string
+	labelSuffix      string
+}
+
+func New(defaultNamespace string,
+	labelPrefix, labelSuffix string,
+	deployer *helmdeployer.Helm,
+	apply apply.Apply) *DriftDetect {
+	return &DriftDetect{
+		defaultNamespace: defaultNamespace,
+		labelPrefix:      labelPrefix,
+		labelSuffix:      labelSuffix,
+		deployer:         deployer,
+		apply:            apply.WithDynamicLookup(),
+	}
+}
+
+// AllResources returns the resources that are deployed by the bundle deployment,
+// according to the helm release history. It adds to be deleted resources to
+// the list, by comparing the desired state to the actual state with apply.
+func (m *DriftDetect) AllResources(bd *fleet.BundleDeployment) (*helmdeployer.Resources, error) {
+	resources, err := m.deployer.Resources(bd.Name, bd.Status.Release)
+	if err != nil {
+		return nil, nil
+	}
+
+	ns := resources.DefaultNamespace
+	if ns == "" {
+		ns = m.defaultNamespace
+	}
+	apply := plan.GetApply(m.apply, plan.Options{
+		LabelPrefix:      m.labelPrefix,
+		LabelSuffix:      m.labelSuffix,
+		DefaultNamespace: ns,
+		Name:             bd.Name,
+	})
+
+	plan, err := plan.Plan(apply, bd, resources.DefaultNamespace, resources.Objects...)
+	if err != nil {
+		return nil, err
+	}
+
+	for gvk, keys := range plan.Delete {
+		for _, key := range keys {
+			u := &unstructured.Unstructured{}
+			u.SetGroupVersionKind(gvk)
+			u.SetNamespace(key.Namespace)
+			u.SetName(key.Name)
+			resources.Objects = append(resources.Objects, u)
+		}
+	}
+
+	return resources, nil
+}
diff --git a/internal/cmd/agent/deployer/manager.go b/internal/cmd/agent/deployer/manager.go
deleted file mode 100644
index 91d70f01..00000000
--- a/internal/cmd/agent/deployer/manager.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package deployer
-
-import (
-	"github.com/sirupsen/logrus"
-
-	"github.com/rancher/fleet/internal/helmdeployer"
-	"github.com/rancher/fleet/internal/manifest"
-	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-	fleetcontrollers "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
-	"github.com/rancher/wrangler/v2/pkg/apply"
-	"github.com/rancher/wrangler/v2/pkg/kv"
-	apierror "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-)
-
-type Manager struct {
-	fleetNamespace             string
-	defaultNamespace           string
-	bundleDeploymentCache      fleetcontrollers.BundleDeploymentCache
-	lookup                     manifest.Lookup
-	deployer                   *helmdeployer.Helm
-	apply                      apply.Apply
-	labelPrefix                string
-	labelSuffix                string
-	bundleDeploymentController fleetcontrollers.BundleDeploymentController
-}
-
-func NewManager(fleetNamespace string,
-	defaultNamespace string,
-	labelPrefix, labelSuffix string,
-	bundleDeploymentCache fleetcontrollers.BundleDeploymentCache,
-	bundleDeploymentController fleetcontrollers.BundleDeploymentController,
-	lookup manifest.Lookup,
-	deployer *helmdeployer.Helm,
-	apply apply.Apply) *Manager {
-	return &Manager{
-		fleetNamespace:             fleetNamespace,
-		defaultNamespace:           defaultNamespace,
-		labelPrefix:                labelPrefix,
-		labelSuffix:                labelSuffix,
-		bundleDeploymentCache:      bundleDeploymentCache,
-		lookup:                     lookup,
-		deployer:                   deployer,
-		apply:                      apply.WithDynamicLookup(),
-		bundleDeploymentController: bundleDeploymentController,
-	}
-}
-
-// releaseKey returns a deploymentKey from namespace+releaseName
-func (m *Manager) releaseKey(bd *fleet.BundleDeployment) string {
-	ns := m.defaultNamespace
-	if bd.Spec.Options.TargetNamespace != "" {
-		ns = bd.Spec.Options.TargetNamespace
-	} else if bd.Spec.Options.DefaultNamespace != "" {
-		ns = bd.Spec.Options.DefaultNamespace
-	}
-
-	if bd.Spec.Options.Helm == nil || bd.Spec.Options.Helm.ReleaseName == "" {
-		return ns + "/" + bd.Name
-	}
-	return ns + "/" + bd.Spec.Options.Helm.ReleaseName
-}
-
-func (m *Manager) Cleanup() error {
-	deployed, err := m.deployer.ListDeployments()
-	if err != nil {
-		return err
-	}
-
-	for _, deployed := range deployed {
-		bundleDeployment, err := m.bundleDeploymentCache.Get(m.fleetNamespace, deployed.BundleID)
-		if apierror.IsNotFound(err) {
-			// found a helm secret, but no bundle deployment, so uninstall the release
-			logrus.Infof("Deleting orphan bundle ID %s, release %s", deployed.BundleID, deployed.ReleaseName)
-			if err := m.deployer.Delete(deployed.BundleID, deployed.ReleaseName); err != nil {
-				return err
-			}
-
-			return nil
-		} else if err != nil {
-			return err
-		}
-
-		key := m.releaseKey(bundleDeployment)
-		if key != deployed.ReleaseName {
-			// found helm secret and bundle deployment for BundleID, but release name doesn't match, so delete the release
-			logrus.Infof("Deleting unknown bundle ID %s, release %s, expecting release %s", deployed.BundleID, deployed.ReleaseName, key)
-			if err := m.deployer.Delete(deployed.BundleID, deployed.ReleaseName); err != nil {
-				return err
-			}
-		}
-	}
-
-	return nil
-}
-
-func (m *Manager) Delete(bundleDeploymentKey string) error {
-	_, name := kv.RSplit(bundleDeploymentKey, "/")
-	return m.deployer.Delete(name, "")
-}
-
-// AllResources returns the resources that are deployed by the bundle deployment,
-// according to the helm release history. It adds to be deleted resources to
-// the list, by comparing the desired state to the actual state with apply.
-func (m *Manager) AllResources(bd *fleet.BundleDeployment) (*helmdeployer.Resources, error) {
-	resources, err := m.deployer.Resources(bd.Name, bd.Status.Release)
-	if err != nil {
-		return nil, nil
-	}
-
-	plan, err := m.plan(bd, resources.DefaultNamespace, resources.Objects...)
-	if err != nil {
-		return nil, err
-	}
-
-	for gvk, keys := range plan.Delete {
-		for _, key := range keys {
-			u := &unstructured.Unstructured{}
-			u.SetGroupVersionKind(gvk)
-			u.SetNamespace(key.Namespace)
-			u.SetName(key.Name)
-			resources.Objects = append(resources.Objects, u)
-		}
-	}
-
-	return resources, nil
-}
-
-// Deploy the bundle deployment, i.e. with helmdeployer.
-// This loads the manifest and the contents from the upstream cluster.
-func (m *Manager) Deploy(bd *fleet.BundleDeployment) (string, error) {
-	if bd.Spec.DeploymentID == bd.Status.AppliedDeploymentID {
-		if ok, err := m.deployer.EnsureInstalled(bd.Name, bd.Status.Release); err != nil {
-			return "", err
-		} else if ok {
-			return bd.Status.Release, nil
-		}
-	}
-	manifestID, _ := kv.Split(bd.Spec.DeploymentID, ":")
-	manifest, err := m.lookup.Get(manifestID)
-	if err != nil {
-		return "", err
-	}
-
-	manifest.Commit = bd.Labels["fleet.cattle.io/commit"]
-	resource, err := m.deployer.Deploy(bd.Name, manifest, bd.Spec.Options)
-	if err != nil {
-		return "", err
-	}
-
-	return resource.ID, nil
-}
diff --git a/internal/cmd/agent/deployer/monitor.go b/internal/cmd/agent/deployer/monitor/monitor.go
similarity index 64%
rename from internal/cmd/agent/deployer/monitor.go
rename to internal/cmd/agent/deployer/monitor/monitor.go
index 388cbdbf..97213e54 100644
--- a/internal/cmd/agent/deployer/monitor.go
+++ b/internal/cmd/agent/deployer/monitor/monitor.go
@@ -1,24 +1,20 @@
-package deployer
+package monitor
 
 import (
-	"encoding/json"
 	"fmt"
 	"sort"
 
-	jsonpatch "github.com/evanphx/json-patch"
-
 	"github.com/pkg/errors"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diff"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diffnormalize"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/resource"
-	fleetnorm "github.com/rancher/fleet/internal/cmd/agent/deployer/normalizers"
+	"github.com/sirupsen/logrus"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/plan"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	fleetcontrollers "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
+
 	"github.com/rancher/wrangler/v2/pkg/apply"
-	"github.com/rancher/wrangler/v2/pkg/merr"
 	"github.com/rancher/wrangler/v2/pkg/objectset"
 	"github.com/rancher/wrangler/v2/pkg/summary"
-	"github.com/sirupsen/logrus"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -26,130 +22,31 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
-// plan first does a dry run of the apply to get the difference between the
-// desired and live state. It relies on the bundledeployment's bundle diff
-// patches to ignore changes.
-func (m *Manager) plan(bd *fleet.BundleDeployment, ns string, objs ...runtime.Object) (apply.Plan, error) {
-	if ns == "" {
-		ns = m.defaultNamespace
-	}
-
-	a := m.getApply(bd, ns)
-	plan, err := a.DryRun(objs...)
-	if err != nil {
-		return plan, err
-	}
-
-	desired := objectset.NewObjectSet(objs...).ObjectsByGVK()
-	live := objectset.NewObjectSet(plan.Objects...).ObjectsByGVK()
-
-	norms, err := m.normalizers(live, bd)
-	if err != nil {
-		return plan, err
-	}
-
-	var errs []error
-	for gvk, objs := range plan.Update {
-		for key := range objs {
-			desiredObj := desired[gvk][key]
-			if desiredObj == nil {
-				desiredKey := key
-				// if different namespace options to guess if resource is namespaced or not
-				if desiredKey.Namespace == "" {
-					desiredKey.Namespace = ns
-				} else {
-					desiredKey.Namespace = ""
-				}
-				desiredObj = desired[gvk][desiredKey]
-				if desiredObj == nil {
-					continue
-				}
-			}
-			desiredObj.(*unstructured.Unstructured).SetNamespace(key.Namespace)
-
-			actualObj := live[gvk][key]
-			if actualObj == nil {
-				continue
-			}
-
-			diffResult, err := diff.Diff(desiredObj.(*unstructured.Unstructured), actualObj.(*unstructured.Unstructured),
-				diff.WithNormalizer(norms),
-				diff.IgnoreAggregatedRoles(true))
-			if err != nil {
-				errs = append(errs, err)
-				continue
-			}
-			if !diffResult.Modified {
-				delete(plan.Update[gvk], key)
-				continue
-			}
-			patch, err := jsonpatch.CreateMergePatch(diffResult.NormalizedLive, diffResult.PredictedLive)
-			if err != nil {
-				errs = append(errs, err)
-				continue
-			}
-			plan.Update.Add(gvk, key.Namespace, key.Name, string(patch))
-		}
-		if len(errs) > 0 {
-			return plan, merr.NewErrors(errs...)
-		}
-	}
-	return plan, nil
+type Monitor struct {
+	defaultNamespace           string
+	deployer                   *helmdeployer.Helm
+	apply                      apply.Apply
+	labelPrefix                string
+	labelSuffix                string
+	bundleDeploymentController fleetcontrollers.BundleDeploymentController
 }
 
-func (m *Manager) normalizers(live objectset.ObjectByGVK, bd *fleet.BundleDeployment) (diff.Normalizer, error) {
-	var ignore []resource.ResourceIgnoreDifferences
-	jsonPatchNorm := &fleetnorm.JSONPatchNormalizer{}
-	if bd.Spec.Options.Diff != nil {
-		for _, patch := range bd.Spec.Options.Diff.ComparePatches {
-			groupVersion, err := schema.ParseGroupVersion(patch.APIVersion)
-			if err != nil {
-				return nil, err
-			}
-			ignore = append(ignore, resource.ResourceIgnoreDifferences{
-				Namespace:    patch.Namespace,
-				Name:         patch.Name,
-				Kind:         patch.Kind,
-				Group:        groupVersion.Group,
-				JSONPointers: patch.JsonPointers,
-			})
-
-			for _, op := range patch.Operations {
-				// compile each operation by itself so that one failing operation doesn't block the others
-				patchData, err := json.Marshal([]interface{}{op})
-				if err != nil {
-					return nil, err
-				}
-				gvk := schema.FromAPIVersionAndKind(patch.APIVersion, patch.Kind)
-				key := objectset.ObjectKey{
-					Name:      patch.Name,
-					Namespace: patch.Namespace,
-				}
-				jsonPatchNorm.Add(gvk, key, patchData)
-			}
-		}
-	}
-
-	ignoreNorm, err := diffnormalize.NewDiffNormalizer(ignore, nil)
-	if err != nil {
-		return nil, err
+func New(defaultNamespace string,
+	labelPrefix, labelSuffix string,
+	deployer *helmdeployer.Helm,
+	apply apply.Apply) *Monitor {
+	return &Monitor{
+		defaultNamespace: defaultNamespace,
+		labelPrefix:      labelPrefix,
+		labelSuffix:      labelSuffix,
+		deployer:         deployer,
+		apply:            apply.WithDynamicLookup(),
 	}
-
-	norm := fleetnorm.New(live, ignoreNorm, jsonPatchNorm)
-	return norm, nil
-}
-
-func (m *Manager) getApply(bd *fleet.BundleDeployment, ns string) apply.Apply {
-	apply := m.apply
-	return apply.
-		WithIgnorePreviousApplied().
-		WithSetID(helmdeployer.GetSetID(bd.Name, m.labelPrefix, m.labelSuffix)).
-		WithDefaultNamespace(ns)
 }
 
 // UpdateBundleDeploymentStatus updates the status with information from the
 // helm release history and an apply dry run.
-func (m *Manager) UpdateBundleDeploymentStatus(mapper meta.RESTMapper, bd *fleet.BundleDeployment) error {
+func (m *Monitor) UpdateBundleDeploymentStatus(mapper meta.RESTMapper, bd *fleet.BundleDeployment) error {
 	resources, err := m.deployer.Resources(bd.Name, bd.Status.Release)
 	if err != nil {
 		return err
@@ -159,7 +56,18 @@ func (m *Manager) UpdateBundleDeploymentStatus(mapper meta.RESTMapper, bd *fleet
 		return err
 	}
 
-	plan, err := m.plan(bd, resources.DefaultNamespace, resources.Objects...)
+	ns := resources.DefaultNamespace
+	if ns == "" {
+		ns = m.defaultNamespace
+	}
+	apply := plan.GetApply(m.apply, plan.Options{
+		LabelPrefix:      m.labelPrefix,
+		LabelSuffix:      m.labelSuffix,
+		DefaultNamespace: ns,
+		Name:             bd.Name,
+	})
+
+	plan, err := plan.Plan(apply, bd, resources.DefaultNamespace, resources.Objects...)
 	if err != nil {
 		return err
 	}
@@ -212,18 +120,6 @@ func (m *Manager) UpdateBundleDeploymentStatus(mapper meta.RESTMapper, bd *fleet
 	return nil
 }
 
-func isNamespaced(mapper meta.RESTMapper, gvk schema.GroupVersionKind) bool {
-	mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
-	if err != nil {
-		return true
-	}
-	return mapping.Scope.Name() == meta.RESTScopeNameNamespace
-}
-
-func sortKey(f fleet.ModifiedStatus) string {
-	return f.APIVersion + "/" + f.Kind + "/" + f.Namespace + "/" + f.Name
-}
-
 func modified(plan apply.Plan, resourcesPreviousRelease *helmdeployer.Resources) (result []fleet.ModifiedStatus) {
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
@@ -383,3 +279,15 @@ func shouldExcludeCondition(conditions map[string]interface{}, ignoredConditions
 
 	return true
 }
+
+func isNamespaced(mapper meta.RESTMapper, gvk schema.GroupVersionKind) bool {
+	mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+	if err != nil {
+		return true
+	}
+	return mapping.Scope.Name() == meta.RESTScopeNameNamespace
+}
+
+func sortKey(f fleet.ModifiedStatus) string {
+	return f.APIVersion + "/" + f.Kind + "/" + f.Namespace + "/" + f.Name
+}
diff --git a/internal/cmd/agent/deployer/plan/plan.go b/internal/cmd/agent/deployer/plan/plan.go
new file mode 100644
index 00000000..336d728c
--- /dev/null
+++ b/internal/cmd/agent/deployer/plan/plan.go
@@ -0,0 +1,144 @@
+package plan
+
+import (
+	"encoding/json"
+
+	jsonpatch "github.com/evanphx/json-patch"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diff"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diffnormalize"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/resource"
+	fleetnorm "github.com/rancher/fleet/internal/cmd/agent/deployer/normalizers"
+	"github.com/rancher/fleet/internal/helmdeployer"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	"github.com/rancher/wrangler/v2/pkg/apply"
+	"github.com/rancher/wrangler/v2/pkg/merr"
+	"github.com/rancher/wrangler/v2/pkg/objectset"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+type Options struct {
+	LabelPrefix      string
+	LabelSuffix      string
+	DefaultNamespace string
+	Name             string
+}
+
+func GetApply(apply apply.Apply, opts Options) apply.Apply {
+	return apply.
+		WithIgnorePreviousApplied().
+		WithSetID(helmdeployer.GetSetID(opts.Name, opts.LabelPrefix, opts.LabelSuffix)).
+		WithDefaultNamespace(opts.DefaultNamespace)
+}
+
+// Plan first does a dry run of the apply to get the difference between the
+// desired and live state. It relies on the bundledeployment's bundle diff
+// patches to ignore changes.
+func Plan(a apply.Apply, bd *fleet.BundleDeployment, ns string, objs ...runtime.Object) (apply.Plan, error) {
+	plan, err := a.DryRun(objs...)
+	if err != nil {
+		return plan, err
+	}
+
+	desired := objectset.NewObjectSet(objs...).ObjectsByGVK()
+	live := objectset.NewObjectSet(plan.Objects...).ObjectsByGVK()
+
+	norms, err := normalizers(live, bd)
+	if err != nil {
+		return plan, err
+	}
+
+	var errs []error
+	for gvk, objs := range plan.Update {
+		for key := range objs {
+			desiredObj := desired[gvk][key]
+			if desiredObj == nil {
+				desiredKey := key
+				// if different namespace options to guess if resource is namespaced or not
+				if desiredKey.Namespace == "" {
+					desiredKey.Namespace = ns
+				} else {
+					desiredKey.Namespace = ""
+				}
+				desiredObj = desired[gvk][desiredKey]
+				if desiredObj == nil {
+					continue
+				}
+			}
+			desiredObj.(*unstructured.Unstructured).SetNamespace(key.Namespace)
+
+			actualObj := live[gvk][key]
+			if actualObj == nil {
+				continue
+			}
+
+			diffResult, err := diff.Diff(desiredObj.(*unstructured.Unstructured), actualObj.(*unstructured.Unstructured),
+				diff.WithNormalizer(norms),
+				diff.IgnoreAggregatedRoles(true))
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+			if !diffResult.Modified {
+				delete(plan.Update[gvk], key)
+				continue
+			}
+			patch, err := jsonpatch.CreateMergePatch(diffResult.NormalizedLive, diffResult.PredictedLive)
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+			plan.Update.Add(gvk, key.Namespace, key.Name, string(patch))
+		}
+		if len(errs) > 0 {
+			return plan, merr.NewErrors(errs...)
+		}
+	}
+	return plan, nil
+}
+
+func normalizers(live objectset.ObjectByGVK, bd *fleet.BundleDeployment) (diff.Normalizer, error) {
+	var ignore []resource.ResourceIgnoreDifferences
+	jsonPatchNorm := &fleetnorm.JSONPatchNormalizer{}
+	if bd.Spec.Options.Diff != nil {
+		for _, patch := range bd.Spec.Options.Diff.ComparePatches {
+			groupVersion, err := schema.ParseGroupVersion(patch.APIVersion)
+			if err != nil {
+				return nil, err
+			}
+			ignore = append(ignore, resource.ResourceIgnoreDifferences{
+				Namespace:    patch.Namespace,
+				Name:         patch.Name,
+				Kind:         patch.Kind,
+				Group:        groupVersion.Group,
+				JSONPointers: patch.JsonPointers,
+			})
+
+			for _, op := range patch.Operations {
+				// compile each operation by itself so that one failing operation doesn't block the others
+				patchData, err := json.Marshal([]interface{}{op})
+				if err != nil {
+					return nil, err
+				}
+				gvk := schema.FromAPIVersionAndKind(patch.APIVersion, patch.Kind)
+				key := objectset.ObjectKey{
+					Name:      patch.Name,
+					Namespace: patch.Namespace,
+				}
+				jsonPatchNorm.Add(gvk, key, patchData)
+			}
+		}
+	}
+
+	ignoreNorm, err := diffnormalize.NewDiffNormalizer(ignore, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	norm := fleetnorm.New(live, ignoreNorm, jsonPatchNorm)
+	return norm, nil
+}
diff --git a/internal/cmd/agent/register.go b/internal/cmd/agent/register.go
new file mode 100644
index 00000000..fc4e09aa
--- /dev/null
+++ b/internal/cmd/agent/register.go
@@ -0,0 +1,67 @@
+package agent
+
+import (
+	"context"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	command "github.com/rancher/fleet/internal/cmd"
+	"github.com/rancher/fleet/internal/cmd/agent/register"
+	"github.com/rancher/wrangler/v2/pkg/kubeconfig"
+)
+
+func NewRegister() *cobra.Command {
+	cmd := command.Command(&Register{}, cobra.Command{
+		Use:   "register [flags]",
+		Short: "Register agent with an upstream cluster",
+	})
+	return cmd
+}
+
+type Register struct {
+	UpstreamOptions
+}
+
+func (r *Register) Run(cmd *cobra.Command, args []string) error {
+	// provide a logger in the context to be compatible with controller-runtime
+	zopts := zap.Options{
+		Development: true,
+	}
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zopts)))
+	ctx := log.IntoContext(cmd.Context(), ctrl.Log)
+
+	clientConfig := kubeconfig.GetNonInteractiveClientConfig(r.Kubeconfig)
+	kc, err := clientConfig.ClientConfig()
+	if err != nil {
+		return err
+	}
+
+	setupLog.Info("starting registration on upstream cluster", "namespace", r.Namespace)
+
+	ctx, cancel := context.WithCancel(ctx)
+	// try to register with upstream fleet controller by obtaining
+	// a kubeconfig for the upstream cluster
+	agentInfo, err := register.Register(ctx, r.Namespace, kc)
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	ns, _, err := agentInfo.ClientConfig.Namespace()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	_, err = agentInfo.ClientConfig.ClientConfig()
+	if err != nil {
+		logrus.Fatal(err)
+	}
+
+	setupLog.Info("successfully registered with upstream cluster", "namespace", ns)
+	cancel()
+
+	return nil
+}
diff --git a/internal/cmd/agent/register/register.go b/internal/cmd/agent/register/register.go
index 8fac2ea5..8412e88d 100644
--- a/internal/cmd/agent/register/register.go
+++ b/internal/cmd/agent/register/register.go
@@ -41,14 +41,52 @@ const (
 )
 
 type AgentInfo struct {
+	// ClusterNamespace is the namespace on upstream, e.g. cluster-fleet-ID
 	ClusterNamespace string
 	ClusterName      string
 	ClientConfig     clientcmd.ClientConfig
 }
 
-func Register(ctx context.Context, namespace, clusterID string, config *rest.Config) (*AgentInfo, error) {
+// Get a registration token from the local cluster and fail if it does not exist.
+// This does not create a new registration process and only works after
+// registration has been completed.
+func Get(ctx context.Context, namespace string, cfg *rest.Config) (*AgentInfo, error) {
+	cfg = rest.CopyConfig(cfg)
+	// disable the rate limiter
+	cfg.RateLimiter = ratelimit.None
+	k8s, err := core.NewFactoryFromConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	secret, err := k8s.Core().V1().Secret().Get(namespace, CredName, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		logrus.Warn("Cannot find fleet-agent secret")
+		return nil, err
+	} else if err != nil {
+		logrus.Error("Cannot get fleet-agent secret")
+		return nil, err
+	}
+
+	clientConfig, err := clientcmd.NewClientConfigFromBytes(secret.Data[Kubeconfig])
+	if err != nil {
+		return nil, err
+	}
+
+	return &AgentInfo{
+		ClusterNamespace: string(secret.Data[ClusterNamespace]),
+		ClusterName:      string(secret.Data[ClusterName]),
+		ClientConfig:     clientConfig,
+	}, nil
+}
+
+// Register creates a fleet-agent secret with the upstream kubeconfig, by
+// running the registration process with the upstream cluster.
+// To run the initial registration, the fleet-agent-bootstrap secret must exist
+// on the local cluster.
+func Register(ctx context.Context, namespace string, config *rest.Config) (*AgentInfo, error) {
 	for {
-		cfg, err := tryRegister(ctx, namespace, clusterID, config)
+		cfg, err := tryRegister(ctx, namespace, config)
 		if err == nil {
 			return cfg, nil
 		}
@@ -63,7 +101,7 @@ func Register(ctx context.Context, namespace, clusterID string, config *rest.Con
 
 // tryRegister makes sure the secret cattle-fleet-system/fleet-agent is
 // populated and the contained kubeconfig is working
-func tryRegister(ctx context.Context, namespace, clusterID string, cfg *rest.Config) (*AgentInfo, error) {
+func tryRegister(ctx context.Context, namespace string, cfg *rest.Config) (*AgentInfo, error) {
 	cfg = rest.CopyConfig(cfg)
 	// disable the rate limiter
 	cfg.RateLimiter = ratelimit.None
@@ -76,7 +114,7 @@ func tryRegister(ctx context.Context, namespace, clusterID string, cfg *rest.Con
 	if apierrors.IsNotFound(err) {
 		logrus.Warn("Cannot find fleet-agent secret, running registration")
 		// fallback to local cattle-fleet-system/fleet-agent-bootstrap
-		secret, err = runRegistration(ctx, k8s.Core().V1(), namespace, clusterID)
+		secret, err = runRegistration(ctx, k8s.Core().V1(), namespace)
 		if err != nil {
 			return nil, fmt.Errorf("registration failed: %w", err)
 		}
@@ -85,7 +123,7 @@ func tryRegister(ctx context.Context, namespace, clusterID string, cfg *rest.Con
 	} else if err := testClientConfig(secret.Data[Kubeconfig]); err != nil {
 		// skip testClientConfig check if previous error, or IsNotFound fallback succeeded
 		logrus.Errorf("Current credential failed, failing back to reregistering: %v", err)
-		secret, err = runRegistration(ctx, k8s.Core().V1(), namespace, clusterID)
+		secret, err = runRegistration(ctx, k8s.Core().V1(), namespace)
 		if err != nil {
 			return nil, fmt.Errorf("re-registration failed: %w", err)
 		}
@@ -123,7 +161,7 @@ type coreInterface interface {
 // Finally uses the client from the config (service account: fleet-agent), to
 // update the "fleet-agent" secret with a new kubeconfig from the registration
 // secret. The new kubeconfig can then be used to query bundledeployments.
-func runRegistration(ctx context.Context, k8s coreInterface, namespace, clusterID string) (*corev1.Secret, error) {
+func runRegistration(ctx context.Context, k8s coreInterface, namespace string) (*corev1.Secret, error) {
 	secret, err := k8s.Secret().Get(namespace, config.AgentBootstrapConfigName, metav1.GetOptions{})
 	if err != nil {
 		return nil, fmt.Errorf("looking up secret %s/%s: %w", namespace, config.AgentBootstrapConfigName, err)
@@ -161,15 +199,17 @@ func runRegistration(ctx context.Context, k8s coreInterface, namespace, clusterI
 		return nil, err
 	}
 
+	clientID := ""
 	if cfg.ClientID != "" {
-		clusterID = cfg.ClientID
-	} else if clusterID == "" {
+		clientID = cfg.ClientID
+	} else {
 		kubeSystem, err := k8s.Namespace().Get("kube-system", metav1.GetOptions{})
 		if err != nil {
 			return nil, fmt.Errorf("cannot retrieve our kubeSystem.UID: %w", err)
 		}
 
-		clusterID = string(kubeSystem.UID)
+		// no configured id, client id is now "clusterID"
+		clientID = string(kubeSystem.UID)
 	}
 
 	// add the name of the pod that created the registration for debugging
@@ -178,20 +218,20 @@ func runRegistration(ctx context.Context, k8s coreInterface, namespace, clusterI
 	}
 	cfg.Labels["fleet.cattle.io/created-by-agent-pod"] = os.Getenv("HOSTNAME")
 
-	logrus.Infof("Creating clusterregistration with id '%s' for new token", clusterID)
+	logrus.Infof("Creating clusterregistration with id '%s' for new token", clientID)
 	request, err := fc.Fleet().V1alpha1().ClusterRegistration().Create(&fleet.ClusterRegistration{
 		ObjectMeta: metav1.ObjectMeta{
 			GenerateName: "request-",
 			Namespace:    ns,
 		},
 		Spec: fleet.ClusterRegistrationSpec{
-			ClientID:      clusterID,
+			ClientID:      clientID,
 			ClientRandom:  token,
 			ClusterLabels: cfg.Labels,
 		},
 	})
 	if err != nil {
-		return nil, fmt.Errorf("cannot create clusterregistration on management cluster for cluster id '%s': %w", clusterID, err)
+		return nil, fmt.Errorf("cannot create clusterregistration on management cluster for cluster id '%s': %w", clientID, err)
 	}
 
 	secretName := registration.SecretName(request.Spec.ClientID, request.Spec.ClientRandom)
diff --git a/internal/cmd/agent/root.go b/internal/cmd/agent/root.go
index e470e66d..7b81dbf6 100644
--- a/internal/cmd/agent/root.go
+++ b/internal/cmd/agent/root.go
@@ -4,23 +4,37 @@ import (
 	"fmt"
 	"log"
 	"net/http"
-	"time"
 
 	"github.com/spf13/cobra"
 
+	ctrl "sigs.k8s.io/controller-runtime"
+	clog "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
 	command "github.com/rancher/fleet/internal/cmd"
 	"github.com/rancher/fleet/pkg/version"
 )
 
+type UpstreamOptions struct {
+	Kubeconfig string `usage:"kubeconfig file for agent's cluster"`
+	Namespace  string `usage:"system namespace is the namespace, the agent runs in, e.g. cattle-fleet-system" env:"NAMESPACE"`
+}
+
 type FleetAgent struct {
 	command.DebugConfig
-	Kubeconfig      string `usage:"kubeconfig file"`
-	Namespace       string `usage:"namespace to watch" env:"NAMESPACE"`
-	AgentScope      string `usage:"An identifier used to scope the agent bundleID names, typically the same as namespace" env:"AGENT_SCOPE"`
-	CheckinInterval string `usage:"How often to post cluster status" env:"CHECKIN_INTERVAL"`
+	UpstreamOptions
+	AgentScope string `usage:"An identifier used to scope the agent bundleID names, typically the same as namespace" env:"AGENT_SCOPE"`
 }
 
+var setupLog = ctrl.Log.WithName("setup")
+
 func (a *FleetAgent) Run(cmd *cobra.Command, args []string) error {
+	zopts := zap.Options{
+		Development: true,
+	}
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zopts)))
+	ctx := clog.IntoContext(cmd.Context(), ctrl.Log)
+
 	go func() {
 		log.Println(http.ListenAndServe("localhost:6060", nil)) // nolint:gosec // Debugging only
 	}()
@@ -28,29 +42,23 @@ func (a *FleetAgent) Run(cmd *cobra.Command, args []string) error {
 	if err := a.SetupDebug(); err != nil {
 		return fmt.Errorf("failed to setup debug logging: %w", err)
 	}
-	var (
-		opts options
-		err  error
-	)
-
-	if a.CheckinInterval != "" {
-		opts.CheckinInterval, err = time.ParseDuration(a.CheckinInterval)
-		if err != nil {
-			return err
-		}
-	}
+
 	if a.Namespace == "" {
 		return fmt.Errorf("--namespace or env NAMESPACE is required to be set")
 	}
-	if err := start(cmd.Context(), a.Kubeconfig, a.Namespace, a.AgentScope, &opts); err != nil {
+	if err := start(ctx, a.Kubeconfig, a.Namespace, a.AgentScope); err != nil {
 		return err
 	}
+
 	<-cmd.Context().Done()
 	return nil
 }
 
 func App() *cobra.Command {
-	return command.Command(&FleetAgent{}, cobra.Command{
+	root := command.Command(&FleetAgent{}, cobra.Command{
 		Version: version.FriendlyVersion(),
 	})
+	root.AddCommand(NewClusterStatus())
+	root.AddCommand(NewRegister())
+	return root
 }
diff --git a/internal/cmd/agent/start.go b/internal/cmd/agent/start.go
deleted file mode 100644
index 1aeadd98..00000000
--- a/internal/cmd/agent/start.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package agent
-
-import (
-	"context"
-	"time"
-
-	"github.com/rancher/fleet/internal/cmd/agent/controllers"
-	"github.com/rancher/fleet/internal/cmd/agent/register"
-	"github.com/sirupsen/logrus"
-
-	"github.com/rancher/lasso/pkg/mapper"
-	"github.com/rancher/wrangler/v2/pkg/kubeconfig"
-	"github.com/rancher/wrangler/v2/pkg/leader"
-	"github.com/rancher/wrangler/v2/pkg/ratelimit"
-	"github.com/rancher/wrangler/v2/pkg/ticker"
-
-	"k8s.io/apimachinery/pkg/api/meta"
-	"k8s.io/client-go/discovery"
-	"k8s.io/client-go/discovery/cached/memory"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/rest"
-	"k8s.io/client-go/restmapper"
-	"k8s.io/client-go/tools/clientcmd"
-)
-
-type options struct {
-	DefaultNamespace string
-	ClusterID        string
-	CheckinInterval  time.Duration
-}
-
-// start the fleet agent
-func start(ctx context.Context, kubeConfig, namespace, agentScope string, opts *options) error {
-	if opts == nil {
-		opts = &options{}
-	}
-	if opts.DefaultNamespace == "" {
-		opts.DefaultNamespace = "default"
-	}
-
-	clientConfig := kubeconfig.GetNonInteractiveClientConfig(kubeConfig)
-	kc, err := clientConfig.ClientConfig()
-	if err != nil {
-		return err
-	}
-
-	// try to claim leadership lease without rate limiting
-	localConfig := rest.CopyConfig(kc)
-	localConfig.RateLimiter = ratelimit.None
-	k8s, err := kubernetes.NewForConfig(localConfig)
-	if err != nil {
-		return err
-	}
-
-	leader.RunOrDie(ctx, namespace, "fleet-agent-lock", k8s, func(ctx context.Context) {
-		// try to register with upstream fleet controller by obtaining
-		// a kubeconfig for the upstream cluster
-		agentInfo, err := register.Register(ctx, namespace, opts.ClusterID, kc)
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		fleetNamespace, _, err := agentInfo.ClientConfig.Namespace()
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		fleetRESTConfig, err := agentInfo.ClientConfig.ClientConfig()
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		fleetMapper, mapper, discovery, err := newMappers(ctx, fleetRESTConfig, clientConfig)
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		appCtx, err := controllers.NewAppContext(
-			fleetNamespace, namespace, agentInfo.ClusterNamespace, agentInfo.ClusterName,
-			fleetRESTConfig, clientConfig, fleetMapper, mapper, discovery)
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		err = controllers.Register(ctx,
-			appCtx,
-			fleetNamespace, opts.DefaultNamespace,
-			agentScope,
-			opts.CheckinInterval)
-		if err != nil {
-			logrus.Fatal(err)
-		}
-
-		if err := appCtx.Start(ctx); err != nil {
-			logrus.Fatal(err)
-		}
-	})
-
-	return nil
-}
-
-func newMappers(ctx context.Context, fleetRESTConfig *rest.Config, clientconfig clientcmd.ClientConfig) (meta.RESTMapper, meta.RESTMapper, discovery.CachedDiscoveryInterface, error) {
-	fleetMapper, err := mapper.New(fleetRESTConfig)
-	if err != nil {
-		return nil, nil, nil, err
-	}
-
-	client, err := clientconfig.ClientConfig()
-	if err != nil {
-		return nil, nil, nil, err
-	}
-	client.RateLimiter = ratelimit.None
-
-	d, err := discovery.NewDiscoveryClientForConfig(client)
-	if err != nil {
-		return nil, nil, nil, err
-	}
-	discovery := memory.NewMemCacheClient(d)
-	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discovery)
-
-	go func() {
-		for range ticker.Context(ctx, 30*time.Second) {
-			discovery.Invalidate()
-			mapper.Reset()
-		}
-	}()
-
-	return fleetMapper, mapper, discovery, nil
-}
diff --git a/internal/cmd/controller/agent/manifest.go b/internal/cmd/controller/agent/manifest.go
index 208211f1..3c870d23 100644
--- a/internal/cmd/controller/agent/manifest.go
+++ b/internal/cmd/controller/agent/manifest.go
@@ -93,7 +93,7 @@ func Manifest(namespace string, agentScope string, opts ManifestOptions) []runti
 		},
 	}
 
-	deployment := agentDeployment(namespace, agentScope, opts)
+	agent := agentApp(namespace, agentScope, opts)
 
 	networkPolicy := &networkv1.NetworkPolicy{
 		ObjectMeta: metav1.ObjectMeta{
@@ -117,7 +117,7 @@ func Manifest(namespace string, agentScope string, opts ManifestOptions) []runti
 
 	var objs []runtime.Object
 	objs = append(objs, clusterRole...)
-	objs = append(objs, admin, defaultSa, deployment, networkPolicy)
+	objs = append(objs, admin, defaultSa, agent, networkPolicy)
 
 	return objs
 }
@@ -133,17 +133,17 @@ func resolve(global, prefix, image string) string {
 	return image
 }
 
-func agentDeployment(namespace string, agentScope string, opts ManifestOptions) *appsv1.Deployment {
+func agentApp(namespace string, agentScope string, opts ManifestOptions) *appsv1.StatefulSet {
 	name := DefaultName
 	serviceAccount := DefaultName
 	image := resolve(opts.SystemDefaultRegistry, opts.PrivateRepoURL, opts.AgentImage)
 
-	dep := &appsv1.Deployment{
+	app := &appsv1.StatefulSet{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
 			Name:      name,
 		},
-		Spec: appsv1.DeploymentSpec{
+		Spec: appsv1.StatefulSetSpec{
 			Selector: &metav1.LabelSelector{
 				MatchLabels: map[string]string{
 					"app": name,
@@ -157,6 +157,27 @@ func agentDeployment(namespace string, agentScope string, opts ManifestOptions)
 				},
 				Spec: corev1.PodSpec{
 					ServiceAccountName: serviceAccount,
+					InitContainers: []corev1.Container{
+						{
+							Name:            name + "-register",
+							Image:           image,
+							ImagePullPolicy: corev1.PullPolicy(opts.AgentImagePullPolicy),
+							Env: []corev1.EnvVar{
+								{
+									Name: "NAMESPACE",
+									ValueFrom: &corev1.EnvVarSource{
+										FieldRef: &corev1.ObjectFieldSelector{
+											FieldPath: "metadata.namespace",
+										},
+									},
+								},
+							},
+							Command: []string{
+								"fleetagent",
+								"register",
+							},
+						},
+					},
 					Containers: []corev1.Container{
 						{
 							Name:            name,
@@ -172,8 +193,30 @@ func agentDeployment(namespace string, agentScope string, opts ManifestOptions)
 									},
 								},
 								{Name: "AGENT_SCOPE", Value: agentScope},
+							},
+							Command: []string{
+								"fleetagent",
+							},
+						},
+						{
+							Name:            name + "-clusterstatus",
+							Image:           image,
+							ImagePullPolicy: corev1.PullPolicy(opts.AgentImagePullPolicy),
+							Env: []corev1.EnvVar{
+								{
+									Name: "NAMESPACE",
+									ValueFrom: &corev1.EnvVarSource{
+										FieldRef: &corev1.ObjectFieldSelector{
+											FieldPath: "metadata.namespace",
+										},
+									},
+								},
 								{Name: "CHECKIN_INTERVAL", Value: opts.CheckinInterval},
 							},
+							Command: []string{
+								"fleetagent",
+								"clusterstatus",
+							},
 						},
 					},
 					NodeSelector: map[string]string{"kubernetes.io/os": "linux"},
@@ -215,15 +258,7 @@ func agentDeployment(namespace string, agentScope string, opts ManifestOptions)
 	}
 
 	if !DebugEnabled {
-		for _, container := range dep.Spec.Template.Spec.Containers {
-			container.SecurityContext = &corev1.SecurityContext{
-				AllowPrivilegeEscalation: &[]bool{false}[0],
-				ReadOnlyRootFilesystem:   &[]bool{true}[0],
-				Privileged:               &[]bool{false}[0],
-				Capabilities:             &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
-			}
-		}
-		dep.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{
+		app.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{
 			RunAsNonRoot: &[]bool{true}[0],
 			RunAsUser:    &[]int64{1000}[0],
 			RunAsGroup:   &[]int64{1000}[0],
@@ -231,33 +266,45 @@ func agentDeployment(namespace string, agentScope string, opts ManifestOptions)
 	}
 
 	// additional tolerations from cluster
-	dep.Spec.Template.Spec.Tolerations = append(dep.Spec.Template.Spec.Tolerations, opts.AgentTolerations...)
+	app.Spec.Template.Spec.Tolerations = append(app.Spec.Template.Spec.Tolerations, opts.AgentTolerations...)
 
 	// overwrite affinity if present on cluster
 	if opts.AgentAffinity != nil {
-		dep.Spec.Template.Spec.Affinity = opts.AgentAffinity
+		app.Spec.Template.Spec.Affinity = opts.AgentAffinity
 	}
 
-	// set resources if present on cluster
-	if opts.AgentResources != nil {
-		dep.Spec.Template.Spec.Containers[0].Resources = *opts.AgentResources
+	// modify containers via pointers to the containers
+	var containers []*corev1.Container
+	for i := range app.Spec.Template.Spec.Containers {
+		containers = append(containers, &app.Spec.Template.Spec.Containers[i])
 	}
-
-	// additional env vars from cluster
-	if opts.AgentEnvVars != nil {
-		dep.Spec.Template.Spec.Containers[0].Env = append(dep.Spec.Template.Spec.Containers[0].Env, opts.AgentEnvVars...)
+	for i := range app.Spec.Template.Spec.InitContainers {
+		containers = append(containers, &app.Spec.Template.Spec.InitContainers[i])
 	}
+	for _, container := range containers {
+		// set resources if present on cluster
+		if opts.AgentResources != nil {
+			container.Resources = *opts.AgentResources
+		}
+
+		// additional env vars from cluster
+		if opts.AgentEnvVars != nil {
+			container.Env = append(container.Env, opts.AgentEnvVars...)
+		}
 
-	if DebugEnabled {
-		dep.Spec.Template.Spec.Containers[0].Command = []string{
-			"fleetagent",
-			"--debug",
-			"--debug-level",
-			strconv.Itoa(DebugLevel),
+		if DebugEnabled {
+			container.Command = append(container.Command, "--debug", "--debug-level", strconv.Itoa(DebugLevel))
+		} else {
+			container.SecurityContext = &corev1.SecurityContext{
+				AllowPrivilegeEscalation: &[]bool{false}[0],
+				ReadOnlyRootFilesystem:   &[]bool{true}[0],
+				Privileged:               &[]bool{false}[0],
+				Capabilities:             &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
+			}
 		}
 	}
 
-	return dep
+	return app
 }
 
 func serviceAccount(namespace, name string) *corev1.ServiceAccount {
diff --git a/internal/cmd/controller/controllers/cluster/import.go b/internal/cmd/controller/controllers/cluster/import.go
index 013a46e1..5d09c3e9 100644
--- a/internal/cmd/controller/controllers/cluster/import.go
+++ b/internal/cmd/controller/controllers/cluster/import.go
@@ -188,33 +188,11 @@ func (i *importHandler) deleteOldAgent(cluster *fleet.Cluster, kc kubernetes.Int
 		return err
 	}
 
-	deployment, err := kc.AppsV1().Deployments(namespace).Get(i.ctx, config.AgentConfigName, metav1.GetOptions{})
-	if apierrors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-
-	if err := kc.AppsV1().Deployments(namespace).Delete(i.ctx, config.AgentConfigName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
+	if err := kc.AppsV1().StatefulSets(namespace).Delete(i.ctx, config.AgentConfigName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
 		return err
 	}
 	logrus.Infof("Deleted old agent for cluster (%s/%s) in namespace %s", cluster.Namespace, cluster.Name, namespace)
 
-	pods, err := kc.CoreV1().Pods(namespace).List(i.ctx, metav1.ListOptions{
-		LabelSelector: metav1.FormatLabelSelector(deployment.Spec.Selector),
-	})
-	if apierrors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-
-	for _, pod := range pods.Items {
-		if err := kc.CoreV1().Pods(namespace).Delete(i.ctx, pod.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
-			return err
-		}
-	}
-
 	return nil
 }
 
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/generic_map.go b/pkg/apis/fleet.cattle.io/v1alpha1/generic_map.go
new file mode 100644
index 00000000..15a303e4
--- /dev/null
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/generic_map.go
@@ -0,0 +1,63 @@
+package v1alpha1
+
+import (
+	"encoding/json"
+)
+
+type GenericMap struct {
+	Data map[string]interface{} `json:"-"`
+}
+
+func (in GenericMap) MarshalJSON() ([]byte, error) {
+	return json.Marshal(in.Data)
+}
+
+func (in *GenericMap) UnmarshalJSON(data []byte) error {
+	in.Data = map[string]interface{}{}
+	return json.Unmarshal(data, &in.Data)
+}
+
+func (in *GenericMap) DeepCopyInto(out *GenericMap) {
+	out.Data = make(map[string]interface{}, len(in.Data))
+	deepCopyMap(in.Data, out.Data)
+}
+
+// deepCopyMap will recursively copy each entry from src into dest, ensuring that nested maps and slices are copied as well
+//
+// NOTE: The implementation relies on GenericMap's data being loaded from a JSON or YAML via Unmarshal, which produces map[string]interface{} and []interface{} types.
+// The recursive copy won't work with other variants as pointers or specific value types.
+func deepCopyMap(src, dest map[string]interface{}) {
+	for key := range src {
+		switch value := src[key].(type) {
+		case map[string]interface{}:
+			destValue := make(map[string]interface{}, len(value))
+			deepCopyMap(value, destValue)
+			dest[key] = destValue
+		case []interface{}:
+			destValue := make([]interface{}, len(value))
+			deepCopySlice(value, destValue)
+			dest[key] = destValue
+		default:
+			dest[key] = value
+		}
+	}
+}
+
+// deepCopySlice will recursively copy each entry from src into dest, ensuring that nested maps and slices are copied as well
+// It assumes that both slices have the same capacity.
+func deepCopySlice(src, dest []interface{}) {
+	for i := range src {
+		switch value := src[i].(type) {
+		case map[string]interface{}:
+			destValue := make(map[string]interface{}, len(value))
+			deepCopyMap(value, destValue)
+			dest[i] = destValue
+		case []interface{}:
+			destValue := make([]interface{}, len(value))
+			deepCopySlice(value, destValue)
+			dest[i] = destValue
+		default:
+			dest[i] = value
+		}
+	}
+}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/values.go b/pkg/apis/fleet.cattle.io/v1alpha1/values.go
deleted file mode 100644
index cc1b2ee9..00000000
--- a/pkg/apis/fleet.cattle.io/v1alpha1/values.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package v1alpha1
-
-import (
-	"encoding/json"
-
-	"github.com/rancher/wrangler/v2/pkg/data/convert"
-)
-
-type GenericMap struct {
-	Data map[string]interface{} `json:"-"`
-}
-
-func (in GenericMap) MarshalJSON() ([]byte, error) {
-	return json.Marshal(in.Data)
-}
-
-func (in *GenericMap) UnmarshalJSON(data []byte) error {
-	in.Data = map[string]interface{}{}
-	return json.Unmarshal(data, &in.Data)
-}
-
-func (in *GenericMap) DeepCopyInto(out *GenericMap) {
-	out.Data = map[string]interface{}{}
-	if err := convert.ToObj(in.Data, &out.Data); err != nil {
-		panic(err)
-	}
-}
diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod
index 3b69a9d3..cbf0e8e7 100644
--- a/pkg/apis/go.mod
+++ b/pkg/apis/go.mod
@@ -4,9 +4,9 @@ go 1.19
 
 require (
 	github.com/rancher/wrangler/v2 v2.1.1
-	k8s.io/api v0.28.0
-	k8s.io/apimachinery v0.28.0
-	sigs.k8s.io/controller-runtime v0.11.0
+	k8s.io/api v0.28.3
+	k8s.io/apimachinery v0.28.3
+	sigs.k8s.io/controller-runtime v0.16.1
 )
 
 require (
diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum
index 6d34c1d4..d30a78a2 100644
--- a/pkg/apis/go.sum
+++ b/pkg/apis/go.sum
@@ -1,16 +1,17 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -22,9 +23,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
+github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rancher/wrangler/v2 v2.1.1 h1:EWAg2UNm+6EEACsUf/Pc7lcjswFpfkU2mCaQdxUkRPM=
@@ -62,6 +63,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
 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=
@@ -70,23 +72,22 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-k8s.io/api v0.28.0 h1:3j3VPWmN9tTDI68NETBWlDiA9qOiGJ7sdKeufehBYsM=
-k8s.io/api v0.28.0/go.mod h1:0l8NZJzB0i/etuWnIXcwfIv+xnDOhL3lLW919AWYDuY=
-k8s.io/apimachinery v0.28.0 h1:ScHS2AG16UlYWk63r46oU3D5y54T53cVI5mMJwwqFNA=
-k8s.io/apimachinery v0.28.0/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/cli-utils v0.28.0 h1:gsvwqygoXlW2y8CmKdflQJNZp1Yhi4geATW3/Ei7oYc=
 sigs.k8s.io/cli-utils v0.28.0/go.mod h1:WDVRa5/eQBKntG++uyKdyT+xU7MLdCR4XsgseqL5uX4=
-sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ=
-sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=
+sigs.k8s.io/controller-runtime v0.16.1 h1:+15lzrmHsE0s2kNl0Dl8cTchI5Cs8qofo5PGcPrV9z0=
+sigs.k8s.io/controller-runtime v0.16.1/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=


Test patch:
diff --git a/.github/scripts/setup-latest-rancher.sh b/.github/scripts/setup-latest-rancher.sh
index fbfc8556..3462d83b 100755
--- a/.github/scripts/setup-latest-rancher.sh
+++ b/.github/scripts/setup-latest-rancher.sh
@@ -29,5 +29,6 @@ kubectl -n cattle-system rollout status deploy/rancher
 # wait for rancher to create fleet namespace, deployment and controller
 { grep -q -m 1 "fleet"; kill $!; } < <(kubectl get deployments -n cattle-fleet-system -w)
 kubectl -n cattle-fleet-system rollout status deploy/fleet-controller
+{ grep -E -q -m 1 "fleet-agent-local.*1/1"; kill $!; } < <(kubectl get bundles -n fleet-local -w)
 
 helm list -A
diff --git a/e2e/installation/verify_test.go b/e2e/installation/verify_test.go
index d2b9f737..a9436358 100644
--- a/e2e/installation/verify_test.go
+++ b/e2e/installation/verify_test.go
@@ -63,7 +63,7 @@ var _ = Describe("Fleet Installation", func() {
 	Context("Verify agents are updated to new image", func() {
 		It("has the expected fleet images", func() {
 			Eventually(func() string {
-				out, _ := k.Namespace(localAgentNamespace).Get("deployments", "-owide")
+				out, _ := k.Namespace(localAgentNamespace).Get("statefulsets", "-owide")
 				return out
 			}).Should(ContainSubstring("rancher/fleet-agent:" + version))
 		})
@@ -71,7 +71,7 @@ var _ = Describe("Fleet Installation", func() {
 		It("has the expected fleet-agent image in the downstream cluster", Label("multi-cluster"), func() {
 			kd := env.Kubectl.Context(env.Downstream)
 			Eventually(func() string {
-				out, _ := kd.Namespace(agentNamespace).Get("deployments", "-owide")
+				out, _ := kd.Namespace(agentNamespace).Get("statefulsets", "-owide")
 				return out
 			}).Should(ContainSubstring("rancher/fleet-agent:" + version))
 		})
diff --git a/e2e/testenv/infra/go.mod b/e2e/testenv/infra/go.mod
index 79d04254..5b171bc4 100644
--- a/e2e/testenv/infra/go.mod
+++ b/e2e/testenv/infra/go.mod
@@ -8,7 +8,7 @@ replace github.com/rancher/fleet => ../../../
 
 replace github.com/rancher/fleet/pkg/apis => ../../../pkg/apis
 
-replace k8s.io/client-go => k8s.io/client-go v0.28.0
+replace k8s.io/client-go => k8s.io/client-go v0.28.3
 
 require (
 	github.com/rancher/fleet v0.0.0
@@ -36,11 +36,11 @@ require (
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
+	github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/klauspost/compress v1.16.5 // indirect
-	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/moby/locker v1.0.1 // indirect
 	github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
@@ -48,10 +48,10 @@ require (
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/prometheus/client_golang v1.16.0 // indirect
-	github.com/prometheus/client_model v0.4.0 // indirect
-	github.com/prometheus/common v0.44.0 // indirect
-	github.com/prometheus/procfs v0.10.1 // indirect
+	github.com/prometheus/client_golang v1.17.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.45.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/rancher/gitjob v0.1.103 // indirect
 	github.com/rogpeppe/go-internal v1.11.0 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -61,12 +61,11 @@ require (
 	golang.org/x/net v0.17.0 // indirect
 	golang.org/x/sync v0.4.0 // indirect
 	golang.org/x/sys v0.13.0 // indirect
-	golang.org/x/tools v0.13.0 // indirect
+	golang.org/x/tools v0.14.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
 	google.golang.org/grpc v1.56.3 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	k8s.io/client-go v12.0.0+incompatible // indirect
 	oras.land/oras-go v1.2.3 // indirect
-	sigs.k8s.io/yaml v1.3.0 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/e2e/testenv/infra/go.sum b/e2e/testenv/infra/go.sum
index 29b75afa..946b3e83 100644
--- a/e2e/testenv/infra/go.sum
+++ b/e2e/testenv/infra/go.sum
@@ -26,9 +26,6 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
 github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
 github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
 github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
@@ -93,18 +90,18 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu
 github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0=
+github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
 github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -121,8 +118,8 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
 github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@@ -156,21 +153,21 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
-github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
+github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
 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.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
-github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
 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=
-github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
-github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rancher/gitjob v0.1.103 h1:s8HB5d1cVMl+XcUMnbGfDrDf1C2P0NJOsVO4ZiebxVM=
 github.com/rancher/gitjob v0.1.103/go.mod h1:ySp7tcP5DOpoTnxFoyCoadiBdXd8rR+urWf1s/0xC10=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -213,8 +210,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -235,7 +232,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -249,8 +245,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
+golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
 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=
@@ -278,9 +274,9 @@ gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
 helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg=
 helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k=
-k8s.io/client-go v0.28.0 h1:ebcPRDZsCjpj62+cMk1eGNX1QkMdRmQ6lmz5BLoFWeM=
-k8s.io/client-go v0.28.0/go.mod h1:0Asy9Xt3U98RypWJmU1ZrRAGKhP6NqDPmptlAzK2kMc=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
 oras.land/oras-go v1.2.3 h1:v8PJl+gEAntI1pJ/LCrDgsuk+1PKVavVEPsYIHFE5uY=
 oras.land/oras-go v1.2.3/go.mod h1:M/uaPdYklze0Vf3AakfarnpoEckvw0ESbRdN8Z1vdJg=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/integrationtests/agent/suite_test.go b/integrationtests/agent/suite_test.go
index 0b38620e..1b2e1b0e 100644
--- a/integrationtests/agent/suite_test.go
+++ b/integrationtests/agent/suite_test.go
@@ -12,6 +12,9 @@ import (
 	"github.com/rancher/fleet/integrationtests/utils"
 	"github.com/rancher/fleet/internal/cmd/agent/controllers/bundledeployment"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/cleanup"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/driftdetect"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/monitor"
 	"github.com/rancher/fleet/internal/cmd/agent/trigger"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	"github.com/rancher/fleet/internal/manifest"
@@ -146,18 +149,29 @@ func registerBundleDeploymentController(cfg *rest.Config, namespace string, look
 	wranglerApply, err := apply.NewForConfig(cfg)
 	Expect(err).ToNot(HaveOccurred())
 
-	deployManager := deployer.NewManager(
-		namespace,
-		namespace,
+	deployManager := deployer.New(
+		lookup,
+		helmDeployer)
+	cleanup := cleanup.New(
 		namespace,
 		namespace,
 		factory.Fleet().V1alpha1().BundleDeployment().Cache(),
 		factory.Fleet().V1alpha1().BundleDeployment(),
-		lookup,
+		helmDeployer)
+	monitor := monitor.New(
+		namespace,
+		namespace,
+		namespace,
+		helmDeployer,
+		wranglerApply)
+	driftdetect := driftdetect.New(
+		namespace,
+		namespace,
+		namespace,
 		helmDeployer,
 		wranglerApply)
 
-	bundledeployment.Register(ctx, trig, mapper, dyn, deployManager, factory.Fleet().V1alpha1().BundleDeployment())
+	bundledeployment.Register(ctx, trig, mapper, dyn, deployManager, cleanup, monitor, driftdetect, factory.Fleet().V1alpha1().BundleDeployment())
 
 	err = factory.Start(ctx, 50)
 	Expect(err).ToNot(HaveOccurred())
diff --git a/internal/cmd/agent/deployer/monitor_test.go b/internal/cmd/agent/deployer/monitor/monitor_test.go
similarity index 99%
rename from internal/cmd/agent/deployer/monitor_test.go
rename to internal/cmd/agent/deployer/monitor/monitor_test.go
index 45f373db..71bc31bb 100644
--- a/internal/cmd/agent/deployer/monitor_test.go
+++ b/internal/cmd/agent/deployer/monitor/monitor_test.go
@@ -1,4 +1,4 @@
-package deployer
+package monitor
 
 import (
 	"testing"
diff --git a/internal/cmd/agent/register/register_test.go b/internal/cmd/agent/register/register_test.go
index 93cf214b..dcc9ccfe 100644
--- a/internal/cmd/agent/register/register_test.go
+++ b/internal/cmd/agent/register/register_test.go
@@ -54,11 +54,13 @@ func TestRunRegistrationLabelSmokeTest(t *testing.T) {
 	mockConfigMapController.EXPECT().Get(secret.Namespace, "fleet-agent", metav1.GetOptions{}).
 		Return(nil, &apierrors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}})
 
-	var mockNamespaceController corecontrollers.NamespaceController // not used
+	mockNamespaceController := fake.NewMockNonNamespacedControllerInterface[*corev1.Namespace, *corev1.NamespaceList](ctrl)
+	mockNamespaceController.EXPECT().Get("kube-system", metav1.GetOptions{}).Return(&corev1.Namespace{}, nil)
+
 	mockCoreController := mockCoreInterface{mockConfigMapController, mockNamespaceController, mockSecretController}
 
 	http.DefaultClient.Timeout = 100 * time.Millisecond // no need to wait longer, the API server URL is a dummy.
-	agentSecret, err := runRegistration(context.Background(), mockCoreController, namespace, "my-clusterID")
+	agentSecret, err := runRegistration(context.Background(), mockCoreController, namespace)
 
 	assert.Nil(t, agentSecret)
 	// expecting an error at cluster registration creation time because the API server URL is a dummy.
diff --git a/internal/cmd/controller/agent/manifest_test.go b/internal/cmd/controller/agent/manifest_test.go
index 16adf257..1aab9910 100644
--- a/internal/cmd/controller/agent/manifest_test.go
+++ b/internal/cmd/controller/agent/manifest_test.go
@@ -32,10 +32,10 @@ func TestImageResolve(t *testing.T) {
 	}
 }
 
-func getDeploymentFromManifests(namespace string, scope string, opts ManifestOptions) *appsv1.Deployment {
+func getAgentFromManifests(namespace string, scope string, opts ManifestOptions) *appsv1.StatefulSet {
 	objects := Manifest(namespace, scope, opts)
 	for _, obj := range objects {
-		dep, ok := obj.(*appsv1.Deployment)
+		dep, ok := obj.(*appsv1.StatefulSet)
 		if ok {
 			return dep
 		}
@@ -92,13 +92,13 @@ func TestManifestAgentTolerations(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.name, func(t *testing.T) {
-			agentDeployment := getDeploymentFromManifests(namespace, scope, testCase.getOpts())
-			if agentDeployment == nil {
+			agent := getAgentFromManifests(namespace, scope, testCase.getOpts())
+			if agent == nil {
 				t.Fatal("there were no deployments returned from the manifests")
 			}
 
-			if !cmp.Equal(agentDeployment.Spec.Template.Spec.Tolerations, testCase.expectedTolerations, cmpOpt) {
-				t.Fatalf("tolerations were not as expected: %v", agentDeployment.Spec.Template.Spec.Tolerations)
+			if !cmp.Equal(agent.Spec.Template.Spec.Tolerations, testCase.expectedTolerations, cmpOpt) {
+				t.Fatalf("tolerations were not as expected: %v", agent.Spec.Template.Spec.Tolerations)
 			}
 		})
 	}
@@ -152,13 +152,13 @@ func TestManifestAgentAffinity(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.name, func(t *testing.T) {
-			agentDeployment := getDeploymentFromManifests(namespace, "", testCase.getOpts())
-			if agentDeployment == nil {
+			agent := getAgentFromManifests(namespace, "", testCase.getOpts())
+			if agent == nil {
 				t.Fatal("there were no deployments returned from the manifests")
 			}
 
-			if !cmp.Equal(agentDeployment.Spec.Template.Spec.Affinity, testCase.expectedAffinity) {
-				t.Fatalf("affinity was not as expected: %v %v", testCase.expectedAffinity, agentDeployment.Spec.Template.Spec.Affinity)
+			if !cmp.Equal(agent.Spec.Template.Spec.Affinity, testCase.expectedAffinity) {
+				t.Fatalf("affinity was not as expected: %v %v", testCase.expectedAffinity, agent.Spec.Template.Spec.Affinity)
 			}
 		})
 	}
@@ -204,13 +204,13 @@ func TestManifestAgentResources(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.name, func(t *testing.T) {
-			agentDeployment := getDeploymentFromManifests(namespace, "", testCase.getOpts())
-			if agentDeployment == nil {
+			agent := getAgentFromManifests(namespace, "", testCase.getOpts())
+			if agent == nil {
 				t.Fatal("there were no deployments returned from the manifests")
 			}
 
-			if !reflect.DeepEqual(agentDeployment.Spec.Template.Spec.Containers[0].Resources, testCase.expectedResources) {
-				t.Fatalf("resources was not as expected: %v %v", testCase.expectedResources, agentDeployment.Spec.Template.Spec.Containers[0].Resources)
+			if !reflect.DeepEqual(agent.Spec.Template.Spec.Containers[0].Resources, testCase.expectedResources) {
+				t.Fatalf("resources was not as expected: %v %v", testCase.expectedResources, agent.Spec.Template.Spec.Containers[0].Resources)
 			}
 		})
 	}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/generic_map_test.go b/pkg/apis/fleet.cattle.io/v1alpha1/generic_map_test.go
new file mode 100644
index 00000000..a8d56ff1
--- /dev/null
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/generic_map_test.go
@@ -0,0 +1,92 @@
+package v1alpha1
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/api/equality"
+)
+
+func Test_deepCopyMap(t *testing.T) {
+	type fixture func(map[string]any)
+	tests := []struct {
+		name string
+		src  map[string]interface{}
+		// perform changes after creation and ensure the original does not change
+		fixtures []fixture
+	}{
+		{name: "copies top-level keys", src: map[string]interface{}{
+			"str":     "value",
+			"list":    []any{1, "dos", true},
+			"boolean": false,
+		}},
+		{name: "copies nested values", src: map[string]interface{}{
+			"first": map[string]any{
+				"second": map[string]any{
+					"third": map[string]any{
+						"str":     "value",
+						"boolean": false,
+					},
+					"list":    []any{1, "dos", true},
+					"boolean": false,
+				},
+			},
+		}},
+		{name: "changing top-level values preserves the original map", src: map[string]interface{}{
+			"str":      "value",
+			"number":   3,
+			"boolean":  false,
+			"todelete": "foo",
+		}, fixtures: []fixture{
+			func(m map[string]interface{}) {
+				m["str"] = "anotherValue"
+			},
+			func(m map[string]interface{}) {
+				m["number"] = "anotherValue"
+			},
+			func(m map[string]interface{}) {
+				m["boolean"] = true
+			},
+			func(m map[string]interface{}) {
+				m["boolean"] = "notanymore"
+			},
+			func(m map[string]interface{}) {
+				delete(m, "todelete")
+			},
+		}},
+		{name: "modifying maps inside a slice preserves the original map", src: map[string]interface{}{
+			"list": []any{map[string]any{
+				"original": "value",
+			}},
+		}, fixtures: []fixture{
+			func(m map[string]interface{}) {
+				list := m["list"].([]any)
+				mapInList := list[0].(map[string]any)
+				mapInList["original"] = "newvalue"
+			},
+			func(m map[string]interface{}) {
+				list := m["list"].([]any)
+				mapInList := list[0].(map[string]any)
+				mapInList["modified"] = "another"
+			},
+		}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			res := map[string]interface{}{}
+			deepCopyMap(tt.src, res)
+
+			if got, want := res, tt.src; !equality.Semantic.DeepEqual(got, want) {
+				t.Errorf("the produced copy is not identical, got: %s, want: %s", got, want)
+			}
+
+			for _, modify := range tt.fixtures {
+				modify(res)
+				if equality.Semantic.DeepEqual(tt.src, res) {
+					t.Errorf("original was modified after modifying the copy")
+				}
+				// Apply same modifications to the original after comparing, to only test the delta
+				modify(tt.src)
+			}
+		})
+	}
+}