Repository: rancher/fleet

Issue:
Disable `0` values in PDB like Rancher does
To be compatible with the upstream implementation in Rancher, we should ignore zero values for PDB in cluster resources for agent scheduling customization.

Follow-up on #3491

Hints from issue comments:
@p-se: # QA Template

1. Setup Fleet on a cluster. Rancher is not required.
2. Edit the Fleet cluster resource, add `spec.agentSchedulingCustomization` like so:
    ```yaml
    agentSchedulingCustomization:
      podDisruptionBudget:
        minAvailable: "1"
        maxUnavailable: "0"
    ```
    This should result in the creation of a `PodDisruptionBudget` with name `fleet-agent-pod-disruption-budget`.
3. Check that the values of `fleet-agent-pod-disruption-budget` are according to the specification in the cluster resource. Note that a non-zero value with a string "0" is effectively disabling the zero string. This is because, for the configuration, both values can be specified but the resulting PodDisruptionBudget resource cannot have both values set at the same timee.
6. Delete the `agentSchedulingCustomization` field and ensure that the PodDisruptionBudget resource has been removed.

Optionally extend testing for other configurable values of the PDB. Setting both, `minAvailable` and `maxUnavailable` on a PodDisruptionBudget is supposed to prevent the Fleet agent from being updated, as those values are mutually exclusive and cannot successfully be set on a PodDisruptionBudget.

Every change in the cluster resource is supposed to redeploy the downstream (or local) agent with the previous PriorityClass and PodDisruptionBudget being deleted and recreated.@mmartin24: Verified in Rancher `v2.13-c1ede7b5b8969528188bcc8b09ba6289c671fc80-head` with Fleet `108.0.0+up0.14.0-alpha.3`

- Basic case with provided example works:

```yaml
agentSchedulingCustomization:
  podDisruptionBudget:
    minAvailable: "1"
    maxUnavailable: "0"
```

`fleet-agent-pod-disruption-budget` is created as can be seen and cluster has status Active. This is also valid for downstream clusters

<img width="1913" height="973" alt="Image" src="https://github.com/user-attachments/assets/4de18197-8e4f-4230-a6e4-13e0ffe13ec0" />

- Checked that removal of this spec automatically removes the poddisruptionbudget.

---

However, I checked also a boundary case with `minAvailable: '2'` and  `maxUnavailable: '1'`  and turned out not to create any `fleet-agent-pod-disruption-budget` leaving the cluster in `Error` state. @p-se , is this expected?

```yaml
  agentSchedulingCustomization:
    podDisruptionBudget:
      maxUnavailable: '1'
      minAvailable: '2'
```

<img width="1913" height="973" alt="Image" src="https://github.com/user-attachments/assets/c435e767-b7f4-4436-9d7c-f2a1bb2721a0" />

<img width="1913" height="973" alt="Image" src="https://github.com/user-attachments/assets/6fc036b9-dfdc-42b2-89fb-35af0f81bbfd" />@mmartin24: 
Clarified offline that `minAvailable` and `maxUnavailable` cannot both have values at once.

Extended testing-

- Verified in this example that `minAvailable:0` shows `N/A` in Rancher

```yaml
 agentSchedulingCustomization:
    podDisruptionBudget:
      maxUnavailable: '4'
      minAvailable: '0'
```

<img width="1887" height="503" alt="Image" src="https://github.com/user-attachments/assets/710bc34b-34f1-4666-86ab-83ee8a204b25" />

- Verified in this example that `maxAvailable:0` is displayed as `N/A` in Rancher:

``` yaml
 agentSchedulingCustomization:
    podDisruptionBudget:
      maxUnavailable: '0'
      minAvailable: '4'
```

<img width="1887" height="750" alt="Image" src="https://github.com/user-attachments/assets/9740fb6a-f015-41bc-880a-63a27bcd0845" />


Adding a video where to better see how `pdb` gets updated and 0 values is not shown even when passed:

https://github.com/user-attachments/assets/6f043ca5-84d9-4bf5-8753-1e851ab71c7b

- Verified if both values are set to nothing (`''`), `maxUnavailable` is selected and set to 0:

```yaml
  agentSchedulingCustomization:
    podDisruptionBudget:
      maxUnavailable: ''
      minAvailable: ''
```

<img width="1890" height="954" alt="Image" src="https://github.com/user-attachments/assets/8ec5aaf3-4d2e-49e5-b4d4-f1c000410727" />

Solution patch:
diff --git a/README.md b/README.md
index 8855414a..371385f6 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Install the Fleet Helm charts (there's two because we separate out CRDs for ulti
 
 ```shell
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet-crd https://github.com/rancher/fleet/releases/download/v0.13.1/fleet-crd-0.13.1.tgz
+    fleet-crd https://github.com/rancher/fleet/releases/download/v0.13.2/fleet-crd-0.13.2.tgz
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet https://github.com/rancher/fleet/releases/download/v0.13.1/fleet-0.13.1.tgz
+    fleet https://github.com/rancher/fleet/releases/download/v0.13.2/fleet-0.13.2.tgz
 ```
 
 ## Add a Git Repo to watch
diff --git a/internal/cmd/controller/agentmanagement/scheduling/scheduling.go b/internal/cmd/controller/agentmanagement/scheduling/scheduling.go
index 846cda96..d85cfeb4 100644
--- a/internal/cmd/controller/agentmanagement/scheduling/scheduling.go
+++ b/internal/cmd/controller/agentmanagement/scheduling/scheduling.go
@@ -29,25 +29,25 @@ func PriorityClass(priorityClass *fleet.PriorityClassSpec) *schedulingv1.Priorit
 	}
 }
 
