Repository: rancher/fleet
Pull request #3203 (2025-01-10 15:07:20)

    Add resourceCounts and incomplete state to BundleDeploymentStatus
Refers to #3186

This PR extends the current public API to 2 new fields to every `BundleDeploymentStatus`:
 - `incompleteState`, which will be set to `true` if non-ready or modified resources status has more than 10 items.
 - `resourceCounts`, which counts the number of resources by their state.

The last commits add a small refactoring in order to make the code more testable, and add a unit test.

kkaempf: Backport this to 2.10 ?

Patch:
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index fa2637da..5e85d769 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -910,6 +910,11 @@ spec:
                       nullable: true
                       type: string
                   type: object
+                incompleteState:
+                  description: IncompleteState is true if there are more than 10 non-ready
+                    or modified resources, meaning that the lists in those fields
+                    have been truncated.
+                  type: boolean
                 modifiedStatus:
                   items:
                     description: 'ModifiedStatus is used to report the status of a
@@ -998,6 +1003,42 @@ spec:
                   description: Release is the Helm release ID
                   nullable: true
                   type: string
+                resourceCounts:
+                  description: ResourceCounts contains the number of resources in
+                    each state.
+                  properties:
+                    desiredReady:
+                      description: DesiredReady is the number of resources that should
+                        be ready.
+                      type: integer
+                    missing:
+                      description: Missing is the number of missing resources.
+                      type: integer
+                    modified:
+                      description: Modified is the number of resources that have been
+                        modified.
+                      type: integer
+                    notReady:
+                      description: 'NotReady is the number of not ready resources.
+                        Resources are not
+
+                        ready if they do not match any other state.'
+                      type: integer
+                    orphaned:
+                      description: Orphaned is the number of orphaned resources.
+                      type: integer
+                    ready:
+                      description: Ready is the number of ready resources.
+                      type: integer
+                    unknown:
+                      description: Unknown is the number of resources in an unknown
+                        state.
+                      type: integer
+                    waitApplied:
+                      description: WaitApplied is the number of resources that are
+                        waiting to be applied.
+                      type: integer
+                  type: object
                 resources:
                   description: 'Resources lists the metadata of resources that were
                     deployed
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
index 98c1835c..ecfb9f28 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
@@ -351,6 +351,8 @@ type BundleDeploymentStatus struct {
 	NonReadyStatus []NonReadyStatus `json:"nonReadyStatus,omitempty"`
 	// +nullable
 	ModifiedStatus []ModifiedStatus `json:"modifiedStatus,omitempty"`
+	// IncompleteState is true if there are more than 10 non-ready or modified resources, meaning that the lists in those fields have been truncated.
+	IncompleteState bool `json:"incompleteState,omitempty"`
 	// +nullable
 	Display BundleDeploymentDisplay `json:"display,omitempty"`
 	// +nullable
@@ -359,6 +361,8 @@ type BundleDeploymentStatus struct {
 	// according to the helm release history.
 	// +nullable
 	Resources []BundleDeploymentResource `json:"resources,omitempty"`
+	// ResourceCounts contains the number of resources in each state.
+	ResourceCounts ResourceCounts `json:"resourceCounts,omitempty"`
 }
 
 type BundleDeploymentDisplay struct {
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 c6e18817..5836a027 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -298,6 +298,7 @@ func (in *BundleDeploymentStatus) DeepCopyInto(out *BundleDeploymentStatus) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	out.ResourceCounts = in.ResourceCounts
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleDeploymentStatus.


Test patch:
diff --git a/internal/cmd/agent/deployer/monitor/updatestatus.go b/internal/cmd/agent/deployer/monitor/updatestatus.go
index 75e7fe92..e3c95889 100644
--- a/internal/cmd/agent/deployer/monitor/updatestatus.go
+++ b/internal/cmd/agent/deployer/monitor/updatestatus.go
@@ -10,8 +10,6 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/go-logr/logr"
-
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -27,6 +25,9 @@ import (
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 )
 
+// limit the length of nonReady and modified resources
+const resourcesDetailsMaxLength = 10
+
 type Monitor struct {
 	client     client.Client
 	desiredset *desiredset.Client
@@ -82,11 +83,12 @@ func ShouldUpdateStatus(bd *fleet.BundleDeployment) bool {
 
 func (m *Monitor) UpdateStatus(ctx context.Context, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) (fleet.BundleDeploymentStatus, error) {
 	logger := log.FromContext(ctx).WithName("update-status")
+	ctx = log.IntoContext(ctx, logger)
 
-	// updateFromResources mutates bd.Status, so copy it first
+	// updateFromPreviousDeployment mutates bd.Status, so copy it first
 	origStatus := *bd.Status.DeepCopy()
 	bd = bd.DeepCopy()
-	err := m.updateFromResources(ctx, logger, bd, resources)
+	err := m.updateFromPreviousDeployment(ctx, bd, resources)
 	if err != nil {
 
 		// Returning an error will cause UpdateStatus to requeue in a loop.
@@ -145,9 +147,9 @@ func readyError(status fleet.BundleDeploymentStatus) error {
 	return errors.New(msg)
 }
 
-// updateFromResources updates the status with information from the
+// updateFromPreviousDeployment updates the status with information from the
 // helm release history and an apply dry run.
-func (m *Monitor) updateFromResources(ctx context.Context, logger logr.Logger, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
+func (m *Monitor) updateFromPreviousDeployment(ctx context.Context, bd *fleet.BundleDeployment, resources *helmdeployer.Resources) error {
 	resourcesPreviousRelease, err := m.deployer.ResourcesFromPreviousReleaseVersion(bd.Name, bd.Status.Release)
 	if err != nil {
 		return err
@@ -172,33 +174,33 @@ func (m *Monitor) updateFromResources(ctx context.Context, logger logr.Logger, b
 		return err
 	}
 
-	bd.Status.NonReadyStatus = nonReady(logger, plan, bd.Spec.Options.IgnoreOptions)
-	bd.Status.ModifiedStatus = modified(ctx, m.client, logger, plan, resourcesPreviousRelease)
-	bd.Status.Ready = false
-	bd.Status.NonModified = false
-
-	if len(bd.Status.NonReadyStatus) == 0 {
-		bd.Status.Ready = true
-	}
-	if len(bd.Status.ModifiedStatus) == 0 {
-		bd.Status.NonModified = true
+	nonReadyResources := nonReady(ctx, plan, bd.Spec.Options.IgnoreOptions)
+	modifiedResources := modified(ctx, m.client, plan, resourcesPreviousRelease)
+	allResources, err := toBundleDeploymentResources(m.client, plan.Objects, resources.DefaultNamespace)
+	if err != nil {
+		return err
 	}
 
-	bd.Status.Resources = []fleet.BundleDeploymentResource{}
-	for _, obj := range plan.Objects {
+	updateFromResources(&bd.Status, allResources, nonReadyResources, modifiedResources)
+	return nil
+}
+
+func toBundleDeploymentResources(client client.Client, objs []runtime.Object, defaultNamespace string) ([]fleet.BundleDeploymentResource, error) {
+	res := make([]fleet.BundleDeploymentResource, 0, len(objs))
+	for _, obj := range objs {
 		ma, err := meta.Accessor(obj)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
 		ns := ma.GetNamespace()
 		gvk := obj.GetObjectKind().GroupVersionKind()
-		if ns == "" && isNamespaced(m.client.RESTMapper(), gvk) {
-			ns = resources.DefaultNamespace
+		if ns == "" && isNamespaced(client.RESTMapper(), gvk) {
+			ns = defaultNamespace
 		}
 
 		version, kind := gvk.ToAPIVersionAndKind()
-		bd.Status.Resources = append(bd.Status.Resources, fleet.BundleDeploymentResource{
+		res = append(res, fleet.BundleDeploymentResource{
 			Kind:       kind,
 			APIVersion: version,
 			Namespace:  ns,
@@ -206,11 +208,82 @@ func (m *Monitor) updateFromResources(ctx context.Context, logger logr.Logger, b
 			CreatedAt:  ma.GetCreationTimestamp(),
 		})
 	}
+	return res, nil
+}
 
-	return nil
+func updateFromResources(bdStatus *fleet.BundleDeploymentStatus, resources []fleet.BundleDeploymentResource, nonReadyResources []fleet.NonReadyStatus, modifiedResources []fleet.ModifiedStatus) {
+	bdStatus.Ready = len(nonReadyResources) == 0
+	bdStatus.NonReadyStatus = nonReadyResources
+	if len(bdStatus.NonReadyStatus) > resourcesDetailsMaxLength {
+		bdStatus.IncompleteState = true
+		bdStatus.NonReadyStatus = nonReadyResources[:resourcesDetailsMaxLength]
+	}
+
+	bdStatus.NonModified = len(modifiedResources) == 0
+	bdStatus.ModifiedStatus = modifiedResources
+	if len(bdStatus.ModifiedStatus) > resourcesDetailsMaxLength {
+		bdStatus.IncompleteState = true
+		bdStatus.ModifiedStatus = modifiedResources[:resourcesDetailsMaxLength]
+	}
+
+	bdStatus.Resources = resources
+	bdStatus.ResourceCounts = calculateResourceCounts(resources, nonReadyResources, modifiedResources)
+}
+
+func calculateResourceCounts(all []fleet.BundleDeploymentResource, nonReady []fleet.NonReadyStatus, modified []fleet.ModifiedStatus) fleet.ResourceCounts {
+	// Create a map with all different resource keys, then remove modified or non-ready keys
+	resourceKeys := make(map[fleet.ResourceKey]struct{}, len(all))
+	for _, r := range all {
+		resourceKeys[fleet.ResourceKey{
+			Kind:       r.Kind,
+			APIVersion: r.APIVersion,
+			Namespace:  r.Namespace,
+			Name:       r.Name,
+		}] = struct{}{}
+	}
+
+	// The agent must have enough visibility to determine the exact state of every resource.
+	// e.g. "WaitApplied" or "Unknown" states do not make sense in this context
+	counts := fleet.ResourceCounts{
+		DesiredReady: len(all),
+	}
+	for _, r := range modified {
+		if r.Create {
+			counts.Missing++
+		} else if r.Delete {
+			counts.Orphaned++
+		} else {
+			counts.Modified++
+		}
+		delete(resourceKeys, fleet.ResourceKey{
+			Kind:       r.Kind,
+			APIVersion: r.APIVersion,
+			Namespace:  r.Namespace,
+			Name:       r.Name,
+		})
+	}
+	for _, r := range nonReady {
+		key := fleet.ResourceKey{
+			Kind:       r.Kind,
+			APIVersion: r.APIVersion,
+			Namespace:  r.Namespace,
+			Name:       r.Name,
+		}
+		// If not present, it was already accounted for as "modified"
+		if _, ok := resourceKeys[key]; ok {
+			counts.NotReady++
+			delete(resourceKeys, key)
+		}
+	}
+
+	// Remaining keys are considered ready
+	counts.Ready = len(resourceKeys)
+
+	return counts
 }
 
-func nonReady(logger logr.Logger, plan desiredset.Plan, ignoreOptions fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
+func nonReady(ctx context.Context, plan desiredset.Plan, ignoreOptions fleet.IgnoreOptions) (result []fleet.NonReadyStatus) {
+	logger := log.FromContext(ctx)
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
 			return result[i].UID < result[j].UID
@@ -218,9 +291,6 @@ func nonReady(logger logr.Logger, plan desiredset.Plan, ignoreOptions fleet.Igno
 	}()
 
 	for _, obj := range plan.Objects {
-		if len(result) >= 10 {
-			return result
-		}
 		if u, ok := obj.(*unstructured.Unstructured); ok {
 			if ignoreOptions.Conditions != nil {
 				if err := excludeIgnoredConditions(u, ignoreOptions); err != nil {
@@ -228,15 +298,15 @@ func nonReady(logger logr.Logger, plan desiredset.Plan, ignoreOptions fleet.Igno
 				}
 			}
 
-			summary := summary.Summarize(u)
-			if !summary.IsReady() {
+			sum := summary.Summarize(u)
+			if !sum.IsReady() {
 				result = append(result, fleet.NonReadyStatus{
 					UID:        u.GetUID(),
 					Kind:       u.GetKind(),
 					APIVersion: u.GetAPIVersion(),
 					Namespace:  u.GetNamespace(),
 					Name:       u.GetName(),
-					Summary:    summary,
+					Summary:    sum,
 				})
 			}
 		}
@@ -249,20 +319,16 @@ func nonReady(logger logr.Logger, plan desiredset.Plan, ignoreOptions fleet.Igno
 // 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(ctx context.Context, c client.Client, logger logr.Logger, plan desiredset.Plan, resourcesPreviousRelease *helmdeployer.Resources) (result []fleet.ModifiedStatus) {
+func modified(ctx context.Context, c client.Client, plan desiredset.Plan, resourcesPreviousRelease *helmdeployer.Resources) (result []fleet.ModifiedStatus) {
+	logger := log.FromContext(ctx)
 	defer func() {
 		sort.Slice(result, func(i, j int) bool {
 			return sortKey(result[i]) < sortKey(result[j])
 		})
 	}()
 	for gvk, keys := range plan.Create {
+		apiVersion, kind := gvk.ToAPIVersionAndKind()
 		for _, key := range keys {
-			if len(result) >= 10 {
-				return result
-			}
-
-			apiVersion, kind := gvk.ToAPIVersionAndKind()
-
 			obj := &unstructured.Unstructured{}
 			obj.SetGroupVersionKind(gvk)
 			key := client.ObjectKey{
@@ -296,12 +362,8 @@ func modified(ctx context.Context, c client.Client, logger logr.Logger, plan des
 	}
 
 	for gvk, keys := range plan.Delete {
+		apiVersion, kind := gvk.ToAPIVersionAndKind()
 		for _, key := range keys {
-			if len(result) >= 10 {
-				return result
-			}
-
-			apiVersion, kind := gvk.ToAPIVersionAndKind()
 			// Check if resource was in a previous release. It is possible that some operators copy the
 			// objectset.rio.cattle.io/hash label into a dynamically created objects. We need to skip these resources
 			// because they are not part of the release, and they would appear as orphaned.
@@ -319,12 +381,8 @@ func modified(ctx context.Context, c client.Client, logger logr.Logger, plan des
 	}
 
 	for gvk, patches := range plan.Update {
+		apiVersion, kind := gvk.ToAPIVersionAndKind()
 		for key, patch := range patches {
-			if len(result) >= 10 {
-				break
-			}
-
-			apiVersion, kind := gvk.ToAPIVersionAndKind()
 			result = append(result, fleet.ModifiedStatus{
 				Kind:       kind,
 				APIVersion: apiVersion,
diff --git a/internal/cmd/agent/deployer/monitor/updatestatus_test.go b/internal/cmd/agent/deployer/monitor/updatestatus_test.go
new file mode 100644
index 00000000..819e03f4
--- /dev/null
+++ b/internal/cmd/agent/deployer/monitor/updatestatus_test.go
@@ -0,0 +1,267 @@
+package monitor
+
+import (
+	"fmt"
+	"testing"
+
+	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1/summary"
+	"github.com/stretchr/testify/assert"
+
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+)
+
+func Test_updateFromResources(t *testing.T) {
+	type args struct {
+		resources []fleet.BundleDeploymentResource
+		nonReady  []fleet.NonReadyStatus
+		modified  []fleet.ModifiedStatus
+	}
+	tests := []struct {
+		name   string
+		args   args
+		assert func(*testing.T, fleet.BundleDeploymentStatus)
+	}{
+		{
+			name: "all ready",
+			args: args{
+				resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+					},
+					{
+						Kind:       "Secret",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testsecret",
+					},
+				},
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 2, Ready: 2})
+				assert.Truef(t, status.Ready, "unexpected ready status")
+				assert.Truef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 2, "unexpected resources length")
+				assert.Emptyf(t, status.NonReadyStatus, "expected non-ready status to be empty")
+				assert.Emptyf(t, status.ModifiedStatus, "expected modified status to be empty")
+			},
+		},
+		{
+			name: "orphaned",
+			args: args{
+				resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+					},
+				},
+				modified: []fleet.ModifiedStatus{
+					{
+						Kind:       "Secret",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testsecret",
+						Delete:     true,
+					},
+				},
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 1, Ready: 1, Orphaned: 1})
+				assert.Truef(t, status.Ready, "unexpected ready status")
+				assert.Falsef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 1, "unexpected resources length")
+				assert.Len(t, status.ModifiedStatus, 1, "incorrect modified status length")
+				assert.True(t, status.ModifiedStatus[0].Delete)
+				assert.Emptyf(t, status.NonReadyStatus, "expected non-ready status to be empty")
+			},
+		},
+		{
+			name: "missing",
+			args: args{
+				resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+					},
+				},
+				modified: []fleet.ModifiedStatus{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+						Create:     true,
+					},
+				},
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 1, Missing: 1})
+				assert.Truef(t, status.Ready, "unexpected ready status")
+				assert.Falsef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 1, "unexpected resources length")
+				assert.Len(t, status.ModifiedStatus, 1, "incorrect modified status length")
+				assert.True(t, status.ModifiedStatus[0].Create)
+				assert.Emptyf(t, status.NonReadyStatus, "expected non-ready status to be empty")
+			},
+		},
+		{
+			name: "modified",
+			args: args{
+				resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+					},
+				},
+				modified: []fleet.ModifiedStatus{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+						Patch:      `{"data": {"foo": "bar"}`,
+					},
+				},
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 1, Modified: 1})
+				assert.Truef(t, status.Ready, "unexpected ready status")
+				assert.Falsef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 1, "unexpected resources length")
+				assert.Len(t, status.ModifiedStatus, 1, "incorrect modified status length")
+				assert.NotEmpty(t, status.ModifiedStatus[0].Patch)
+				assert.Emptyf(t, status.NonReadyStatus, "expected non-ready status to be empty")
+			},
+		},
+		{
+			name: "missing and non-ready",
+			args: args{
+				resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+					},
+					{
+						Kind:       "Pod",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testpod",
+					},
+				},
+				modified: []fleet.ModifiedStatus{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testcm",
+						Patch:      `{"data": {"foo": "bar"}`,
+					},
+				},
+				nonReady: []fleet.NonReadyStatus{
+					{
+						Kind:       "Pod",
+						APIVersion: "v1",
+						Namespace:  "testns",
+						Name:       "testpod",
+						Summary: fleetv1.Summary{
+							State:   "Evicted",
+							Error:   true,
+							Message: []string{"no space left on device"},
+						},
+					},
+				},
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 2, Modified: 1, NotReady: 1})
+				assert.Falsef(t, status.Ready, "unexpected ready status")
+				assert.Falsef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 2, "unexpected resources length")
+				assert.Len(t, status.NonReadyStatus, 1, "incorrect non-ready status length")
+				assert.NotEmptyf(t, status.NonReadyStatus[0].Summary, "unexpected empty summary for non-ready resource")
+				assert.Len(t, status.ModifiedStatus, 1, "incorrect modified status length")
+				assert.NotEmpty(t, status.ModifiedStatus[0].Patch)
+			},
+		},
+		{
+			name: "non-ready and modified status lists have a max length",
+			args: args{
+				resources: func(n int) []fleet.BundleDeploymentResource {
+					cms := make([]fleet.BundleDeploymentResource, n)
+					for x := range n {
+						cms[x] = fleet.BundleDeploymentResource{
+							Kind:       "ConfigMap",
+							APIVersion: "v1",
+							Namespace:  "testns",
+							Name:       fmt.Sprintf("testcm-%d", x),
+						}
+					}
+					pods := make([]fleet.BundleDeploymentResource, n)
+					for x := range n {
+						pods[x] = fleet.BundleDeploymentResource{
+							Kind:       "Pod",
+							APIVersion: "v1",
+							Namespace:  "testns",
+							Name:       fmt.Sprintf("pod-%d", x),
+						}
+					}
+					return append(cms, pods...)
+				}(12),
+				nonReady: func(n int) []fleet.NonReadyStatus {
+					pods := make([]fleet.NonReadyStatus, n)
+					for x := range n {
+						pods[x] = fleet.NonReadyStatus{
+							Kind:       "Pod",
+							APIVersion: "v1",
+							Namespace:  "testns",
+							Name:       fmt.Sprintf("pod-%d", x),
+							Summary: fleetv1.Summary{
+								State: "Evicted",
+							},
+						}
+					}
+					return pods
+				}(12),
+				modified: func(n int) []fleet.ModifiedStatus {
+					cms := make([]fleet.ModifiedStatus, n)
+					for x := range n {
+						cms[x] = fleet.ModifiedStatus{
+							Kind:       "ConfigMap",
+							APIVersion: "v1",
+							Namespace:  "testns",
+							Name:       fmt.Sprintf("testcm-%d", x),
+							Create:     true,
+						}
+					}
+					return cms
+				}(12),
+			},
+			assert: func(t *testing.T, status fleet.BundleDeploymentStatus) {
+				assert.Equal(t, status.ResourceCounts, fleet.ResourceCounts{DesiredReady: 24, Missing: 12, NotReady: 12})
+				assert.Falsef(t, status.Ready, "unexpected ready status")
+				assert.Falsef(t, status.NonModified, "unexpected non-modified status")
+				assert.Lenf(t, status.Resources, 24, "unexpected resources length")
+
+				assert.Len(t, status.NonReadyStatus, 10, "non-ready status length exceeds maximum")
+				assert.Len(t, status.ModifiedStatus, 10, "incorrect modified exceeds maximum")
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var status fleet.BundleDeploymentStatus
+			updateFromResources(&status, tt.args.resources, tt.args.nonReady, tt.args.modified)
+
+			tt.assert(t, status)
+		})
+	}
+}