Skip to content
Open
3 changes: 3 additions & 0 deletions api/shared/nodenetworkconfigurationpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ type NodeNetworkConfigurationPolicySpec struct {

// MaxUnavailable specifies percentage or number
// of machines that can be updating at a time. Default is "50%".
// Must be greater than 0. When set as a percentage, it must be a positive percentage.
// +optional
//nolint:lll
// +kubebuilder:validation:XValidation:rule="!has(self) || (self.type == 0 && self.intVal > 0) || (self.type == 1 && int(self.strVal.replace('%', '')) > 0)",message="maxUnavailable must be greater than 0"
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"`
Comment on lines +49 to 53
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ spec:
description: |-
MaxUnavailable specifies percentage or number
of machines that can be updating at a time. Default is "50%".
The computed value must result in at least 1 node being allowed to undergo updates at a time.
If set to 0 or a percentage that rounds to 0, the policy will fail with FailedToConfigure.
x-kubernetes-int-or-string: true
nodeSelector:
additionalProperties:
Expand Down Expand Up @@ -190,6 +192,8 @@ spec:
description: |-
MaxUnavailable specifies percentage or number
of machines that can be updating at a time. Default is "50%".
The computed value must result in at least 1 node being allowed to undergo updates at a time.
If set to 0 or a percentage that rounds to 0, the policy will fail with FailedToConfigure.
x-kubernetes-int-or-string: true
nodeSelector:
additionalProperties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ func (r *NodeNetworkConfigurationPolicyReconciler) shouldAbortReconcile(
logger.Info("Error getting max unavailable count")
return false, err
}

filter := enactmentconditions.LogicalConditionCountFilter{
nmstateapi.NodeNetworkConfigurationEnactmentConditionFailing: corev1.ConditionTrue,
nmstateapi.NodeNetworkConfigurationEnactmentConditionProgressing: corev1.ConditionFalse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ var _ = Describe("NodeNetworkConfigurationPolicy controller predicates", func()
nnce := nmstatev1beta1.NodeNetworkConfigurationEnactment{
ObjectMeta: metav1.ObjectMeta{
Name: shared.EnactmentKey(nodeName, nncp.Name).Name,
Labels: map[string]string{
shared.EnactmentPolicyLabel: nncp.Name,
},
},
Status: shared.NodeNetworkConfigurationEnactmentStatus{},
}
Expand Down
10 changes: 10 additions & 0 deletions deploy/crds/nmstate.io_nodenetworkconfigurationpolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ spec:
description: |-
MaxUnavailable specifies percentage or number
of machines that can be updating at a time. Default is "50%".
Must be greater than 0. When set as a percentage, it must be a positive percentage.
x-kubernetes-int-or-string: true
x-kubernetes-validations:
- message: maxUnavailable must be greater than 0
rule: '!has(self) || (self.type == 0 && self.intVal > 0) || (self.type
== 1 && int(self.strVal.replace(''%'', '''')) > 0)'
nodeSelector:
additionalProperties:
type: string
Expand Down Expand Up @@ -190,7 +195,12 @@ spec:
description: |-
MaxUnavailable specifies percentage or number
of machines that can be updating at a time. Default is "50%".
Must be greater than 0. When set as a percentage, it must be a positive percentage.
x-kubernetes-int-or-string: true
x-kubernetes-validations:
- message: maxUnavailable must be greater than 0
rule: '!has(self) || (self.type == 0 && self.intVal > 0) || (self.type
== 1 && int(self.strVal.replace(''%'', '''')) > 0)'
nodeSelector:
additionalProperties:
type: string
Expand Down
10 changes: 2 additions & 8 deletions pkg/node/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,6 @@ func MaxUnavailableNodeCount(ctx context.Context, cli client.Reader, policy *nms
}

func ScaledMaxUnavailableNodeCount(matchingNodes int, intOrPercent intstr.IntOrString) (int, error) {
correctMaxUnavailable := func(maxUnavailable int) int {
if maxUnavailable < 1 {
return MinMaxunavailable
}
return maxUnavailable
}
maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(&intOrPercent, matchingNodes, true)
if err != nil {
defaultMaxUnavailable := intstr.FromString(DefaultMaxunavailable)
Expand All @@ -122,9 +116,9 @@ func ScaledMaxUnavailableNodeCount(matchingNodes int, intOrPercent intstr.IntOrS
matchingNodes,
true,
)
return correctMaxUnavailable(maxUnavailable), err
return maxUnavailable, err
}
return correctMaxUnavailable(maxUnavailable), nil
return maxUnavailable, nil
}

// Return true if the event name is the name of
Expand Down
6 changes: 4 additions & 2 deletions pkg/node/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var _ = Describe("MaxUnavailable nodes", func() {
expectedScaledMaxUnavailable int
expectedError types.GomegaMatcher
}
// Note: ValidateMaxUnavailable is tested at the controller integration test level
// since it requires a full client context with policies and enactments
DescribeTable("testing ScaledMaxUnavailableNodeCount",
func(c maxUnavailableCase) {
maxUnavailable, err := ScaledMaxUnavailableNodeCount(c.nmstateEnabledNodes, c.maxUnavailable)
Expand Down Expand Up @@ -84,14 +86,14 @@ var _ = Describe("MaxUnavailable nodes", func() {
maxUnavailableCase{
nmstateEnabledNodes: 5,
maxUnavailable: intstr.FromString("0%"),
expectedScaledMaxUnavailable: 1,
expectedScaledMaxUnavailable: 0,
expectedError: Not(HaveOccurred()),
}),
Entry("Zero value",
maxUnavailableCase{
nmstateEnabledNodes: 5,
maxUnavailable: intstr.FromInt(0),
expectedScaledMaxUnavailable: 1,
expectedScaledMaxUnavailable: 0,
expectedError: Not(HaveOccurred()),
}))
})
27 changes: 27 additions & 0 deletions test/e2e/handler/nnce_conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/util/intstr"

nmstate "github.com/nmstate/kubernetes-nmstate/api/shared"
enactmentconditions "github.com/nmstate/kubernetes-nmstate/pkg/enactmentstatus/conditions"
Expand Down Expand Up @@ -208,4 +209,30 @@ var _ = Describe("EnactmentCondition", func() {
}, 2*time.Second, 100*time.Millisecond).Should(policyconditions.ContainPolicyDegraded(), "policy should be marked as Degraded")
})
})

Context("when maxUnavailable configuration is invalid", func() {
It("should be rejected by API validation when maxUnavailable is 0", func() {
By("Attempt to apply policy with maxUnavailable: 0")
err := setDesiredStateWithPolicyMaxUnavailableAndNodeSelector(
TestPolicy,
linuxBrUp(bridge1),
intstr.FromInt(0),
map[string]string{"node-role.kubernetes.io/worker": ""},
)
Expect(err).To(HaveOccurred(), "API should reject policy with maxUnavailable: 0")
Expect(err.Error()).To(ContainSubstring("maxUnavailable must be greater than 0"))
})

It("should be rejected by API validation when maxUnavailable is 0%", func() {
By("Attempt to apply policy with maxUnavailable: 0%")
err := setDesiredStateWithPolicyMaxUnavailableAndNodeSelector(
TestPolicy,
linuxBrUp(bridge1),
intstr.FromString("0%"),
map[string]string{"node-role.kubernetes.io/worker": ""},
)
Expect(err).To(HaveOccurred(), "API should reject policy with maxUnavailable: 0%")
Expect(err.Error()).To(ContainSubstring("maxUnavailable must be greater than 0"))
})
Comment on lines +219 to +241
})
Comment on lines +213 to +242
})
2 changes: 1 addition & 1 deletion test/e2e/handler/nodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var _ = Describe("Nodes", func() {
})
Context("and node network state is deleted", func() {
BeforeEach(func() {
deleteNodeNeworkStates()
deleteNodeNetworkStates()
})
It("should recreate it with currentState", func() {
for _, node := range nodes {
Expand Down
24 changes: 23 additions & 1 deletion test/e2e/handler/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,28 @@ func setDesiredStateWithPolicyAndCapture(name string, desiredState nmstate.State
setDesiredStateWithPolicyAndCaptureAndNodeSelectorEventually(name, desiredState, capture, runAtWorkers)
}

func setDesiredStateWithPolicyMaxUnavailableAndNodeSelector(
name string,
desiredState nmstate.State,
maxUnavailableValue intstr.IntOrString,
nodeSelector map[string]string,
) error {
policy := nmstatev1.NodeNetworkConfigurationPolicy{}
policy.Name = name
key := types.NamespacedName{Name: name}
err := testenv.Client.Get(context.TODO(), key, &policy)
policy.Spec.DesiredState = desiredState
policy.Spec.NodeSelector = nodeSelector
policy.Spec.MaxUnavailable = &maxUnavailableValue
if err != nil {
if apierrors.IsNotFound(err) {
return testenv.Client.Create(context.TODO(), &policy)
}
return err
}
return testenv.Client.Update(context.TODO(), &policy)
}

// nodeMatchesSelector checks if a node's labels match the given selector.
// An empty selector matches all nodes.
func nodeMatchesSelector(nodeName string, nodeSelector map[string]string) bool {
Expand Down Expand Up @@ -279,7 +301,7 @@ func nodeNetworkConfigurationPolicy(policyName string) nmstatev1.NodeNetworkConf
return policy
}

func deleteNodeNeworkStates() {
func deleteNodeNetworkStates() {
nodeNetworkStateList := &nmstatev1beta1.NodeNetworkStateList{}
err := testenv.Client.List(context.TODO(), nodeNetworkStateList, &dynclient.ListOptions{})
Expect(err).ToNot(HaveOccurred())
Expand Down