Skip to content

Replace maxUnavailable auto-correction with validation that fails NNCP when value is 0 - #1550

Open
emy wants to merge 9 commits into
nmstate:mainfrom
emy:maxunavailable-zero
Open

Replace maxUnavailable auto-correction with validation that fails NNCP when value is 0#1550
emy wants to merge 9 commits into
nmstate:mainfrom
emy:maxunavailable-zero

Conversation

@emy

@emy emy commented Jul 9, 2026

Copy link
Copy Markdown
Member

Is this a BUG FIX or a FEATURE?:

Uncomment only one, leave it on its own line:

/kind bug

/kind enhancement

What this PR does / why we need it:
Replace maxUnavailable auto-correction with validation that fails NNCP when value is 0

Special notes for your reviewer:

Release note:

Replace maxUnavailable auto-correction with validation that fails NNCP when value is 0

@kubevirt-prow kubevirt-prow Bot added kind/bug release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: no Indicates the PR's author has not DCO signed all their commits. labels Jul 9, 2026
@kubevirt-prow
kubevirt-prow Bot requested review from mkowalski and phoracek July 9, 2026 14:22
@kubevirt-prow

kubevirt-prow Bot commented Jul 9, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mkowalski for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubevirt-prow kubevirt-prow Bot added the size/M label Jul 9, 2026
…P when value is 0

