Repository: rancher/fleet
Pull request #1433 (2023-03-23 15:07:28)

Add agent config for tolerations, affinity, resources on cluster
Changes are tracked via the, hashed, status fields and will redeploy the agent.

This also adds more code comments to the custom resources.

refers to #1392


Patch:
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index b71f6510..b009ad07 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -1727,6 +1727,409 @@ spec:
             type: object
           spec:
             properties:
+              agentAffinity:
+                nullable: true
+                properties:
+                  nodeAffinity:
+                    nullable: true
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            preference:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        nullable: true
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        nullable: true
+                        properties:
+                          nodeSelectorTerms:
+                            items:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                              type: object
+                            nullable: true
+                            type: array
+                        type: object
+                    type: object
+                  podAffinity:
+                    nullable: true
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  nullable: true
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            nullable: true
+                                            type: string
+                                          operator:
+                                            nullable: true
+                                            type: string
+                                          values:
+                                            items:
+                                              nullable: true
+                                              type: string
+                                            nullable: true
+                                            type: array
+                                        type: object
+                                      nullable: true
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        nullable: true
+                                        type: string
+                                      nullable: true
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  nullable: true
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            nullable: true
+                                            type: string
+                                          operator:
+                                            nullable: true
+                                            type: string
+                                          values:
+                                            items:
+                                              nullable: true
+                                              type: string
+                                            nullable: true
+                                            type: array
+                                        type: object
+                                      nullable: true
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        nullable: true
+                                        type: string
+                                      nullable: true
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: array
+                                topologyKey:
+                                  nullable: true
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        nullable: true
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              nullable: true
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              nullable: true
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                nullable: true
+                                type: string
+                              nullable: true
+                              type: array
+                            topologyKey:
+                              nullable: true
+                              type: string
+                          type: object
+                        nullable: true
+                        type: array
+                    type: object
+                  podAntiAffinity:
+                    nullable: true
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  nullable: true
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            nullable: true
+                                            type: string
+                                          operator:
+                                            nullable: true
+                                            type: string
+                                          values:
+                                            items:
+                                              nullable: true
+                                              type: string
+                                            nullable: true
+                                            type: array
+                                        type: object
+                                      nullable: true
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        nullable: true
+                                        type: string
+                                      nullable: true
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  nullable: true
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            nullable: true
+                                            type: string
+                                          operator:
+                                            nullable: true
+                                            type: string
+                                          values:
+                                            items:
+                                              nullable: true
+                                              type: string
+                                            nullable: true
+                                            type: array
+                                        type: object
+                                      nullable: true
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        nullable: true
+                                        type: string
+                                      nullable: true
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: array
+                                topologyKey:
+                                  nullable: true
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        nullable: true
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              nullable: true
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              nullable: true
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        nullable: true
+                                        type: string
+                                      operator:
+                                        nullable: true
+                                        type: string
+                                      values:
+                                        items:
+                                          nullable: true
+                                          type: string
+                                        nullable: true
+                                        type: array
+                                    type: object
+                                  nullable: true
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    nullable: true
+                                    type: string
+                                  nullable: true
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                nullable: true
+                                type: string
+                              nullable: true
+                              type: array
+                            topologyKey:
+                              nullable: true
+                              type: string
+                          type: object
+                        nullable: true
+                        type: array
+                    type: object
+                type: object
               agentEnvVars:
                 items:
                   properties:
@@ -1795,6 +2198,22 @@ spec:
               agentNamespace:
                 nullable: true
                 type: string
+              agentResources:
+                nullable: true
+                properties:
+                  limits:
+                    additionalProperties:
+                      nullable: true
+                      type: string
+                    nullable: true
+                    type: object
+                  requests:
+                    additionalProperties:
+                      nullable: true
+                      type: string
+                    nullable: true
+                    type: object
+                type: object
               agentTolerations:
                 items:
                   properties:
@@ -1861,6 +2280,9 @@ spec:
                   readyNodes:
                     type: integer
                 type: object
+              agentAffinityHash:
+                nullable: true
+                type: string
               agentDeployedGeneration:
                 nullable: true
                 type: integer
@@ -1874,6 +2296,12 @@ spec:
               agentPrivateRepoURL:
                 nullable: true
                 type: string
+              agentResourcesHash:
+                nullable: true
+                type: string
+              agentTolerationsHash:
+                nullable: true
+                type: string
               cattleNamespaceMigrated:
                 type: boolean
               conditions:
