Skip to content

Commit 3b00d50

Browse files
authored
Mark agentSchedulingCustomization nullable in CRD (#4731)
* Mark agentSchedulingCustomization nullable in CRD * Add integration test for agentSchedulingCustomization nullability
1 parent 7afa158 commit 3b00d50

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

charts/fleet-crd/templates/crds.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6092,6 +6092,7 @@ spec:
60926092
type: object
60936093
type: object
60946094
agentSchedulingCustomization:
6095+
nullable: true
60956096
properties:
60966097
podDisruptionBudget:
60976098
properties:
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package cluster
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
7+
"github.com/rancher/fleet/integrationtests/utils"
8+
"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/apimachinery/pkg/types"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
)
17+
18+
// agentSchedulingCustomizationNullable retrieves the CRD from the API server
19+
// and returns whether agentSchedulingCustomization carries nullable: true in
20+
// the served OpenAPI schema. This is the exact property that clients such as
21+
// the Rancher UI read when deciding whether to display the field as commented
22+
// out (nullable / optional) or as an active object section.
23+
//
24+
// See https://github.com/rancher/rancher/issues/53781.
25+
func agentSchedulingCustomizationNullable() (bool, error) {
26+
// Build a dedicated client that has the CRD type in its scheme. The
27+
// shared k8sClient uses kubectl's default scheme which does not include
28+
// apiextensionsv1, so reading a CustomResourceDefinition through it
29+
// would fail with "no kind is registered".
30+
extScheme := runtime.NewScheme()
31+
if err := apiextensionsv1.AddToScheme(extScheme); err != nil {
32+
return false, err
33+
}
34+
crdClient, err := client.New(cfg, client.Options{Scheme: extScheme})
35+
if err != nil {
36+
return false, err
37+
}
38+
39+
crd := &apiextensionsv1.CustomResourceDefinition{}
40+
if err := crdClient.Get(ctx, types.NamespacedName{Name: "clusters.fleet.cattle.io"}, crd); err != nil {
41+
return false, err
42+
}
43+
for _, ver := range crd.Spec.Versions {
44+
if ver.Name != "v1alpha1" {
45+
continue
46+
}
47+
if ver.Schema == nil || ver.Schema.OpenAPIV3Schema == nil {
48+
return false, nil
49+
}
50+
specProps := ver.Schema.OpenAPIV3Schema.Properties["spec"]
51+
fieldSchema, ok := specProps.Properties["agentSchedulingCustomization"]
52+
if !ok {
53+
return false, nil
54+
}
55+
return fieldSchema.Nullable, nil
56+
}
57+
return false, nil
58+
}
59+
60+
// These tests verify the expected API behaviour of the agentSchedulingCustomization
61+
// field.
62+
//
63+
// The CRD schema for agentSchedulingCustomization was missing nullable: true.
64+
// The Rancher UI (and other OpenAPI-schema-aware clients) use that marker to
65+
// decide whether a field is truly optional:
66+
//
67+
// - nullable: true → field is omitted / shown commented-out when not set
68+
// - absent → field is treated as a required-to-have-value object
69+
// and shown uncommented, causing the UI to render and
70+
// serialize an empty agentSchedulingCustomization: {}
71+
// even when the user never configured it
72+
var _ = Describe("Cluster agentSchedulingCustomization nullability", func() {
73+
newNamespace := func() string {
74+
ns, err := utils.NewNamespaceName()
75+
Expect(err).ToNot(HaveOccurred())
76+
obj := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}
77+
Expect(k8sClient.Create(ctx, obj)).ToNot(HaveOccurred())
78+
DeferCleanup(func() { Expect(k8sClient.Delete(ctx, obj)).ToNot(HaveOccurred()) })
79+
return ns
80+
}
81+
82+
It("is marked nullable in the CRD OpenAPI schema served by the API server", func() {
83+
nullable, err := agentSchedulingCustomizationNullable()
84+
Expect(err).ToNot(HaveOccurred())
85+
Expect(nullable).To(BeTrue(),
86+
"agentSchedulingCustomization must have nullable: true in the CRD schema "+
87+
"so OpenAPI-aware clients (e.g. Rancher UI) treat it as optional and "+
88+
"do not render or persist an empty agentSchedulingCustomization: {} "+
89+
"when the user has not configured it")
90+
})
91+
92+
It("is nil when not set on a new cluster", func() {
93+
namespace = newNamespace()
94+
cluster := &v1alpha1.Cluster{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Name: "test-nullable-absent",
97+
Namespace: namespace,
98+
},
99+
}
100+
Expect(k8sClient.Create(ctx, cluster)).ToNot(HaveOccurred())
101+
DeferCleanup(func() {
102+
Expect(k8sClient.Delete(ctx, cluster)).ToNot(HaveOccurred())
103+
})
104+
105+
fetched := &v1alpha1.Cluster{}
106+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: cluster.Name, Namespace: namespace}, fetched)).ToNot(HaveOccurred())
107+
Expect(fetched.Spec.AgentSchedulingCustomization).To(BeNil(),
108+
"agentSchedulingCustomization should be absent, not serialised as an empty object")
109+
})
110+
111+
It("can be cleared to nil via JSON merge patch after being set", func() {
112+
// Simulate the state from the issue: agentSchedulingCustomization: {}
113+
// was written by a previous client; verify it can be removed via a null
114+
// merge patch, which is how kubectl and the Rancher UI clear fields.
115+
namespace = newNamespace()
116+
cluster := &v1alpha1.Cluster{
117+
ObjectMeta: metav1.ObjectMeta{
118+
Name: "test-nullable-clear",
119+
Namespace: namespace,
120+
},
121+
Spec: v1alpha1.ClusterSpec{
122+
AgentSchedulingCustomization: &v1alpha1.AgentSchedulingCustomization{
123+
PriorityClass: &v1alpha1.PriorityClassSpec{Value: 100},
124+
},
125+
},
126+
}
127+
Expect(k8sClient.Create(ctx, cluster)).ToNot(HaveOccurred())
128+
DeferCleanup(func() {
129+
Expect(k8sClient.Delete(ctx, cluster)).ToNot(HaveOccurred())
130+
})
131+
132+
fetched := &v1alpha1.Cluster{}
133+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: cluster.Name, Namespace: namespace}, fetched)).ToNot(HaveOccurred())
134+
Expect(fetched.Spec.AgentSchedulingCustomization).ToNot(BeNil())
135+
136+
nullPatch := []byte(`{"spec":{"agentSchedulingCustomization":null}}`)
137+
Expect(k8sClient.Patch(ctx, fetched, client.RawPatch(types.MergePatchType, nullPatch))).ToNot(HaveOccurred(),
138+
"clearing agentSchedulingCustomization via null merge patch must succeed (requires nullable: true in CRD)")
139+
140+
cleared := &v1alpha1.Cluster{}
141+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: cluster.Name, Namespace: namespace}, cleared)).ToNot(HaveOccurred())
142+
Expect(cleared.Spec.AgentSchedulingCustomization).To(BeNil(),
143+
"agentSchedulingCustomization should be nil after being cleared via null merge patch")
144+
})
145+
})

pkg/apis/fleet.cattle.io/v1alpha1/cluster_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ type ClusterSpec struct {
150150
// Allows for provisioning of network related bundles (CNI configuration).
151151
HostNetwork *bool `json:"hostNetwork,omitempty"`
152152

153+
// +nullable
153154
AgentSchedulingCustomization *AgentSchedulingCustomization `json:"agentSchedulingCustomization,omitempty"`
154155
}
155156

0 commit comments

Comments
 (0)