Signed-off-by: Emilia Desch <emy@redhat.com>
@emy
emy force-pushed the maxunavailable-zero branch from d5d968c to 9257efd Compare July 9, 2026 14:23
@kubevirt-prow kubevirt-prow Bot added dco-signoff: yes Indicates the PR's author has DCO signed all their commits. and removed dco-signoff: no Indicates the PR's author has not DCO signed all their commits. labels Jul 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the NodeNetworkConfigurationPolicy controller to enforce that the computed maxUnavailable value must result in at least 1 node. If maxUnavailable is less than 1, the policy fails with FailedToConfigure. The feedback highlights a critical issue where returning a standard error on validation failure in both incrementUnavailableNodeCount and shouldAbortReconcile causes an infinite requeue loop and redundant retries. It is recommended to use a custom non-retryable error type to prevent this. Additionally, clarifying the API documentation for MaxUnavailable is suggested to avoid confusion regarding node availability during updates.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +594 to +603
if maxUnavailable < 1 {
errMsg := fmt.Sprintf("invalid maxUnavailable configuration: computed value is %d. The maxUnavailable field must result in at least 1 node. Either increase the value (e.g., set to 1 or higher) or use a higher percentage (e.g., \"1%%\")", maxUnavailable)
r.Log.Error(errors.New(errMsg), "Invalid maxUnavailable configuration")
policyconditions.SetPolicyFailedToConfigure(&policy.Status.Conditions, errMsg)
updateErr := r.Client.Status().Update(ctx, policy)
if updateErr != nil {
return updateErr
}
return errors.New(errMsg)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Issue: Infinite Requeue Loop and Redundant Retries on Validation Failure

Returning a standard error from incrementUnavailableNodeCount when maxUnavailable < 1 causes two major issues:

  1. Infinite Requeue Loop: The error propagates back to Reconcile, which returns it directly to the controller-runtime manager. This causes the controller to requeue the request indefinitely with exponential backoff, leading to high CPU usage, API server thrashing, and log spam. Since a validation error is terminal for the current policy generation, the reconciliation should stop without requeuing (i.e., return ctrl.Result{}, nil).
  2. Redundant Retries inside retry.OnError: The validation check is executed inside a retry.OnError block with a classifier that retries on all errors (func(error) bool { return true }). Consequently, when maxUnavailable < 1 is detected, the controller will log the error, update the policy status, and retry 5 times (the default retry limit) for a single reconcile attempt, resulting in redundant API calls and log spam.

Recommendation

To resolve this, you should:

  1. Define a custom non-retryable error type (e.g., InvalidMaxUnavailableError).
  2. Update the retry.OnError classifier to not retry when the error is of this type.
  3. Modify Reconcile to catch this error and return ctrl.Result{}, nil to prevent requeuing.

Comment on lines +697 to +706
if maxUnavailable < 1 {
errMsg := fmt.Sprintf("invalid maxUnavailable configuration: computed value is %d. The maxUnavailable field must result in at least 1 node. Either increase the value (e.g., set to 1 or higher) or use a higher percentage (e.g., \"1%%\")", maxUnavailable)
logger.Error(errors.New(errMsg), "Invalid maxUnavailable configuration")
policyconditions.SetPolicyFailedToConfigure(&instance.Status.Conditions, errMsg)
updateErr := r.Client.Status().Update(ctx, instance)
if updateErr != nil {
return false, updateErr
}
return false, errors.New(errMsg)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Issue: Infinite Requeue Loop on Validation Failure

Similar to the check in incrementUnavailableNodeCount, returning a standard error here when maxUnavailable < 1 propagates to Reconcile, which returns it directly to the controller-runtime manager. This causes the controller to requeue the request indefinitely.

Since this is a terminal validation error, it should be returned as a non-retryable error type, and Reconcile should handle it by returning ctrl.Result{}, nil to stop reconciliation without requeuing.

Comment on lines +49 to +50
// The computed value must result in at least 1 node being available for updates.
// If set to 0 or a percentage that rounds to 0, the policy will fail with FailedToConfigure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Suggestion: Clarify API Documentation Comment

The phrase "at least 1 node being available for updates" is confusing because in Kubernetes parlance, "available" typically refers to nodes that are ready and serving, whereas maxUnavailable specifies the number of nodes that can be unavailable (i.e., undergoing updates) at a time.

To prevent confusion, consider rephrasing this to clarify that the computed value must allow at least 1 node to be updated (or be unavailable) at a time.

Example:

// The computed value must result in at least 1 node being allowed to undergo updates at a time.

emy added 2 commits July 9, 2026 16:57
…ailableError type

Signed-off-by: Emilia Desch <emy@redhat.com>
Signed-off-by: Emilia Desch <emy@redhat.com>
@qinqon

qinqon commented Jul 14, 2026

Copy link
Copy Markdown
Member

@emy why we don't implement this as a CEL validation of > 1 for MaxUnavailable ?

@mkowalski

Copy link
Copy Markdown
Member

@emy why we don't implement this as a CEL validation of > 1 for MaxUnavailable ?

You cannot make CEL validation stricter once you introduced API because your already existing objects won't be forward-compatible. Correct me where I am wrong

@qinqon

qinqon commented Jul 15, 2026

Copy link
Copy Markdown
Member

@emy why we don't implement this as a CEL validation of > 1 for MaxUnavailable ?

You cannot make CEL validation stricter once you introduced API because your already existing objects won't be forward-compatible. Correct me where I am wrong

well same thing will happend if we do it at the controller, the old NNCP will have NNCP status with failed to configure, doing it at CEL level is more aparent (maybe customer's script are not checking the status so they have a fail possitive).

@mkowalski

Copy link
Copy Markdown
Member

Yeah actually over all those years I never saw anyone using maxUnavailable: 0. Whatever you wish, CEL is fine. If there is really someone with value 0 we can handle him like snowflake.

@qinqon

qinqon commented Jul 16, 2026

Copy link
Copy Markdown
Member

Yeah actually over all those years I never saw anyone using maxUnavailable: 0. Whatever you wish, CEL is fine. If there is really someone with value 0 we can handle him like snowflake.

Let's go with CEL it will make issue more aparent and there wil not be false possitive of users not checking status but finding that networking is not "networketing".

Copilot AI review requested due to automatic review settings July 28, 2026 12:55
Signed-off-by: Emilia Desch <emy@redhat.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR changes maxUnavailable behavior by removing the previous “auto-correct to minimum” logic and instead enforcing an API-level validation that rejects maxUnavailable: 0 (and 0%), with accompanying test updates.

Changes:

  • Add CRD/API validation to reject spec.maxUnavailable when set to zero (int or percent).
  • Remove maxUnavailable auto-correction behavior in scaling logic so zero stays zero.
  • Add/adjust E2E and unit tests to reflect the new validation and scaling behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/e2e/handler/utils.go Adds a helper to create/update NNCP with explicit maxUnavailable for validation tests.
test/e2e/handler/nodes_test.go Updates call site for renamed helper (deleteNodeNetworkStates).
test/e2e/handler/nnce_conditions_test.go Adds E2E coverage ensuring API rejects maxUnavailable values of 0 and 0%.
pkg/node/nodes_test.go Updates unit test expectations for zero maxUnavailable scaling results.
pkg/node/nodes.go Removes auto-correction/clamping of scaled maxUnavailable values.
api/shared/nodenetworkconfigurationpolicy_types.go Adds kubebuilder XValidation rule/message for maxUnavailable > 0.
deploy/crds/nmstate.io_nodenetworkconfigurationpolicies.yaml Propagates the new CRD validation rule/message into deploy CRDs.
bundle/manifests/nmstate.io_nodenetworkconfigurationpolicies.yaml Propagates the new CRD validation rule/message into bundle manifests.
controllers/handler/nodenetworkconfigurationpolicy_controller_test.go Adds enactment label used by controller logic/predicates in tests.
controllers/handler/nodenetworkconfigurationpolicy_controller.go Minor formatting-only change (blank line).
Comments suppressed due to low confidence (1)

pkg/node/nodes.go:1

  • With the clamping/auto-correction removed, this function now can return 0 for inputs like 0 or 0%. Even if new NNCPs are blocked by CRD validation going forward, clusters upgrading from older versions may still have existing persisted NNCPs with maxUnavailable=0; returning 0 can cause the controller’s “max unavailable” gating to behave pathologically (e.g., permanently block progress if the logic requires allowing at least one node). Consider adding a backward-compatibility safeguard at reconciliation time for existing objects (e.g., treat 0 as invalid and mark policy degraded with a clear reason, or clamp to 1 with a warning), so upgrades don’t create stuck policies.
/*

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to 53
// 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 +214 to +236
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 +213 to +237
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"))
})
})
Signed-off-by: Emilia Desch <emy@redhat.com>
@kubevirt-prow kubevirt-prow Bot added size/L and removed size/M labels Jul 29, 2026
emy added 3 commits July 29, 2026 13:31
… value

Signed-off-by: Emilia Desch <emy@redhat.com>
… value

Signed-off-by: Emilia Desch <emy@redhat.com>
… value

Signed-off-by: Emilia Desch <emy@redhat.com>
@kubevirt-prow

kubevirt-prow Bot commented Jul 29, 2026

Copy link
Copy Markdown

@emy: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-kubernetes-nmstate-e2e-handler-k8s f543260 link true /test pull-kubernetes-nmstate-e2e-handler-k8s
pull-kubernetes-nmstate-e2e-upgrade-k8s f543260 link false /test pull-kubernetes-nmstate-e2e-upgrade-k8s
pull-kubernetes-nmstate-e2e-operator-k8s f543260 link true /test pull-kubernetes-nmstate-e2e-operator-k8s
Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has DCO signed all their commits. kind/bug release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants