Repository: rancher/fleet

Issue:
[backport v2.12] GitRepo has incorrect `status.resources`
This is a backport issue for #3866, automatically created via [GitHub Actions workflow](https://github.com/rancher/fleet/actions/runs/16752391368) initiated by @p-se


Original issue body:

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Steps To Reproduce

0. Prepare an environment with `local` + 2 downstream clusters
1. Create a new GitRepo
2. URL: `https://github.com/rancher/fleet-examples` , path: `simple`
3. Deployment `redis-slave` has to be in `updating` status in 1 cluster and `ready` in 1 the other cluster

cluster: `fleet-default/rc-ec2` -  `redis-slave` deployment is not ready

<img width="931" height="404" alt="Image" src="https://github.com/user-attachments/assets/89b77d06-3ffa-4fbc-9045-8e48eca4b8d8" />

cluster: `fleet-default/c-rkrnf` -  `redis-slave` deployment is ready

<img width="946" height="359" alt="Image" src="https://github.com/user-attachments/assets/04e85f2e-b3ba-4b4a-8803-4942fa30325a" />


`GitRepo.status.resources[index]` is incorrect:

```
    - apiVersion: apps/v1
      id: default/redis-slave
      kind: Deployment
      name: redis-slave
      namespace: default
      perClusterState:
        ready:
          - fleet-default/c-rkrnf
      state: updating
      type: apps.deployment
```

It should be:

```
    - apiVersion: apps/v1
      id: default/redis-slave
      kind: Deployment
      name: redis-slave
      namespace: default
      perClusterState:
        ready:
          - fleet-default/c-rkrnf
        actual-not-ready-status:
          - fleet-default/rc-ec2
      state: updating
      type: apps.deployment
```

As result of above error, the UI is not able to get the clusterId where the resource has not-ready status and it's not possible to add it as a row in Resources table in the Dashboard slide-in panel and in the GitRepo details:

<img width="511" height="356" alt="Image" src="https://github.com/user-attachments/assets/0dd5125e-08a5-4749-98da-1b77174784af" />


Similarly, when a resource is `missing` from a cluster, the `perClusterState` is empty:

```
    - apiVersion: apps/v1
      id: default/redis-slave
      kind: Deployment
      name: redis-slave
      namespace: default
      perClusterState: {}
      state: updating
      type: apps.deployment
```

It should be:

```
    - apiVersion: apps/v1
      id: default/redis-slave
      kind: Deployment
      name: redis-slave
      namespace: default
      perClusterState:
        ready:
          - fleet-default/c-rkrnf
        missing:
          - fleet-default/rc-ec2
      state: updating
      type: apps.deployment
```


### Environment

```markdown
- Architecture:
- Fleet Version: `rancher/fleet-agent:v0.13.0-beta.2`
- Cluster:
  - Provider:
  - Options:
  - Kubernetes Version:
```

### Logs

```markdown

```

### Anything else?

_No response_


Discussion:
@mmartin24: Verified in Rancher `v2.12-b42bbcdf4c51cf644966f8598ba0c1bb89c4eacc-head` with fleet `0.13.1-rc.1`

--- 

Using example provided [here](https://github.com/rancher/fleet/issues/3866#issuecomment-3155578829) I can see how `Not Ready` entry is created when `status.resources[].state` is in `updating`:

<img width="2534" height="1289" alt="Image" src="https://github.com/user-attachments/assets/fd40a3b2-a17c-4835-bc9e-24d5c61c23f1" />

as opposed in previous version (ie: `fleet:107.0.1+up0.13.1-beta.1` where value `notReady` in `perClusterState` is not created:

<img width="2527" height="1282" alt="Image" src="https://github.com/user-attachments/assets/af6a1e80-836d-46d1-976f-ffe9e804aa27" />


Solution patch:
diff --git a/internal/resourcestatus/resourcekey.go b/internal/resourcestatus/resourcekey.go
index 5b91a05a..b29a3ee0 100644
--- a/internal/resourcestatus/resourcekey.go
+++ b/internal/resourcestatus/resourcekey.go
@@ -200,7 +200,11 @@ func appendToPerClusterState(states *fleet.PerClusterState, state, clusterID str
 		states.Pending = append(states.Pending, clusterID)
 	case "Modified":
 		states.Modified = append(states.Modified, clusterID)
-	case "NotReady":
+	case "NotReady", "updating":
+		// `updating` comes from checkTransitioning summarizer in the
+		// fleet-agent. When the `Available` condition is set to false, the
+		// state is set to `updating`, but we treat it as nonReady for per
+		// cluster states.
 		states.NotReady = append(states.NotReady, clusterID)
 	case "Orphaned":
 		states.Orphaned = append(states.Orphaned, clusterID)



Test patch:
diff --git a/internal/resourcestatus/resourcekey_test.go b/internal/resourcestatus/resourcekey_test.go
index 882ac03c..0c699239 100644
--- a/internal/resourcestatus/resourcekey_test.go
+++ b/internal/resourcestatus/resourcekey_test.go
@@ -214,3 +214,94 @@ func TestSetResources(t *testing.T) {
 	}, status.PerClusterResourceCounts)
 
 }
+
+func TestPerClusterState(t *testing.T) {
+	bundleDeploymentWithState := func(state string) fleet.BundleDeployment {
+		return fleet.BundleDeployment{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd1",
+				Namespace: "ns1-cluster1",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster",
+					fleet.ClusterNamespaceLabel: "namespace",
+				},
+			},
+			Spec: fleet.BundleDeploymentSpec{
+				DeploymentID: "bd1",
+			},
+			Status: fleet.BundleDeploymentStatus{
+				AppliedDeploymentID: "bd1",
+				NonReadyStatus: []fleet.NonReadyStatus{
+					{
+						Kind:       "Deployment",
+						APIVersion: "v1",
+						Namespace:  "default",
+						Name:       "web",
+						Summary: summary.Summary{
+							State: state,
+						},
+					},
+				},
+			},
+		}
+	}
+	tests := []struct {
+		name              string
+		bundleDeployments []fleet.BundleDeployment
+		expectedStatus    fleet.StatusBase
+	}{
+		{
+			name:              "if the state of the NonReadyStatus resource is updating, then it should report it as NotReady",
+			bundleDeployments: []fleet.BundleDeployment{bundleDeploymentWithState("updating")},
+			expectedStatus: fleet.StatusBase{
+				Resources: []fleet.Resource{
+					{
+						Namespace: "default",
+						Name:      "web",
+						PerClusterState: fleet.PerClusterState{
+							NotReady: []string{"namespace/cluster"},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:              "if the state of the NonReadyStatus resource is unknown, then it should ignore the state",
+			bundleDeployments: []fleet.BundleDeployment{bundleDeploymentWithState("")},
+			expectedStatus: fleet.StatusBase{
+				Resources: []fleet.Resource{
+					{
+						Namespace:       "default",
+						Name:            "web",
+						PerClusterState: fleet.PerClusterState{},
+					},
+				},
+			},
+		},
+		{
+			name:              "if the state of the NonReadyStatus resource is NotReady, then it should report it as NotReady",
+			bundleDeployments: []fleet.BundleDeployment{bundleDeploymentWithState("NotReady")},
+			expectedStatus: fleet.StatusBase{
+				Resources: []fleet.Resource{
+					{
+						Namespace: "default",
+						Name:      "web",
+						PerClusterState: fleet.PerClusterState{
+							NotReady: []string{"namespace/cluster"},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var status fleet.GitRepoStatus
+			SetResources(test.bundleDeployments, &status.StatusBase)
+
+			assert.Equal(t, test.expectedStatus.Resources[0].PerClusterState, status.StatusBase.Resources[0].PerClusterState,
+				"Expected resources to match for bundle deployments: %v", test.bundleDeployments,
+			)
+		})
+	}
+}