Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apis/placement/v1beta1/clusterresourceplacement_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ type ClusterResourcePlacement struct {

// The desired state of ClusterResourcePlacement.
// +kubebuilder:validation:Required
// +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"
// +kubebuilder:validation:XValidation:rule="!(has(oldSelf.policy) && !has(self.policy))",message="policy cannot be removed once set"
// +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"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.statusReportingScope) || self.statusReportingScope == oldSelf.statusReportingScope",message="statusReportingScope is immutable"
Spec PlacementSpec `json:"spec"`
Expand All @@ -135,6 +135,7 @@ type PlacementSpec struct {
// Policy defines how to select member clusters to place the selected resources.
// If unspecified, all the joined member clusters are selected.
// +kubebuilder:validation:Optional
// +kubebuilder:validation:XValidation:rule="!(self.placementType != oldSelf.placementType)",message="placement type is immutable"
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation rule will fail when Policy is initially set from nil to a non-nil value because oldSelf will be nil and accessing oldSelf.placementType will cause an error. The rule should check if oldSelf exists first: rule=\"!has(oldSelf) || !(self.placementType != oldSelf.placementType)\"

Suggested change
// +kubebuilder:validation:XValidation:rule="!(self.placementType != oldSelf.placementType)",message="placement type is immutable"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf) || !(self.placementType != oldSelf.placementType)",message="placement type is immutable"

Copilot uses AI. Check for mistakes.
Policy *PlacementPolicy `json:"policy,omitempty"`

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

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

// The observed status of ResourcePlacement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,9 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: placement type is immutable
rule: '!(self.placementType != oldSelf.placementType)'
resourceSelectors:
description: |-
ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`.
Expand Down Expand Up @@ -2059,10 +2062,8 @@ spec:
- resourceSelectors
type: object
x-kubernetes-validations:
- message: placement type is immutable
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: policy cannot be removed once set
rule: '!(has(oldSelf.policy) && !has(self.policy))'
- message: when statusReportingScope is NamespaceAccessible, exactly one
resourceSelector with kind 'Namespace' is required
rule: '!(self.statusReportingScope == ''NamespaceAccessible'' && size(self.resourceSelectors.filter(x,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: placement type is immutable
rule: '!(self.placementType != oldSelf.placementType)'
resourceSelectors:
description: |-
ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`.
Expand Down Expand Up @@ -993,6 +996,9 @@ spec:
required:
- resourceSelectors
type: object
x-kubernetes-validations:
- message: policy cannot be removed once set
rule: '!(has(oldSelf.policy) && !has(self.policy))'
status:
description: The observed status of ResourcePlacement.
properties:
Expand Down
51 changes: 50 additions & 1 deletion test/apis/placement/v1beta1/api_validation_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ var _ = Describe("Test placement v1beta1 API validation", func() {
err := hubClient.Update(ctx, &crp)
var statusErr *k8sErrors.StatusError
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{})))
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("placement type is immutable"))
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("policy cannot be removed once set"))
})

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

Context("Test ResourcePlacement API validation - invalid cases", func() {
var rp placementv1beta1.ResourcePlacement
rpName := fmt.Sprintf(rpNameTemplate, GinkgoParallelProcess())

BeforeEach(func() {
rp = placementv1beta1.ResourcePlacement{
ObjectMeta: metav1.ObjectMeta{
Name: rpName,
Namespace: testNamespace,
},
Spec: placementv1beta1.PlacementSpec{
ResourceSelectors: []placementv1beta1.ResourceSelectorTerm{
{
Group: "",
Version: "v1",
Kind: "ConfigMap",
Name: "test-cm",
},
},
Policy: &placementv1beta1.PlacementPolicy{
PlacementType: placementv1beta1.PickFixedPlacementType,
ClusterNames: []string{"cluster1", "cluster2"},
},
},
}
Expect(hubClient.Create(ctx, &rp)).Should(Succeed())
})

AfterEach(func() {
Expect(hubClient.Delete(ctx, &rp)).Should(Succeed())
})

It("should deny update of ResourcePlacement with nil policy", func() {
rp.Spec.Policy = nil
err := hubClient.Update(ctx, &rp)
var statusErr *k8sErrors.StatusError
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{})))
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("policy cannot be removed once set"))
})

It("should deny update of ResourcePlacement with different placement type", func() {
rp.Spec.Policy.PlacementType = placementv1beta1.PickAllPlacementType
err := hubClient.Update(ctx, &rp)
var statusErr *k8sErrors.StatusError
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{})))
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp("placement type is immutable"))
})
})

Context("Test ResourcePlacement StatusReportingScope validation, allow cases", func() {
var rp placementv1beta1.ResourcePlacement
rpName := fmt.Sprintf(rpNameTemplate, GinkgoParallelProcess())
Expand Down
Loading