diff --git a/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go b/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go index c9f3f9ab6..732e0aced 100644 --- a/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go +++ b/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go @@ -46,6 +46,7 @@ type ClusterClaim struct { Status ClusterClaimStatus `json:"status,omitempty"` } +// +kubebuilder:validation:XValidation:rule="has(self.clusterSelectorTerms) == has(oldSelf.clusterSelectorTerms)",message="the clusterSelectorTerms field cannot be added or removed after creation" type ClusterClaimSpec struct { // The reference to the placement policy that adds the cluster claim. // diff --git a/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml b/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml index 2bd4a6ad8..bb1fc81e0 100644 --- a/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml +++ b/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml @@ -220,6 +220,10 @@ spec: required: - placementPolicyRef type: object + x-kubernetes-validations: + - message: the clusterSelectorTerms field cannot be added or removed after + creation + rule: has(self.clusterSelectorTerms) == has(oldSelf.clusterSelectorTerms) status: description: The observed status of the cluster claim. properties: diff --git a/test/apis/placement/v1beta1/clusterclaim_validation_integration_test.go b/test/apis/placement/v1beta1/clusterclaim_validation_integration_test.go new file mode 100644 index 000000000..448198c67 --- /dev/null +++ b/test/apis/placement/v1beta1/clusterclaim_validation_integration_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2026 The KubeFleet Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + placementv1alpha1 "github.com/kubefleet-dev/kubefleet/apis/kubefleet.dev/placement/v1alpha1" +) + +var _ = Describe("Test ClusterClaim API validation", func() { + It("should deny unsetting clusterSelectorTerms", func() { + clusterClaim := &placementv1alpha1.ClusterClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-claim-selector-terms-immutability", + }, + Spec: placementv1alpha1.ClusterClaimSpec{ + PlacementPolicyRef: &placementv1alpha1.ObjectReference{ + Name: "test-placement-policy", + APIVersion: placementv1alpha1.GroupVersion.Version, + Kind: "PlacementPolicy", + }, + ClusterSelectorTerms: []placementv1alpha1.ClusterLabelAndPropertySelectorTerm{ + { + MatchLabels: map[string]string{"region": "west"}, + }, + }, + }, + } + Expect(hubClient.Create(ctx, clusterClaim)).Should(Succeed()) + DeferCleanup(func() { + Expect(client.IgnoreNotFound(hubClient.Delete(ctx, clusterClaim))).Should(Succeed()) + }) + + clusterClaim.Spec.ClusterSelectorTerms = nil + Expect(hubClient.Update(ctx, clusterClaim)).Should(MatchError(ContainSubstring("the clusterSelectorTerms field cannot be added or removed after creation"))) + }) +}) diff --git a/test/apis/placement/v1beta1/suite_test.go b/test/apis/placement/v1beta1/suite_test.go index 58ff76bfa..5ea5cf06d 100644 --- a/test/apis/placement/v1beta1/suite_test.go +++ b/test/apis/placement/v1beta1/suite_test.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + placementv1alpha1 "github.com/kubefleet-dev/kubefleet/apis/kubefleet.dev/placement/v1alpha1" placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" ) @@ -71,6 +72,7 @@ var _ = BeforeSuite(func() { Expect(hubCfg).NotTo(BeNil()) Expect(placementv1beta1.AddToScheme(scheme.Scheme)).Should(Succeed()) + Expect(placementv1alpha1.AddToScheme(scheme.Scheme)).Should(Succeed()) klog.InitFlags(flag.CommandLine) flag.Parse()