-func PodDisruptionBudget(agentNamespace string, podDisruptionBudgetSpec *fleet.PodDisruptionBudgetSpec) (*policyv1.PodDisruptionBudget, error) {
+func PodDisruptionBudget(agentNamespace string, pdbs *fleet.PodDisruptionBudgetSpec) (*policyv1.PodDisruptionBudget, error) {
 	pdbSpec := policyv1.PodDisruptionBudgetSpec{
 		Selector: &metav1.LabelSelector{
 			MatchLabels: map[string]string{"app": "fleet-agent"},
 		},
 	}
 
-	if podDisruptionBudgetSpec.MaxUnavailable == "" && podDisruptionBudgetSpec.MinAvailable == "" {
+	if pdbs.MaxUnavailable == "" && pdbs.MinAvailable == "" {
 		logrus.Warnf("Neither MaxUnavailable nor MinAvailable is set, defaulting to 0 for MaxUnavailable")
 		pdbSpec.MaxUnavailable = &intstr.IntOrString{IntVal: 0}
-	} else if podDisruptionBudgetSpec.MaxUnavailable != "" && podDisruptionBudgetSpec.MinAvailable != "" {
-		return &policyv1.PodDisruptionBudget{},
-			fmt.Errorf("both MaxUnavailable (%s) and MinAvailable (%s) are set, not creating PDB", podDisruptionBudgetSpec.MaxUnavailable, podDisruptionBudgetSpec.MinAvailable)
-	} else if podDisruptionBudgetSpec.MaxUnavailable != "" {
-		mu := intstr.Parse(podDisruptionBudgetSpec.MaxUnavailable)
+	} else if pdbs.MaxUnavailable != "" && (pdbs.MinAvailable == "" || pdbs.MinAvailable == "0") {
+		mu := intstr.Parse(pdbs.MaxUnavailable)
 		pdbSpec.MaxUnavailable = &mu
-	} else if podDisruptionBudgetSpec.MinAvailable != "" {
-		mu := intstr.Parse(podDisruptionBudgetSpec.MinAvailable)
-		pdbSpec.MinAvailable = &mu
+	} else if pdbs.MinAvailable != "" && (pdbs.MaxUnavailable == "" || pdbs.MaxUnavailable == "0") {
+		ma := intstr.Parse(pdbs.MinAvailable)
+		pdbSpec.MinAvailable = &ma
+	} else if pdbs.MaxUnavailable != "" && pdbs.MinAvailable != "" {
+		return &policyv1.PodDisruptionBudget{},
+			fmt.Errorf("both MaxUnavailable (%s) and MinAvailable (%s) are set, not creating PDB", pdbs.MaxUnavailable, pdbs.MinAvailable)
 	}
 
 	return &policyv1.PodDisruptionBudget{



Test patch:
diff --git a/internal/cmd/controller/agentmanagement/scheduling/scheduling_test.go b/internal/cmd/controller/agentmanagement/scheduling/scheduling_test.go
index c4607150..5fe1814f 100644
--- a/internal/cmd/controller/agentmanagement/scheduling/scheduling_test.go
+++ b/internal/cmd/controller/agentmanagement/scheduling/scheduling_test.go
@@ -50,6 +50,31 @@ func TestPodDisruptionBudget(t *testing.T) {
 			spec:    &fleet.PodDisruptionBudgetSpec{MinAvailable: "1", MaxUnavailable: "1"},
 			wantErr: true,
 		},
+		{
+			name:   "having both values, MinAvailable as zero, should disable the zero value",
+			spec:   &fleet.PodDisruptionBudgetSpec{MinAvailable: "0", MaxUnavailable: "1"},
+			wantMU: ptr(intstr.FromInt(1)),
+		},
+		{
+			name:   "having both values, MaxUnavailable as zero, should disable the zero value",
+			spec:   &fleet.PodDisruptionBudgetSpec{MinAvailable: "1", MaxUnavailable: "0"},
+			wantMA: ptr(intstr.FromInt(1)),
+		},
+		{
+			name:   "having a percent value while MinAvailable is zero should disable the zero value",
+			spec:   &fleet.PodDisruptionBudgetSpec{MinAvailable: "0", MaxUnavailable: "10%"},
+			wantMU: ptr(intstr.FromString("10%")),
+		},
+		{
+			name:   "having a percent value while MaxUnavailable is zero should disable the zero value",
+			spec:   &fleet.PodDisruptionBudgetSpec{MinAvailable: "10%", MaxUnavailable: "0"},
+			wantMA: ptr(intstr.FromString("10%")),
+		},
+		{
+			name:    "two non-zero values shouldn't work",
+			spec:    &fleet.PodDisruptionBudgetSpec{MinAvailable: "1", MaxUnavailable: "1"},
+			wantErr: true,
+		},
 	}
 
 	for _, test := range tests {