Skip to content

OPNET-783: statusmanager: a DaemonSet matching zero nodes is not progressing#3046

Open
mkowalski wants to merge 1 commit into
openshift:masterfrom
mkowalski:statusmanager-empty-daemonset
Open

OPNET-783: statusmanager: a DaemonSet matching zero nodes is not progressing#3046
mkowalski wants to merge 1 commit into
openshift:masterfrom
mkowalski:statusmanager-empty-daemonset

Conversation

@mkowalski

Copy link
Copy Markdown
Contributor

What

A DaemonSet whose node selector or affinity currently matches no nodes (status.desiredNumberScheduled == 0 with the generation observed) is fully rolled out — but the status manager reported it as DaemonSet ... is not yet scheduled on any nodes (Progressing) and, after the timeout, as a hung rollout (Degraded, rollout is not making progress), blocking co/network.

The guard keeps an unreconciled DaemonSet (observedGeneration < generation, i.e. all-zero status right after creation) counted as progressing, so freshly created DaemonSets do not flap Available during install.

Why now

Hit by BGP-based VIP management (openshift/enhancements#1982): on compact clusters the frr-k8s DaemonSet legitimately schedules zero pods (control plane nodes run the frr-k8s static pod instead and are excluded by node affinity), which degraded co/network and stalled installation. The bug is feature-independent though — any optional DaemonSet whose affinity matches no nodes triggers it.

Testing

New TestStatusManagerDaemonSetMatchingZeroNodes (TDD — reproduces the exact live symptom before the fix): desired=0/observed-current ⇒ Available, not Progressing, not Degraded, no rollout state tracked; includes a stale-rollout-state + clock-step case proving no hung-rollout Degraded fires and state is pruned. Validated live in the enhancement-1982 reference implementation (https://github.com/mkowalski/bgp-vip-demo).

Part of OPNET-595; CNO subtask: OPNET-783.

A DaemonSet whose node selector or affinity currently matches no nodes
(desiredNumberScheduled == 0, observed generation current) is fully
rolled out. The status manager treated it as "not yet scheduled on any
nodes" and eventually as a hung rollout, degrading the operator. Guard
the empty case while still treating an unreconciled DaemonSet
(observedGeneration < generation) as progressing so freshly created
DaemonSets do not flap Available.
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jul 10, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@mkowalski: This pull request references OPNET-783 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

What

A DaemonSet whose node selector or affinity currently matches no nodes (status.desiredNumberScheduled == 0 with the generation observed) is fully rolled out — but the status manager reported it as DaemonSet ... is not yet scheduled on any nodes (Progressing) and, after the timeout, as a hung rollout (Degraded, rollout is not making progress), blocking co/network.

The guard keeps an unreconciled DaemonSet (observedGeneration < generation, i.e. all-zero status right after creation) counted as progressing, so freshly created DaemonSets do not flap Available during install.

Why now

Hit by BGP-based VIP management (openshift/enhancements#1982): on compact clusters the frr-k8s DaemonSet legitimately schedules zero pods (control plane nodes run the frr-k8s static pod instead and are excluded by node affinity), which degraded co/network and stalled installation. The bug is feature-independent though — any optional DaemonSet whose affinity matches no nodes triggers it.

Testing

New TestStatusManagerDaemonSetMatchingZeroNodes (TDD — reproduces the exact live symptom before the fix): desired=0/observed-current ⇒ Available, not Progressing, not Degraded, no rollout state tracked; includes a stale-rollout-state + clock-step case proving no hung-rollout Degraded fires and state is pruned. Validated live in the enhancement-1982 reference implementation (https://github.com/mkowalski/bgp-vip-demo).

Part of OPNET-595; CNO subtask: OPNET-783.

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 openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Walkthrough

DaemonSet rollout status handling now distinguishes zero desired pods from unscheduled pods. A focused test verifies conditions, rollout tracking, stale-state pruning, and hung-rollout annotation behavior for DaemonSets matching no nodes.

Changes

DaemonSet rollout status

Layer / File(s) Summary
Handle DaemonSets matching zero nodes
pkg/controller/statusmanager/pod_status.go, pkg/controller/statusmanager/status_manager_test.go
The unscheduled DaemonSet condition now requires desired pods or an unobserved generation. Tests cover rollout conditions, tracked state, stale-state cleanup, and annotation behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed The added test name is a static Go function name; no dynamic or unstable test titles were introduced.
Test Structure And Quality ✅ Passed The new test is a focused unit test with fake client setup, no cluster ops/timeouts, clear failure messages, and all assertions target one regression family.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the change is a standard Go unit test using fake clients, so MicroShift compatibility rules don’t apply.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The added test is a plain unit test (testing.T), not Ginkgo/e2e, and it contains no multi-node or SNO-sensitive assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed Changes only alter DaemonSet rollout status logic/tests; no pod specs, replica counts, selectors, affinities, tolerations, or topology checks were introduced.
Ote Binary Stdout Contract ✅ Passed No process-level stdout writes were added; init() only registers schemes, and all Print/log calls are inside normal test functions or use stderr.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the new test is a unit test and contains no IPv4 or external connectivity assumptions.
No-Weak-Crypto ✅ Passed Touched files only adjust DaemonSet status logic/tests; no weak-crypto APIs, custom crypto, or secret comparisons found.
Container-Privileges ✅ Passed HEAD changes only Go status logic and tests; no container/K8s manifests or privilege fields were added.
No-Sensitive-Data-In-Logs ✅ Passed The PR adds no new logging and the touched code paths only adjust rollout logic/tests; no sensitive data exposure was introduced.
Title check ✅ Passed The title clearly summarizes the main fix: DaemonSets matching zero nodes are no longer marked progressing.
Description check ✅ Passed The description matches the code changes and explains the zero-node DaemonSet status fix and added test coverage.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot requested review from arkadeepsen and miheer July 10, 2026 08:31
@openshift-ci

openshift-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: mkowalski
Once this PR has been reviewed and has the lgtm label, please assign jcaamano 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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/controller/statusmanager/status_manager_test.go (1)

1621-1728: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the unreconciled zero-node case.

This test only uses ObservedGeneration == Generation, so it never exercises the new stale-generation branch. Add a case after status.installComplete is true with Generation > ObservedGeneration and DesiredNumberScheduled == 0, asserting Progressing=True until the observed generation catches up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/controller/statusmanager/status_manager_test.go` around lines 1621 -
1728, Extend TestStatusManagerDaemonSetMatchingZeroNodes with an unreconciled
zero-node scenario after installation is complete: update the DaemonSet to have
Generation greater than ObservedGeneration while DesiredNumberScheduled remains
zero, reconcile, and assert Progressing=True. Then set ObservedGeneration equal
to Generation, reconcile again, and assert Progressing=False, covering the
stale-generation branch without treating the DaemonSet as a rollout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/controller/statusmanager/pod_status.go`:
- Around line 115-119: Ensure stale-generation DaemonSets remain in rollout
processing when scheduled counts are zero: update the dsRolloutActive logic used
by the NumberAvailable == 0 branch to include ds.Status.ObservedGeneration <
ds.Generation, or bypass the gate for that condition. Preserve the existing
handling for DaemonSets with no desired pods while allowing newly updated
resources to progress until their controller observes the generation.

In `@pkg/controller/statusmanager/status_manager_test.go`:
- Around line 1633-1673: Make the DaemonSet fixture in the relevant
status-manager test deterministic with respect to RELEASE_VERSION: either add
the release annotation expected by the production availability logic using
os.Getenv("RELEASE_VERSION"), or use t.Setenv to explicitly control the
environment value before calling status.SetFromPods. Update the
fixture/assertion around status.SetFromPods and getStatuses so Available=True
remains valid regardless of the inherited environment.

---

Nitpick comments:
In `@pkg/controller/statusmanager/status_manager_test.go`:
- Around line 1621-1728: Extend TestStatusManagerDaemonSetMatchingZeroNodes with
an unreconciled zero-node scenario after installation is complete: update the
DaemonSet to have Generation greater than ObservedGeneration while
DesiredNumberScheduled remains zero, reconcile, and assert Progressing=True.
Then set ObservedGeneration equal to Generation, reconcile again, and assert
Progressing=False, covering the stale-generation branch without treating the
DaemonSet as a rollout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: d8f5a23a-0904-4292-a372-6d0a49668439

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9965a and bf90e2d.

📒 Files selected for processing (2)
  • pkg/controller/statusmanager/pod_status.go
  • pkg/controller/statusmanager/status_manager_test.go

Comment on lines +115 to +119
} else if ds.Status.NumberAvailable == 0 && (ds.Status.DesiredNumberScheduled > 0 || ds.Status.ObservedGeneration < ds.Generation) && dsRolloutActive {
// A DaemonSet whose observed status desires zero pods (e.g. its
// node selector matches no nodes) is fully rolled out. Only treat
// NumberAvailable == 0 as "unscheduled" if pods are desired or the
// DaemonSet controller has not yet observed the current generation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep stale-generation DaemonSets progressing after installation.

When status.installComplete is true and the scheduled counts are both zero, dsRolloutActive is false. Thus, even with ObservedGeneration < Generation, the trailing gate prevents this branch and can report a newly updated zero-node DaemonSet as complete before its controller observes the generation. Include generation drift in dsRolloutActive or bypass this gate for stale generations.

Suggested fix
-	dsRolloutActive := !status.installComplete || ds.Status.UpdatedNumberScheduled < ds.Status.CurrentNumberScheduled
+	dsRolloutActive := !status.installComplete ||
+		ds.Status.UpdatedNumberScheduled < ds.Status.CurrentNumberScheduled ||
+		ds.Status.ObservedGeneration < ds.Generation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if ds.Status.NumberAvailable == 0 && (ds.Status.DesiredNumberScheduled > 0 || ds.Status.ObservedGeneration < ds.Generation) && dsRolloutActive {
// A DaemonSet whose observed status desires zero pods (e.g. its
// node selector matches no nodes) is fully rolled out. Only treat
// NumberAvailable == 0 as "unscheduled" if pods are desired or the
// DaemonSet controller has not yet observed the current generation.
dsRolloutActive := !status.installComplete ||
ds.Status.UpdatedNumberScheduled < ds.Status.CurrentNumberScheduled ||
ds.Status.ObservedGeneration < ds.Generation
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/controller/statusmanager/pod_status.go` around lines 115 - 119, Ensure
stale-generation DaemonSets remain in rollout processing when scheduled counts
are zero: update the dsRolloutActive logic used by the NumberAvailable == 0
branch to include ds.Status.ObservedGeneration < ds.Generation, or bypass the
gate for that condition. Preserve the existing handling for DaemonSets with no
desired pods while allowing newly updated resources to progress until their
controller observes the generation.

Comment on lines +1633 to +1673
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{Namespace: "one", Name: "alpha", Generation: 1, Labels: sl},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "alpha"},
},
},
Status: appsv1.DaemonSetStatus{
CurrentNumberScheduled: 0,
DesiredNumberScheduled: 0,
UpdatedNumberScheduled: 0,
NumberAvailable: 0,
NumberReady: 0,
ObservedGeneration: 1,
},
}
set(t, client, ds)
status.SetFromPods()

_, oc, err := getStatuses(client, "testing")
if err != nil {
t.Fatalf("error getting ClusterOperator: %v", err)
}
if !conditionsInclude(oc.Status.Conditions, []operv1.OperatorCondition{
{
Type: operv1.OperatorStatusTypeDegraded,
Status: operv1.ConditionFalse,
},
{
Type: operv1.OperatorStatusTypeProgressing,
Status: operv1.ConditionFalse,
},
{
Type: operv1.OperatorStatusTypeUpgradeable,
Status: operv1.ConditionTrue,
},
{
Type: operv1.OperatorStatusTypeAvailable,
Status: operv1.ConditionTrue,
},
}) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the availability assertion independent of RELEASE_VERSION.

The production path requires the DaemonSet release annotation to equal os.Getenv("RELEASE_VERSION"), but this fixture omits that annotation. If the test process inherits a non-empty RELEASE_VERSION, the expected Available=True condition will fail. Set the matching annotation or explicitly control the environment with t.Setenv.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/controller/statusmanager/status_manager_test.go` around lines 1633 -
1673, Make the DaemonSet fixture in the relevant status-manager test
deterministic with respect to RELEASE_VERSION: either add the release annotation
expected by the production availability logic using
os.Getenv("RELEASE_VERSION"), or use t.Setenv to explicitly control the
environment value before calling status.SetFromPods. Update the
fixture/assertion around status.SetFromPods and getStatuses so Available=True
remains valid regardless of the inherited environment.

@openshift-ci

openshift-ci Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@mkowalski: 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
ci/prow/e2e-metal-ipi-ovn-ipv6-ipsec bf90e2d link true /test e2e-metal-ipi-ovn-ipv6-ipsec
ci/prow/e2e-metal-ipi-ovn-dualstack-bgp-local-gw bf90e2d link true /test e2e-metal-ipi-ovn-dualstack-bgp-local-gw
ci/prow/5.0-upgrade-from-stable-4.22-e2e-azure-ovn-upgrade bf90e2d link false /test 5.0-upgrade-from-stable-4.22-e2e-azure-ovn-upgrade
ci/prow/e2e-aws-ovn-upgrade bf90e2d link true /test e2e-aws-ovn-upgrade
ci/prow/e2e-aws-ovn-upgrade-ipsec bf90e2d link true /test e2e-aws-ovn-upgrade-ipsec
ci/prow/e2e-aws-ovn-hypershift-conformance bf90e2d link true /test e2e-aws-ovn-hypershift-conformance
ci/prow/e2e-gcp-ovn bf90e2d link true /test e2e-gcp-ovn
ci/prow/5.0-upgrade-from-stable-4.22-e2e-aws-ovn-upgrade bf90e2d link false /test 5.0-upgrade-from-stable-4.22-e2e-aws-ovn-upgrade
ci/prow/e2e-azure-ovn-upgrade bf90e2d link true /test e2e-azure-ovn-upgrade
ci/prow/5.0-upgrade-from-stable-4.22-e2e-gcp-ovn-upgrade bf90e2d link false /test 5.0-upgrade-from-stable-4.22-e2e-gcp-ovn-upgrade
ci/prow/e2e-ovn-ipsec-step-registry bf90e2d link true /test e2e-ovn-ipsec-step-registry

Full PR test history. Your PR dashboard.

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

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants