Repository: rancher/fleet
Pull request #2854 (2024-09-16 14:59:26)

Vendor apply.DryRun
Fleet's helmdeployer used the [wranger/apply](https://github.com/rancher/wrangler/tree/master/pkg/apply) module to do a "DryRun" to find modified objects.
Apply is very configurable, however Fleet only used a specific set of options. The relevant code paths to build the "plan" have been copied into the "deployer" packager. Several options, parameters, interfaces and switches have been removed.

* copy helmdeployer's pkgs from wrangler
* summary, had to move to the API pkg, as API cannot import fleet
* apply "patcher" was always replaced with an "append" function, patchers were never used
* delete is never used since ErrReplaced is never returned
* two structs implement "Apply" for the sugared interface, we only need
  one as "apply" is embedded in "desiredset"
* "original" is never used in patches
* rename applied package to desiredset
* fix/exclude linter warnings
* remove context from struct, pass in context directly
* replace logrus debug messages and increase verbosity


## Notes

* this affects how detect drift finds deleted resources
* this affects how modified resources are detected
* the `patch:` field is populated by the "dryrun" code, but always overwritten by the ["diff" code](https://github.com/rancher/fleet/blob/54f46c8396deba271511d866155b265eac0294b7/internal/cmd/agent/deployer/applied/diff.go#L76). It's only calculated to fill the `plan.Update` struct. That struct is similar to the `plan.Objects' struct and `Diff` already skips non modified resources. Data structures are different enough to make an optimization complicated, though. So removing `plan.Update` is not part of this PR.

```
  modifiedStatus:
  - apiVersion: v1
    kind: ConfigMap
    name: test-simple-manifest-config
    namespace: simple
    patch: '{"data":{"test":"value"}}'
```




Patch:
diff --git a/charts/fleet/templates/deployment.yaml b/charts/fleet/templates/deployment.yaml
index bf3ebfd3..025eb071 100644
--- a/charts/fleet/templates/deployment.yaml
+++ b/charts/fleet/templates/deployment.yaml
@@ -69,10 +69,6 @@ spec:
         - name: CATTLE_DEV_MODE
           value: "true"
         {{- end }}
-        {{- if $.Values.controller.reconciler.workers.gitrepo }}
-        - name: GITREPO_RECONCILER_WORKERS
-          value: {{ quote $.Values.controller.reconciler.workers.gitrepo }}
-        {{- end }}
         {{- if $.Values.controller.reconciler.workers.bundle }}
         - name: BUNDLE_RECONCILER_WORKERS
           value: {{ quote $.Values.controller.reconciler.workers.bundle }}
diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml
index 990a6f33..f2931017 100644
--- a/charts/fleet/values.yaml
+++ b/charts/fleet/values.yaml
@@ -108,9 +108,9 @@ controller:
   reconciler:
     # The number of workers that are allowed to each type of reconciler
     workers:
-      gitrepo: "1"
-      bundle: "1"
-      bundledeployment: "1"
+      gitrepo: "50"
+      bundle: "50"
+      bundledeployment: "50"
 
 # Extra environment variables passed to the fleet pods.
 # extraEnv:
diff --git a/internal/cmd/agent/controller/bundledeployment_controller.go b/internal/cmd/agent/controller/bundledeployment_controller.go
index f1e54fbb..4d516227 100644
--- a/internal/cmd/agent/controller/bundledeployment_controller.go
+++ b/internal/cmd/agent/controller/bundledeployment_controller.go
@@ -11,8 +11,6 @@ import (
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/monitor"
 	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
-	"github.com/rancher/wrangler/v3/pkg/condition"
-
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
@@ -21,6 +19,7 @@ import (
 	"k8s.io/client-go/util/retry"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -78,6 +77,7 @@ func (r *BundleDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
 					},
 				},
 			)).
+		WithOptions(controller.Options{MaxConcurrentReconciles: 50}).
 		Complete(r)
 }
 
@@ -129,11 +129,11 @@ func (r *BundleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
 		logger.V(1).Error(err, "Failed to deploy bundle", "status", status)
 
 		// do not use the returned status, instead set the condition and possibly a timestamp
-		bd.Status = setCondition(bd.Status, err, condition.Cond(fleetv1.BundleDeploymentConditionDeployed))
+		bd.Status = setCondition(bd.Status, err, monitor.Cond(fleetv1.BundleDeploymentConditionDeployed))
 
 		merr = append(merr, fmt.Errorf("failed deploying bundle: %w", err))
 	} else {
-		bd.Status = setCondition(status, nil, condition.Cond(fleetv1.BundleDeploymentConditionDeployed))
+		bd.Status = setCondition(status, nil, monitor.Cond(fleetv1.BundleDeploymentConditionDeployed))
 	}
 
 	// retrieve the resources from the helm history.
@@ -155,11 +155,11 @@ func (r *BundleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
 			logger.Error(err, "Cannot monitor deployed bundle")
 
 			// if there is an error, do not use the returned status from monitor
-			bd.Status = setCondition(bd.Status, err, condition.Cond(fleetv1.BundleDeploymentConditionMonitored))
+			bd.Status = setCondition(bd.Status, err, monitor.Cond(fleetv1.BundleDeploymentConditionMonitored))
 			merr = append(merr, fmt.Errorf("failed updating status: %w", err))
 		} else {
 			// we add to the status from deployer.DeployBundle
-			bd.Status = setCondition(status, nil, condition.Cond(fleetv1.BundleDeploymentConditionMonitored))
+			bd.Status = setCondition(status, nil, monitor.Cond(fleetv1.BundleDeploymentConditionMonitored))
 		}
 
 		// Run drift correction
@@ -167,7 +167,7 @@ func (r *BundleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
 			if release, err := r.Deployer.RemoveExternalChanges(ctx, bd); err != nil {
 				merr = append(merr, fmt.Errorf("failed reconciling drift: %w", err))
 				// Propagate drift correction error to bundle deployment status.
-				condition.Cond(fleetv1.BundleDeploymentConditionReady).SetError(&status, "", err)
+				monitor.Cond(fleetv1.BundleDeploymentConditionReady).SetError(&status, "", err)
 			} else {
 				bd.Status.Release = release
 			}
@@ -182,7 +182,7 @@ func (r *BundleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
 	}
 
 	// update our mini controller, which watches deployed resources for drift
-	err = r.DriftDetect.Refresh(logger, req.String(), bd, resources)
+	err = r.DriftDetect.Refresh(ctx, req.String(), bd, resources)
 	if err != nil {
 		logger.V(1).Error(err, "Failed to refresh drift detection", "step", "drift")
 		merr = append(merr, fmt.Errorf("failed refreshing drift detection: %w", err))
@@ -218,7 +218,7 @@ func (r *BundleDeploymentReconciler) updateStatus(ctx context.Context, req types
 }
 
 // setCondition sets the condition and updates the timestamp, if the condition changed
-func setCondition(newStatus fleetv1.BundleDeploymentStatus, err error, cond condition.Cond) fleetv1.BundleDeploymentStatus {
+func setCondition(newStatus fleetv1.BundleDeploymentStatus, err error, cond monitor.Cond) fleetv1.BundleDeploymentStatus {
 	cond.SetError(&newStatus, "", ignoreConflict(err))
 	return newStatus
 }
diff --git a/internal/cmd/agent/deployer/applied/applied.go b/internal/cmd/agent/deployer/applied/applied.go
deleted file mode 100644
index 43f2e7ea..00000000
--- a/internal/cmd/agent/deployer/applied/applied.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package applied
-
-import (
-	"strings"
-
-	"github.com/rancher/fleet/internal/config"
-	"github.com/rancher/fleet/internal/names"
-
-	"github.com/rancher/wrangler/v3/pkg/apply"
-
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/client-go/rest"
-)
-
-type Applied struct {
-	apply apply.Apply
-}
-
-// GetSetID constructs a identifier from the provided args, bundleID "fleet-agent" is special
-func GetSetID(bundleID, labelPrefix, labelSuffix string) string {
-	// bundle is fleet-agent bundle, we need to use setID fleet-agent-bootstrap since it was applied with import controller
-	if strings.HasPrefix(bundleID, "fleet-agent") {
-		if labelSuffix == "" {
-			return config.AgentBootstrapConfigName
-		}
-		return names.SafeConcatName(config.AgentBootstrapConfigName, labelSuffix)
-	}
-	if labelSuffix != "" {
-		return names.SafeConcatName(labelPrefix, bundleID, labelSuffix)
-	}
-	return names.SafeConcatName(labelPrefix, bundleID)
-}
-
-// GetLabelsAndAnnotations returns the labels and annotations, like
-// "objectset.rio.cattle.io/hash" and owners, to be able to use apply.DryRun
-func GetLabelsAndAnnotations(setID string, owner runtime.Object) (map[string]string, map[string]string, error) {
-	return apply.GetLabelsAndAnnotations(setID, owner)
-}
-
-func NewWithClient(config *rest.Config) (*Applied, error) {
-	apply, err := apply.NewForConfig(config)
-	if err != nil {
-		return nil, err
-	}
-	return &Applied{
-		apply: apply.WithDynamicLookup(),
-	}, nil
-}
-
-// DryRun does a dry run of the apply to get the difference between the
-// desired and live state. It needs a client.
-// This adds the "objectset.rio.cattle.io/applied" annotation, which is used for tracking changes.
-func (a *Applied) DryRun(defaultNS string, setID string, objs ...runtime.Object) (apply.Plan, error) {
-	apply := a.apply.
-		WithIgnorePreviousApplied().
-		WithSetID(setID).
-		WithDefaultNamespace(defaultNS)
-	return apply.DryRun(objs...)
-}
diff --git a/internal/cmd/agent/deployer/cleanup/cleanup.go b/internal/cmd/agent/deployer/cleanup/cleanup.go
index d0df8564..607c704a 100644
--- a/internal/cmd/agent/deployer/cleanup/cleanup.go
+++ b/internal/cmd/agent/deployer/cleanup/cleanup.go
@@ -9,13 +9,12 @@ import (
 	"github.com/go-logr/logr"
 	"helm.sh/helm/v3/pkg/action"
 
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/kv"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/merr"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/durations"
 
-	"github.com/rancher/wrangler/v3/pkg/kv"
-	"github.com/rancher/wrangler/v3/pkg/merr"
-
 	apierror "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/internal/cmd/agent/deployer/data/convert/convert.go b/internal/cmd/agent/deployer/data/convert/convert.go
new file mode 100644
index 00000000..e1f87ef3
--- /dev/null
+++ b/internal/cmd/agent/deployer/data/convert/convert.go
@@ -0,0 +1,140 @@
+package convert
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+func singular(value interface{}) interface{} {
+	if slice, ok := value.([]string); ok {
+		if len(slice) == 0 {
+			return nil
+		}
+		return slice[0]
+	}
+	if slice, ok := value.([]interface{}); ok {
+		if len(slice) == 0 {
+			return nil
+		}
+		return slice[0]
+	}
+	return value
+}
+
+func toStringNoTrim(value interface{}) string {
+	if t, ok := value.(time.Time); ok {
+		return t.Format(time.RFC3339)
+	}
+	single := singular(value)
+	if single == nil {
+		return ""
+	}
+	return fmt.Sprint(single)
+}
+
+func ToString(value interface{}) string {
+	return strings.TrimSpace(toStringNoTrim(value))
+}
+
+func ToTimestamp(value interface{}) (int64, error) {
+	str := ToString(value)
+	if str == "" {
+		return 0, errors.New("invalid date")
+	}
+	t, err := time.Parse(time.RFC3339, str)
+	if err != nil {
+		return 0, err
+	}
+	return t.UnixNano() / 1000000, nil
+}
+
+func ToBool(value interface{}) bool {
+	value = singular(value)
+
+	b, ok := value.(bool)
+	if ok {
+		return b
+	}
+
+	str := strings.ToLower(ToString(value))
+	return str == "true" || str == "t" || str == "yes" || str == "y"
+}
+
+func ToInterfaceSlice(obj interface{}) []interface{} {
+	if v, ok := obj.([]interface{}); ok {
+		return v
+	}
+	return nil
+}
+
+func ToMapSlice(obj interface{}) []map[string]interface{} {
+	if v, ok := obj.([]map[string]interface{}); ok {
+		return v
+	}
+	vs, _ := obj.([]interface{})
+	var result []map[string]interface{}
+	for _, item := range vs {
+		if v, ok := item.(map[string]interface{}); ok {
+			result = append(result, v)
+		} else {
+			return nil
+		}
+	}
+
+	return result
+}
+
+func ToStringSlice(data interface{}) []string {
+	if v, ok := data.([]string); ok {
+		return v
+	}
+	if v, ok := data.([]interface{}); ok {
+		var result []string
+		for _, item := range v {
+			result = append(result, ToString(item))
+		}
+		return result
+	}
+	if v, ok := data.(string); ok {
+		return []string{v}
+	}
+	return nil
+}
+
+func ToMapInterface(obj interface{}) map[string]interface{} {
+	v, _ := obj.(map[string]interface{})
+	return v
+}
+
+func ToObj(data interface{}, into interface{}) error {
+	bytes, err := json.Marshal(data)
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(bytes, into)
+}
+
+func EncodeToMap(obj interface{}) (map[string]interface{}, error) {
+	if m, ok := obj.(map[string]interface{}); ok {
+		return m, nil
+	}
+
+	if unstr, ok := obj.(*unstructured.Unstructured); ok {
+		return unstr.Object, nil
+	}
+
+	b, err := json.Marshal(obj)
+	if err != nil {
+		return nil, err
+	}
+	result := map[string]interface{}{}
+	dec := json.NewDecoder(bytes.NewBuffer(b))
+	dec.UseNumber()
+	return result, dec.Decode(&result)
+}
diff --git a/internal/cmd/agent/deployer/data/data.go b/internal/cmd/agent/deployer/data/data.go
new file mode 100644
index 00000000..84e3f9a9
--- /dev/null
+++ b/internal/cmd/agent/deployer/data/data.go
@@ -0,0 +1,35 @@
+package data
+
+import "github.com/rancher/fleet/internal/cmd/agent/deployer/data/convert"
+
+type List []map[string]interface{}
+
+type Object map[string]interface{}
+
+func (o Object) Map(names ...string) Object {
+	v := GetValueN(o, names...)
+	m := convert.ToMapInterface(v)
+	return m
+}
+
+func (o Object) Slice(names ...string) (result []Object) {
+	v := GetValueN(o, names...)
+	for _, item := range convert.ToInterfaceSlice(v) {
+		result = append(result, convert.ToMapInterface(item))
+	}
+	return
+}
+
+func (o Object) String(names ...string) string {
+	v := GetValueN(o, names...)
+	return convert.ToString(v)
+}
+
+func (o Object) StringSlice(names ...string) []string {
+	v := GetValueN(o, names...)
+	return convert.ToStringSlice(v)
+}
+
+func (o Object) Bool(key ...string) bool {
+	return convert.ToBool(GetValueN(o, key...))
+}
diff --git a/internal/cmd/agent/deployer/data/values.go b/internal/cmd/agent/deployer/data/values.go
new file mode 100644
index 00000000..18b45c44
--- /dev/null
+++ b/internal/cmd/agent/deployer/data/values.go
@@ -0,0 +1,21 @@
+// Package data contains functions for working with unstructured values like []interface or map[string]interface{}.
+// It allows reading/writing to these values without having to convert to structured items.
+package data
+
+func GetValueN(data map[string]interface{}, keys ...string) interface{} {
+	val, _ := getValue(data, keys...)
+	return val
+}
+
+// getValue works similar to GetValueFromAny, but can only process maps. Kept this way to avoid breaking changes with
+// the previous interface, GetValueFromAny should be used in most cases since that can handle slices as well.
+func getValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
+	for i, key := range keys {
+		if i == len(keys)-1 {
+			val, ok := data[key]
+			return val, ok
+		}
+		data, _ = data[key].(map[string]interface{})
+	}
+	return nil, false
+}
diff --git a/internal/cmd/agent/deployer/desiredset/clients.go b/internal/cmd/agent/deployer/desiredset/clients.go
new file mode 100644
index 00000000..9bfcda5f
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/clients.go
@@ -0,0 +1,123 @@
+package desiredset
+
+import (
+	"fmt"
+	"sync"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+)
+
+const (
+	defaultNamespace = "default"
+)
+
+type ClientFactory func(gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error)
+
+type InformerFactory interface {
+	Get(gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) (cache.SharedIndexInformer, error)
+}
+
+type InformerGetter interface {
+	Informer() cache.SharedIndexInformer
+	GroupVersionKind() schema.GroupVersionKind
+}
+
+func newForConfig(cfg *rest.Config) (*Client, error) {
+	discovery, err := discovery.NewDiscoveryClientForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	cf := newClientFactory(cfg)
+	return &Client{
+		clients: &clients{
+			clientFactory: cf,
+			discovery:     discovery,
+			namespaced:    map[schema.GroupVersionKind]bool{},
+			gvkToGVR:      map[schema.GroupVersionKind]schema.GroupVersionResource{},
+			clients:       map[schema.GroupVersionKind]dynamic.NamespaceableResourceInterface{},
+		},
+		informers: map[schema.GroupVersionKind]cache.SharedIndexInformer{},
+	}, nil
+}
+
+type Client struct {
+	clients   *clients
+	informers map[schema.GroupVersionKind]cache.SharedIndexInformer
+}
+
+type clients struct {
+	sync.Mutex
+
+	clientFactory ClientFactory
+	discovery     discovery.DiscoveryInterface
+	namespaced    map[schema.GroupVersionKind]bool
+	gvkToGVR      map[schema.GroupVersionKind]schema.GroupVersionResource
+	clients       map[schema.GroupVersionKind]dynamic.NamespaceableResourceInterface
+}
+
+func (c *clients) IsNamespaced(gvk schema.GroupVersionKind) (bool, error) {
+	c.Lock()
+	ok, exists := c.namespaced[gvk]
+	c.Unlock()
+
+	if exists {
+		return ok, nil
+	}
+	_, err := c.client(gvk)
+	if err != nil {
+		return false, err
+	}
+
+	c.Lock()
+	defer c.Unlock()
+	return c.namespaced[gvk], nil
+}
+
+func (c *clients) client(gvk schema.GroupVersionKind) (dynamic.NamespaceableResourceInterface, error) {
+	c.Lock()
+	defer c.Unlock()
+
+	if client, ok := c.clients[gvk]; ok {
+		return client, nil
+	}
+
+	resources, err := c.discovery.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
+	if err != nil {
+		return nil, err
+	}
+
+	for _, resource := range resources.APIResources {
+		if resource.Kind != gvk.Kind {
+			continue
+		}
+
+		client, err := c.clientFactory(gvk.GroupVersion().WithResource(resource.Name))
+		if err != nil {
+			return nil, err
+		}
+
+		c.namespaced[gvk] = resource.Namespaced
+		c.clients[gvk] = client
+		c.gvkToGVR[gvk] = gvk.GroupVersion().WithResource(resource.Name)
+		return client, nil
+	}
+
+	return nil, fmt.Errorf("failed to discover client for %s", gvk)
+}
+
+func newClientFactory(config *rest.Config) ClientFactory {
+	return func(gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) {
+		client, err := dynamic.NewForConfig(config)
+		if err != nil {
+			return nil, err
+		}
+
+		return client.Resource(gvr), nil
+	}
+}
diff --git a/internal/cmd/agent/deployer/desiredset/desiredset.go b/internal/cmd/agent/deployer/desiredset/desiredset.go
new file mode 100644
index 00000000..c6115b12
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/desiredset.go
@@ -0,0 +1,73 @@
+package desiredset
+
+import (
+	"context"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/merr"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// Indexer name added for cached types
+const byHash = "wrangler.byObjectSetHash"
+
+type desiredSet struct {
+	client           *Client
+	defaultNamespace string
+
+	ratelimitingQps float32
+
+	remove     bool
+	setID      string
+	objs       *objectset.ObjectSet
+	errs       []error
+	onlyDelete bool
+
+	plan Plan
+}
+
+func newDesiredSet(client *Client) desiredSet {
+	return desiredSet{
+		client:           client,
+		defaultNamespace: defaultNamespace,
+		ratelimitingQps:  1,
+	}
+}
+
+func (o *desiredSet) addErr(err error) error {
+	o.errs = append(o.errs, err)
+	return o.Err()
+}
+
+func (o desiredSet) Err() error {
+	return merr.NewErrors(append(o.errs, o.objs.Err())...)
+}
+
+func (o *desiredSet) setup(ns string, setID string, objs ...runtime.Object) {
+	if ns == "" {
+		o.defaultNamespace = defaultNamespace
+	} else {
+		o.defaultNamespace = ns
+	}
+
+	o.setID = setID
+
+	o.objs = objectset.NewObjectSet()
+	o.objs.Add(objs...)
+
+	o.plan.Create = objectset.ObjectKeyByGVK{}
+	o.plan.Update = PatchByGVK{}
+	o.plan.Delete = objectset.ObjectKeyByGVK{}
+}
+
+func (o desiredSet) dryRun(ctx context.Context) (Plan, error) {
+	err := o.apply(ctx)
+	return o.plan, err
+}
+
+func (o desiredSet) dryRunDelete(ctx context.Context) (objectset.ObjectKeyByGVK, error) {
+	o.onlyDelete = true
+	err := o.apply(ctx)
+	return o.plan.Delete, err
+}
diff --git a/internal/cmd/agent/deployer/desiredset/desiredset_apply.go b/internal/cmd/agent/deployer/desiredset/desiredset_apply.go
new file mode 100644
index 00000000..a8649504
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/desiredset_apply.go
@@ -0,0 +1,176 @@
+package desiredset
+
+import (
+	"context"
+	"crypto/sha1" // nolint:gosec // non crypto usage
+	"encoding/hex"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/selection"
+	"k8s.io/client-go/util/flowcontrol"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+const (
+	LabelApplied   = "objectset.rio.cattle.io/applied"
+	LabelID        = "objectset.rio.cattle.io/id"
+	LabelGVK       = "objectset.rio.cattle.io/owner-gvk"
+	LabelName      = "objectset.rio.cattle.io/owner-name"
+	LabelNamespace = "objectset.rio.cattle.io/owner-namespace"
+	LabelHash      = "objectset.rio.cattle.io/hash"
+	LabelPrefix    = "objectset.rio.cattle.io/"
+	LabelPrune     = "objectset.rio.cattle.io/prune"
+)
+
+var (
+	hashOrder = []string{
+		LabelID,
+		LabelGVK,
+		LabelName,
+		LabelNamespace,
+	}
+	rls     = map[string]flowcontrol.RateLimiter{}
+	rlsLock sync.Mutex
+)
+
+func (o *desiredSet) getRateLimit(labelHash string) flowcontrol.RateLimiter {
+	var rl flowcontrol.RateLimiter
+
+	rlsLock.Lock()
+	defer rlsLock.Unlock()
+	if o.remove {
+		delete(rls, labelHash)
+	} else {
+		rl = rls[labelHash]
+		if rl == nil {
+			rl = flowcontrol.NewTokenBucketRateLimiter(o.ratelimitingQps, 10)
+			rls[labelHash] = rl
+		}
+	}
+
+	return rl
+}
+
+func (o *desiredSet) apply(ctx context.Context) error {
+	if o.objs == nil || o.objs.Len() == 0 {
+		o.remove = true
+	}
+
+	if err := o.Err(); err != nil {
+		return err
+	}
+
+	labelSet, annotationSet, err := GetLabelsAndAnnotations(o.setID)
+	if err != nil {
+		return o.addErr(err)
+	}
+
+	rl := o.getRateLimit(labelSet[LabelHash])
+	if rl != nil {
+		t := time.Now()
+		rl.Accept()
+		if d := time.Since(t); d.Seconds() > 1 {
+			logger := log.FromContext(ctx)
+			logger.Info("rate limited", "setID", o.setID, "labels", labelSet, "duration", d)
+		}
+	}
+
+	objList, err := o.injectLabelsAndAnnotations(labelSet, annotationSet)
+	if err != nil {
+		return o.addErr(err)
+	}
+
+	objs := o.collect(objList)
+
+	sel, err := getSelector(labelSet)
+	if err != nil {
+		return o.addErr(err)
+	}
+
+	for _, gvk := range o.objs.GVKOrder(o.knownGVK()...) {
+		o.process(ctx, sel, gvk, objs[gvk])
+	}
+
+	return o.Err()
+}
+
+func (o *desiredSet) knownGVK() (ret []schema.GroupVersionKind) {
+	return
+}
+
+func (o *desiredSet) collect(objList []runtime.Object) objectset.ObjectByGVK {
+	result := objectset.ObjectByGVK{}
+	for _, obj := range objList {
+		_, _ = result.Add(obj)
+	}
+	return result
+}
+
+func getSelector(labelSet map[string]string) (labels.Selector, error) {
+	req, err := labels.NewRequirement(LabelHash, selection.Equals, []string{labelSet[LabelHash]})
+	if err != nil {
+		return nil, err
+	}
+	return labels.NewSelector().Add(*req), nil
+}
+
+func (o *desiredSet) injectLabelsAndAnnotations(labels, annotations map[string]string) ([]runtime.Object, error) {
+	var result []runtime.Object
+
+	for _, objMap := range o.objs.ObjectsByGVK() {
+		for key, obj := range objMap {
+			obj = obj.DeepCopyObject()
+			meta, err := meta.Accessor(obj)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get metadata for %s: %w", key, err)
+			}
+
+			setLabels(meta, labels)
+			setAnnotations(meta, annotations)
+
+			result = append(result, obj)
+		}
+	}
+
+	return result, nil
+}
+
+func setAnnotations(meta metav1.Object, annotations map[string]string) {
+	objAnn := meta.GetAnnotations()
+	if objAnn == nil {
+		objAnn = map[string]string{}
+	}
+	delete(objAnn, LabelApplied)
+	for k, v := range annotations {
+		objAnn[k] = v
+	}
+	meta.SetAnnotations(objAnn)
+}
+
+func setLabels(meta metav1.Object, labels map[string]string) {
+	objLabels := meta.GetLabels()
+	if objLabels == nil {
+		objLabels = map[string]string{}
+	}
+	for k, v := range labels {
+		objLabels[k] = v
+	}
+	meta.SetLabels(objLabels)
+}
+
+func objectSetHash(labels map[string]string) string {
+	dig := sha1.New() // nolint:gosec // non crypto usage
+	for _, key := range hashOrder {
+		dig.Write([]byte(labels[key]))
+	}
+	return hex.EncodeToString(dig.Sum(nil))
+}
diff --git a/internal/cmd/agent/deployer/desiredset/desiredset_compare.go b/internal/cmd/agent/deployer/desiredset/desiredset_compare.go
new file mode 100644
index 00000000..4988333a
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/desiredset_compare.go
@@ -0,0 +1,321 @@
+package desiredset
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/base64"
+	"fmt"
+	"strings"
+
+	"github.com/go-logr/logr"
+
+	data2 "github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data/convert"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/json"
+	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
+	"k8s.io/apimachinery/pkg/util/strategicpatch"
+)
+
+var (
+	knownListKeys = map[string]bool{
+		"apiVersion":    true,
+		"containerPort": true,
+		"devicePath":    true,
+		"ip":            true,
+		"kind":          true,
+		"mountPath":     true,
+		"name":          true,
+		"port":          true,
+		"topologyKey":   true,
+		"type":          true,
+	}
+)
+
+func prepareObjectForCreate(gvk schema.GroupVersionKind, obj runtime.Object) (runtime.Object, error) {
+	serialized, err := serializeApplied(obj)
+	if err != nil {
+		return nil, err
+	}
+
+	obj = obj.DeepCopyObject()
+	m, err := meta.Accessor(obj)
+	if err != nil {
+		return nil, err
+	}
+	annotations := m.GetAnnotations()
+	if annotations == nil {
+		annotations = map[string]string{}
+	}
+
+	annotations[LabelApplied] = appliedToAnnotation(serialized)
+	m.SetAnnotations(annotations)
+
+	typed, err := meta.TypeAccessor(obj)
+	if err != nil {
+		return nil, err
+	}
+
+	apiVersion, kind := gvk.ToAPIVersionAndKind()
+	typed.SetAPIVersion(apiVersion)
+	typed.SetKind(kind)
+
+	return obj, nil
+}
+
+func modifiedObj(gvk schema.GroupVersionKind, newObject runtime.Object) ([]byte, error) {
+	newObject, err := prepareObjectForCreate(gvk, newObject)
+	if err != nil {
+		return nil, err
+	}
+
+	modified, err := json.Marshal(newObject)
+
+	return modified, err
+}
+
+func emptyMaps(data map[string]interface{}, keys ...string) bool {
+	for _, key := range append(keys, "__invalid_key__") {
+		if len(data) == 0 {
+			// map is empty so all children are empty too
+			return true
+		} else if len(data) > 1 {
+			// map has more than one key so not empty
+			return false
+		}
+
+		value, ok := data[key]
+		if !ok {
+			// map has one key but not what we are expecting so not considered empty
+			return false
+		}
+
+		data = convert.ToMapInterface(value)
+	}
+
+	return true
+}
+
+func sanitizePatch(patch []byte, removeObjectSetAnnotation bool) ([]byte, error) {
+	mod := false
+	data := map[string]interface{}{}
+	err := json.Unmarshal(patch, &data)
+	if err != nil {
+		return nil, err
+	}
+
+	if _, ok := data["kind"]; ok {
+		mod = true
+		delete(data, "kind")
+	}
+
+	if _, ok := data["apiVersion"]; ok {
+		mod = true
+		delete(data, "apiVersion")
+	}
+
+	if _, ok := data["status"]; ok {
+		mod = true
+		delete(data, "status")
+	}
+
+	if deleted := removeCreationTimestamp(data); deleted {
+		mod = true
+	}
+
+	if removeObjectSetAnnotation {
+		metadata := convert.ToMapInterface(data2.GetValueN(data, "metadata"))
+		annotations := convert.ToMapInterface(data2.GetValueN(data, "metadata", "annotations"))
+		for k := range annotations {
+			if strings.HasPrefix(k, LabelPrefix) {
+				mod = true
+				delete(annotations, k)
+			}
+		}
+		if mod && len(annotations) == 0 {
+			delete(metadata, "annotations")
+			if len(metadata) == 0 {
+				delete(data, "metadata")
+			}
+		}
+	}
+
+	if emptyMaps(data, "metadata", "annotations") {
+		return []byte("{}"), nil
+	}
+
+	if !mod {
+		return patch, nil
+	}
+
+	return json.Marshal(data)
+}
+
+func (o *desiredSet) applyPatch(logger logr.Logger, gvk schema.GroupVersionKind, oldObject, newObject runtime.Object) (bool, error) {
+	oldMetadata, err := meta.Accessor(oldObject)
+	if err != nil {
+		return false, err
+	}
+
+	modified, err := modifiedObj(gvk, newObject)
+	if err != nil {
+		return false, err
+	}
+
+	current, err := json.Marshal(oldObject)
+	if err != nil {
+		return false, err
+	}
+
+	patch, err := doPatch(logger, gvk, modified, current)
+	if err != nil {
+		return false, fmt.Errorf("patch generation: %w", err)
+	}
+
+	if string(patch) == "{}" {
+		return false, nil
+	}
+
+	patch, err = sanitizePatch(patch, false)
+	if err != nil {
+		return false, err
+	}
+
+	if string(patch) == "{}" {
+		return false, nil
+	}
+
+	logger.V(4).Info("DesiredSet - Looking at patch for fields", "patch", string(patch), "modified", string(modified), "current", string(current))
+
+	patch, err = sanitizePatch(patch, true)
+	if err != nil {
+		return false, err
+	}
+
+	if string(patch) != "{}" {
+		o.plan.Update.Set(gvk, oldMetadata.GetNamespace(), oldMetadata.GetName(), string(patch))
+		logger.V(1).Info("DesiredSet - Updated plan with patch", "patch", string(patch))
+	}
+
+	return true, nil
+}
+
+func (o *desiredSet) compareObjects(logger logr.Logger, gvk schema.GroupVersionKind, oldObject runtime.Object, newObject runtime.Object) error {
+	if ran, err := o.applyPatch(logger, gvk, oldObject, newObject); err != nil {
+		return err
+	} else if !ran {
+		logger.V(1).Info("DesiredSet - No change")
+	}
+
+	return nil
+}
+
+func removeCreationTimestamp(data map[string]interface{}) bool {
+	metadata, ok := data["metadata"]
+	if !ok {
+		return false
+	}
+
+	data = convert.ToMapInterface(metadata)
+	if _, ok := data["creationTimestamp"]; ok {
+		delete(data, "creationTimestamp")
+		return true
+	}
+
+	return false
+}
+
+func pruneList(data []interface{}) []interface{} {
+	result := make([]interface{}, 0, len(data))
+	for _, v := range data {
+		switch typed := v.(type) {
+		case map[string]interface{}:
+			result = append(result, pruneValues(typed, true))
+		case []interface{}:
+			result = append(result, pruneList(typed))
+		default:
+			result = append(result, v)
+		}
+	}
+	return result
+}
+
+func pruneValues(data map[string]interface{}, isList bool) map[string]interface{} {
+	result := map[string]interface{}{}
+	for k, v := range data {
+		switch typed := v.(type) {
+		case map[string]interface{}:
+			result[k] = pruneValues(typed, false)
+		case []interface{}:
+			result[k] = pruneList(typed)
+		default:
+			if isList && knownListKeys[k] {
+				result[k] = v
+			} else {
+				switch x := v.(type) {
+				case string:
+					if len(x) > 64 {
+						result[k] = x[:64]
+					} else {
+						result[k] = v
+					}
+				case []byte:
+					result[k] = nil
+				default:
+					result[k] = v
+				}
+			}
+		}
+	}
+	return result
+}
+
+func serializeApplied(obj runtime.Object) ([]byte, error) {
+	data, err := convert.EncodeToMap(obj)
+	if err != nil {
+		return nil, err
+	}
+	data = pruneValues(data, false)
+	return json.Marshal(data)
+}
+
+func appliedToAnnotation(b []byte) string {
+	buf := &bytes.Buffer{}
+	w := gzip.NewWriter(buf)
+	if _, err := w.Write(b); err != nil {
+		return string(b)
+	}
+	if err := w.Close(); err != nil {
+		return string(b)
+	}
+	return base64.RawStdEncoding.EncodeToString(buf.Bytes())
+}
+
+// doPatch is adapted from "kubectl apply"
+func doPatch(logger logr.Logger, gvk schema.GroupVersionKind, modified, current []byte) ([]byte, error) {
+	var (
+		patchType types.PatchType
+		patch     []byte
+	)
+
+	patchType, lookupPatchMeta, err := getMergeStyle(gvk)
+	if err != nil {
+		return nil, err
+	}
+
+	if patchType == types.StrategicMergePatchType {
+		patch, err = strategicpatch.CreateThreeWayMergePatch(nil, modified, current, lookupPatchMeta, true)
+	} else {
+		patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(nil, modified, current)
+	}
+
+	if err != nil {
+		logger.V(1).Error(err, "Failed to calculate patch", "gvk", gvk, "error", err)
+	}
+
+	return patch, err
+}
diff --git a/internal/cmd/agent/deployer/desiredset/desiredset_process.go b/internal/cmd/agent/deployer/desiredset/desiredset_process.go
new file mode 100644
index 00000000..8d377f95
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/desiredset_process.go
@@ -0,0 +1,325 @@
+package desiredset
+
+import (
+	"context"
+	"fmt"
+	"sort"
+	"sync"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/merr"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
+	"golang.org/x/sync/errgroup"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/tools/cache"
+)
+
+func (o *desiredSet) getControllerAndClient(gvk schema.GroupVersionKind) (cache.SharedIndexInformer, dynamic.NamespaceableResourceInterface, error) {
+	// client needs to be accessed first so that the gvk->gvr mapping gets cached
+	client, err := o.client.clients.client(gvk)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return o.client.informers[gvk], client, nil
+}
+
+func (o *desiredSet) adjustNamespace(objs objectset.ObjectByKey) error {
+	for k, v := range objs {
+		if k.Namespace != "" {
+			continue
+		}
+
+		v = v.DeepCopyObject()
+		meta, err := meta.Accessor(v)
+		if err != nil {
+			return err
+		}
+
+		meta.SetNamespace(o.defaultNamespace)
+		delete(objs, k)
+		k.Namespace = o.defaultNamespace
+		objs[k] = v
+	}
+
+	return nil
+}
+
+func (o *desiredSet) clearNamespace(objs objectset.ObjectByKey) error {
+	for k, v := range objs {
+		if k.Namespace == "" {
+			continue
+		}
+
+		v = v.DeepCopyObject()
+		meta, err := meta.Accessor(v)
+		if err != nil {
+			return err
+		}
+
+		meta.SetNamespace("")
+
+		delete(objs, k)
+		k.Namespace = ""
+		objs[k] = v
+	}
+
+	return nil
+}
+
+func (o *desiredSet) filterCrossVersion(gvk schema.GroupVersionKind, keys []objectset.ObjectKey) []objectset.ObjectKey {
+	result := make([]objectset.ObjectKey, 0, len(keys))
+	gk := gvk.GroupKind()
+	for _, key := range keys {
+		if o.objs.Contains(gk, key) {
+			continue
+		}
+		if key.Namespace == o.defaultNamespace && o.objs.Contains(gk, objectset.ObjectKey{Name: key.Name}) {
+			continue
+		}
+		result = append(result, key)
+	}
+	return result
+}
+
+func (o *desiredSet) process(ctx context.Context, set labels.Selector, gvk schema.GroupVersionKind, objs objectset.ObjectByKey) {
+	controller, client, err := o.getControllerAndClient(gvk)
+	if err != nil {
+		_ = o.addErr(err)
+		return
+	}
+
+	nsed, err := o.client.clients.IsNamespaced(gvk)
+	if err != nil {
+		_ = o.addErr(err)
+		return
+	}
+
+	if nsed {
+		if err := o.adjustNamespace(objs); err != nil {
+			_ = o.addErr(err)
+			return
+		}
+	} else {
+		if err := o.clearNamespace(objs); err != nil {
+			_ = o.addErr(err)
+			return
+		}
+	}
+
+	existing, err := o.list(ctx, controller, client, set, objs)
+	if err != nil {
+		_ = o.addErr(fmt.Errorf("failed to list %s for %s: %w", gvk, o.setID, err))
+		return
+	}
+
+	toCreate, toDelete, toUpdate := compareSets(existing, objs)
+
+	// check for resources in the objectset but under a different version of the same group/kind
+	toDelete = o.filterCrossVersion(gvk, toDelete)
+
+	o.plan.Create[gvk] = toCreate
+	o.plan.Delete[gvk] = toDelete
+
+	// this is not needed for driftdetect.allResources, so PlanDelete exits early
+	if o.onlyDelete {
+		return
+	}
+
+	logger := log.FromContext(ctx).WithValues("setID", o.setID, "gvk", gvk)
+	for _, k := range toUpdate {
+		oldObject := existing[k]
+		newObject := objs[k]
+
+		oldMetadata, err := meta.Accessor(oldObject)
+		if err != nil {
+			_ = o.addErr(fmt.Errorf("failed to update patch %s for %s, access meta: %w", gvk, o.setID, err))
+		}
+
+		o.plan.Objects = append(o.plan.Objects, oldObject)
+
+		logger := logger.WithValues("name", oldMetadata.GetName(), "namespace", oldMetadata.GetNamespace())
+		err = o.compareObjects(logger, gvk, oldObject, newObject)
+		if err != nil {
+			_ = o.addErr(fmt.Errorf("failed to update patch %s for %s: %w", gvk, o.setID, err))
+		}
+	}
+}
+
+func (o *desiredSet) list(ctx context.Context, informer cache.SharedIndexInformer, client dynamic.NamespaceableResourceInterface, selector labels.Selector, desiredObjects objectset.ObjectByKey) (map[objectset.ObjectKey]runtime.Object, error) {
+	var (
+		errs []error
+		objs = objectset.ObjectByKey{}
+	)
+
+	if informer == nil {
+		// If a lister namespace is set, assume all objects belong to the listerNamespace.  If the
+		// desiredSet has an owner but no lister namespace, list objects from all namespaces to ensure
+		// we're cleaning up any owned resources.  Otherwise, search only objects from the namespaces
+		// used by the objects.  Note: desiredSets without owners will never return objects to delete;
+		// deletion requires an owner to track object references across multiple apply runs.
+		var namespaces []string = desiredObjects.Namespaces()
+
+		// no owner or lister namespace intentionally restricted; only search in specified namespaces
+		err := multiNamespaceList(ctx, namespaces, client, selector, func(obj unstructured.Unstructured) {
+			if err := addObjectToMap(objs, &obj); err != nil {
+				errs = append(errs, err)
+			}
+		})
+		if err != nil {
+			errs = append(errs, err)
+		}
+
+		return objs, merr.NewErrors(errs...)
+	}
+
+	var namespace string
+
+	// Special case for listing only by hash using indexers
+	indexer := informer.GetIndexer()
+	if hash, ok := getIndexableHash(indexer, selector); ok {
+		return listByHash(indexer, hash, namespace)
+	}
+
+	if err := cache.ListAllByNamespace(indexer, namespace, selector, func(obj interface{}) {
+		if err := addObjectToMap(objs, obj); err != nil {
+			errs = append(errs, err)
+		}
+	}); err != nil {
+		errs = append(errs, err)
+	}
+
+	return objs, merr.NewErrors(errs...)
+}
+
+func shouldPrune(obj runtime.Object) bool {
+	meta, err := meta.Accessor(obj)
+	if err != nil {
+		return true
+	}
+	return meta.GetLabels()[LabelPrune] != "false"
+}
+
+func compareSets(existingSet, newSet objectset.ObjectByKey) (toCreate, toDelete, toUpdate []objectset.ObjectKey) {
+	for k := range newSet {
+		if _, ok := existingSet[k]; ok {
+			toUpdate = append(toUpdate, k)
+		} else {
+			toCreate = append(toCreate, k)
+		}
+	}
+
+	for k, obj := range existingSet {
+		if _, ok := newSet[k]; !ok {
+			if shouldPrune(obj) {
+				toDelete = append(toDelete, k)
+			}
+		}
+	}
+
+	sortObjectKeys(toCreate)
+	sortObjectKeys(toDelete)
+	sortObjectKeys(toUpdate)
+
+	return
+}
+
+func sortObjectKeys(keys []objectset.ObjectKey) {
+	sort.Slice(keys, func(i, j int) bool {
+		return keys[i].String() < keys[j].String()
+	})
+}
+
+func addObjectToMap(objs objectset.ObjectByKey, obj interface{}) error {
+	metadata, err := meta.Accessor(obj)
+	if err != nil {
+		return err
+	}
+
+	objs[objectset.ObjectKey{
+		Namespace: metadata.GetNamespace(),
+		Name:      metadata.GetName(),
+	}] = obj.(runtime.Object)
+
+	return nil
+}
+
+// multiNamespaceList lists objects across all given namespaces, because requests are concurrent it is possible for appendFn to be called before errors are reported.
+func multiNamespaceList(ctx context.Context, namespaces []string, baseClient dynamic.NamespaceableResourceInterface, selector labels.Selector, appendFn func(obj unstructured.Unstructured)) error {
+	var mu sync.Mutex
+	wg, _ctx := errgroup.WithContext(ctx)
+
+	// list all namespaces concurrently
+	for _, namespace := range namespaces {
+		namespace := namespace
+		wg.Go(func() error {
+			list, err := baseClient.Namespace(namespace).List(_ctx, v1.ListOptions{
+				LabelSelector: selector.String(),
+			})
+			if err != nil {
+				return err
+			}
+
+			mu.Lock()
+			for _, obj := range list.Items {
+				appendFn(obj)
+			}
+			mu.Unlock()
+
+			return nil
+		})
+	}
+
+	return wg.Wait()
+}
+
+// getIndexableHash detects if provided selector can be replaced by using the hash index, if configured, in which case returns the hash value
+func getIndexableHash(indexer cache.Indexer, selector labels.Selector) (string, bool) {
+	// Check if indexer was added
+	if indexer == nil || indexer.GetIndexers()[byHash] == nil {
+		return "", false
+	}
+
+	// Check specific case of listing with exact hash label selector
+	if req, selectable := selector.Requirements(); len(req) != 1 || !selectable {
+		return "", false
+	}
+
+	return selector.RequiresExactMatch(LabelHash)
+}
+
+// inNamespace checks whether a given object is a Kubernetes object and is part of the provided namespace
+func inNamespace(namespace string, obj interface{}) bool {
+	metadata, err := meta.Accessor(obj)
+	return err == nil && metadata.GetNamespace() == namespace
+}
+
+// listByHash use a pre-configured indexer to list objects of a certain type by their hash label
+func listByHash(indexer cache.Indexer, hash string, namespace string) (map[objectset.ObjectKey]runtime.Object, error) {
+	var (
+		errs []error
+		objs = objectset.ObjectByKey{}
+	)
+	res, err := indexer.ByIndex(byHash, hash)
+	if err != nil {
+		return nil, err
+	}
+	for _, obj := range res {
+		if namespace != "" && !inNamespace(namespace, obj) {
+			continue
+		}
+		if err := addObjectToMap(objs, obj); err != nil {
+			errs = append(errs, err)
+		}
+	}
+	return objs, merr.NewErrors(errs...)
+}
diff --git a/internal/cmd/agent/deployer/applied/diff.go b/internal/cmd/agent/deployer/desiredset/diff.go
similarity index 92%
rename from internal/cmd/agent/deployer/applied/diff.go
rename to internal/cmd/agent/deployer/desiredset/diff.go
index dfe2c5df..7e840b37 100644
--- a/internal/cmd/agent/deployer/applied/diff.go
+++ b/internal/cmd/agent/deployer/desiredset/diff.go
@@ -1,4 +1,4 @@
-package applied
+package desiredset
 
 import (
 	"encoding/json"
@@ -8,13 +8,11 @@ import (
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diff"
 	argo "github.com/rancher/fleet/internal/cmd/agent/deployer/internal/normalizers"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/resource"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/merr"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/normalizers"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
-	"github.com/rancher/wrangler/v3/pkg/apply"
-	"github.com/rancher/wrangler/v3/pkg/merr"
-	"github.com/rancher/wrangler/v3/pkg/objectset"
-
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -23,7 +21,7 @@ import (
 // Diff factors the bundledeployment's bundle diff patches into the plan from
 // DryRun. This way, the status of the bundledeployment can be updated
 // accurately.
-func Diff(plan apply.Plan, bd *fleet.BundleDeployment, ns string, objs ...runtime.Object) (apply.Plan, error) {
+func Diff(plan Plan, bd *fleet.BundleDeployment, ns string, objs ...runtime.Object) (Plan, error) {
 	desired := objectset.NewObjectSet(objs...).ObjectsByGVK()
 	live := objectset.NewObjectSet(plan.Objects...).ObjectsByGVK()
 
@@ -73,7 +71,7 @@ func Diff(plan apply.Plan, bd *fleet.BundleDeployment, ns string, objs ...runtim
 				continue
 			}
 			// this will overwrite an existing entry in the Update map
-			plan.Update.Add(gvk, key.Namespace, key.Name, string(patch))
+			plan.Update.Set(gvk, key.Namespace, key.Name, string(patch))
 		}
 		if len(errs) > 0 {
 			return plan, merr.NewErrors(errs...)
diff --git a/internal/cmd/agent/deployer/desiredset/plan.go b/internal/cmd/agent/deployer/desiredset/plan.go
new file mode 100644
index 00000000..c4ccd010
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/plan.go
@@ -0,0 +1,102 @@
+package desiredset
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+	"github.com/rancher/fleet/internal/config"
+	"github.com/rancher/fleet/internal/names"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/rest"
+)
+
+type PatchByGVK map[schema.GroupVersionKind]map[objectset.ObjectKey]string
+
+func (p PatchByGVK) Set(gvk schema.GroupVersionKind, namespace, name, patch string) {
+	d, ok := p[gvk]
+	if !ok {
+		d = map[objectset.ObjectKey]string{}
+		p[gvk] = d
+	}
+	d[objectset.ObjectKey{
+		Name:      name,
+		Namespace: namespace,
+	}] = patch
+}
+
+type Plan struct {
+	Create objectset.ObjectKeyByGVK
+
+	// Delete contains existing objects that are not in the desired state,
+	// unless their prune label is set to "false".
+	Delete objectset.ObjectKeyByGVK
+
+	// Update contains objects, already existing in the cluster, that have
+	// changes. The patch would restore their desired state, as represented
+	// in the helm manifest's resources passed into Plan(..., objs).
+	Update PatchByGVK
+
+	// Objects contains objects, already existing in the cluster, that have
+	// valid metadata
+	Objects []runtime.Object
+}
+
+func New(config *rest.Config) (*Client, error) {
+	client, err := newForConfig(config)
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+// Plan does a dry run of the apply to get the difference between the
+// desired and live state. It needs a client.
+// This adds the "objectset.rio.cattle.io/applied" annotation, which is used for tracking changes.
+func (a *Client) Plan(ctx context.Context, defaultNS string, setID string, objs ...runtime.Object) (Plan, error) {
+	ds := newDesiredSet(a)
+	ds.setup(defaultNS, setID, objs...)
+	return ds.dryRun(ctx)
+}
+
+func (a *Client) PlanDelete(ctx context.Context, defaultNS string, setID string, objs ...runtime.Object) (objectset.ObjectKeyByGVK, error) {
+	ds := newDesiredSet(a)
+	ds.setup(defaultNS, setID, objs...)
+	return ds.dryRunDelete(ctx)
+}
+
+// GetSetID constructs a identifier from the provided args, bundleID "fleet-agent" is special
+func GetSetID(bundleID, labelPrefix, labelSuffix string) string {
+	// bundle is fleet-agent bundle, we need to use setID fleet-agent-bootstrap since it was applied with import controller
+	if strings.HasPrefix(bundleID, "fleet-agent") {
+		if labelSuffix == "" {
+			return config.AgentBootstrapConfigName
+		}
+		return names.SafeConcatName(config.AgentBootstrapConfigName, labelSuffix)
+	}
+	if labelSuffix != "" {
+		return names.SafeConcatName(labelPrefix, bundleID, labelSuffix)
+	}
+	return names.SafeConcatName(labelPrefix, bundleID)
+}
+
+// GetLabelsAndAnnotations returns the labels and annotations, like
+// "objectset.rio.cattle.io/hash" and owners, to be able to use apply.DryRun
+func GetLabelsAndAnnotations(setID string) (map[string]string, map[string]string, error) {
+	if setID == "" {
+		return nil, nil, fmt.Errorf("set ID or owner must be set")
+	}
+
+	annotations := map[string]string{
+		LabelID: setID,
+	}
+
+	labels := map[string]string{
+		LabelHash: objectSetHash(annotations),
+	}
+
+	return labels, annotations, nil
+}
diff --git a/internal/cmd/agent/deployer/desiredset/style.go b/internal/cmd/agent/deployer/desiredset/style.go
new file mode 100644
index 00000000..0339a1b1
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/style.go
@@ -0,0 +1,59 @@
+package desiredset
+
+import (
+	"sync"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/strategicpatch"
+	"k8s.io/client-go/kubernetes/scheme"
+)
+
+var (
+	patchCache     = map[schema.GroupVersionKind]patchCacheEntry{}
+	patchCacheLock = sync.Mutex{}
+)
+
+type patchCacheEntry struct {
+	patchType types.PatchType
+	lookup    strategicpatch.LookupPatchMeta
+}
+
+func getMergeStyle(gvk schema.GroupVersionKind) (types.PatchType, strategicpatch.LookupPatchMeta, error) {
+	var (
+		patchType       types.PatchType
+		lookupPatchMeta strategicpatch.LookupPatchMeta
+	)
+
+	patchCacheLock.Lock()
+	entry, ok := patchCache[gvk]
+	patchCacheLock.Unlock()
+
+	if ok {
+		return entry.patchType, entry.lookup, nil
+	}
+
+	versionedObject, err := scheme.Scheme.New(gvk)
+
+	if runtime.IsNotRegisteredError(err) || gvk.Kind == "CustomResourceDefinition" {
+		patchType = types.MergePatchType
+	} else if err != nil {
+		return patchType, nil, err
+	} else {
+		patchType = types.StrategicMergePatchType
+		lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
+		if err != nil {
+			return patchType, nil, err
+		}
+	}
+
+	patchCacheLock.Lock()
+	patchCache[gvk] = patchCacheEntry{
+		patchType: patchType,
+		lookup:    lookupPatchMeta,
+	}
+	patchCacheLock.Unlock()
+
+	return patchType, lookupPatchMeta, nil
+}
diff --git a/internal/cmd/agent/deployer/driftdetect/driftdetect.go b/internal/cmd/agent/deployer/driftdetect/driftdetect.go
index f177f45e..29669732 100644
--- a/internal/cmd/agent/deployer/driftdetect/driftdetect.go
+++ b/internal/cmd/agent/deployer/driftdetect/driftdetect.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/go-logr/logr"
 
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
 	"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"
@@ -14,6 +14,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
 type DriftDetect struct {
@@ -23,7 +24,7 @@ type DriftDetect struct {
 	upstreamClient client.Client
 	upstreamReader client.Reader
 
-	applied          *applied.Applied
+	desiredset       *desiredset.Client
 	defaultNamespace string
 	labelPrefix      string
 	labelSuffix      string
@@ -33,7 +34,7 @@ func New(
 	trigger *trigger.Trigger,
 	upstreamClient client.Client,
 	upstreamReader client.Reader,
-	applied *applied.Applied,
+	desiredset *desiredset.Client,
 	defaultNamespace string,
 	labelPrefix string,
 	labelSuffix string,
@@ -42,7 +43,7 @@ func New(
 		trigger:          trigger,
 		upstreamClient:   upstreamClient,
 		upstreamReader:   upstreamReader,
-		applied:          applied,
+		desiredset:       desiredset,
 		defaultNamespace: defaultNamespace,
 		labelPrefix:      labelPrefix,
 		labelSuffix:      labelSuffix,
@@ -54,11 +55,11 @@ func (d *DriftDetect) Clear(bdKey string) error {
 }
 
 // Refresh triggers a sync of all resources of the provided bd which may have drifted from their desired state.
-func (d *DriftDetect) Refresh(logger logr.Logger, bdKey string, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
-	logger = logger.WithName("DriftDetect")
+func (d *DriftDetect) Refresh(ctx context.Context, bdKey string, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
+	logger := log.FromContext(ctx).WithName("DriftDetect")
 	logger.V(1).Info("Refreshing drift detection")
 
-	resources, err := d.allResources(bd, resources)
+	resources, err := d.allResources(ctx, bd, resources)
 	if err != nil {
 		return err
 	}
@@ -123,18 +124,18 @@ func (d *DriftDetect) requeueBD(logger logr.Logger, handleID int, namespace stri
 // 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 (d *DriftDetect) allResources(bd *fleet.BundleDeployment, resources *helmdeployer.Resources) (*helmdeployer.Resources, error) {
+func (d *DriftDetect) allResources(ctx context.Context, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) (*helmdeployer.Resources, error) {
 	ns := resources.DefaultNamespace
 	if ns == "" {
 		ns = d.defaultNamespace
 	}
 
-	plan, err := d.applied.DryRun(ns, applied.GetSetID(bd.Name, d.labelPrefix, d.labelSuffix), resources.Objects...)
+	plan, err := d.desiredset.PlanDelete(ctx, ns, desiredset.GetSetID(bd.Name, d.labelPrefix, d.labelSuffix), resources.Objects...)
 	if err != nil {
 		return nil, err
 	}
 
-	for gvk, keys := range plan.Delete {
+	for gvk, keys := range plan {
 		for _, key := range keys {
 			u := &unstructured.Unstructured{}
 			u.SetGroupVersionKind(gvk)
diff --git a/internal/cmd/agent/deployer/kv/split.go b/internal/cmd/agent/deployer/kv/split.go
new file mode 100644
index 00000000..65359b25
--- /dev/null
+++ b/internal/cmd/agent/deployer/kv/split.go
@@ -0,0 +1,24 @@
+package kv
+
+import "strings"
+
+func Split(s, sep string) (string, string) {
+	parts := strings.SplitN(s, sep, 2)
+	return strings.TrimSpace(parts[0]), strings.TrimSpace(safeIndex(parts, 1))
+}
+
+// Like split but if there is only one item return "", item
+func RSplit(s, sep string) (string, string) {
+	parts := strings.SplitN(s, sep, 2)
+	if len(parts) == 1 {
+		return "", strings.TrimSpace(parts[0])
+	}
+	return strings.TrimSpace(parts[0]), strings.TrimSpace(safeIndex(parts, 1))
+}
+
+func safeIndex(parts []string, idx int) string {
+	if len(parts) <= idx {
+		return ""
+	}
+	return parts[idx]
+}
diff --git a/internal/cmd/agent/deployer/merr/error.go b/internal/cmd/agent/deployer/merr/error.go
new file mode 100644
index 00000000..23b3c2e0
--- /dev/null
+++ b/internal/cmd/agent/deployer/merr/error.go
@@ -0,0 +1,37 @@
+package merr
+
+import "bytes"
+
+type Errors []error
+
+func (e Errors) Err() error {
+	return NewErrors(e...)
+}
+
+func (e Errors) Error() string {
+	buf := bytes.NewBuffer(nil)
+	for _, err := range e {
+		if buf.Len() > 0 {
+			buf.WriteString(", ")
+		}
+		buf.WriteString(err.Error())
+	}
+
+	return buf.String()
+}
+
+func NewErrors(inErrors ...error) error {
+	var errors []error
+	for _, err := range inErrors {
+		if err != nil {
+			errors = append(errors, err)
+		}
+	}
+
+	if len(errors) == 0 {
+		return nil
+	} else if len(errors) == 1 {
+		return errors[0]
+	}
+	return Errors(errors)
+}
diff --git a/internal/cmd/agent/deployer/monitor/condition.go b/internal/cmd/agent/deployer/monitor/condition.go
new file mode 100644
index 00000000..87c3e69e
--- /dev/null
+++ b/internal/cmd/agent/deployer/monitor/condition.go
@@ -0,0 +1,174 @@
+package monitor
+
+import (
+	"reflect"
+	"time"
+
+	"github.com/rancher/lasso/pkg/controller"
+
+	"github.com/sirupsen/logrus"
+)
+
+type Cond string
+
+var ErrSkip = controller.ErrIgnore
+
+func (c Cond) SetError(obj interface{}, reason string, err error) {
+	if err == nil || err == ErrSkip {
+		c.True(obj)
+		c.Message(obj, "")
+		c.Reason(obj, reason)
+		return
+	}
+	if reason == "" {
+		reason = "Error"
+	}
+	c.False(obj)
+	c.Message(obj, err.Error())
+	c.Reason(obj, reason)
+}
+
+func (c Cond) True(obj interface{}) {
+	setStatus(obj, string(c), "True")
+}
+
+func (c Cond) IsTrue(obj interface{}) bool {
+	return getStatus(obj, string(c)) == "True"
+}
+
+func (c Cond) False(obj interface{}) {
+	setStatus(obj, string(c), "False")
+}
+
+func (c Cond) IsFalse(obj interface{}) bool {
+	return getStatus(obj, string(c)) == "False"
+}
+
+func (c Cond) Reason(obj interface{}, reason string) {
+	cond := findOrCreateCond(obj, string(c))
+	getFieldValue(cond, "Reason").SetString(reason)
+}
+
+func (c Cond) GetReason(obj interface{}) string {
+	cond := findOrNotCreateCond(obj, string(c))
+	if cond == nil {
+		return ""
+	}
+	return getFieldValue(*cond, "Reason").String()
+}
+
+func (c Cond) Message(obj interface{}, message string) {
+	cond := findOrCreateCond(obj, string(c))
+	setValue(cond, "Message", message)
+}
+
+func (c Cond) GetMessage(obj interface{}) string {
+	cond := findOrNotCreateCond(obj, string(c))
+	if cond == nil {
+		return ""
+	}
+	return getFieldValue(*cond, "Message").String()
+}
+
+func touchTS(value reflect.Value) {
+	now := time.Now().UTC().Format(time.RFC3339)
+	getFieldValue(value, "LastUpdateTime").SetString(now)
+}
+
+func getStatus(obj interface{}, condName string) string {
+	cond := findOrNotCreateCond(obj, condName)
+	if cond == nil {
+		return ""
+	}
+	return getFieldValue(*cond, "Status").String()
+}
+
+func setStatus(obj interface{}, condName, status string) {
+	if reflect.TypeOf(obj).Kind() != reflect.Ptr {
+		panic("obj passed must be a pointer")
+	}
+	cond := findOrCreateCond(obj, condName)
+	setValue(cond, "Status", status)
+}
+
+func setValue(cond reflect.Value, fieldName, newValue string) {
+	value := getFieldValue(cond, fieldName)
+	if value.String() != newValue {
+		value.SetString(newValue)
+		touchTS(cond)
+	}
+}
+
+func findOrNotCreateCond(obj interface{}, condName string) *reflect.Value {
+	condSlice := getValue(obj, "Status", "Conditions")
+	if !condSlice.IsValid() {
+		condSlice = getValue(obj, "Conditions")
+	}
+	return findCond(obj, condSlice, condName)
+}
+
+func findOrCreateCond(obj interface{}, condName string) reflect.Value {
+	condSlice := getValue(obj, "Status", "Conditions")
+	if !condSlice.IsValid() {
+		condSlice = getValue(obj, "Conditions")
+	}
+	cond := findCond(obj, condSlice, condName)
+	if cond != nil {
+		return *cond
+	}
+
+	newCond := reflect.New(condSlice.Type().Elem()).Elem()
+	newCond.FieldByName("Type").SetString(condName)
+	newCond.FieldByName("Status").SetString("Unknown")
+	condSlice.Set(reflect.Append(condSlice, newCond))
+	return *findCond(obj, condSlice, condName)
+}
+
+func findCond(obj interface{}, val reflect.Value, name string) *reflect.Value {
+	defer func() {
+		if recover() != nil {
+			logrus.Fatalf("failed to find .Status.Conditions field on %v", reflect.TypeOf(obj))
+		}
+	}()
+
+	for i := 0; i < val.Len(); i++ {
+		cond := val.Index(i)
+		typeVal := getFieldValue(cond, "Type")
+		if typeVal.String() == name {
+			return &cond
+		}
+	}
+
+	return nil
+}
+
+func getValue(obj interface{}, name ...string) reflect.Value {
+	if obj == nil {
+		return reflect.Value{}
+	}
+	v := reflect.ValueOf(obj)
+	t := v.Type()
+	if t.Kind() == reflect.Ptr {
+		v = v.Elem()
+	}
+
+	field := v.FieldByName(name[0])
+	if len(name) == 1 {
+		return field
+	}
+	return getFieldValue(field, name[1:]...)
+}
+
+func getFieldValue(v reflect.Value, name ...string) reflect.Value {
+	if !v.IsValid() {
+		return v
+	}
+	if len(name) == 0 {
+		return v
+	}
+	field := v.FieldByName(name[0])
+	if len(name) == 1 {
+		return field
+	}
+	return getFieldValue(field, name[1:]...)
+}
diff --git a/internal/cmd/agent/deployer/normalizers/jsonpatch.go b/internal/cmd/agent/deployer/normalizers/jsonpatch.go
index 3cb19290..72768414 100644
--- a/internal/cmd/agent/deployer/normalizers/jsonpatch.go
+++ b/internal/cmd/agent/deployer/normalizers/jsonpatch.go
@@ -2,9 +2,9 @@ package normalizers
 
 import (
 	jsonpatch "github.com/evanphx/json-patch"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
 	"github.com/sirupsen/logrus"
 
-	"github.com/rancher/wrangler/v3/pkg/objectset"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/internal/cmd/agent/deployer/normalizers/mutatingwebhook.go b/internal/cmd/agent/deployer/normalizers/mutatingwebhook.go
index 98b19100..6368656d 100644
--- a/internal/cmd/agent/deployer/normalizers/mutatingwebhook.go
+++ b/internal/cmd/agent/deployer/normalizers/mutatingwebhook.go
@@ -3,7 +3,8 @@ package normalizers
 import (
 	"github.com/sirupsen/logrus"
 
-	"github.com/rancher/wrangler/v3/pkg/objectset"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
 	adregv1 "k8s.io/api/admissionregistration/v1"
 	adregv1beta1 "k8s.io/api/admissionregistration/v1beta1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
diff --git a/internal/cmd/agent/deployer/normalizers/norm.go b/internal/cmd/agent/deployer/normalizers/norm.go
index 7686727b..54f67825 100644
--- a/internal/cmd/agent/deployer/normalizers/norm.go
+++ b/internal/cmd/agent/deployer/normalizers/norm.go
@@ -2,7 +2,8 @@ package normalizers
 
 import (
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/internal/diff"
-	"github.com/rancher/wrangler/v3/pkg/objectset"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
diff --git a/internal/cmd/agent/deployer/normalizers/validatingwebhook.go b/internal/cmd/agent/deployer/normalizers/validatingwebhook.go
index 8479b4cf..bd106979 100644
--- a/internal/cmd/agent/deployer/normalizers/validatingwebhook.go
+++ b/internal/cmd/agent/deployer/normalizers/validatingwebhook.go
@@ -3,7 +3,8 @@ package normalizers
 import (
 	"github.com/sirupsen/logrus"
 
-	"github.com/rancher/wrangler/v3/pkg/objectset"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+
 	adregv1 "k8s.io/api/admissionregistration/v1"
 	adregv1beta1 "k8s.io/api/admissionregistration/v1beta1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
diff --git a/internal/cmd/agent/deployer/objectset/objectset.go b/internal/cmd/agent/deployer/objectset/objectset.go
new file mode 100644
index 00000000..4ba26441
--- /dev/null
+++ b/internal/cmd/agent/deployer/objectset/objectset.go
@@ -0,0 +1,212 @@
+package objectset
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/merr"
+
+	"github.com/rancher/wrangler/v3/pkg/schemes"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+type ObjectKey struct {
+	Name      string
+	Namespace string
+}
+
+func NewObjectKey(obj v1.Object) ObjectKey {
+	return ObjectKey{
+		Namespace: obj.GetNamespace(),
+		Name:      obj.GetName(),
+	}
+}
+
+func (o ObjectKey) String() string {
+	if o.Namespace == "" {
+		return o.Name
+	}
+	return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
+}
+
+type ObjectKeyByGVK map[schema.GroupVersionKind][]ObjectKey
+
+type ObjectByGVK map[schema.GroupVersionKind]map[ObjectKey]runtime.Object
+
+func (o ObjectByGVK) Add(obj runtime.Object) (schema.GroupVersionKind, error) {
+	metadata, err := meta.Accessor(obj)
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+
+	gvk, err := getGVK(obj)
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+
+	objs := o[gvk]
+	if objs == nil {
+		objs = ObjectByKey{}
+		o[gvk] = objs
+	}
+
+	objs[ObjectKey{
+		Namespace: metadata.GetNamespace(),
+		Name:      metadata.GetName(),
+	}] = obj
+
+	return gvk, nil
+}
+
+type ObjectSet struct {
+	errs        []error
+	objects     ObjectByGVK
+	objectsByGK ObjectByGK
+	order       []runtime.Object
+	gvkOrder    []schema.GroupVersionKind
+	gvkSeen     map[schema.GroupVersionKind]bool
+}
+
+func NewObjectSet(objs ...runtime.Object) *ObjectSet {
+	os := &ObjectSet{
+		objects:     ObjectByGVK{},
+		objectsByGK: ObjectByGK{},
+		gvkSeen:     map[schema.GroupVersionKind]bool{},
+	}
+	os.Add(objs...)
+	return os
+}
+
+func (o *ObjectSet) ObjectsByGVK() ObjectByGVK {
+	if o == nil {
+		return nil
+	}
+	return o.objects
+}
+
+func (o *ObjectSet) Contains(gk schema.GroupKind, key ObjectKey) bool {
+	_, ok := o.objectsByGK[gk][key]
+	return ok
+}
+
+func (o *ObjectSet) Add(objs ...runtime.Object) *ObjectSet {
+	for _, obj := range objs {
+		o.add(obj)
+	}
+	return o
+}
+
+func (o *ObjectSet) add(obj runtime.Object) {
+	if obj == nil || reflect.ValueOf(obj).IsNil() {
+		return
+	}
+
+	gvk, err := o.objects.Add(obj)
+	if err != nil {
+		o.err(fmt.Errorf("failed to add %T: %w", obj, err))
+		return
+	}
+
+	_, err = o.objectsByGK.add(obj)
+	if err != nil {
+		o.err(fmt.Errorf("failed to add %T: %w", obj, err))
+		return
+	}
+
+	o.order = append(o.order, obj)
+	if !o.gvkSeen[gvk] {
+		o.gvkSeen[gvk] = true
+		o.gvkOrder = append(o.gvkOrder, gvk)
+	}
+}
+
+func (o *ObjectSet) err(err error) {
+	o.errs = append(o.errs, err)
+}
+
+func (o *ObjectSet) Err() error {
+	return merr.NewErrors(o.errs...)
+}
+
+func (o *ObjectSet) Len() int {
+	return len(o.objects)
+}
+
+func (o *ObjectSet) GVKOrder(known ...schema.GroupVersionKind) []schema.GroupVersionKind {
+	var rest []schema.GroupVersionKind
+
+	for _, gvk := range known {
+		if o.gvkSeen[gvk] {
+			continue
+		}
+		rest = append(rest, gvk)
+	}
+
+	sort.Slice(rest, func(i, j int) bool {
+		return rest[i].String() < rest[j].String()
+	})
+
+	return append(o.gvkOrder, rest...)
+}
+
+type ObjectByKey map[ObjectKey]runtime.Object
+
+func (o ObjectByKey) Namespaces() []string {
+	namespaces := Set{}
+	for objKey := range o {
+		namespaces.Add(objKey.Namespace)
+	}
+	return namespaces.Values()
+}
+
+type ObjectByGK map[schema.GroupKind]map[ObjectKey]runtime.Object
+
+func (o ObjectByGK) add(obj runtime.Object) (schema.GroupKind, error) {
+	metadata, err := meta.Accessor(obj)
+	if err != nil {
+		return schema.GroupKind{}, err
+	}
+
+	gvk, err := getGVK(obj)
+	if err != nil {
+		return schema.GroupKind{}, err
+	}
+
+	gk := gvk.GroupKind()
+
+	objs := o[gk]
+	if objs == nil {
+		objs = ObjectByKey{}
+		o[gk] = objs
+	}
+
+	objs[ObjectKey{
+		Namespace: metadata.GetNamespace(),
+		Name:      metadata.GetName(),
+	}] = obj
+
+	return gk, nil
+}
+
+func getGVK(obj runtime.Object) (schema.GroupVersionKind, error) {
+	gvk := obj.GetObjectKind().GroupVersionKind()
+	if gvk.Kind != "" {
+		return gvk, nil
+	}
+
+	gvks, _, err := schemes.All.ObjectKinds(obj)
+	if err != nil {
+		return schema.GroupVersionKind{}, fmt.Errorf("failed to find gvk for %T, you may need to import the wrangler generated controller package: %w", obj, err)
+	}
+
+	if len(gvks) == 0 {
+		return schema.GroupVersionKind{}, fmt.Errorf("failed to find gvk for %T", obj)
+	}
+
+	return gvks[0], nil
+}
diff --git a/internal/cmd/agent/deployer/objectset/stringset.go b/internal/cmd/agent/deployer/objectset/stringset.go
new file mode 100644
index 00000000..b04f32e3
--- /dev/null
+++ b/internal/cmd/agent/deployer/objectset/stringset.go
@@ -0,0 +1,30 @@
+package objectset
+
+var empty struct{}
+
+// Set is an exceptionally simple `set` implementation for strings.
+// It is not threadsafe, but can be used in place of a simple `map[string]struct{}`
+// as long as you don't want to do too much with it.
+type Set struct {
+	m map[string]struct{}
+}
+
+func (s *Set) Add(ss ...string) {
+	if s.m == nil {
+		s.m = make(map[string]struct{}, len(ss))
+	}
+	for _, k := range ss {
+		s.m[k] = empty
+	}
+}
+
+func (s *Set) Values() []string {
+	i := 0
+	keys := make([]string, len(s.m))
+	for key := range s.m {
+		keys[i] = key
+		i++
+	}
+
+	return keys
+}
diff --git a/internal/cmd/agent/deployer/summary/cattletypes.go b/internal/cmd/agent/deployer/summary/cattletypes.go
new file mode 100644
index 00000000..272e92a0
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/cattletypes.go
@@ -0,0 +1,44 @@
+package summary
+
+import (
+	"strings"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+)
+
+func checkCattleReady(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if strings.Contains(obj.String("apiVersion"), "cattle.io/") {
+		for _, condition := range condition {
+			if condition.Type() == "Ready" && condition.Status() == "False" && condition.Message() != "" {
+				summary.Message = append(summary.Message, condition.Message())
+				summary.Error = true
+				return summary
+			}
+		}
+	}
+
+	return summary
+}
+
+func checkCattleTypes(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	return checkRelease(obj, condition, summary)
+}
+
+func checkRelease(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if !isKind(obj, "App", "catalog.cattle.io") {
+		return summary
+	}
+	if obj.String("status", "summary", "state") != "deployed" {
+		return summary
+	}
+	for _, resources := range obj.Slice("spec", "resources") {
+		summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+			Name:       resources.String("name"),
+			Kind:       resources.String("kind"),
+			APIVersion: resources.String("apiVersion"),
+			Type:       "helmresource",
+		})
+	}
+	return summary
+}
diff --git a/internal/cmd/agent/deployer/summary/condition.go b/internal/cmd/agent/deployer/summary/condition.go
new file mode 100644
index 00000000..28877cfc
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/condition.go
@@ -0,0 +1,52 @@
+package summary
+
+import (
+	"encoding/json"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+)
+
+func getRawConditions(obj data.Object) []data.Object {
+	statusAnn := obj.String("metadata", "annotations", "cattle.io/status")
+	if statusAnn != "" {
+		status := data.Object{}
+		if err := json.Unmarshal([]byte(statusAnn), &status); err == nil {
+			return append(obj.Slice("status", "conditions"), status.Slice("conditions")...)
+		}
+	}
+	return obj.Slice("status", "conditions")
+}
+
+func getConditions(obj data.Object) (result []Condition) {
+	for _, condition := range getRawConditions(obj) {
+		result = append(result, Condition{Object: condition})
+	}
+	return
+}
+
+type Condition struct {
+	data.Object
+}
+
+func (c Condition) Type() string {
+	return c.String("type")
+}
+
+func (c Condition) Status() string {
+	return c.String("status")
+}
+
+func (c Condition) Reason() string {
+	return c.String("reason")
+}
+
+func (c Condition) Message() string {
+	return c.String("message")
+}
+
+func (c Condition) Equals(other Condition) bool {
+	return c.Type() == other.Type() &&
+		c.Status() == other.Status() &&
+		c.Reason() == other.Reason() &&
+		c.Message() == other.Message()
+}
diff --git a/internal/cmd/agent/deployer/summary/coretypes.go b/internal/cmd/agent/deployer/summary/coretypes.go
new file mode 100644
index 00000000..aa5749fa
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/coretypes.go
@@ -0,0 +1,212 @@
+package summary
+
+import (
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data/convert"
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func checkHasPodTemplate(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	template := obj.Map("spec", "template")
+	if template == nil {
+		return summary
+	}
+
+	if !isKind(obj, "ReplicaSet", "apps/", "extension/") &&
+		!isKind(obj, "DaemonSet", "apps/", "extension/") &&
+		!isKind(obj, "StatefulSet", "apps/", "extension/") &&
+		!isKind(obj, "Deployment", "apps/", "extension/") &&
+		!isKind(obj, "Job", "batch/") &&
+		!isKind(obj, "Service") {
+		return summary
+	}
+
+	return checkPodTemplate(template, condition, summary)
+}
+
+func checkHasPodSelector(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	selector := obj.Map("spec", "selector")
+	if selector == nil {
+		return summary
+	}
+
+	if !isKind(obj, "ReplicaSet", "apps/", "extension/") &&
+		!isKind(obj, "DaemonSet", "apps/", "extension/") &&
+		!isKind(obj, "StatefulSet", "apps/", "extension/") &&
+		!isKind(obj, "Deployment", "apps/", "extension/") &&
+		!isKind(obj, "Job", "batch/") &&
+		!isKind(obj, "Service") {
+		return summary
+	}
+
+	_, hasMatch := selector["matchLabels"]
+	if !hasMatch {
+		_, hasMatch = selector["matchExpressions"]
+	}
+	sel := metav1.LabelSelector{}
+	if hasMatch {
+		if err := convert.ToObj(selector, &sel); err != nil {
+			return summary
+		}
+	} else {
+		sel.MatchLabels = map[string]string{}
+		for k, v := range selector {
+			sel.MatchLabels[k] = convert.ToString(v)
+		}
+	}
+
+	t := "creates"
+	if obj["kind"] == "Service" {
+		t = "selects"
+	}
+
+	summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+		Kind:       "Pod",
+		APIVersion: "v1",
+		Type:       t,
+		Selector:   &sel,
+	})
+	return summary
+}
+
+func checkPod(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if !isKind(obj, "Pod") {
+		return summary
+	}
+	if obj.String("kind") != "Pod" || obj.String("apiVersion") != "v1" {
+		return summary
+	}
+	return checkPodTemplate(obj, condition, summary)
+}
+
+func checkPodTemplate(obj data.Object, condition []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	summary = checkPodConfigMaps(obj, condition, summary)
+	summary = checkPodSecrets(obj, condition, summary)
+	summary = checkPodServiceAccount(obj, condition, summary)
+	summary = checkPodProjectedVolume(obj, condition, summary)
+	summary = checkPodPullSecret(obj, condition, summary)
+	return summary
+}
+
+func checkPodPullSecret(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	for _, pullSecret := range obj.Slice("imagePullSecrets") {
+		if name := pullSecret.String("name"); name != "" {
+			summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+				Name:       name,
+				Kind:       "Secret",
+				APIVersion: "v1",
+				Type:       "uses",
+			})
+		}
+	}
+	return summary
+}
+
+func checkPodProjectedVolume(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	for _, vol := range obj.Slice("spec", "volumes") {
+		for _, source := range vol.Slice("projected", "sources") {
+			if secretName := source.String("secret", "name"); secretName != "" {
+				summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+					Name:       secretName,
+					Kind:       "Secret",
+					APIVersion: "v1",
+					Type:       "uses",
+				})
+			}
+			if configMap := source.String("configMap", "name"); configMap != "" {
+				summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+					Name:       configMap,
+					Kind:       "ConfigMap",
+					APIVersion: "v1",
+					Type:       "uses",
+				})
+			}
+		}
+	}
+	return summary
+}
+
+func addEnvRef(summary fleetv1.Summary, names map[string]bool, obj data.Object, fieldPrefix, kind string) fleetv1.Summary {
+	for _, container := range obj.Slice("spec", "containers") {
+		for _, env := range container.Slice("envFrom") {
+			name := env.String(fieldPrefix+"Ref", "name")
+			if name == "" || names[name] {
+				continue
+			}
+			names[name] = true
+			summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+				Name:       name,
+				Kind:       kind,
+				APIVersion: "v1",
+				Type:       "uses",
+			})
+		}
+		for _, env := range container.Slice("env") {
+			name := env.String("valueFrom", fieldPrefix+"KeyRef", "name")
+			if name == "" || names[name] {
+				continue
+			}
+			names[name] = true
+			summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+				Name:       name,
+				Kind:       kind,
+				APIVersion: "v1",
+				Type:       "uses",
+			})
+		}
+	}
+
+	return summary
+}
+
+func checkPodConfigMaps(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	names := map[string]bool{}
+	for _, vol := range obj.Slice("spec", "volumes") {
+		name := vol.String("configMap", "name")
+		if name == "" || names[name] {
+			continue
+		}
+		names[name] = true
+		summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+			Name:       name,
+			Kind:       "ConfigMap",
+			APIVersion: "v1",
+			Type:       "uses",
+		})
+	}
+	summary = addEnvRef(summary, names, obj, "configMap", "ConfigMap")
+	return summary
+}
+
+func checkPodSecrets(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	names := map[string]bool{}
+	for _, vol := range obj.Slice("spec", "volumes") {
+		name := vol.String("secret", "secretName")
+		if name == "" || names[name] {
+			continue
+		}
+		names[name] = true
+		summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+			Name:       name,
+			Kind:       "Secret",
+			APIVersion: "v1",
+			Type:       "uses",
+		})
+	}
+	summary = addEnvRef(summary, names, obj, "secret", "Secret")
+	return summary
+}
+
+func checkPodServiceAccount(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	saName := obj.String("spec", "serviceAccountName")
+	summary.Relationships = append(summary.Relationships, fleetv1.Relationship{
+		Name:       saName,
+		Kind:       "ServiceAccount",
+		APIVersion: "v1",
+		Type:       "uses",
+	})
+	return summary
+
+}
diff --git a/internal/cmd/agent/deployer/summary/summarize.go b/internal/cmd/agent/deployer/summary/summarize.go
new file mode 100644
index 00000000..df920c85
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/summarize.go
@@ -0,0 +1,69 @@
+package summary
+
+import (
+	"strings"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
+	unstructured2 "github.com/rancher/wrangler/v3/pkg/unstructured"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func dedupMessage(messages []string) []string {
+	seen := map[string]bool{}
+	var result []string
+
+	for _, message := range messages {
+		message = strings.TrimSpace(message)
+		if message == "" {
+			continue
+		}
+		if seen[message] {
+			continue
+		}
+		seen[message] = true
+		result = append(result, message)
+	}
+
+	return result
+}
+
+func Summarize(runtimeObj runtime.Object) fleetv1.Summary {
+	var (
+		obj     data.Object
+		err     error
+		summary fleetv1.Summary
+	)
+
+	if s, ok := runtimeObj.(*SummarizedObject); ok {
+		return s.Summary
+	}
+
+	unstr, ok := runtimeObj.(*unstructured.Unstructured)
+	if !ok {
+		unstr, err = unstructured2.ToUnstructured(runtimeObj)
+		if err != nil {
+			return summary
+		}
+	}
+
+	if unstr != nil {
+		obj = unstr.Object
+	}
+
+	conditions := getConditions(obj)
+
+	for _, summarizer := range Summarizers {
+		summary = summarizer(obj, conditions, summary)
+	}
+
+	if summary.State == "" {
+		summary.State = "active"
+	}
+
+	summary.State = strings.ToLower(summary.State)
+	summary.Message = dedupMessage(summary.Message)
+	return summary
+}
diff --git a/internal/cmd/agent/deployer/summary/summarized.go b/internal/cmd/agent/deployer/summary/summarized.go
new file mode 100644
index 00000000..d62bedf6
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/summarized.go
@@ -0,0 +1,12 @@
+package summary
+
+import (
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type SummarizedObject struct {
+	metav1.PartialObjectMetadata
+	fleetv1.Summary
+}
diff --git a/internal/cmd/agent/deployer/summary/summarizers.go b/internal/cmd/agent/deployer/summary/summarizers.go
new file mode 100644
index 00000000..1085da51
--- /dev/null
+++ b/internal/cmd/agent/deployer/summary/summarizers.go
@@ -0,0 +1,482 @@
+package summary
+
+import (
+	"strings"
+	"time"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data/convert"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/kv"
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
+)
+
+const (
+	kindSep = ", Kind="
+	reason  = "%REASON%"
+)
+
+var (
+	// True ==
+	// False == error
+	// Unknown == transitioning
+	TransitioningUnknown = map[string]string{
+		"Active":                      "activating",
+		"AddonDeploy":                 "provisioning",
+		"AgentDeployed":               "provisioning",
+		"BackingNamespaceCreated":     "configuring",
+		"Built":                       "building",
+		"CertsGenerated":              "provisioning",
+		"ConfigOK":                    "configuring",
+		"Created":                     "creating",
+		"CreatorMadeOwner":            "configuring",
+		"DefaultNamespaceAssigned":    "configuring",
+		"DefaultNetworkPolicyCreated": "configuring",
+		"DefaultProjectCreated":       "configuring",
+		"DockerProvisioned":           "provisioning",
+		"Deployed":                    "deploying",
+		"Drained":                     "draining",
+		"Downloaded":                  "downloading",
+		"etcd":                        "provisioning",
+		"Inactive":                    "deactivating",
+		"Initialized":                 "initializing",
+		"Installed":                   "installing",
+		"NodesCreated":                "provisioning",
+		"Pending":                     "pending",
+		"PodScheduled":                "scheduling",
+		"Provisioned":                 "provisioning",
+		"Reconciled":                  "reconciling",
+		"Refreshed":                   "refreshed",
+		"Registered":                  "registering",
+		"Removed":                     "removing",
+		"Saved":                       "saving",
+		"Updated":                     "updating",
+		"Updating":                    "updating",
+		"Upgraded":                    "upgrading",
+		"Waiting":                     "waiting",
+		"InitialRolesPopulated":       "activating",
+		"ScalingActive":               "pending",
+		"AbleToScale":                 "pending",
+		"RunCompleted":                "running",
+		"Processed":                   "processed",
+	}
+
+	// True == error
+	// False ==
+	// Unknown ==
+	ErrorTrue = map[string]bool{
+		"OutOfDisk":           true,
+		"MemoryPressure":      true,
+		"DiskPressure":        true,
+		"NetworkUnavailable":  true,
+		"KernelHasNoDeadlock": true,
+		"Unschedulable":       true,
+		"ReplicaFailure":      true,
+		"Stalled":             true,
+	}
+
+	// True ==
+	// False == error
+	// Unknown ==
+	ErrorFalse = map[string]bool{
+		"Failed": true,
+	}
+
+	// True ==
+	// False == transitioning
+	// Unknown == error
+	TransitioningFalse = map[string]string{
+		"Completed":           "activating",
+		"Ready":               "unavailable",
+		"Available":           "updating",
+		"BootstrapReady":      reason,
+		"InfrastructureReady": reason,
+		"NodeHealthy":         reason,
+	}
+
+	// True == transitioning
+	// False ==
+	// Unknown == error
+	TransitioningTrue = map[string]string{
+		"Reconciling": "reconciling",
+	}
+
+	Summarizers          []Summarizer
+	ConditionSummarizers []Summarizer
+)
+
+type Summarizer func(obj data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary
+
+func init() {
+	ConditionSummarizers = []Summarizer{
+		checkErrors,
+		checkTransitioning,
+		checkRemoving,
+		checkCattleReady,
+	}
+
+	Summarizers = []Summarizer{
+		checkStatusSummary,
+		checkErrors,
+		checkTransitioning,
+		checkActive,
+		checkPhase,
+		checkInitializing,
+		checkRemoving,
+		checkStandard,
+		checkLoadBalancer,
+		checkPod,
+		checkHasPodSelector,
+		checkHasPodTemplate,
+		checkOwner,
+		checkApplyOwned,
+		checkCattleTypes,
+	}
+}
+
+func checkOwner(obj data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	ustr := &unstructured.Unstructured{
+		Object: obj,
+	}
+	for _, ownerref := range ustr.GetOwnerReferences() {
+		rel := fleetv1.Relationship{
+			Name:       ownerref.Name,
+			Kind:       ownerref.Kind,
+			APIVersion: ownerref.APIVersion,
+			Type:       "owner",
+			Inbound:    true,
+		}
+		if ownerref.Controller != nil && *ownerref.Controller {
+			rel.ControlledBy = true
+		}
+
+		summary.Relationships = append(summary.Relationships, rel)
+	}
+
+	return summary
+}
+
+func checkStatusSummary(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	summaryObj := obj.Map("status", "display")
+	if len(summaryObj) == 0 {
+		summaryObj = obj.Map("status", "summary")
+		if len(summaryObj) == 0 {
+			return summary
+		}
+	}
+	obj = summaryObj
+
+	if _, ok := obj["state"]; ok {
+		summary.State = obj.String("state")
+	}
+	if _, ok := obj["transitioning"]; ok {
+		summary.Transitioning = obj.Bool("transitioning")
+	}
+	if _, ok := obj["error"]; ok {
+		summary.Error = obj.Bool("error")
+	}
+	if _, ok := obj["message"]; ok {
+		summary.Message = append(summary.Message, obj.String("message"))
+	}
+
+	return summary
+}
+
+func checkStandard(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if summary.State != "" {
+		return summary
+	}
+
+	// this is a hack to not call the standard summarizers on norman mapped objects
+	if strings.HasPrefix(obj.String("type"), "/") {
+		return summary
+	}
+
+	result, err := kstatus.Compute(&unstructured.Unstructured{Object: obj})
+	if err != nil {
+		return summary
+	}
+
+	switch result.Status {
+	case kstatus.InProgressStatus:
+		summary.State = "in-progress"
+		summary.Message = append(summary.Message, result.Message)
+		summary.Transitioning = true
+	case kstatus.FailedStatus:
+		summary.State = "failed"
+		summary.Message = append(summary.Message, result.Message)
+		summary.Error = true
+	case kstatus.CurrentStatus:
+		summary.State = "active"
+		summary.Message = append(summary.Message, result.Message)
+	case kstatus.TerminatingStatus:
+		summary.State = "removing"
+		summary.Message = append(summary.Message, result.Message)
+		summary.Transitioning = true
+	}
+
+	return summary
+}
+
+func checkErrors(_ data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	for _, c := range conditions {
+		if (ErrorFalse[c.Type()] && c.Status() == "False") || c.Reason() == "Error" {
+			summary.Error = true
+			summary.Message = append(summary.Message, c.Message())
+			if summary.State == "active" || summary.State == "" {
+				summary.State = "error"
+			}
+			break
+		}
+	}
+
+	if summary.Error {
+		return summary
+	}
+
+	for _, c := range conditions {
+		if ErrorTrue[c.Type()] && c.Status() == "True" {
+			summary.Error = true
+			summary.Message = append(summary.Message, c.Message())
+		}
+	}
+	return summary
+}
+
+func checkTransitioning(_ data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	for _, c := range conditions {
+		newState, ok := TransitioningUnknown[c.Type()]
+		if !ok {
+			continue
+		}
+
+		if c.Status() == "False" {
+			summary.Error = true
+			summary.State = newState
+			summary.Message = append(summary.Message, c.Message())
+		} else if c.Status() == "Unknown" && summary.State == "" {
+			summary.Transitioning = true
+			summary.State = newState
+			summary.Message = append(summary.Message, c.Message())
+		}
+	}
+
+	for _, c := range conditions {
+		if summary.State != "" {
+			break
+		}
+		newState, ok := TransitioningTrue[c.Type()]
+		if !ok {
+			continue
+		}
+		if c.Status() == "True" {
+			summary.Transitioning = true
+			summary.State = newState
+			summary.Message = append(summary.Message, c.Message())
+		}
+	}
+
+	ready := true
+	readyMessage := ""
+	for _, c := range conditions {
+		if summary.State != "" {
+			break
+		}
+
+		if c.Type() == "Ready" && c.Status() == "False" {
+			ready = false
+			readyMessage = c.Message()
+			continue
+		}
+		newState, ok := TransitioningFalse[c.Type()]
+		if !ok {
+			continue
+		}
+		if newState == reason {
+			newState = c.Reason()
+		}
+		if c.Status() == "False" {
+			summary.Transitioning = true
+			summary.State = newState
+			summary.Message = append(summary.Message, c.Message())
+		} else if c.Status() == "Unknown" {
+			summary.Error = true
+			summary.State = newState
+			summary.Message = append(summary.Message, c.Message())
+		}
+	}
+
+	if summary.State == "" && !ready {
+		summary.Transitioning = true
+		summary.State = "unavailable"
+		summary.Message = append(summary.Message, readyMessage)
+	}
+
+	return summary
+}
+
+func checkActive(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if summary.State != "" {
+		return summary
+	}
+
+	switch obj.String("spec", "active") {
+	case "true":
+		summary.State = "active"
+	case "false":
+		summary.State = "inactive"
+	}
+
+	return summary
+}
+
+func checkPhase(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	phase := obj.String("status", "phase")
+	if phase == "Succeeded" {
+		summary.State = "succeeded"
+		summary.Transitioning = false
+	} else if phase == "Bound" {
+		summary.State = "bound"
+		summary.Transitioning = false
+	} else if phase != "" && summary.State == "" {
+		summary.State = phase
+	}
+	return summary
+}
+
+func checkInitializing(obj data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	apiVersion := obj.String("apiVersion")
+	_, hasConditions := obj.Map("status")["conditions"]
+	if summary.State == "" && hasConditions && len(conditions) == 0 && strings.Contains(apiVersion, "cattle.io") {
+		val := obj.String("metadata", "created")
+		if i, err := convert.ToTimestamp(val); err == nil {
+			if time.Unix(i/1000, 0).Add(5 * time.Second).After(time.Now()) {
+				summary.State = "initializing"
+				summary.Transitioning = true
+			}
+		}
+	}
+	return summary
+}
+
+func checkRemoving(obj data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	removed := obj.String("metadata", "removed")
+	if removed == "" {
+		return summary
+	}
+
+	summary.State = "removing"
+	summary.Transitioning = true
+
+	finalizers := obj.StringSlice("metadata", "finalizers")
+	if len(finalizers) == 0 {
+		finalizers = obj.StringSlice("spec", "finalizers")
+	}
+
+	for _, cond := range conditions {
+		if cond.Type() == "Removed" && (cond.Status() == "Unknown" || cond.Status() == "False") && cond.Message() != "" {
+			summary.Message = append(summary.Message, cond.Message())
+		}
+	}
+
+	if len(finalizers) == 0 {
+		return summary
+	}
+
+	_, f := kv.RSplit(finalizers[0], "controller.cattle.io/")
+	if f == "foregroundDeletion" {
+		f = "object cleanup"
+	}
+
+	summary.Message = append(summary.Message, "waiting on "+f)
+	if i, err := convert.ToTimestamp(removed); err == nil {
+		if time.Unix(i/1000, 0).Add(5 * time.Minute).Before(time.Now()) {
+			summary.Error = true
+		}
+	}
+
+	return summary
+}
+
+func checkLoadBalancer(obj data.Object, _ []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if (summary.State == "active" || summary.State == "") &&
+		obj.String("kind") == "Service" &&
+		(obj.String("spec", "serviceKind") == "LoadBalancer" ||
+			obj.String("spec", "type") == "LoadBalancer") {
+		addresses := obj.Slice("status", "loadBalancer", "ingress")
+		if len(addresses) == 0 {
+			summary.State = "pending"
+			summary.Transitioning = true
+			summary.Message = append(summary.Message, "Load balancer is being provisioned")
+		}
+	}
+
+	return summary
+}
+
+func isKind(obj data.Object, kind string, apiGroups ...string) bool {
+	if obj.String("kind") != kind {
+		return false
+	}
+
+	if len(apiGroups) == 0 {
+		return obj.String("apiVersion") == "v1"
+	}
+
+	if len(apiGroups) == 0 {
+		apiGroups = []string{""}
+	}
+
+	for _, group := range apiGroups {
+		switch {
+		case group == "":
+			if obj.String("apiVersion") == "v1" {
+				return true
+			}
+		case group[len(group)-1] == '/':
+			if strings.HasPrefix(obj.String("apiVersion"), group) {
+				return true
+			}
+		default:
+			if obj.String("apiVersion") != group {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func checkApplyOwned(obj data.Object, conditions []Condition, summary fleetv1.Summary) fleetv1.Summary {
+	if len(obj.Slice("metadata", "ownerReferences")) > 0 {
+		return summary
+	}
+
+	annotations := obj.Map("metadata", "annotations")
+	gvkString := convert.ToString(annotations["objectset.rio.cattle.io/owner-gvk"])
+	i := strings.Index(gvkString, kindSep)
+	if i <= 0 {
+		return summary
+	}
+
+	name := convert.ToString(annotations["objectset.rio.cattle.io/owner-name"])
+	namespace := convert.ToString(annotations["objectset.rio.cattle.io/owner-namespace"])
+
+	apiVersion := gvkString[:i]
+	kind := gvkString[i+len(kindSep):]
+
+	rel := fleetv1.Relationship{
+		Name:       name,
+		Namespace:  namespace,
+		Kind:       kind,
+		APIVersion: apiVersion,
+		Type:       "applies",
+		Inbound:    true,
+	}
+
+	summary.Relationships = append(summary.Relationships, rel)
+
+	return summary
+}
diff --git a/internal/cmd/agent/operator.go b/internal/cmd/agent/operator.go
index 5b83b8cb..c5019d93 100644
--- a/internal/cmd/agent/operator.go
+++ b/internal/cmd/agent/operator.go
@@ -7,8 +7,8 @@ import (
 
 	"github.com/rancher/fleet/internal/cmd/agent/controller"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/cleanup"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
 	"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/register"
@@ -212,13 +212,13 @@ func newReconciler(
 	if err != nil {
 		return nil, err
 	}
-	applied, err := applied.NewWithClient(localConfig)
+	ds, err := desiredset.New(localConfig)
 	if err != nil {
 		return nil, err
 	}
 	monitor := monitor.New(
 		localClient,
-		applied,
+		ds,
 		helmDeployer,
 		defaultNamespace,
 		agentScope,
@@ -230,7 +230,7 @@ func newReconciler(
 		trigger,
 		upstreamClient,
 		mgr.GetAPIReader(),
-		applied,
+		ds,
 		defaultNamespace,
 		defaultNamespace,
 		agentScope,
diff --git a/internal/cmd/agent/trigger/watcher.go b/internal/cmd/agent/trigger/watcher.go
index d42f4f8d..321a64c8 100644
--- a/internal/cmd/agent/trigger/watcher.go
+++ b/internal/cmd/agent/trigger/watcher.go
@@ -6,8 +6,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
 	"github.com/rancher/fleet/pkg/durations"
-	"github.com/rancher/wrangler/v3/pkg/objectset"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/internal/cmd/controller/agentmanagement/controllers/cluster/import.go b/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
index 9e2201b2..998bff59 100644
--- a/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
+++ b/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
@@ -12,7 +12,7 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/rancher/fleet/internal/client"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
 	"github.com/rancher/fleet/internal/cmd/controller/agentmanagement/agent"
 	"github.com/rancher/fleet/internal/cmd/controller/agentmanagement/connection"
 	"github.com/rancher/fleet/internal/cmd/controller/agentmanagement/controllers/manageagent"
@@ -268,7 +268,7 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust
 	if err != nil {
 		return status, err
 	}
-	setID := applied.GetSetID(config.AgentBootstrapConfigName, "", cluster.Spec.AgentNamespace)
+	setID := desiredset.GetSetID(config.AgentBootstrapConfigName, "", cluster.Spec.AgentNamespace)
 	apply = apply.WithDynamicLookup().WithSetID(setID).WithNoDeleteGVK(fleetns.GVK())
 
 	tokenName := names.SafeConcatName(ImportTokenPrefix + cluster.Name)
diff --git a/internal/cmd/controller/reconciler/cluster_controller.go b/internal/cmd/controller/reconciler/cluster_controller.go
index 808edb6f..7d7c5ba3 100644
--- a/internal/cmd/controller/reconciler/cluster_controller.go
+++ b/internal/cmd/controller/reconciler/cluster_controller.go
@@ -29,6 +29,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
@@ -100,6 +101,7 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
 			}),
 		).
 		WithEventFilter(sharding.FilterByShardID(r.ShardID)).
+		WithOptions(controller.Options{MaxConcurrentReconciles: 50}).
 		Complete(r)
 }
 
diff --git a/internal/cmd/controller/reconciler/clustergroup_controller.go b/internal/cmd/controller/reconciler/clustergroup_controller.go
index d175e57a..f1cfc28f 100644
--- a/internal/cmd/controller/reconciler/clustergroup_controller.go
+++ b/internal/cmd/controller/reconciler/clustergroup_controller.go
@@ -27,6 +27,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
@@ -67,6 +68,7 @@ func (r *ClusterGroupReconciler) SetupWithManager(mgr ctrl.Manager) error {
 			handler.EnqueueRequestsFromMapFunc(r.mapClusterToClusterGroup),
 		).
 		WithEventFilter(sharding.FilterByShardID(r.ShardID)).
+		WithOptions(controller.Options{MaxConcurrentReconciles: 50}).
 		Complete(r)
 }
 
diff --git a/internal/cmd/controller/reconciler/imagescan_controller.go b/internal/cmd/controller/reconciler/imagescan_controller.go
index cbd48336..7e2ced1e 100644
--- a/internal/cmd/controller/reconciler/imagescan_controller.go
+++ b/internal/cmd/controller/reconciler/imagescan_controller.go
@@ -15,6 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 )
@@ -42,6 +43,7 @@ func (r *ImageScanReconciler) SetupWithManager(mgr ctrl.Manager) error {
 					predicate.LabelChangedPredicate{},
 				),
 			)).
+		WithOptions(controller.Options{MaxConcurrentReconciles: 50}).
 		Complete(r)
 }
 
diff --git a/internal/cmd/debug.go b/internal/cmd/debug.go
index a219b1d0..3d37127c 100644
--- a/internal/cmd/debug.go
+++ b/internal/cmd/debug.go
@@ -48,7 +48,7 @@ func (c *DebugConfig) OverrideZapOpts(zopts *crzap.Options) *crzap.Options {
 	zopts.Development = c.Debug
 
 	if c.Debug && c.DebugLevel > 0 {
-		zopts.Level = zapcore.Level(c.DebugLevel * -1)
+		zopts.Level = zapcore.Level(c.DebugLevel * -1) //nolint:gosec // no risk, just debug level
 	}
 
 	return zopts
diff --git a/internal/helmdeployer/delete.go b/internal/helmdeployer/delete.go
index f9242e5a..ea5a9ebb 100644
--- a/internal/helmdeployer/delete.go
+++ b/internal/helmdeployer/delete.go
@@ -9,10 +9,9 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/release"
 
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/kv"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
-	"github.com/rancher/wrangler/v3/pkg/kv"
-
 	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
diff --git a/internal/helmdeployer/history.go b/internal/helmdeployer/history.go
index 4030e19e..a51b6c0c 100644
--- a/internal/helmdeployer/history.go
+++ b/internal/helmdeployer/history.go
@@ -10,7 +10,8 @@ import (
 	"helm.sh/helm/v3/pkg/storage/driver"
 	"k8s.io/apimachinery/pkg/runtime"
 
-	"github.com/rancher/wrangler/v3/pkg/kv"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/kv"
+
 	"github.com/rancher/wrangler/v3/pkg/yaml"
 )
 
diff --git a/internal/helmdeployer/kustomize/kstatus.go b/internal/helmdeployer/kustomize/kstatus.go
index 9c63780b..a4f6425e 100644
--- a/internal/helmdeployer/kustomize/kstatus.go
+++ b/internal/helmdeployer/kustomize/kstatus.go
@@ -1,8 +1,10 @@
 package kustomize
 
 import (
-	"github.com/rancher/wrangler/v3/pkg/data"
-	"github.com/rancher/wrangler/v3/pkg/summary"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/summary"
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
 )
@@ -11,7 +13,7 @@ func init() {
 	summary.Summarizers = append(summary.Summarizers, KStatusSummarizer)
 }
 
-func KStatusSummarizer(obj data.Object, conditions []summary.Condition, summary summary.Summary) summary.Summary {
+func KStatusSummarizer(obj data.Object, conditions []summary.Condition, summary fleetv1.Summary) fleetv1.Summary {
 	result, err := status.Compute(&unstructured.Unstructured{Object: obj})
 	if err != nil {
 		return summary
diff --git a/internal/helmdeployer/kustomize/kustomize.go b/internal/helmdeployer/kustomize/kustomize.go
index 0cbdad14..dc6b723b 100644
--- a/internal/helmdeployer/kustomize/kustomize.go
+++ b/internal/helmdeployer/kustomize/kustomize.go
@@ -3,12 +3,10 @@ package kustomize
 import (
 	"path/filepath"
 
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/data/convert"
 	"github.com/rancher/fleet/internal/content"
 	"github.com/rancher/fleet/internal/manifest"
 
-	"github.com/rancher/wrangler/v3/pkg/data/convert"
-	"github.com/rancher/wrangler/v3/pkg/slice"
-
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/kustomize/api/krusty"
@@ -47,6 +45,15 @@ func Process(m *manifest.Manifest, content []byte, dir string) ([]runtime.Object
 	return objs, true, err
 }
 
+func containsString(slice []string, item string) bool {
+	for _, j := range slice {
+		if j == item {
+			return true
+		}
+	}
+	return false
+}
+
 func modifyKustomize(f filesys.FileSystem, dir string) error {
 	file := filepath.Join(dir, KustomizeYAML)
 	fileBytes, err := f.ReadFile(file)
@@ -60,7 +67,7 @@ func modifyKustomize(f filesys.FileSystem, dir string) error {
 	}
 
 	resources := convert.ToStringSlice(data["resources"])
-	if slice.ContainsString(resources, ManifestsYAML) {
+	if containsString(resources, ManifestsYAML) {
 		return nil
 	}
 
diff --git a/internal/helmdeployer/postrender.go b/internal/helmdeployer/postrender.go
index 53ac7836..6dd13385 100644
--- a/internal/helmdeployer/postrender.go
+++ b/internal/helmdeployer/postrender.go
@@ -9,7 +9,7 @@ import (
 
 	"helm.sh/helm/v3/pkg/chart"
 
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
 	"github.com/rancher/fleet/internal/helmdeployer/kustomize"
 	"github.com/rancher/fleet/internal/helmdeployer/rawyaml"
 	"github.com/rancher/fleet/internal/manifest"
@@ -72,8 +72,8 @@ func (p *postRender) Run(renderedManifests *bytes.Buffer) (modifiedManifests *by
 	}
 	objs = append(objs, yamlObjs...)
 
-	setID := applied.GetSetID(p.bundleID, p.labelPrefix, p.labelSuffix)
-	labels, annotations, err := applied.GetLabelsAndAnnotations(setID, nil)
+	setID := desiredset.GetSetID(p.bundleID, p.labelPrefix, p.labelSuffix)
+	labels, annotations, err := desiredset.GetLabelsAndAnnotations(setID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
index debedea2..fce5d7f5 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
@@ -4,11 +4,12 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
 	"github.com/rancher/wrangler/v3/pkg/genericcondition"
-	"github.com/rancher/wrangler/v3/pkg/summary"
 )
 
 const BundleDeploymentResourceNamePlural = "bundledeployments"
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/summary/summary.go b/pkg/apis/fleet.cattle.io/v1alpha1/summary/summary.go
new file mode 100644
index 00000000..49c4bce9
--- /dev/null
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/summary/summary.go
@@ -0,0 +1,63 @@
+package summary
+
+import (
+	"strings"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type Summary struct {
+	State         string                 `json:"state,omitempty"`
+	Error         bool                   `json:"error,omitempty"`
+	Transitioning bool                   `json:"transitioning,omitempty"`
+	Message       []string               `json:"message,omitempty"`
+	Attributes    map[string]interface{} `json:"-"`
+	Relationships []Relationship         `json:"-"`
+}
+
+type Relationship struct {
+	Name         string
+	Namespace    string
+	ControlledBy bool
+	Kind         string
+	APIVersion   string
+	Inbound      bool
+	Type         string
+	Selector     *metav1.LabelSelector
+}
+
+func (s Summary) String() string {
+	if !s.Transitioning && !s.Error {
+		return s.State
+	}
+	var msg string
+	if s.Transitioning {
+		msg = "[progressing"
+	}
+	if s.Error {
+		if len(msg) > 0 {
+			msg += ",error]"
+		} else {
+			msg = "error]"
+		}
+	} else {
+		msg += "]"
+	}
+	if len(s.Message) > 0 {
+		msg = msg + " " + strings.Join(s.Message, ", ")
+	}
+	return msg
+}
+
+func (s Summary) IsReady() bool {
+	return !s.Error && !s.Transitioning
+}
+
+func (s *Summary) DeepCopy() *Summary {
+	v := *s
+	return &v
+}
+
+func (s *Summary) DeepCopyInto(v *Summary) {
+	*v = *s
+}
diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod
index 5c65d7c9..2576060c 100644
--- a/pkg/apis/go.mod
+++ b/pkg/apis/go.mod
@@ -23,7 +23,6 @@ require (
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	k8s.io/klog/v2 v2.120.1 // indirect
 	k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
-	sigs.k8s.io/cli-utils v0.35.0 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
 )
diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum
index 80690643..fad36522 100644
--- a/pkg/apis/go.sum
+++ b/pkg/apis/go.sum
@@ -85,8 +85,6 @@ k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
 k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y=
-sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE=
 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.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=


Test patch:
diff --git a/.github/workflows/e2e-aks-ci.yml b/.github/workflows/e2e-aks-ci.yml
index e4ae9bd2..387f5d7a 100644
--- a/.github/workflows/e2e-aks-ci.yml
+++ b/.github/workflows/e2e-aks-ci.yml
@@ -137,7 +137,7 @@ jobs:
           FLEET_E2E_NS: fleet-local
         run: |
           export KUBECONFIG="$GITHUB_WORKSPACE/kubeconfig-fleet-ci"
-          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources e2e/drift
+          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources
       -
         name: Acceptance Tests for Examples
         env:
diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml
index e7695565..e9bb4085 100644
--- a/.github/workflows/e2e-ci.yml
+++ b/.github/workflows/e2e-ci.yml
@@ -90,7 +90,7 @@ jobs:
         env:
           FLEET_E2E_NS: fleet-local
         run: |
-          ginkgo --github-output --label-filter='!infra-setup && !sharding' e2e/single-cluster e2e/keep-resources e2e/drift
+          ginkgo --github-output --label-filter='!infra-setup && !sharding' e2e/single-cluster e2e/keep-resources
       -
         name: E2E Sharding/Metrics Tests
         if: ${{ matrix.test_type.name == 'sharding' }}
diff --git a/.github/workflows/e2e-gke-ci.yml b/.github/workflows/e2e-gke-ci.yml
index ed1466af..831f43cd 100644
--- a/.github/workflows/e2e-gke-ci.yml
+++ b/.github/workflows/e2e-gke-ci.yml
@@ -140,7 +140,7 @@ jobs:
         env:
           FLEET_E2E_NS: fleet-local
         run: |
-          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources e2e/drift
+          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources
       -
         name: Acceptance Tests for Examples
         env:
diff --git a/.github/workflows/e2e-nightly-ci.yml b/.github/workflows/e2e-nightly-ci.yml
index 3e9f6781..5d03377d 100644
--- a/.github/workflows/e2e-nightly-ci.yml
+++ b/.github/workflows/e2e-nightly-ci.yml
@@ -97,7 +97,7 @@ jobs:
           export CI_OCI_CERTS_DIR="$(git rev-parse --show-toplevel)/FleetCI-RootCA"
 
           # 1. Run test cases not needing infra
-          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources e2e/drift
+          ginkgo --github-output --label-filter='!infra-setup' e2e/single-cluster e2e/keep-resources
 
           # 2. Run tests for metrics
           ginkgo --github-output e2e/metrics
diff --git a/e2e/assets/single-cluster/bundle-diffs.yaml b/e2e/assets/single-cluster/bundle-diffs.yaml
deleted file mode 100644
index 6c683557..00000000
--- a/e2e/assets/single-cluster/bundle-diffs.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-kind: GitRepo
-apiVersion: fleet.cattle.io/v1alpha1
-metadata:
-  name: bundle-diffs-test
-spec:
-  repo: https://github.com/rancher/fleet-test-data
-  branch: master
-  paths:
-  - bundle-diffs
diff --git a/e2e/single-cluster/bundle_diffs_test.go b/e2e/single-cluster/bundle_diffs_test.go
deleted file mode 100644
index fc24c7a8..00000000
--- a/e2e/single-cluster/bundle_diffs_test.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package singlecluster_test
-
-import (
-	"encoding/json"
-	"time"
-
-	"github.com/rancher/fleet/e2e/testenv"
-	"github.com/rancher/fleet/e2e/testenv/kubectl"
-	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-)
-
-var _ = Describe("BundleDiffs", func() {
-	var (
-		name            string
-		targetNamespace string
-
-		k        kubectl.Command
-		kw       kubectl.Command
-		interval = 1 * time.Second
-		duration = 15 * time.Second
-	)
-
-	bundleDeploymentStatus := func(repo string) (*v1alpha1.BundleDeploymentStatus, error) {
-		out, err := k.Get("bundledeployments", "-A", "-l", "fleet.cattle.io/repo-name="+repo, "-o=jsonpath={.items[*].status}")
-		if err != nil {
-			return nil, err
-		}
-
-		if out == "" {
-			return nil, apierrors.NewNotFound(v1alpha1.Resource("bundledeployment"), repo)
-		}
-
-		status := &v1alpha1.BundleDeploymentStatus{}
-		err = json.Unmarshal([]byte(out), status)
-		if err != nil {
-			return nil, err
-		}
-
-		return status, nil
-	}
-
-	BeforeEach(func() {
-		k = env.Kubectl.Namespace(env.Namespace)
-
-		asset := "single-cluster/bundle-diffs.yaml"
-
-		// name matches name from gitrepo for use in label selectors
-		name = "bundle-diffs-test"
-
-		// namespace needs to match diff.comparePatches in fleet.yaml
-		targetNamespace = "bundle-diffs-example"
-		kw = k.Namespace(targetNamespace)
-
-		out, err := k.Apply("-f", testenv.AssetPath(asset))
-		Expect(err).ToNot(HaveOccurred(), out)
-
-		By("waiting for resources to be ready", func() {
-			Eventually(func() bool {
-				status, err := bundleDeploymentStatus(name)
-				if err != nil || status == nil {
-					return false
-				}
-				GinkgoWriter.Printf("bundledeployment status: %v", status)
-				return status.Ready && status.NonModified
-			}).Should(BeTrue())
-
-			Eventually(func() string {
-				out, _ := kw.Get("services")
-
-				return out
-			}).Should(ContainSubstring("app-service"))
-
-			// wait for potential redeploys to finish
-			time.Sleep(5 * time.Second)
-		})
-
-		DeferCleanup(func() {
-			out, err := k.Delete("-f", testenv.AssetPath(asset))
-			Expect(err).ToNot(HaveOccurred(), out)
-
-			// test cases use the same namespace, so we have to
-			// make sure resources are cleaned up
-			Eventually(func() bool {
-				_, err := bundleDeploymentStatus(name)
-				if err != nil && apierrors.IsNotFound(err) {
-					return true
-				}
-
-				return false
-			}).Should(BeTrue(), "bundledeployment should be deleted")
-
-			_, _ = k.Delete("namespace", targetNamespace)
-		})
-
-	})
-
-	When("fleet.yaml contains bundle-diff patches", func() {
-		Context("adding new values", func() {
-			BeforeEach(func() {
-				out, err := kw.Patch(
-					"service", "app-service",
-					"--type=json",
-					"-p", `[{"op": "add", "path": "/spec/ports/0", "value": {"name":"test","port":1023,"protocol":"TCP"}}]`,
-				)
-				Expect(err).ToNot(HaveOccurred(), out)
-
-				// adds a value
-				out, err = kw.Patch(
-					"configmap", "app-config",
-					"--type=merge",
-					"-p", `{"data":{"newvalue":"by-test-code"}}`,
-				)
-				Expect(err).ToNot(HaveOccurred(), out)
-			})
-
-			It("ignores changes", func() {
-				Consistently(func() bool {
-					status, err := bundleDeploymentStatus(name)
-					if err != nil || status == nil {
-						return false
-					}
-
-					return status.NonModified
-				}, duration, interval).Should(BeTrue(), "ignores modification")
-			})
-		})
-
-		Context("modifying existing values, that are ignored", func() {
-			BeforeEach(func() {
-				out, err := kw.Patch(
-					"configmap", "app-config",
-					"--type=merge",
-					"-p", `{"data":{"test":"by-test-code"}}`,
-				)
-				Expect(err).ToNot(HaveOccurred(), out)
-			})
-
-			It("ignores changes", func() {
-				Consistently(func() bool {
-					status, err := bundleDeploymentStatus(name)
-					if err != nil || status == nil {
-						return false
-					}
-
-					return status.NonModified
-				}, duration, interval).Should(BeTrue(), "ignores modification")
-			})
-		})
-
-		Context("modifying existing values, that are not ignored", func() {
-			BeforeEach(func() {
-				out, err := kw.Patch(
-					"service", "app-service",
-					"--type=json",
-					"-p", `[{"op": "add", "path": "/spec/selector", "value": {"env": "modification"}}]`,
-				)
-				Expect(err).ToNot(HaveOccurred(), out)
-			})
-
-			It("detects modifications", func() {
-				Eventually(func() bool {
-					status, err := bundleDeploymentStatus(name)
-					if err != nil || status == nil {
-						return false
-					}
-
-					GinkgoWriter.Printf("bundledeployment status: %v", status)
-					return status.NonModified
-				}, duration*2, interval).Should(BeFalse(), "detects modification")
-			})
-		})
-	})
-})
diff --git a/integrationtests/agent/adoption_test.go b/integrationtests/agent/adoption_test.go
index 5703aae9..1b85dd8e 100644
--- a/integrationtests/agent/adoption_test.go
+++ b/integrationtests/agent/adoption_test.go
@@ -1,4 +1,4 @@
-package agent
+package agent_test
 
 import (
 	"context"
diff --git a/integrationtests/agent/bundle_deployment_diffs_test.go b/integrationtests/agent/bundle_deployment_diffs_test.go
new file mode 100644
index 00000000..ee1c36d9
--- /dev/null
+++ b/integrationtests/agent/bundle_deployment_diffs_test.go
@@ -0,0 +1,202 @@
+package agent_test
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/rancher/fleet/integrationtests/utils"
+	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("BundleDeployment diff", func() {
+	const (
+		svcName    = "svc-test"
+		cfgMapName = "cm-test"
+	)
+
+	var (
+		namespace string
+		name      string
+		deplID    string
+		env       *specEnv
+		patches   []v1alpha1.ComparePatch
+	)
+
+	createBundleDeployment := func(name string) {
+		bundled := v1alpha1.BundleDeployment{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: clusterNS,
+			},
+			Spec: v1alpha1.BundleDeploymentSpec{
+				DeploymentID: deplID,
+				Options: v1alpha1.BundleDeploymentOptions{
+					DefaultNamespace: namespace,
+					Diff: &v1alpha1.DiffOptions{
+						ComparePatches: patches,
+					},
+				},
+			},
+		}
+
+		err := k8sClient.Create(context.TODO(), &bundled)
+		Expect(err).To(BeNil())
+		Expect(bundled).To(Not(BeNil()))
+	}
+
+	createNamespace := func() string {
+		newNs, err := utils.NewNamespaceName()
+		Expect(err).ToNot(HaveOccurred())
+
+		ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: newNs}}
+		Expect(k8sClient.Create(context.Background(), ns)).ToNot(HaveOccurred())
+
+		return newNs
+	}
+
+	When("A bundle deployment is created with a bundle diff", func() {
+		JustBeforeEach(func() {
+			namespace = createNamespace()
+			deplID = "v1"
+			patches = []v1alpha1.ComparePatch{
+				{
+					APIVersion: "v1",
+					Kind:       "ConfigMap",
+					Name:       cfgMapName,
+					Namespace:  namespace,
+					Operations: []v1alpha1.Operation{
+						{
+							Op:   "remove",
+							Path: "/data",
+						},
+					},
+				},
+				{
+					APIVersion: "v1",
+					Kind:       "Service",
+					Name:       svcName,
+					Namespace:  namespace,
+					Operations: []v1alpha1.Operation{
+						{
+							Op:   "remove",
+							Path: "/spec/ports",
+						},
+					},
+				},
+			}
+
+			env = &specEnv{namespace: namespace}
+
+			createBundleDeployment(name)
+			Eventually(func(g Gomega) {
+				g.Expect(env.isBundleDeploymentReadyAndNotModified(name)).To(BeTrue())
+				_, err := env.getService(svcName)
+				g.Expect(err).NotTo(HaveOccurred())
+			}).Should(Succeed())
+
+			DeferCleanup(func() {
+				Expect(k8sClient.Delete(
+					context.TODO(),
+					&v1alpha1.BundleDeployment{ObjectMeta: metav1.ObjectMeta{Namespace: clusterNS, Name: name}},
+				)).ToNot(HaveOccurred())
+			})
+		})
+
+		Context("Modifying values covered by the diff", func() {
+			BeforeEach(func() {
+				name = "diff-update-test"
+			})
+
+			It("Keeps the bundle deployment in ready state", func() {
+				svc, err := env.getService(svcName)
+				Expect(err).NotTo(HaveOccurred())
+				patchedSvc := svc.DeepCopy()
+				patchedSvc.Spec.Ports[0].Port = 4242
+				Expect(k8sClient.Patch(ctx, patchedSvc, client.StrategicMergeFrom(&svc))).NotTo(HaveOccurred())
+
+				Consistently(func(g Gomega) {
+					bd := &v1alpha1.BundleDeployment{}
+					err := k8sClient.Get(
+						context.TODO(),
+						types.NamespacedName{Namespace: clusterNS, Name: name},
+						bd,
+						&client.GetOptions{},
+					)
+					g.Expect(err).NotTo(HaveOccurred())
+					g.Expect(bd.Status.NonModified).To(BeTrue())
+				}, 5*time.Second, time.Second).Should(Succeed())
+			})
+		})
+
+		Context("Deleting values covered by the diff", func() {
+			BeforeEach(func() {
+				name = "diff-delete-test"
+			})
+
+			It("Keeps the bundle deployment in ready state", func() {
+				cm, err := env.getConfigMap(cfgMapName)
+				Expect(err).ToNot(HaveOccurred())
+
+				patchedCM := cm.DeepCopy()
+				patchedCM.Data = nil
+				Expect(k8sClient.Patch(ctx, patchedCM, client.StrategicMergeFrom(&cm))).NotTo(HaveOccurred())
+
+				Consistently(func(g Gomega) {
+					bd := &v1alpha1.BundleDeployment{}
+					err := k8sClient.Get(
+						context.TODO(),
+						types.NamespacedName{Namespace: clusterNS, Name: name},
+						bd,
+						&client.GetOptions{},
+					)
+					g.Expect(err).NotTo(HaveOccurred())
+					g.Expect(bd.Status.NonModified).To(BeTrue())
+				}, 5*time.Second, time.Second).Should(Succeed())
+			})
+		})
+
+		Context("Modifying values not covered by the diff", func() {
+			BeforeEach(func() {
+				name = "diff-modif-uncovered-test"
+			})
+
+			It("Updates the bundle deployment status to modified", func() {
+				svc, err := env.getService(svcName)
+				Expect(err).NotTo(HaveOccurred())
+				patchedSvc := svc.DeepCopy()
+				patchedSvc.Spec.Selector = map[string]string{"env": "modification"}
+				Expect(k8sClient.Patch(ctx, patchedSvc, client.StrategicMergeFrom(&svc))).NotTo(HaveOccurred())
+
+				Eventually(func(g Gomega) {
+					modifiedStatus := v1alpha1.ModifiedStatus{
+						Kind:       "Service",
+						APIVersion: "v1",
+						Namespace:  namespace,
+						Name:       svcName,
+						Create:     false,
+						Delete:     false,
+						Patch:      `{"spec":{"selector":{"app.kubernetes.io/name":"MyApp"}}}`,
+					}
+					isOK, status := env.isNotReadyAndModified(
+						name,
+						modifiedStatus,
+						fmt.Sprintf(
+							`service.v1 %s/%s modified {"spec":{"selector":{"app.kubernetes.io/name":"MyApp"}}}`,
+							namespace,
+							svcName,
+						),
+					)
+					g.Expect(isOK).To(BeTrue(), status)
+				}).Should(Succeed())
+			})
+		})
+	})
+})
diff --git a/integrationtests/agent/bundle_deployment_drift_test.go b/integrationtests/agent/bundle_deployment_drift_test.go
index 86e5da33..3e687ede 100644
--- a/integrationtests/agent/bundle_deployment_drift_test.go
+++ b/integrationtests/agent/bundle_deployment_drift_test.go
@@ -1,4 +1,4 @@
-package agent
+package agent_test
 
 import (
 	"context"
diff --git a/integrationtests/agent/bundle_deployment_status_test.go b/integrationtests/agent/bundle_deployment_status_test.go
index ed51f2c7..8da98a9d 100644
--- a/integrationtests/agent/bundle_deployment_status_test.go
+++ b/integrationtests/agent/bundle_deployment_status_test.go
@@ -1,4 +1,4 @@
-package agent
+package agent_test
 
 import (
 	"context"
diff --git a/integrationtests/agent/dryrun_test.go b/integrationtests/agent/dryrun_test.go
new file mode 100644
index 00000000..f052efaf
--- /dev/null
+++ b/integrationtests/agent/dryrun_test.go
@@ -0,0 +1,178 @@
+package agent
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var _ = Describe("DryRun determines difference between desired state and actual", func() {
+	setID := "set123"
+	ctx := context.Background()
+
+	When("a pod changes", func() {
+		var pod *corev1.Pod
+		unchanged := &corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "test",
+				Namespace: "default",
+			},
+			Data: map[string]string{},
+		}
+
+		BeforeEach(func() {
+			pod = &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: "default",
+				},
+				Spec: corev1.PodSpec{},
+			}
+			labels, annotations, _ := desiredset.GetLabelsAndAnnotations(setID)
+			pod.SetLabels(labels)
+			pod.SetAnnotations(annotations)
+		})
+
+		It("reflects in the plan", func() {
+			plan, err := dsClient.Plan(ctx, "default", "set123", pod, unchanged)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(plan.Create).To(HaveLen(2))
+			Expect(plan.Delete).To(HaveLen(2))
+			Expect(plan.Update).To(HaveLen(0))
+			Expect(plan.Objects).To(HaveLen(0))
+
+			By("creating the pod", func() {
+				pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{
+					Name:  "test",
+					Image: "test",
+				})
+
+				_ = k8sClient.Create(context.Background(), pod)
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(context.Background(), pod)
+				})
+
+				plan, err = dsClient.Plan(ctx, "default", "set123", pod, unchanged)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(2))
+				Expect(plan.Delete).To(HaveLen(2))
+				Expect(plan.Update).To(HaveLen(0))
+				Expect(plan.Objects).To(HaveLen(1))
+			})
+
+			By("modifying the desired state", func() {
+				pod.Spec.Containers[0].Command = []string{"echo", "test"}
+
+				plan, err = dsClient.Plan(ctx, "default", "set123", pod, unchanged)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(2))
+				Expect(plan.Delete).To(HaveLen(2))
+				Expect(plan.Update).To(HaveLen(1))
+				Expect(plan.Objects).To(HaveLen(1))
+			})
+		})
+	})
+
+	When("a ConfigMap changes", func() {
+		var cm *corev1.ConfigMap
+
+		BeforeEach(func() {
+			cm = &corev1.ConfigMap{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: "default",
+				},
+				Data: map[string]string{},
+			}
+		})
+
+		It("reflects in the plan", func() {
+			labels, annotations, err := desiredset.GetLabelsAndAnnotations(setID)
+			Expect(err).ToNot(HaveOccurred())
+			cm.SetLabels(labels)
+			cm.SetAnnotations(annotations)
+
+			plan, err := dsClient.Plan(ctx, "default", "set123", cm)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(plan.Create).To(HaveLen(1))
+			Expect(plan.Delete).To(HaveLen(1))
+			Expect(plan.Update).To(HaveLen(0))
+			Expect(plan.Objects).To(HaveLen(0))
+
+			By("creating the cm", func() {
+				err := k8sClient.Create(context.Background(), cm)
+				Expect(err).ToNot(HaveOccurred())
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(context.Background(), cm)
+				})
+
+				plan, err = dsClient.Plan(ctx, "default", "set123", cm)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(1))
+				Expect(plan.Delete).To(HaveLen(1))
+				Expect(plan.Update).To(HaveLen(0))
+				Expect(plan.Objects).To(HaveLen(1))
+			})
+
+			By("modifying the desired state", func() {
+				cm.Data = map[string]string{"test": "test"}
+
+				plan, err = dsClient.Plan(ctx, "default", "set123", cm)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(1))
+				Expect(plan.Delete).To(HaveLen(1))
+				Expect(plan.Update).To(HaveLen(1))
+				Expect(plan.Objects).To(HaveLen(1))
+			})
+		})
+
+		It("should ignore configmap without wrangler labels", func() {
+			plan, err := dsClient.Plan(ctx, "default", "set123", cm)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(plan.Create).To(HaveLen(1))
+			Expect(plan.Delete).To(HaveLen(1))
+			Expect(plan.Update).To(HaveLen(0))
+			Expect(plan.Objects).To(HaveLen(0))
+
+			By("creating the cm", func() {
+				err := k8sClient.Create(context.Background(), cm)
+				Expect(err).ToNot(HaveOccurred())
+				DeferCleanup(func() {
+					_ = k8sClient.Delete(context.Background(), cm)
+				})
+				plan, err = dsClient.Plan(ctx, "default", "set123", cm)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(1))
+				Expect(plan.Delete).To(HaveLen(1))
+				Expect(plan.Update).To(HaveLen(0))
+				Expect(plan.Objects).To(HaveLen(0))
+			})
+
+			By("modifying the desired state", func() {
+				cm.Data = map[string]string{"test": "test"}
+
+				plan, err = dsClient.Plan(ctx, "default", "set123", cm)
+				Expect(err).ToNot(HaveOccurred())
+
+				Expect(plan.Create).To(HaveLen(1))
+				Expect(plan.Delete).To(HaveLen(1))
+				Expect(plan.Update).To(HaveLen(0))
+				Expect(plan.Objects).To(HaveLen(0))
+			})
+		})
+	})
+})
diff --git a/integrationtests/agent/helm_capabilities_test.go b/integrationtests/agent/helm_capabilities_test.go
index cd33580d..408e2a46 100644
--- a/integrationtests/agent/helm_capabilities_test.go
+++ b/integrationtests/agent/helm_capabilities_test.go
@@ -1,4 +1,4 @@
-package agent
+package agent_test
 
 import (
 	"context"
diff --git a/integrationtests/agent/suite_test.go b/integrationtests/agent/suite_test.go
index e7b0291c..f32f02ae 100644
--- a/integrationtests/agent/suite_test.go
+++ b/integrationtests/agent/suite_test.go
@@ -1,4 +1,4 @@
-package agent
+package agent_test
 
 import (
 	"context"
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 
@@ -37,8 +38,8 @@ import (
 	"github.com/rancher/fleet/integrationtests/utils"
 	"github.com/rancher/fleet/internal/cmd/agent/controller"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer"
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
 	"github.com/rancher/fleet/internal/cmd/agent/deployer/cleanup"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
 	"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"
@@ -53,6 +54,7 @@ var (
 	ctx       context.Context
 	cancel    context.CancelFunc
 	k8sClient client.Client
+	dsClient  *desiredset.Client
 )
 
 const (
@@ -157,12 +159,12 @@ func newReconciler(ctx context.Context, mgr manager.Manager, lookup *lookup) *co
 	localDynamic, err := dynamic.NewForConfig(mgr.GetConfig())
 	Expect(err).ToNot(HaveOccurred())
 
-	applied, err := applied.NewWithClient(mgr.GetConfig())
+	dsClient, err = desiredset.New(mgr.GetConfig())
 	Expect(err).ToNot(HaveOccurred())
 
 	monitor := monitor.New(
 		localClient,
-		applied,
+		dsClient,
 		helmDeployer,
 		defaultNamespace,
 		agentScope,
@@ -174,7 +176,7 @@ func newReconciler(ctx context.Context, mgr manager.Manager, lookup *lookup) *co
 		trigger,
 		upstreamClient,
 		mgr.GetAPIReader(),
-		applied,
+		dsClient,
 		defaultNamespace,
 		defaultNamespace,
 		agentScope,
diff --git a/internal/cmd/agent/deployer/desiredset/desiredset_process_test.go b/internal/cmd/agent/deployer/desiredset/desiredset_process_test.go
new file mode 100644
index 00000000..6ec20449
--- /dev/null
+++ b/internal/cmd/agent/deployer/desiredset/desiredset_process_test.go
@@ -0,0 +1,199 @@
+package desiredset
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/dynamic/fake"
+	k8stesting "k8s.io/client-go/testing"
+	"k8s.io/client-go/tools/cache"
+)
+
+func Test_multiNamespaceList(t *testing.T) {
+	results := map[string]*unstructured.UnstructuredList{
+		"ns1": {Items: []unstructured.Unstructured{
+			{Object: map[string]interface{}{"name": "o1", "namespace": "ns1"}},
+			{Object: map[string]interface{}{"name": "o2", "namespace": "ns1"}},
+			{Object: map[string]interface{}{"name": "o3", "namespace": "ns1"}},
+		}},
+		"ns2": {Items: []unstructured.Unstructured{
+			{Object: map[string]interface{}{"name": "o4", "namespace": "ns2"}},
+			{Object: map[string]interface{}{"name": "o5", "namespace": "ns2"}},
+		}},
+		"ns3": {Items: []unstructured.Unstructured{}},
+	}
+	s := runtime.NewScheme()
+	err := appsv1.SchemeBuilder.AddToScheme(s)
+	assert.NoError(t, err, "Failed to build schema.")
+	baseClient := fake.NewSimpleDynamicClient(s)
+	baseClient.PrependReactor("list", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
+		if strings.Contains(action.GetNamespace(), "error") {
+			return true, nil, errors.New("simulated failure")
+		}
+
+		return true, results[action.GetNamespace()], nil
+	})
+
+	type args struct {
+		namespaces []string
+	}
+	tests := []struct {
+		name          string
+		args          args
+		expectedCalls int
+		expectError   bool
+	}{
+		{
+			name: "no namespaces",
+			args: args{
+				namespaces: []string{},
+			},
+			expectError:   false,
+			expectedCalls: 0,
+		},
+		{
+			name: "1 namespace",
+			args: args{
+				namespaces: []string{"ns1"},
+			},
+			expectError:   false,
+			expectedCalls: 3,
+		},
+		{
+			name: "many namespaces",
+			args: args{
+				namespaces: []string{"ns1", "ns2", "ns3"},
+			},
+			expectError:   false,
+			expectedCalls: 5,
+		},
+		{
+			name: "1 namespace error",
+			args: args{
+				namespaces: []string{"error", "ns2", "ns3"},
+			},
+			expectError:   true,
+			expectedCalls: -1,
+		},
+		{
+			name: "many namespace errors",
+			args: args{
+				namespaces: []string{"error", "error1", "error2"},
+			},
+			expectError:   true,
+			expectedCalls: -1,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var calls int
+			err := multiNamespaceList(context.TODO(), tt.args.namespaces, baseClient.Resource(appsv1.SchemeGroupVersion.WithResource("deployments")), labels.NewSelector(), func(obj unstructured.Unstructured) {
+				calls += 1
+			})
+
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+
+			if tt.expectedCalls >= 0 {
+				assert.Equal(t, tt.expectedCalls, calls)
+			}
+		})
+	}
+}
+
+func Test_getIndexableHash(t *testing.T) {
+	const hash = "somehash"
+	hashSelector, err := getSelector(map[string]string{LabelHash: hash})
+	if err != nil {
+		t.Fatal(err)
+	}
+	envLabelSelector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"env": "dev"}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{byHash: func(obj interface{}) ([]string, error) {
+		return nil, nil
+	}})
+	type args struct {
+		indexer  cache.Indexer
+		selector labels.Selector
+	}
+	tests := []struct {
+		name     string
+		args     args
+		wantHash string
+		want     bool
+	}{
+		{name: "indexer configured", args: args{
+			indexer:  indexer,
+			selector: hashSelector,
+		}, wantHash: hash, want: true},
+		{name: "indexer not configured", args: args{
+			indexer:  cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}),
+			selector: hashSelector,
+		}, wantHash: "", want: false},
+		{name: "using Everything selector", args: args{
+			indexer:  indexer,
+			selector: labels.Everything(),
+		}, wantHash: "", want: false},
+		{name: "using other label selectors", args: args{
+			indexer:  indexer,
+			selector: envLabelSelector,
+		}, wantHash: "", want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotHash, got := getIndexableHash(tt.args.indexer, tt.args.selector)
+			assert.Equalf(t, tt.wantHash, gotHash, "getIndexableHash(%v, %v)", tt.args.indexer, tt.args.selector)
+			assert.Equalf(t, tt.want, got, "getIndexableHash(%v, %v)", tt.args.indexer, tt.args.selector)
+		})
+	}
+}
+
+func Test_inNamespace(t *testing.T) {
+	type args struct {
+		namespace string
+		obj       interface{}
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{name: "object in namespace", args: args{
+			namespace: "ns", obj: &metav1.ObjectMeta{
+				Namespace: "ns",
+			},
+		}, want: true},
+		{name: "object not in namespace", args: args{
+			namespace: "ns", obj: &metav1.ObjectMeta{
+				Namespace: "another-ns",
+			},
+		}, want: false},
+		{name: "object not namespaced", args: args{
+			namespace: "ns", obj: &corev1.Namespace{},
+		}, want: false},
+		{name: "non k8s object", args: args{
+			namespace: "ns", obj: &struct{}{},
+		}, want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, inNamespace(tt.args.namespace, tt.args.obj), "inNamespace(%v, %v)", tt.args.namespace, tt.args.obj)
+		})
+	}
+}
diff --git a/internal/cmd/agent/deployer/monitor/updatestatus.go b/internal/cmd/agent/deployer/monitor/updatestatus.go
index 21fbde96..39b0960a 100644
--- a/internal/cmd/agent/deployer/monitor/updatestatus.go
+++ b/internal/cmd/agent/deployer/monitor/updatestatus.go
@@ -17,23 +17,19 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
-	"github.com/rancher/wrangler/v3/pkg/apply"
-	"github.com/rancher/wrangler/v3/pkg/condition"
-	"github.com/rancher/wrangler/v3/pkg/objectset"
-	"github.com/rancher/wrangler/v3/pkg/summary"
-
-	"github.com/rancher/fleet/internal/cmd/agent/deployer/applied"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/desiredset"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/objectset"
+	"github.com/rancher/fleet/internal/cmd/agent/deployer/summary"
 	"github.com/rancher/fleet/internal/helmdeployer"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 )
 
 type Monitor struct {
-	client  client.Client
-	applied *applied.Applied
+	client     client.Client
+	desiredset *desiredset.Client
 
 	deployer *helmdeployer.Helm
 
@@ -42,10 +38,10 @@ type Monitor struct {
 	labelSuffix      string
 }
 
-func New(client client.Client, applied *applied.Applied, deployer *helmdeployer.Helm, defaultNamespace string, labelSuffix string) *Monitor {
+func New(client client.Client, ds *desiredset.Client, deployer *helmdeployer.Helm, defaultNamespace string, labelSuffix string) *Monitor {
 	return &Monitor{
 		client:           client,
-		applied:          applied,
+		desiredset:       ds,
 		deployer:         deployer,
 		defaultNamespace: defaultNamespace,
 		labelPrefix:      defaultNamespace,
@@ -77,7 +73,7 @@ func ShouldUpdateStatus(bd *fleet.BundleDeployment) bool {
 
 	// If the bundle failed to install the status should not be updated. Updating
 	// here would remove the condition message that was previously set on it.
-	if condition.Cond(fleet.BundleDeploymentConditionInstalled).IsFalse(bd) {
+	if Cond(fleet.BundleDeploymentConditionInstalled).IsFalse(bd) {
 		return false
 	}
 
@@ -90,7 +86,7 @@ func (m *Monitor) UpdateStatus(ctx context.Context, bd *fleet.BundleDeployment,
 	// updateFromResources mutates bd.Status, so copy it first
 	origStatus := *bd.Status.DeepCopy()
 	bd = bd.DeepCopy()
-	err := m.updateFromResources(logger, bd, resources)
+	err := m.updateFromResources(ctx, logger, bd, resources)
 	if err != nil {
 
 		// Returning an error will cause UpdateStatus to requeue in a loop.
@@ -106,7 +102,7 @@ func (m *Monitor) UpdateStatus(ctx context.Context, bd *fleet.BundleDeployment,
 	status := bd.Status
 
 	readyError := readyError(status)
-	condition.Cond(fleet.BundleDeploymentConditionReady).SetError(&status, "", readyError)
+	Cond(fleet.BundleDeploymentConditionReady).SetError(&status, "", readyError)
 
 	status.SyncGeneration = &bd.Spec.Options.ForceSyncGeneration
 	if readyError != nil {
@@ -151,7 +147,7 @@ func readyError(status fleet.BundleDeploymentStatus) error {
 
 // updateFromResources updates the status with information from the
 // helm release history and an apply dry run.
-func (m *Monitor) updateFromResources(logger logr.Logger, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
+func (m *Monitor) updateFromResources(ctx context.Context, logger logr.Logger, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
 	resourcesPreviousRelease, err := m.deployer.ResourcesFromPreviousReleaseVersion(bd.Name, bd.Status.Release)
 	if err != nil {
 		return err
@@ -163,15 +159,15 @@ func (m *Monitor) updateFromResources(logger logr.Logger, bd *fleet.BundleDeploy
 	}
 
 	// resources.Objects contains the desired state of the resources from helm history
-	plan, err := m.applied.DryRun(ns, applied.GetSetID(bd.Name, m.labelPrefix, m.labelSuffix), resources.Objects...)
+	plan, err := m.desiredset.Plan(ctx, ns, desiredset.GetSetID(bd.Name, m.labelPrefix, m.labelSuffix), resources.Objects...)
 	if err != nil {
 		return err
 	}
 
-	// applied.Diff only takes plan.Update into account. plan.Update
+	// dryrun.Diff only takes plan.Update into account. plan.Update
 	// contains objects which have changes to existing values. Adding a new
 	// key to a map is not considered an update.
-	plan, err = applied.Diff(plan, bd, resources.DefaultNamespace, resources.Objects...)
+	plan, err = desiredset.Diff(plan, bd, resources.DefaultNamespace, resources.Objects...)
 	if err != nil {
 		return err
 	}
@@ -214,7 +210,7 @@ func (m *Monitor) updateFromResources(logger logr.Logger, bd *fleet.BundleDeploy
 	return nil
 }
 
-func nonReady(logger logr.Logger, plan apply.Plan, ignoreOptions fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
+func nonReady(logger logr.Logger, plan desiredset.Plan, ignoreOptions fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
 			return result[i].UID < result[j].UID
@@ -253,7 +249,7 @@ func nonReady(logger logr.Logger, plan apply.Plan, ignoreOptions fleet.IgnoreOpt
 // The function iterates through the plan's create, delete, and update actions and constructs a modified status
 // for each resource.
 // If the number of modified statuses exceeds 10, the function stops and returns the current result.
-func modified(c client.Client, logger logr.Logger, plan apply.Plan, resourcesPreviousRelease *helmdeployer.Resources) (result []fleet.ModifiedStatus) {
+func modified(c client.Client, logger logr.Logger, plan desiredset.Plan, resourcesPreviousRelease *helmdeployer.Resources) (result []fleet.ModifiedStatus) {
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
 			return sortKey(result[i]) < sortKey(result[j])
diff --git a/internal/cmd/agent/deployer/objectset/objectset_test.go b/internal/cmd/agent/deployer/objectset/objectset_test.go
new file mode 100644
index 00000000..08c40876
--- /dev/null
+++ b/internal/cmd/agent/deployer/objectset/objectset_test.go
@@ -0,0 +1,60 @@
+package objectset
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestObjectByKey_Namespaces(t *testing.T) {
+	tests := []struct {
+		name           string
+		objects        ObjectByKey
+		wantNamespaces []string
+	}{
+		{
+			name:           "empty",
+			objects:        ObjectByKey{},
+			wantNamespaces: nil,
+		},
+		{
+			name: "1 namespace",
+			objects: ObjectByKey{
+				ObjectKey{Namespace: "ns1", Name: "a"}: nil,
+				ObjectKey{Namespace: "ns1", Name: "b"}: nil,
+			},
+			wantNamespaces: []string{"ns1"},
+		},
+		{
+			name: "many namespaces",
+			objects: ObjectByKey{
+				ObjectKey{Namespace: "ns1", Name: "a"}: nil,
+				ObjectKey{Namespace: "ns2", Name: "b"}: nil,
+			},
+			wantNamespaces: []string{"ns1", "ns2"},
+		},
+		{
+			name: "many namespaces with duplicates",
+			objects: ObjectByKey{
+				ObjectKey{Namespace: "ns1", Name: "a"}: nil,
+				ObjectKey{Namespace: "ns2", Name: "b"}: nil,
+				ObjectKey{Namespace: "ns1", Name: "c"}: nil,
+			},
+			wantNamespaces: []string{"ns1", "ns2"},
+		},
+		{
+			name: "missing namespace",
+			objects: ObjectByKey{
+				ObjectKey{Namespace: "ns1", Name: "a"}: nil,
+				ObjectKey{Name: "b"}:                   nil,
+			},
+			wantNamespaces: []string{"", "ns1"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotNamespaces := tt.objects.Namespaces()
+			assert.ElementsMatchf(t, tt.wantNamespaces, gotNamespaces, "Namespaces() = %v, want %v", gotNamespaces, tt.wantNamespaces)
+		})
+	}
+}