Skip to content

Commit 2fd4460

Browse files
feat: add CEL validation to make placement type immutable for RP (#315)
1 parent f136911 commit 2fd4460

File tree

4 files changed

+64
-6
lines changed

4 files changed

+64
-6
lines changed

apis/placement/v1beta1/clusterresourceplacement_types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ type ClusterResourcePlacement struct {
113113

114114
// The desired state of ClusterResourcePlacement.
115115
// +kubebuilder:validation:Required
116-
// +kubebuilder:validation:XValidation:rule="!((has(oldSelf.policy) && !has(self.policy)) || (has(oldSelf.policy) && has(self.policy) && has(self.policy.placementType) && has(oldSelf.policy.placementType) && self.policy.placementType != oldSelf.policy.placementType))",message="placement type is immutable"
116+
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.policy) && !has(self.policy))",message="policy cannot be removed once set"
117117
// +kubebuilder:validation:XValidation:rule="!(self.statusReportingScope == 'NamespaceAccessible' && size(self.resourceSelectors.filter(x, x.kind == 'Namespace')) != 1)",message="when statusReportingScope is NamespaceAccessible, exactly one resourceSelector with kind 'Namespace' is required"
118118
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.statusReportingScope) || self.statusReportingScope == oldSelf.statusReportingScope",message="statusReportingScope is immutable"
119119
Spec PlacementSpec `json:"spec"`
@@ -135,6 +135,7 @@ type PlacementSpec struct {
135135
// Policy defines how to select member clusters to place the selected resources.
136136
// If unspecified, all the joined member clusters are selected.
137137
// +kubebuilder:validation:Optional
138+
// +kubebuilder:validation:XValidation:rule="!(self.placementType != oldSelf.placementType)",message="placement type is immutable"
138139
Policy *PlacementPolicy `json:"policy,omitempty"`
139140

140141
// The rollout strategy to use to replace existing placement with new ones.
@@ -1628,6 +1629,7 @@ type ResourcePlacement struct {
16281629

16291630
// The desired state of ResourcePlacement.
16301631
// +kubebuilder:validation:Required
1632+
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.policy) && !has(self.policy))",message="policy cannot be removed once set"
16311633
Spec PlacementSpec `json:"spec"`
16321634

16331635
// The observed status of ResourcePlacement.

config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,9 @@ spec:
15831583
type: object
15841584
type: array
15851585
type: object
1586+
x-kubernetes-validations:
1587+
- message: placement type is immutable
1588+
rule: '!(self.placementType != oldSelf.placementType)'
15861589
resourceSelectors:
15871590
description: |-
15881591
ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`.
@@ -2059,10 +2062,8 @@ spec:
20592062
- resourceSelectors
20602063
type: object
20612064
x-kubernetes-validations:
2062-
- message: placement type is immutable
2063-
rule: '!((has(oldSelf.policy) && !has(self.policy)) || (has(oldSelf.policy)
2064-
&& has(self.policy) && has(self.policy.placementType) && has(oldSelf.policy.placementType)
2065-
&& self.policy.placementType != oldSelf.policy.placementType))'
2065+
- message: policy cannot be removed once set
2066+
rule: '!(has(oldSelf.policy) && !has(self.policy))'
20662067
- message: when statusReportingScope is NamespaceAccessible, exactly one
20672068
resourceSelector with kind 'Namespace' is required
20682069
rule: '!(self.statusReportingScope == ''NamespaceAccessible'' && size(self.resourceSelectors.filter(x,

config/crd/bases/placement.kubernetes-fleet.io_resourceplacements.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,9 @@ spec:
518518
type: object
519519
type: array
520520
type: object
521+
x-kubernetes-validations:
522+
- message: placement type is immutable
523+
rule: '!(self.placementType != oldSelf.placementType)'
521524
resourceSelectors:
522525
description: |-
523526
ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`.
@@ -993,6 +996,9 @@ spec:
993996
required:
994997
- resourceSelectors
995998
type: object
999+
x-kubernetes-validations:
1000+
- message: policy cannot be removed once set
1001+
rule: '!(has(oldSelf.policy) && !has(self.policy))'
9961002
status:
9971003
description: The observed status of ResourcePlacement.
9981004
properties:

test/apis/placement/v1beta1/api_validation_integration_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() {
156156
err := hubClient.Update(ctx, &crp)
157157
var statusErr *k8sErrors.StatusError
158158
Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update CRP call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{})))
159-
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("placement type is immutable"))
159+
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("policy cannot be removed once set"))
160160
})
161161

162162
It("should deny update of ClusterResourcePlacement with different placement type", func() {
@@ -613,6 +613,55 @@ var _ = Describe("Test placement v1beta1 API validation", func() {
613613
})
614614
})
615615

616+
Context("Test ResourcePlacement API validation - invalid cases", func() {
617+
var rp placementv1beta1.ResourcePlacement
618+
rpName := fmt.Sprintf(rpNameTemplate, GinkgoParallelProcess())
619+
620+
BeforeEach(func() {
621+
rp = placementv1beta1.ResourcePlacement{
622+
ObjectMeta: metav1.ObjectMeta{
623+
Name: rpName,
624+
Namespace: testNamespace,
625+
},
626+
Spec: placementv1beta1.PlacementSpec{
627+
ResourceSelectors: []placementv1beta1.ResourceSelectorTerm{
628+
{
629+
Group: "",
630+
Version: "v1",
631+
Kind: "ConfigMap",
632+
Name: "test-cm",
633+
},
634+
},
635+
Policy: &placementv1beta1.PlacementPolicy{
636+
PlacementType: placementv1beta1.PickFixedPlacementType,
637+
ClusterNames: []string{"cluster1", "cluster2"},
638+
},
639+
},
640+
}
641+
Expect(hubClient.Create(ctx, &rp)).Should(Succeed())
642+
})
643+
644+
AfterEach(func() {
645+
Expect(hubClient.Delete(ctx, &rp)).Should(Succeed())
646+
})
647+
648+
It("should deny update of ResourcePlacement with nil policy", func() {
649+
rp.Spec.Policy = nil
650+
err := hubClient.Update(ctx, &rp)
651+
var statusErr *k8sErrors.StatusError
652+
Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update RP call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{})))
653+
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("policy cannot be removed once set"))
654+
})
655+
656+
It("should deny update of ResourcePlacement with different placement type", func() {
657+
rp.Spec.Policy.PlacementType = placementv1beta1.PickAllPlacementType
658+
err := hubClient.Update(ctx, &rp)
659+
var statusErr *k8sErrors.StatusError
660+
Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update RP call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{})))
661+
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("placement type is immutable"))
662+
})
663+
})
664+
616665
Context("Test ResourcePlacement StatusReportingScope validation, allow cases", func() {
617666
var rp placementv1beta1.ResourcePlacement
618667
rpName := fmt.Sprintf(rpNameTemplate, GinkgoParallelProcess())

0 commit comments

Comments
 (0)