diff --git a/go.mod b/go.mod
index 274ff79c..29627169 100644
--- a/go.mod
+++ b/go.mod
@@ -42,6 +42,7 @@ require (
 	github.com/go-git/go-git/v5 v5.6.1
 	github.com/go-logr/logr v1.2.3
 	github.com/gobwas/glob v0.2.3
+	github.com/golang/mock v1.6.0
 	github.com/google/go-cmp v0.5.9
 	github.com/google/go-containerregistry v0.13.0
 	github.com/hashicorp/go-getter v1.7.1
diff --git a/internal/mocks/namespace_mock.go b/internal/mocks/namespace_mock.go
new file mode 100644
index 00000000..85caef38
--- /dev/null
+++ b/internal/mocks/namespace_mock.go
@@ -0,0 +1,296 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/wrangler/pkg/generated/controllers/core/v1 (interfaces: NamespaceController)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+	time "time"
+
+	gomock "github.com/golang/mock/gomock"
+	v1 "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
+	generic "github.com/rancher/wrangler/pkg/generic"
+	v10 "k8s.io/api/core/v1"
+	v11 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// MockNamespaceController is a mock of NamespaceController interface.
+type MockNamespaceController struct {
+	ctrl     *gomock.Controller
+	recorder *MockNamespaceControllerMockRecorder
+}
+
+// MockNamespaceControllerMockRecorder is the mock recorder for MockNamespaceController.
+type MockNamespaceControllerMockRecorder struct {
+	mock *MockNamespaceController
+}
+
+// NewMockNamespaceController creates a new mock instance.
+func NewMockNamespaceController(ctrl *gomock.Controller) *MockNamespaceController {
+	mock := &MockNamespaceController{ctrl: ctrl}
+	mock.recorder = &MockNamespaceControllerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockNamespaceController) EXPECT() *MockNamespaceControllerMockRecorder {
+	return m.recorder
+}
+
+// AddGenericHandler mocks base method.
+func (m *MockNamespaceController) AddGenericHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericHandler", arg0, arg1, arg2)
+}
+
+// AddGenericHandler indicates an expected call of AddGenericHandler.
+func (mr *MockNamespaceControllerMockRecorder) AddGenericHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockNamespaceController)(nil).AddGenericHandler), arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler mocks base method.
+func (m *MockNamespaceController) AddGenericRemoveHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericRemoveHandler", arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler.
+func (mr *MockNamespaceControllerMockRecorder) AddGenericRemoveHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockNamespaceController)(nil).AddGenericRemoveHandler), arg0, arg1, arg2)
+}
+
+// Cache mocks base method.
+func (m *MockNamespaceController) Cache() v1.NamespaceCache {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Cache")
+	ret0, _ := ret[0].(v1.NamespaceCache)
+	return ret0
+}
+
+// Cache indicates an expected call of Cache.
+func (mr *MockNamespaceControllerMockRecorder) Cache() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockNamespaceController)(nil).Cache))
+}
+
+// Create mocks base method.
+func (m *MockNamespaceController) Create(arg0 *v10.Namespace) (*v10.Namespace, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0)
+	ret0, _ := ret[0].(*v10.Namespace)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockNamespaceControllerMockRecorder) Create(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNamespaceController)(nil).Create), arg0)
+}
+
+// Delete mocks base method.
+func (m *MockNamespaceController) Delete(arg0 string, arg1 *v11.DeleteOptions) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockNamespaceControllerMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockNamespaceController)(nil).Delete), arg0, arg1)
+}
+
+// Enqueue mocks base method.
+func (m *MockNamespaceController) Enqueue(arg0 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Enqueue", arg0)
+}
+
+// Enqueue indicates an expected call of Enqueue.
+func (mr *MockNamespaceControllerMockRecorder) Enqueue(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockNamespaceController)(nil).Enqueue), arg0)
+}
+
+// EnqueueAfter mocks base method.
+func (m *MockNamespaceController) EnqueueAfter(arg0 string, arg1 time.Duration) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "EnqueueAfter", arg0, arg1)
+}
+
+// EnqueueAfter indicates an expected call of EnqueueAfter.
+func (mr *MockNamespaceControllerMockRecorder) EnqueueAfter(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockNamespaceController)(nil).EnqueueAfter), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockNamespaceController) Get(arg0 string, arg1 v11.GetOptions) (*v10.Namespace, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(*v10.Namespace)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockNamespaceControllerMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNamespaceController)(nil).Get), arg0, arg1)
+}
+
+// GroupVersionKind mocks base method.
+func (m *MockNamespaceController) GroupVersionKind() schema.GroupVersionKind {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GroupVersionKind")
+	ret0, _ := ret[0].(schema.GroupVersionKind)
+	return ret0
+}
+
+// GroupVersionKind indicates an expected call of GroupVersionKind.
+func (mr *MockNamespaceControllerMockRecorder) GroupVersionKind() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockNamespaceController)(nil).GroupVersionKind))
+}
+
+// Informer mocks base method.
+func (m *MockNamespaceController) Informer() cache.SharedIndexInformer {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Informer")
+	ret0, _ := ret[0].(cache.SharedIndexInformer)
+	return ret0
+}
+
+// Informer indicates an expected call of Informer.
+func (mr *MockNamespaceControllerMockRecorder) Informer() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockNamespaceController)(nil).Informer))
+}
+
+// List mocks base method.
+func (m *MockNamespaceController) List(arg0 v11.ListOptions) (*v10.NamespaceList, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0)
+	ret0, _ := ret[0].(*v10.NamespaceList)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockNamespaceControllerMockRecorder) List(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNamespaceController)(nil).List), arg0)
+}
+
+// OnChange mocks base method.
+func (m *MockNamespaceController) OnChange(arg0 context.Context, arg1 string, arg2 v1.NamespaceHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnChange", arg0, arg1, arg2)
+}
+
+// OnChange indicates an expected call of OnChange.
+func (mr *MockNamespaceControllerMockRecorder) OnChange(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnChange", reflect.TypeOf((*MockNamespaceController)(nil).OnChange), arg0, arg1, arg2)
+}
+
+// OnRemove mocks base method.
+func (m *MockNamespaceController) OnRemove(arg0 context.Context, arg1 string, arg2 v1.NamespaceHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnRemove", arg0, arg1, arg2)
+}
+
+// OnRemove indicates an expected call of OnRemove.
+func (mr *MockNamespaceControllerMockRecorder) OnRemove(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRemove", reflect.TypeOf((*MockNamespaceController)(nil).OnRemove), arg0, arg1, arg2)
+}
+
+// Patch mocks base method.
+func (m *MockNamespaceController) Patch(arg0 string, arg1 types.PatchType, arg2 []byte, arg3 ...string) (*v10.Namespace, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2}
+	for _, a := range arg3 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Patch", varargs...)
+	ret0, _ := ret[0].(*v10.Namespace)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Patch indicates an expected call of Patch.
+func (mr *MockNamespaceControllerMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockNamespaceController)(nil).Patch), varargs...)
+}
+
+// Update mocks base method.
+func (m *MockNamespaceController) Update(arg0 *v10.Namespace) (*v10.Namespace, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Update", arg0)
+	ret0, _ := ret[0].(*v10.Namespace)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockNamespaceControllerMockRecorder) Update(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockNamespaceController)(nil).Update), arg0)
+}
+
+// UpdateStatus mocks base method.
+func (m *MockNamespaceController) UpdateStatus(arg0 *v10.Namespace) (*v10.Namespace, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateStatus", arg0)
+	ret0, _ := ret[0].(*v10.Namespace)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateStatus indicates an expected call of UpdateStatus.
+func (mr *MockNamespaceControllerMockRecorder) UpdateStatus(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockNamespaceController)(nil).UpdateStatus), arg0)
+}
+
+// Updater mocks base method.
+func (m *MockNamespaceController) Updater() generic.Updater {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Updater")
+	ret0, _ := ret[0].(generic.Updater)
+	return ret0
+}
+
+// Updater indicates an expected call of Updater.
+func (mr *MockNamespaceControllerMockRecorder) Updater() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockNamespaceController)(nil).Updater))
+}
+
+// Watch mocks base method.
+func (m *MockNamespaceController) Watch(arg0 v11.ListOptions) (watch.Interface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Watch", arg0)
+	ret0, _ := ret[0].(watch.Interface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Watch indicates an expected call of Watch.
+func (mr *MockNamespaceControllerMockRecorder) Watch(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockNamespaceController)(nil).Watch), arg0)
+}
diff --git a/pkg/agent/manifest.go b/pkg/agent/manifest.go
index 2f98df49..5e40ca67 100644
--- a/pkg/agent/manifest.go
+++ b/pkg/agent/manifest.go
@@ -36,6 +36,8 @@ type ManifestOptions struct {
 	Generation            string
 	PrivateRepoURL        string // PrivateRepoURL = registry.yourdomain.com:5000
 	SystemDefaultRegistry string
+	AgentAffinity         *corev1.Affinity
+	AgentResources        *corev1.ResourceRequirements
 }
 
 // Manifest builds and returns a deployment manifest for the fleet-agent with a
@@ -229,6 +231,16 @@ func agentDeployment(namespace string, agentScope string, opts ManifestOptions,
 	// additional tolerations from cluster
 	dep.Spec.Template.Spec.Tolerations = append(dep.Spec.Template.Spec.Tolerations, opts.AgentTolerations...)
 
+	// overwrite affinity if present on cluster
+	if opts.AgentAffinity != nil {
+		dep.Spec.Template.Spec.Affinity = opts.AgentAffinity
+	}
+
+	// set resources if present on cluster
+	if opts.AgentResources != nil {
+		dep.Spec.Template.Spec.Containers[0].Resources = *opts.AgentResources
+	}
+
 	// additional env vars from cluster
 	if opts.AgentEnvVars != nil {
 		dep.Spec.Template.Spec.Containers[0].Env = append(dep.Spec.Template.Spec.Containers[0].Env, opts.AgentEnvVars...)
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
index 7805cc26..4ec0f0fe 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
@@ -62,13 +62,24 @@ type BundleNamespaceMapping struct {
 type BundleSpec struct {
 	BundleDeploymentOptions
 
-	// Paused if set to true, will stop any BundleDeployments from being updated.
-	Paused             bool                      `json:"paused,omitempty"`
-	RolloutStrategy    *RolloutStrategy          `json:"rolloutStrategy,omitempty"`
-	Resources          []BundleResource          `json:"resources,omitempty"`
-	Targets            []BundleTarget            `json:"targets,omitempty"`
+	// Paused if set to true, will stop any BundleDeployments from being updated. It will be marked as out of sync.
+	Paused bool `json:"paused,omitempty"`
+
+	// RolloutStrategy controls the rollout of bundles, by defining
+	// partitions, canaries and percentages for cluster availability.
+	RolloutStrategy *RolloutStrategy `json:"rolloutStrategy,omitempty"`
+
+	// Resources contain the actual resources from the git repo which will be deployed.
+	Resources []BundleResource `json:"resources,omitempty"`
+
+	// Targets refer to the clusters which will be deployed to.
+	Targets []BundleTarget `json:"targets,omitempty"`
+
+	// TargetRestrictions restrict which clusters the bundle will be deployed to.
 	TargetRestrictions []BundleTargetRestriction `json:"targetRestrictions,omitempty"`
-	DependsOn          []BundleRef               `json:"dependsOn,omitempty"`
+
+	// DependsOn refers to the bundles which must be ready before this bundle can be deployed.
+	DependsOn []BundleRef `json:"dependsOn,omitempty"`
 }
 
 type BundleRef struct {
@@ -190,15 +201,38 @@ type BundleDeployment struct {
 }
 
 type BundleDeploymentOptions struct {
-	DefaultNamespace    string            `json:"defaultNamespace,omitempty"`
-	TargetNamespace     string            `json:"namespace,omitempty"`
-	Kustomize           *KustomizeOptions `json:"kustomize,omitempty"`
-	Helm                *HelmOptions      `json:"helm,omitempty"`
-	ServiceAccount      string            `json:"serviceAccount,omitempty"`
-	ForceSyncGeneration int64             `json:"forceSyncGeneration,omitempty"`
-	YAML                *YAMLOptions      `json:"yaml,omitempty"`
-	Diff                *DiffOptions      `json:"diff,omitempty"`
-	KeepResources       bool              `json:"keepResources,omitempty"`
+	// DefaultNamespace is the namespace to use for resources that do not
+	// specify a namespace. This field is not used to enforce or lock down
+	// the deployment to a specific namespace.
+	DefaultNamespace string `json:"defaultNamespace,omitempty"`
+
+	// TargetNamespace if present will assign all resource to this
+	// namespace and if any cluster scoped resource exists the deployment
+	// will fail.
+	TargetNamespace string `json:"namespace,omitempty"`
+
+	// Kustomize options for the deployment, like the dir containing the
+	// kustomization.yaml file.
+	Kustomize *KustomizeOptions `json:"kustomize,omitempty"`
+
+	// Helm options for the deployment, like the chart name, repo and values.
+	Helm *HelmOptions `json:"helm,omitempty"`
+
+	// ServiceAccount which will be used to perform this deployment.
+	ServiceAccount string `json:"serviceAccount,omitempty"`
+
+	// ForceSyncGeneration is used to force a redeployment
+	ForceSyncGeneration int64 `json:"forceSyncGeneration,omitempty"`
+
+	// YAML options, if using raw YAML these are names that map to
+	// overlays/{name} that will be used to replace or patch a resource.
+	YAML *YAMLOptions `json:"yaml,omitempty"`
+
+	// Diff can be used to ignore the modified state of objects which are amended at runtime.
+	Diff *DiffOptions `json:"diff,omitempty"`
+
+	// KeepResources can be used to keep the deployed resources when removing the bundle
+	KeepResources bool `json:"keepResources,omitempty"`
 }
 
 type DiffOptions struct {
@@ -229,18 +263,47 @@ type KustomizeOptions struct {
 }
 
 type HelmOptions struct {
-	Chart          string       `json:"chart,omitempty"`
-	Repo           string       `json:"repo,omitempty"`
-	ReleaseName    string       `json:"releaseName,omitempty"`
-	Version        string       `json:"version,omitempty"`
-	TimeoutSeconds int          `json:"timeoutSeconds,omitempty"`
-	Values         *GenericMap  `json:"values,omitempty"`
-	ValuesFrom     []ValuesFrom `json:"valuesFrom,omitempty"`
-	Force          bool         `json:"force,omitempty"`
-	TakeOwnership  bool         `json:"takeOwnership,omitempty"`
-	MaxHistory     int          `json:"maxHistory,omitempty"`
-	ValuesFiles    []string     `json:"valuesFiles,omitempty"`
-	WaitForJobs    bool         `json:"waitForJobs,omitempty"`
+	// Chart can refer to any go-getter URL or OCI registry based helm
+	// chart URL. The chart will be downloaded.
+	Chart string `json:"chart,omitempty"`
+
+	// Repo is the name of the HTTPS helm repo to download the chart from.
+	Repo string `json:"repo,omitempty"`
+
+	// ReleaseName sets a custom release name to deploy the chart as. If
+	// not specified a release name will be generated by combining the
+	// invoking GitRepo.name + GitRepo.path.
+	ReleaseName string `json:"releaseName,omitempty"`
+
+	// Version of the chart to download
+	Version string `json:"version,omitempty"`
+
+	// TimeoutSeconds is the time to wait for Helm operations.
+	TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
+
+	// Values passed to Helm. It is possible to specify the keys and values
+	// as go template strings.
+	Values *GenericMap `json:"values,omitempty"`
+
+	// ValuesFrom loads the values from configmaps and secrets.
+	ValuesFrom []ValuesFrom `json:"valuesFrom,omitempty"`
+
+	// Force allows to override immutable resources. This could be dangerous.
+	Force bool `json:"force,omitempty"`
+
+	// TakeOwnership makes helm skip the check for its own annotations
+	TakeOwnership bool `json:"takeOwnership,omitempty"`
+
+	// MaxHistory limits the maximum number of revisions saved per release by Helm.
+	MaxHistory int `json:"maxHistory,omitempty"`
+
+	// ValuesFiles is a list of files to load values from.
+	ValuesFiles []string `json:"valuesFiles,omitempty"`
+
+	// WaitForJobs if set and timeoutSeconds provided, will wait until all
+	// Jobs have been completed before marking the GitRepo as ready. It
+	// will wait for as long as timeoutSeconds
+	WaitForJobs bool `json:"waitForJobs,omitempty"`
 
 	// Atomic sets the --atomic flag when Helm is performing an upgrade
 	Atomic bool `json:"atomic,omitempty"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/target.go b/pkg/apis/fleet.cattle.io/v1alpha1/target.go
index e1aa3de4..c5e117b8 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/target.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/target.go
@@ -59,25 +59,39 @@ type Cluster struct {
 
 type ClusterSpec struct {
 	// Paused if set to true, will stop any BundleDeployments from being updated.
-	Paused                  bool   `json:"paused,omitempty"`
-	ClientID                string `json:"clientID,omitempty"`
-	KubeConfigSecret        string `json:"kubeConfigSecret,omitempty"`
-	RedeployAgentGeneration int64  `json:"redeployAgentGeneration,omitempty"`
+	Paused bool `json:"paused,omitempty"`
 
-	// AgentEnvVars are extra environment variables to be added to the agent deployment
+	// ClientID is a unique string that will identify the cluster. It can
+	// either be predefined, or generated when importing the cluster.
+	ClientID string `json:"clientID,omitempty"`
+
+	// KubeConfigSecret is the name of the secret containing the kubeconfig for the downstream cluster.
+	KubeConfigSecret string `json:"kubeConfigSecret,omitempty"`
+
+	// RedeployAgentGeneration can be used to force redeploying the agent.
+	RedeployAgentGeneration int64 `json:"redeployAgentGeneration,omitempty"`
+
+	// AgentEnvVars are extra environment variables to be added to the agent deployment.
 	AgentEnvVars []v1.EnvVar `json:"agentEnvVars,omitempty"`
 
-	// AgentNamespace defaults to the system namespace, e.g. cattle-fleet-system
+	// AgentNamespace defaults to the system namespace, e.g. cattle-fleet-system.
 	AgentNamespace string `json:"agentNamespace,omitempty"`
 
-	// PrivateRepoURL prefixes the image name and overrides a global repo URL from the agents config
+	// PrivateRepoURL prefixes the image name and overrides a global repo URL from the agents config.
 	PrivateRepoURL string `json:"privateRepoURL,omitempty"`
 
-	// TemplateValues defines a cluster specific mapping of values to be sent to fleet.yaml values templating
+	// TemplateValues defines a cluster specific mapping of values to be sent to fleet.yaml values templating.
 	TemplateValues *GenericMap `json:"templateValues,omitempty"`
 
-	// AgentTolerations defines an extra set of Tolerations to be added to the Agent deployment
+	// AgentTolerations defines an extra set of Tolerations to be added to the Agent deployment.
 	AgentTolerations []v1.Toleration `json:"agentTolerations,omitempty"`
+
+	// AgentAffinity overrides the default affinity for the cluster's agent
+	// deployment. If this value is nil the default affinity is used.
+	AgentAffinity *v1.Affinity `json:"agentAffinity,omitempty"`
+
+	// AgentResources sets the resources for the cluster's agent deployment.
+	AgentResources *v1.ResourceRequirements `json:"agentResources,omitempty"`
 }
 
 type ClusterStatus struct {
@@ -100,6 +114,10 @@ type ClusterStatus struct {
 	AgentNamespaceMigrated  bool   `json:"agentNamespaceMigrated,omitempty"`
 	CattleNamespaceMigrated bool   `json:"cattleNamespaceMigrated,omitempty"`
 
+	AgentAffinityHash    string `json:"agentAffinityHash,omitempty"`
+	AgentResourcesHash   string `json:"agentResourcesHash,omitempty"`
+	AgentTolerationsHash string `json:"agentTolerationsHash,omitempty"`
+
 	Display ClusterDisplay `json:"display,omitempty"`
 	Agent   AgentStatus    `json:"agent,omitempty"`
 }
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
index 3e3d6adb..7777b7f2 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
@@ -1018,6 +1018,16 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.AgentAffinity != nil {
+		in, out := &in.AgentAffinity, &out.AgentAffinity
+		*out = new(corev1.Affinity)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.AgentResources != nil {
+		in, out := &in.AgentResources, &out.AgentResources
+		*out = new(corev1.ResourceRequirements)
+		(*in).DeepCopyInto(*out)
+	}
 	return
 }
 
diff --git a/pkg/controllers/cluster/import.go b/pkg/controllers/cluster/import.go
index 8b4f3cb7..0f2fff97 100644
--- a/pkg/controllers/cluster/import.go
+++ b/pkg/controllers/cluster/import.go
@@ -278,6 +278,8 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust
 				CheckinInterval:  cfg.AgentCheckinInterval.Duration.String(),
 				Generation:       string(cluster.UID) + "-" + strconv.FormatInt(cluster.Generation, 10),
 				PrivateRepoURL:   cluster.Spec.PrivateRepoURL,
+				AgentAffinity:    cluster.Spec.AgentAffinity,
+				AgentResources:   cluster.Spec.AgentResources,
 			},
 		})
 	if err != nil {
diff --git a/pkg/controllers/manageagent/manageagent.go b/pkg/controllers/manageagent/manageagent.go
index 7800a9bd..b167c9ff 100644
--- a/pkg/controllers/manageagent/manageagent.go
+++ b/pkg/controllers/manageagent/manageagent.go
@@ -74,13 +74,57 @@ func (h *handler) onClusterStatusChange(cluster *fleet.Cluster, status fleet.Clu
 	if err != nil {
 		return status, err
 	}
-	status, repo := h.reconcileAgentPrivateRepoURL(cluster, status)
-	if vars || repo {
+
+	status, changed, err := h.updateClusterStatus(cluster, status)
+	if err != nil {
+		return status, err
+	}
+
+	if vars || changed {
 		h.namespaces.Enqueue(cluster.Namespace)
 	}
 	return status, nil
 }
 
+func hashStatusField(field any) (string, error) {
+	hasher := sha256.New224()
+	b, err := json.Marshal(field)
+	if err != nil {
+		return "", err
+	}
+	hasher.Write(b)
+	return fmt.Sprintf("%x", hasher.Sum(nil)), nil
+}
+
+func hashChanged(field any, statusHash string) (bool, string, error) {
+	isNil := func(field any) bool {
+		switch field := field.(type) {
+		case *corev1.Affinity:
+			return field == nil
+		case *corev1.ResourceRequirements:
+			return field == nil
+		case []corev1.Toleration:
+			return len(field) == 0
+		default:
+			return false
+		}
+	}
+
+	if isNil(field) {
+		if statusHash != "" {
+			return true, "", nil
+		}
+		return false, "", nil
+	}
+
+	hash, err := hashStatusField(field)
+	if err != nil {
+		return false, "", err
+	}
+
+	return statusHash != hash, hash, nil
+}
+
 // reconcileAgentEnvVars checks if the agent environment variables field was
 // updated by hashing its contents into a status field.
 func (h *handler) reconcileAgentEnvVars(cluster *fleet.Cluster, status fleet.ClusterStatus) (fleet.ClusterStatus, bool, error) {
@@ -96,13 +140,10 @@ func (h *handler) reconcileAgentEnvVars(cluster *fleet.Cluster, status fleet.Clu
 		return status, enqueue, nil
 	}
 
-	hasher := sha256.New224()
-	b, err := json.Marshal(cluster.Spec.AgentEnvVars)
+	hash, err := hashStatusField(cluster.Spec.AgentEnvVars)
 	if err != nil {
 		return status, enqueue, err
 	}
-	hasher.Write(b)
-	hash := fmt.Sprintf("%x", hasher.Sum(nil))
 
 	if status.AgentEnvVarsHash != hash {
 		// We enqueue to ensure that we edit the status after other controllers.
@@ -113,12 +154,36 @@ func (h *handler) reconcileAgentEnvVars(cluster *fleet.Cluster, status fleet.Clu
 	return status, enqueue, nil
 }
 
-func (h *handler) reconcileAgentPrivateRepoURL(cluster *fleet.Cluster, status fleet.ClusterStatus) (fleet.ClusterStatus, bool) {
+func (h *handler) updateClusterStatus(cluster *fleet.Cluster, status fleet.ClusterStatus) (fleet.ClusterStatus, bool, error) {
+	changed := false
+
 	if status.AgentPrivateRepoURL != cluster.Spec.PrivateRepoURL {
 		status.AgentPrivateRepoURL = cluster.Spec.PrivateRepoURL
-		return status, true
+		changed = true
+	}
+
+	if c, hash, err := hashChanged(cluster.Spec.AgentAffinity, status.AgentAffinityHash); err != nil {
+		return status, changed, err
+	} else if c {
+		status.AgentAffinityHash = hash
+		changed = c
+	}
+
+	if c, hash, err := hashChanged(cluster.Spec.AgentResources, status.AgentResourcesHash); err != nil {
+		return status, changed, err
+	} else if c {
+		status.AgentResourcesHash = hash
+		changed = c
+	}
+
+	if c, hash, err := hashChanged(cluster.Spec.AgentTolerations, status.AgentTolerationsHash); err != nil {
+		return status, changed, err
+	} else if c {
+		status.AgentTolerationsHash = hash
+		changed = c
 	}
-	return status, false
+
+	return status, changed, nil
 }
 
 func (h *handler) resolveNS(namespace, _ string, obj runtime.Object) ([]relatedresource.Key, error) {
@@ -189,6 +254,8 @@ func (h *handler) newAgentBundle(ns string, cluster *fleet.Cluster) (runtime.Obj
 			Generation:            "bundle",
 			PrivateRepoURL:        cluster.Spec.PrivateRepoURL,
 			SystemDefaultRegistry: cfg.SystemDefaultRegistry,
+			AgentAffinity:         cluster.Spec.AgentAffinity,
+			AgentResources:        cluster.Spec.AgentResources,
 		},
 	)
 	agentYAML, err := yaml.Export(objs...)


Test patch:
diff --git a/pkg/agent/manifest_test.go b/pkg/agent/manifest_test.go
index 012512f4..9e9edc3e 100644
--- a/pkg/agent/manifest_test.go
+++ b/pkg/agent/manifest_test.go
@@ -1,10 +1,12 @@
 package agent
 
 import (
+	"reflect"
 	"testing"
 
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
@@ -41,7 +43,7 @@ func getDeploymentFromManifests(namespace string, scope string, opts ManifestOpt
 	return nil
 }
 
-func TestManifestAdditionalTolerations(t *testing.T) {
+func TestManifestAgentTolerations(t *testing.T) {
 	const namespace = "fleet-system"
 	const scope = "test-scope"
 	baseOpts := ManifestOptions{
@@ -81,12 +83,7 @@ func TestManifestAdditionalTolerations(t *testing.T) {
 			getOpts: func() ManifestOptions {
 				withTolerationsOpts := baseOpts
 				withTolerationsOpts.AgentTolerations = []corev1.Toleration{
-					{
-						Key:      "fleet-agent",
-						Operator: "Equals",
-						Value:    "true",
-						Effect:   "NoSchedule",
-					},
+					{Key: "fleet-agent", Operator: "Equals", Value: "true", Effect: "NoSchedule"},
 				}
 				return withTolerationsOpts
 			},
@@ -96,7 +93,6 @@ func TestManifestAdditionalTolerations(t *testing.T) {
 		},
 	} {
 		t.Run(testCase.name, func(t *testing.T) {
-
 			agentDeployment := getDeploymentFromManifests(namespace, scope, testCase.getOpts())
 			if agentDeployment == nil {
 				t.Fatal("there were no deployments returned from the manifests")
@@ -108,3 +104,115 @@ func TestManifestAdditionalTolerations(t *testing.T) {
 		})
 	}
 }
+
+func TestManifestAgentAffinity(t *testing.T) {
+	const namespace = "fleet-system"
+
+	// this is the value from manifest.go
+	builtinAffinity := &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{
+		PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{{
+			Weight: 1,
+			Preference: corev1.NodeSelectorTerm{
+				MatchExpressions: []corev1.NodeSelectorRequirement{
+					{Key: "fleet.cattle.io/agent", Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}},
+				},
+			},
+		}},
+	}}
+
+	customAffinity := &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{
+		PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{{
+			Weight: 1,
+			Preference: corev1.NodeSelectorTerm{
+				MatchExpressions: []corev1.NodeSelectorRequirement{
+					{Key: "custom/label", Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}},
+				},
+			},
+		}},
+	}}
+
+	for _, testCase := range []struct {
+		name             string
+		getOpts          func() ManifestOptions
+		expectedAffinity *corev1.Affinity
+	}{
+		{
+			name:             "Builtin Affinity",
+			getOpts:          func() ManifestOptions { return ManifestOptions{} },
+			expectedAffinity: builtinAffinity,
+		},
+		{
+			name:             "Remove Affinity",
+			getOpts:          func() ManifestOptions { return ManifestOptions{AgentAffinity: &corev1.Affinity{}} },
+			expectedAffinity: &corev1.Affinity{},
+		},
+		{
+			name:             "Override Affinity",
+			getOpts:          func() ManifestOptions { return ManifestOptions{AgentAffinity: customAffinity} },
+			expectedAffinity: customAffinity,
+		},
+	} {
+		t.Run(testCase.name, func(t *testing.T) {
+			agentDeployment := getDeploymentFromManifests(namespace, "", testCase.getOpts())
+			if agentDeployment == nil {
+				t.Fatal("there were no deployments returned from the manifests")
+			}
+
+			if !cmp.Equal(agentDeployment.Spec.Template.Spec.Affinity, testCase.expectedAffinity) {
+				t.Fatalf("affinity was not as expected: %v %v", testCase.expectedAffinity, agentDeployment.Spec.Template.Spec.Affinity)
+			}
+		})
+	}
+}
+
+func TestManifestAgentResources(t *testing.T) {
+	const namespace = "fleet-system"
+
+	// this is the value from manifest.go
+	builtinResources := corev1.ResourceRequirements{}
+
+	customResources := corev1.ResourceRequirements{
+		Limits: corev1.ResourceList{
+			corev1.ResourceCPU:    resource.MustParse("100m"),
+			corev1.ResourceMemory: resource.MustParse("100Mi"),
+		},
+
+		Requests: corev1.ResourceList{
+			corev1.ResourceCPU:    resource.MustParse("50m"),
+			corev1.ResourceMemory: resource.MustParse("50Mi"),
+		},
+	}
+
+	for _, testCase := range []struct {
+		name              string
+		getOpts           func() ManifestOptions
+		expectedResources corev1.ResourceRequirements
+	}{
+		{
+			name:              "Builtin Resources",
+			getOpts:           func() ManifestOptions { return ManifestOptions{} },
+			expectedResources: builtinResources,
+		},
+		{
+			name:              "Remove Resources",
+			getOpts:           func() ManifestOptions { return ManifestOptions{AgentResources: &corev1.ResourceRequirements{}} },
+			expectedResources: corev1.ResourceRequirements{},
+		},
+		{
+			name:              "Override Resources",
+			getOpts:           func() ManifestOptions { return ManifestOptions{AgentResources: &customResources} },
+			expectedResources: customResources,
+		},
+	} {
+		t.Run(testCase.name, func(t *testing.T) {
+			agentDeployment := getDeploymentFromManifests(namespace, "", testCase.getOpts())
+			if agentDeployment == nil {
+				t.Fatal("there were no deployments returned from the manifests")
+			}
+
+			if !reflect.DeepEqual(agentDeployment.Spec.Template.Spec.Containers[0].Resources, testCase.expectedResources) {
+				t.Fatalf("resources was not as expected: %v %v", testCase.expectedResources, agentDeployment.Spec.Template.Spec.Containers[0].Resources)
+			}
+		})
+	}
+}
diff --git a/pkg/controllers/manageagent/manageagent_test.go b/pkg/controllers/manageagent/manageagent_test.go
new file mode 100644
index 00000000..822d0419
--- /dev/null
+++ b/pkg/controllers/manageagent/manageagent_test.go
@@ -0,0 +1,276 @@
+package manageagent
+
+//go:generate mockgen --build_flags=--mod=mod -destination=../../../internal/mocks/namespace_mock.go -package=mocks github.com/rancher/wrangler/pkg/generated/controllers/core/v1 NamespaceController
+
+import (
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/rancher/fleet/internal/mocks"
+
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+func TestOnClusterChangeAffinity(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	namespaces := mocks.NewMockNamespaceController(ctrl)
+	h := &handler{namespaces: namespaces}
+
+	// defaultAffinity from the manifest in manifest.go
+	defaultAffinity := &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{
+		RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+			NodeSelectorTerms: []corev1.NodeSelectorTerm{{
+				MatchExpressions: []corev1.NodeSelectorRequirement{
+					{Key: "fleet.cattle.io/agent", Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}},
+				},
+			}},
+		}},
+	}
+	hash, _ := hashStatusField(defaultAffinity)
+
+	customAffinity := &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{
+		RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+			NodeSelectorTerms: []corev1.NodeSelectorTerm{{
+				MatchExpressions: []corev1.NodeSelectorRequirement{
+					{Key: "foo", Operator: corev1.NodeSelectorOpIn, Values: []string{"bar"}},
+				},
+			}},
+		}},
+	}
+	customHash, _ := hashStatusField(customAffinity)
+
+	emptyHash, _ := hashStatusField(&corev1.Affinity{})
+
+	for _, tt := range []struct {
+		name           string
+		cluster        *fleet.Cluster
+		status         fleet.ClusterStatus
+		expectedStatus fleet.ClusterStatus
+		enqueues       int
+	}{
+		{
+			name:           "Empty Affinity",
+			cluster:        &fleet.Cluster{},
+			status:         fleet.ClusterStatus{},
+			expectedStatus: fleet.ClusterStatus{},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal Affinity",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentAffinity: defaultAffinity}},
+			status:         fleet.ClusterStatus{AgentAffinityHash: hash},
+			expectedStatus: fleet.ClusterStatus{AgentAffinityHash: hash},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal Custom Affinity",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentAffinity: customAffinity}},
+			status:         fleet.ClusterStatus{AgentAffinityHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentAffinityHash: customHash},
+			enqueues:       0,
+		},
+		{
+			name:           "Changed Affinity",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentAffinity: customAffinity}},
+			status:         fleet.ClusterStatus{AgentAffinityHash: hash},
+			expectedStatus: fleet.ClusterStatus{AgentAffinityHash: customHash},
+			enqueues:       1,
+		},
+		{
+			name:           "Changed to Empty Affinity",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{}},
+			status:         fleet.ClusterStatus{AgentAffinityHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentAffinityHash: ""},
+			enqueues:       1,
+		},
+		{
+			name:           "Removed Affinity",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentAffinity: &corev1.Affinity{}}},
+			status:         fleet.ClusterStatus{AgentAffinityHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentAffinityHash: emptyHash},
+			enqueues:       1,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			namespaces.EXPECT().Enqueue(gomock.Any()).Times(tt.enqueues)
+
+			status, err := h.onClusterStatusChange(tt.cluster, tt.status)
+			if err != nil {
+				t.Error(err)
+			}
+
+			if status.AgentAffinityHash != tt.expectedStatus.AgentAffinityHash {
+				t.Fatalf("agent affinity hash is not equal: %v vs %v", status.AgentAffinityHash, tt.expectedStatus.AgentAffinityHash)
+			}
+		})
+	}
+}
+
+func TestOnClusterChangeResources(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	namespaces := mocks.NewMockNamespaceController(ctrl)
+	h := &handler{namespaces: namespaces}
+
+	customResources := corev1.ResourceRequirements{
+		Limits: corev1.ResourceList{
+			corev1.ResourceCPU:    resource.MustParse("100m"),
+			corev1.ResourceMemory: resource.MustParse("100Mi"),
+		},
+
+		Requests: corev1.ResourceList{
+			corev1.ResourceCPU:    resource.MustParse("50m"),
+			corev1.ResourceMemory: resource.MustParse("50Mi"),
+		},
+	}
+	customHash, _ := hashStatusField(customResources)
+
+	for _, tt := range []struct {
+		name           string
+		cluster        *fleet.Cluster
+		status         fleet.ClusterStatus
+		expectedStatus fleet.ClusterStatus
+		enqueues       int
+	}{
+		{
+			name:           "Empty Resources",
+			cluster:        &fleet.Cluster{},
+			status:         fleet.ClusterStatus{},
+			expectedStatus: fleet.ClusterStatus{},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal Resources",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentResources: &customResources}},
+			status:         fleet.ClusterStatus{AgentResourcesHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentResourcesHash: customHash},
+			enqueues:       0,
+		},
+		{
+			name:           "Changed Resources",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentResources: &customResources}},
+			status:         fleet.ClusterStatus{AgentResourcesHash: ""},
+			expectedStatus: fleet.ClusterStatus{AgentResourcesHash: customHash},
+			enqueues:       1,
+		},
+		{
+			name:           "Removed Resources",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: ""},
+			enqueues:       1,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			namespaces.EXPECT().Enqueue(gomock.Any()).Times(tt.enqueues)
+
+			status, err := h.onClusterStatusChange(tt.cluster, tt.status)
+			if err != nil {
+				t.Error(err)
+			}
+
+			if status.AgentResourcesHash != tt.expectedStatus.AgentResourcesHash {
+				t.Fatalf("agent resources hash is not equal: %v vs %v", status.AgentResourcesHash, tt.expectedStatus.AgentResourcesHash)
+			}
+		})
+	}
+}
+
+func TestOnClusterChangeTolerations(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	namespaces := mocks.NewMockNamespaceController(ctrl)
+	h := &handler{namespaces: namespaces}
+
+	// defaultTolerations from the manifest in manifest.go
+	defaultTolerations := []corev1.Toleration{
+		{
+			Key:      "node.cloudprovider.kubernetes.io/uninitialized",
+			Operator: corev1.TolerationOpEqual,
+			Value:    "true",
+			Effect:   corev1.TaintEffectNoSchedule,
+		},
+		{
+			Key:      "cattle.io/os",
+			Operator: corev1.TolerationOpEqual,
+			Value:    "linux",
+			Effect:   corev1.TaintEffectNoSchedule,
+		},
+	}
+	hash, _ := hashStatusField(defaultTolerations)
+
+	customTolerations := []corev1.Toleration{
+		{
+			Key:      "node.cloudprovider.kubernetes.io/windows",
+			Operator: corev1.TolerationOpEqual,
+			Value:    "false",
+			Effect:   corev1.TaintEffectNoSchedule,
+		},
+	}
+	customHash, _ := hashStatusField(customTolerations)
+
+	for _, tt := range []struct {
+		name           string
+		cluster        *fleet.Cluster
+		status         fleet.ClusterStatus
+		expectedStatus fleet.ClusterStatus
+		enqueues       int
+	}{
+		{
+			name:           "Empty Resources",
+			cluster:        &fleet.Cluster{},
+			status:         fleet.ClusterStatus{},
+			expectedStatus: fleet.ClusterStatus{},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal Tolerations",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentTolerations: defaultTolerations}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: hash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: hash},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal Custom Tolerations",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentTolerations: customTolerations}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			enqueues:       0,
+		},
+		{
+			name:           "Changed Tolerations",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentTolerations: customTolerations}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: hash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			enqueues:       1,
+		},
+		{
+			name:           "Removed Tolerations, omitted",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: ""},
+			enqueues:       1,
+		},
+		{
+			name:           "Removed Tolerations, empty list",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{AgentTolerations: []corev1.Toleration{}}},
+			status:         fleet.ClusterStatus{AgentTolerationsHash: customHash},
+			expectedStatus: fleet.ClusterStatus{AgentTolerationsHash: ""},
+			enqueues:       1,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			namespaces.EXPECT().Enqueue(gomock.Any()).Times(tt.enqueues)
+
+			status, err := h.onClusterStatusChange(tt.cluster, tt.status)
+			if err != nil {
+				t.Error(err)
+			}
+
+			if status.AgentTolerationsHash != tt.expectedStatus.AgentTolerationsHash {
+				t.Fatalf("agent tolerations hash is not equal: %v vs %v", status.AgentTolerationsHash, tt.expectedStatus.AgentTolerationsHash)
+			}
+		})
+	}
+}