Skip to content

feat(nodes): drain plan — read-only estimate before draining (#1584) - #1718

Merged
hisco merged 3 commits into
skyhook-io:mainfrom
alexeymoskalev-devops:feat/drain-plan-classifier
Sep 14, 2026
Merged

hisco merged 3 commits into
skyhook-io:mainfrom
alexeymoskalev-devops:feat/drain-plan-classifier

Conversation

@alexeymoskalev-devops

@alexeymoskalev-devops alexeymoskalev-devops commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

Implements #1584 end to end: a pure drain classifier, a read-only drain-plan endpoint, and a plan dialog that replaces the generic Drain Node confirmation.

Backend

  • pkg/k8score: ClassifyPodForDrain(pod, opts, pdbs) returns evict / skip / may-block with an operator-facing reason. Rules follow kubectl drain's filters: terminal and mirror pods skipped, DaemonSet pods never evicted, no controller owner needs force, emptyDir needs deleteEmptyDirData. A pod counts as managed only with a controller owner reference (metav1.GetControllerOf), the way the rest of the codebase already does it; hasManagedOwner accepted any owner reference before. A pod that is already terminating is reported as evict without consulting PDBs, because the Eviction API admits it regardless of budgets.
  • PlanNodeDrain lists the node's pods and per-namespace PDBs with the given client and classifies them. Reads only. A namespace whose PDBs cannot be listed is reported as not evaluated (pdbsEvaluated, pdbError, per-pod pdbChecked) rather than as "no PDB". may-block is evidence, not a verdict: a matching PDB with disruptionsAllowed == 0. The plan says estimate: true; the drain endpoint re-lists live state as before.
  • POST /api/nodes/{name}/drain-plan next to cordon/uncordon (regular timeout group), same JSON body as drain, evaluated with the requesting user's client. deleteEmptyDirData defaults to false here and the response echoes the options used.
  • DrainNode runs on the same classifier and returns skippedPods with reasons (additive field). The MCP manage_node tool builds its own response and is unchanged.

Frontend

  • DrainPlanDialog (packages/k8s-ui) replaces the confirmation: it fetches the plan when opened and again whenever an option changes, keeps Drain disabled until a plan for this node and these options is shown, labels the result an estimate that the drain re-evaluates, and lists every pod with its outcome and reason. deleteEmptyDirData starts off; enabling it shows an acknowledgement that names the pods whose data would be discarded and must be ticked before Drain enables. Force stays an explicit, explained checkbox. Both options are always sent explicitly to the drain endpoint.
  • Post-drain toast lists evictions, skipped pods with reasons and individual failures.
  • Hosts of the shared package that do not pass onPlanDrain keep working: the dialog then gates deleteEmptyDirData on the acknowledgement alone.
  • Docs: endpoint in CLAUDE.md, RBAC needs (get nodes, list pods, list poddisruptionbudgets) in docs/in-cluster.md.

Things I want to flag rather than have you find

  • The Drain button behaves differently after upgrade. The old dialog always sent deleteEmptyDirData: true; the new one starts with it off, so pods using emptyDir are skipped unless the operator enables the option and ticks the acknowledgement. This is what the issue asks for, but it is a visible change for anyone who relied on the old default; worth a line in release notes.
  • Behaviour changes to the live drain through the shared classifier. (a) Managed-owner and DaemonSet detection use the controller reference: a pod whose only DaemonSet or ReplicaSet reference is not a controller reference was skipped before and is now unmanaged (skipped without force, evicted with it). (b) DaemonSet pods are never evicted, even with IgnoreDaemonSets=false (previously they were); Radar always passes true, so nothing observable changes, but the option is now only a reason string. (c) A terminating pod is evicted without a PDB pre-check. All three match kubectl / the Eviction API and also apply to the MCP manage_node drain, whose code is untouched.
  • The emptyDir acknowledgement is required whenever the option is on, even if the estimate lists no emptyDir pod: the plan is a snapshot and the drain runs against live state. The text says so when the list is empty.
  • The drain endpoint itself still defaults deleteEmptyDirData to true when the body omits it. The dialog no longer relies on that (it sends both options explicitly), so the acceptance criterion holds for the UI; API callers that omit the field keep today's behaviour. Flipping the endpoint default is a one-line change if you prefer it.
  • may-block scope. Only a currently exhausted budget is reported, against the first matching PDB. A budget that this very drain would exhaust (disruptionsAllowed: 1, several evictable pods) is not modelled; those pods show as evict, and the reasons say "may be refused". That is the literal reading of "currently PDB-constrained".
  • PDB listing is one call per namespace so namespace-scoped users get a partial plan instead of a 403. Slower than one cluster-wide list on nodes hosting many namespaces; happy to switch to cluster-wide with a per-namespace fallback.

Type of change

  • New feature (non-breaking change that adds functionality)

How has this been tested?

  • Added/updated unit tests. Go: 22 classifier table cases (owner reference variants, DaemonSet with and without ignore, terminating pod under an exhausted PDB, emptyDir, PDB matching incl. nil and empty selectors, skip winning over PDB); PlanNodeDrain and the handler on a fake clientset asserting only get/list actions; 404 / 403 mapping; forbidden PDB list degrading to an unevaluated plan; DrainNode reporting skipped pods; request option parsing. Frontend: canConfirmDrain (no drain without a current plan, emptyDir acknowledgement required only when emptyDir pods would be evicted, hosts without plan support), DrainPlanContent rendering (outcomes and reasons, estimate label, stale plan ignored, acknowledgement rendered unchecked and naming the pods, PDB-not-evaluated warning), plan request path and body (targets /drain-plan, both options explicit), drain result summary.
  • Tested against a remote cluster: k3s (Colima) with a 2-replica Deployment under a minAvailable: 2 PDB, one of its pods stuck terminating, a Deployment with emptyDir, a DaemonSet and a bare Pod. In the built app: opening Drain fetched the plan (2 evict, 2 may-block, 4 skip, terminating pod evict), the node stayed schedulable; enabling deleteEmptyDirData refetched the plan and showed the acknowledgement naming the two emptyDir pods with Drain disabled until ticked; the drain then evicted 5, timed out on exactly the two may-block pods and skipped the DaemonSet and bare pods; the toast listed the skips with reasons. Same flow without the PDB: "Node drained: 5 evicted, 4 skipped" with reasons.

go vet ./internal/server/ and tsc in packages/k8s-ui report pre-existing issues in files this PR does not touch (exec_origin_test.go, localterm_disable_test.go, trace/*.test.ts).

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my code
  • I have added comments where necessary
  • My changes generate no new warnings
  • Any dependent changes have been merged

Related issues

Fixes #1584


Note

Medium Risk
Changes live drain pod-selection semantics via a shared classifier (controller refs, DaemonSet handling, terminating pods) and alters the UI default for emptyDir eviction; the plan path is read-only but mistaken reliance on estimates could still surprise operators during drain.

Overview
Adds a read-only drain plan so operators can preview node drain impact before cordoning or evicting anything.

API & core: New POST /api/nodes/{name}/drain-plan (same optional body as drain, but deleteEmptyDirData defaults to false for previews). pkg/k8score gains ClassifyPodForDrain / PlanNodeDrain, classifying each pod as evict, skip, or may-block (PDB evidence) with reasons; PDB list failures degrade to “not evaluated” instead of implying no budgets. DrainNode now uses the same classifier and returns structured skippedPods (additive); execution still relies on the Eviction API for PDBs.

UI: The node Drain flow is replaced by DrainPlanDialog: fetches/refetches the plan when options change, keeps Drain disabled until a matching plan is shown (when the host supports planning), and requires an explicit acknowledgement when delete emptyDir data is enabled (UI default is now off, unlike the old dialog that always sent deleteEmptyDirData: true). Post-drain toasts summarize evicted, skipped (with reasons), and failures.

Docs: CLAUDE.md and docs/in-cluster.md document the endpoint and RBAC (get nodes, list pods, optional list poddisruptionbudgets).

Reviewed by Cursor Bugbot for commit 1d5122f. Bugbot is set up for automated code reviews on this repo. Configure here.

…log (skyhook-io#1584)

Replaces the generic Drain Node confirmation with a server-backed plan.

Backend (pkg/k8score, internal/server):
- Pure classifier ClassifyPodForDrain returning a per-pod outcome
  (evict / skip / may-block) with an operator-facing reason. Rules follow
  kubectl drain's filters: terminal and mirror pods skipped, DaemonSet pods
  never evicted, no controller owner needs force, emptyDir needs
  deleteEmptyDirData. A pod counts as managed only with a controller owner
  reference (metav1.GetControllerOf); hasManagedOwner accepted any owner
  reference before. A pod that is already terminating is reported as evict
  without consulting PDBs, as the Eviction API admits it regardless.
- POST /api/nodes/{name}/drain-plan: reads only, requesting user's client,
  same body as drain, returns evaluated options, generation time, summary,
  per-pod outcomes with reasons and an emptyDir flag; PDBs the caller cannot
  list are reported as not evaluated instead of counted as zero.
- DrainNode runs on the same classifier and returns skippedPods with reasons
  (additive); the MCP manage_node tool is unchanged.

Frontend (packages/k8s-ui, web):
- DrainPlanDialog fetches the plan before enabling Drain, labels it an
  estimate re-evaluated on execution, lists every pod with its outcome and
  reason, starts with deleteEmptyDirData off and requires an explicit
  acknowledgement naming the affected pods, keeps force explicit. Both
  options are always sent explicitly; nothing relies on server defaults.
- Post-drain toast lists evictions, skipped pods with reasons and individual
  failures.

Tests: classifier table, PlanNodeDrain and the handler on a fake clientset
asserting only get/list actions, status mapping, DrainNode skip reporting;
frontend: confirm gating (plan required, emptyDir acknowledgement), dialog
content rendering, plan request shape, drain result summary.
Docs: endpoint list and RBAC note.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add read-only node drain plans and safety-gated confirmation

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds PDB-aware, read-only drain plans with per-pod outcomes and operator-facing reasons.
• Reuses the classifier during execution and reports skipped pods alongside evictions and failures.
• Replaces generic confirmation with estimate-driven safety checks for force and emptyDir data loss.
Diagram

sequenceDiagram
    actor Operator
    participant Dialog as Drain Plan Dialog
    participant Client as Web API Client
    participant Handler as Node API Handler
    participant Planner as Drain Planner
    participant K8s as Kubernetes API
    participant Executor as Drain Executor
    Operator->>Dialog: Open or change options
    Dialog->>Client: Request drain plan
    Client->>Handler: POST drain-plan
    Handler->>Planner: Plan with user client
    Planner->>K8s: Read node pods PDBs
    K8s-->>Planner: Cluster snapshot
    Planner-->>Handler: Classified pod outcomes
    Handler-->>Client: Estimated plan
    Client-->>Dialog: Render plan and warnings
    Operator->>Dialog: Acknowledge and confirm
    Dialog->>Client: POST drain options
    Client->>Executor: Execute drain
    Executor->>K8s: Cordon relist and evict
    K8s-->>Executor: Eviction results
    Executor-->>Client: Evicted skipped failures
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Client-side drain classification
  • ➕ Avoids adding a dedicated server endpoint.
  • ➕ Could derive a preview from existing frontend resource data.
  • ➖ Duplicates kubectl-compatible rules across Go and TypeScript.
  • ➖ May use incomplete or differently authorized pod and PDB data.
  • ➖ Makes execution and preview behavior more likely to diverge.
2. Dry-run eviction requests
  • ➕ Delegates PDB and admission decisions to the Kubernetes API.
  • ➕ More closely reflects current eviction admission behavior.
  • ➖ Requires one request per candidate pod and still needs drain filtering.
  • ➖ Dry-run support and admission side effects may vary across clusters.
  • ➖ Does not naturally explain skipped pods or aggregate unreadable PDB state.
3. Combined plan-and-drain endpoint
  • ➕ Could bind preview and execution into one server workflow.
  • ➕ Reduces the number of public API routes.
  • ➖ Weakens the safety boundary between read-only planning and mutation.
  • ➖ Cannot preserve an interactive review step without server-side plan tokens.
  • ➖ Live state must still be re-evaluated before execution.

Recommendation: Keep the separate read-only planner backed by a shared pure classifier. It provides a clear non-mutating API boundary, preserves caller RBAC, explains every pod decision, and minimizes drift by reusing the same filtering logic during execution; live eviction remains authoritative for PDB and admission behavior.

Files changed (16) +1333 / -111

Enhancement (9) +748 / -53
node_ops.goExpose caller-scoped drain planning +12/-0

Expose caller-scoped drain planning

• Aliases the reusable drain plan type and adds a client-injected planning wrapper. The wrapper rejects missing clients and preserves the caller's Kubernetes identity.

internal/k8s/node_ops.go

node_handlers.goImplement the read-only drain-plan handler +89/-16

Implement the read-only drain-plan handler

• Adds request decoding, endpoint-specific emptyDir defaults, caller-scoped planning, and Kubernetes-to-HTTP error mapping. Refactors drain option construction so planning and execution share option handling without changing the drain's historical default.

internal/server/node_handlers.go

server.goRegister the drain-plan route +3/-1

Register the drain-plan route

• Registers POST '/nodes/{name}/drain-plan' in the regular timeout route group while leaving drain execution outside it.

internal/server/server.go

DrainPlanDialog.tsxAdd the safety-gated drain plan dialog +275/-0

Add the safety-gated drain plan dialog

• Introduces shared plan types and a dialog showing estimated per-pod outcomes and reasons. Confirmation requires a matching current plan when supported and explicit acknowledgement whenever emptyDir deletion is enabled.

packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx

ResourceActionsBar.tsxReplace generic drain confirmation with planning +35/-26

Replace generic drain confirmation with planning

• Fetches and refreshes plans while the dialog is open, invalidates stale results when options change, and sends force and emptyDir choices explicitly. Hosts without plan support retain acknowledgement-based safety behavior.

packages/k8s-ui/src/components/shared/ResourceActionsBar.tsx

index.tsExport drain planning UI contracts +4/-0

Export drain planning UI contracts

• Exports the dialog, presentation helpers, confirmation predicates, defaults, and drain plan types from the shared package entry point.

packages/k8s-ui/src/components/shared/index.ts

drain_plan.goAdd the pure pod classifier and drain planner +221/-0

Add the pure pod classifier and drain planner

• Implements kubectl-aligned evict, skip, and may-block classification with operator-facing reasons. Builds sorted, read-only node plans from pods and namespace PDBs while explicitly tracking unavailable PDB knowledge.

pkg/k8score/drain_plan.go

client.tsAdd drain-plan API hooks and detailed result summaries +101/-10

Add drain-plan API hooks and detailed result summaries

• Adds explicit plan request builders and an on-demand mutation hook for the read-only endpoint. Drain notifications now summarize evictions, reasoned skips, and capped failure lists.

web/src/api/client.ts

WorkloadView.tsxWire drain planning into node actions +8/-0

Wire drain planning into node actions

• Connects the drain-plan mutation, loading state, errors, result data, and reset callback to the shared resource actions bar.

web/src/components/workload/WorkloadView.tsx

Refactor (1) +15 / -56
node_ops.goReuse classification during drain execution +15/-56

Reuse classification during drain execution

• Replaces the older skip helpers with the shared classifier and adds reasoned skipped pods to drain results. The Eviction API remains authoritative for PDB handling during execution.

pkg/k8score/node_ops.go

Tests (4) +568 / -0
node_handlers_test.goTest drain-plan HTTP behavior and safety +150/-0

Test drain-plan HTTP behavior and safety

• Covers option defaults and overrides, malformed bodies, read-only behavior, response contents, status mapping, and graceful degradation when PDB listing is forbidden.

internal/server/node_handlers_test.go

DrainPlanDialog.test.tsxTest plan rendering and confirmation gates +148/-0

Test plan rendering and confirmation gates

• Verifies stale-plan rejection, loading behavior, per-pod explanations, PDB warnings, fallback hosts, and mandatory emptyDir acknowledgements.

packages/k8s-ui/src/components/shared/DrainPlanDialog.test.tsx

drain_plan_test.goTest drain classification and planning semantics +218/-0

Test drain classification and planning semantics

• Exercises terminal, mirror, DaemonSet, unmanaged, emptyDir, terminating, and PDB cases. It also verifies read-only planning, summary counts, unknown nodes, unavailable PDB reporting, and skipped execution results.

pkg/k8score/drain_plan_test.go

drain-plan.test.tsTest plan requests and drain result messages +52/-0

Test plan requests and drain result messages

• Verifies endpoint isolation, URL encoding, explicit options, detailed failure reporting, list truncation, and clean-drain success summaries.

web/src/api/drain-plan.test.ts

Documentation (2) +2 / -2
CLAUDE.mdDocument the drain-plan node endpoint +1/-1

Document the drain-plan node endpoint

• Adds the read-only drain-plan endpoint and its per-pod estimate semantics to the API overview.

CLAUDE.md

in-cluster.mdDocument drain-plan RBAC requirements +1/-1

Document drain-plan RBAC requirements

• Lists the node, pod, and PodDisruptionBudget permissions needed for planning. Clarifies that missing PDB access produces an explicitly unevaluated plan rather than failing the request.

docs/in-cluster.md

@qodo-code-review

qodo-code-review Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Drain plans bypass shared URL config ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
useDrainPlan constructs the request URL with ${getApiBase()}${drainPlanPath(name)} instead of
passing the path to apiUrl(). This new call site duplicates the shared helper's composition logic,
so later API URL handling changes can leave drain planning inconsistent with other frontend
requests.
Code

web/src/api/client.ts[4925]

+      const response = await apiFetch(`${getApiBase()}${drainPlanPath(name)}`, {
Relevance

●●● Strong

Using the shared URL helper is a trivial deterministic consistency fix, and recent frontend API
findings were accepted.

PR-#1585

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036564 requires new backend HTTP URLs under web/src to use apiUrl(), while the added call
manually concatenates getApiBase() and the endpoint path.

Rule 3036564: Use shared API config helpers for all new frontend HTTP/WebSocket calls
web/src/api/client.ts[4924-4929]
web/src/api/config.ts[97-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new drain-plan HTTP call manually concatenates the API base and path rather than using the shared frontend URL helper.

## Fix Focus Areas
- web/src/api/client.ts[4924-4929]

## Recommended Fix
Replace the manual template literal with `apiUrl(drainPlanPath(name))`, retaining the existing `apiFetch` options.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Drain failures break the log format ✓ Resolved 📘 Rule violation ◔ Observability
Description
writeDrainPlan logs its unexpected error as node %s: %v, omitting the required %s/%s: %v
resource format. When plan generation reaches the new 500 branch, that error is recorded differently
from standardized handler failures and cannot be parsed under the same logging convention.
Code

internal/server/node_handlers.go[R173-174]

+		log.Printf("[node-ops] Failed to plan drain for node %s: %v", nodeName, err)
+		s.writeError(w, http.StatusInternalServerError, err.Error())
Relevance

●●● Strong

The requested log format is a deterministic consistency fix for a newly added 500 error path.

PR-#1380

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036628 requires every new 500 path to log with %s/%s: %v; this branch uses only one %s
before the error even though it writes status 500 immediately afterward.

Rule 3036628: Log 500 errors with standardized module/action format before writing the response
internal/server/node_handlers.go[163-175]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new drain-plan 500 path logs before responding but does not use the required module/action and `%s/%s: %v` resource format.

## Fix Focus Areas
- internal/server/node_handlers.go[173-174]

## Recommended Fix
Change the log call to the standardized `[node-ops] Failed to ... %s/%s: %v` form, supplying an appropriate cluster-scoped resource prefix and `nodeName`, while preserving the same `err` and logging before `writeError`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. One drain comment repeats its value ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The comment above DrainOutcomeEvict only says that the pod would be evicted, directly restating
the constant name and its evict value. Removing it leaves the behavior equally clear, while
retaining it creates another description that must remain synchronized with the declaration.
Code

pkg/k8score/drain_plan.go[20]

+	// DrainOutcomeEvict: the pod would be evicted.
Relevance

●●● Strong

The comment only restates an obvious constant value, matching the repository’s maintainability
preference for nonredundant comments.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036542 rejects comments that only translate obvious code behavior, and this comment restates
both the constant name and its literal value.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
pkg/k8score/drain_plan.go[20-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The comment for `DrainOutcomeEvict` merely restates the immediately following constant and adds no rationale, constraint, or semantic detail.

## Fix Focus Areas
- pkg/k8score/drain_plan.go[20-21]

## Recommended Fix
Delete the redundant comment or replace it with genuinely non-obvious contract information if such information is needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (5)
4. A comment preserves change history ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The drainOptionsFromRequest comment describes the drain setting as its historical default rather
than documenting only the current default and rationale. A later reader must interpret
implementation history that can become stale even though the relevant behavior is fully explained by
the current default and its kubectl compatibility.
Code

internal/server/node_handlers.go[100]

+// deleteEmptyDirData means: the drain keeps its historical default (true, matching
Relevance

●●● Strong

Recent precedent accepted removing explicit history references from added comments under the same
compliance rule.

PR-#1621

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036538 prohibits explicit change-history references in added comments, and the added comment
calls the behavior a historical default.

Rule 3036538: Disallow references to tickets or PR history in code comments
internal/server/node_handlers.go[98-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `drainOptionsFromRequest` comment refers to a `historical default`, contrary to the rule prohibiting change-history references in code comments.

## Fix Focus Areas
- internal/server/node_handlers.go[98-102]

## Recommended Fix
Rewrite the comment to describe the current drain and plan defaults and the kubectl compatibility rationale without referring to history.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Plan mutation lacks toast metadata ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
useDrainPlan creates a useMutation without top-level meta.errorMessage or
meta.successMessage, and its comment explicitly documents the omission. Every plan request
therefore bypasses the metadata contract consumed by centralized mutation handling, regardless of
whether the request succeeds or fails.
Code

web/src/api/client.ts[R4938-4939]

+    // No meta.errorMessage: the dialog shows plan errors inline; a toast on top would double them.
+  });
Relevance

●● Moderate

The metadata contract is explicit, but the inline-error rationale creates a legitimate semantic
exception without close precedent.

PR-#1680

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036648 requires every changed useMutation definition to include both metadata keys, while
the added mutation has no meta object and explicitly states that meta.errorMessage is absent.

Rule 3036648: React Query mutations must define toast metadata and avoid per-mutation toast handlers
web/src/api/client.ts[4918-4939]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new React Query mutation omits both required toast metadata keys, intentionally relying only on the dialog's inline error display.

## Fix Focus Areas
- web/src/api/client.ts[4918-4939]

## Recommended Fix
Add a top-level `meta` object containing suitable `errorMessage` and `successMessage` values to the mutation and remove the comment documenting their omission.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. The plan warns about unavailable disruption budgets unnecessarily ✓ Resolved 🐞 Bug ≡ Correctness
Description
PlanNodeDrain lists PodDisruptionBudgets for every namespace in the node pod list before calling
ClassifyPodForDrain, so a failed list sets the plan-wide PDBsEvaluated flag false even for pods
that are terminal, mirror, DaemonSet-managed, unmanaged without force, blocked by emptyDir, or
already terminating. Those decisions return before the classifier consults disruption budgets, but
the frontend consequently hides the may-block count and displays a warning that budget evaluation is
incomplete for a drain that has no budget-dependent pods.
Code

pkg/k8score/drain_plan.go[R182-186]

+	for _, pod := range podList.Items {
+		if _, seen := pdbsByNamespace[pod.Namespace]; seen {
+			continue
+		}
+		list, err := client.PolicyV1().PodDisruptionBudgets(pod.Namespace).List(ctx, metav1.ListOptions{})
Relevance

●● Moderate

The planner’s global evaluation behavior is a meaningful design change, with no close precedent
establishing this optimization as required.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The planner enumerates distinct namespaces and lists their disruption budgets before it makes any
per-pod decision. The classifier then returns before its PDB check for the listed skip conditions
and for terminating pods, proving those list calls and resulting global warning are unnecessary for
such pods.

pkg/k8score/drain_plan.go[180-204]
pkg/k8score/drain_plan.go[59-85]
packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx[153-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PlanNodeDrain` fetches PodDisruptionBudgets for all pod namespaces before applying the drain filters. This marks a plan as having unevaluated budgets when the failed namespace contains only pods whose outcome is already determined without a budget lookup.

## Fix Focus Areas
- pkg/k8score/drain_plan.go[180-204]
- pkg/k8score/drain_plan.go[52-99]

## Recommended Fix
First determine which pods can reach the budget-check stage using the non-PDB drain filters. List PDBs only for namespaces containing eligible, non-terminating pods, retain `PDBChecked=false` for decisions that do not require a PDB, and only set `PDBsEvaluated` false when a required namespace cannot be listed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. The data-loss warning ignores themes ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DrainPlanContent styles the empty-directory acknowledgement with the hardcoded bg-red-500/10
background utility instead of an approved theme background token. Whenever operators enable data
deletion, the warning renders with a fixed palette color that does not adapt through the
application's background theme contract.
Code

packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx[197]

+            'border-red-500/40 bg-red-500/10 text-theme-text-primary',
Relevance

●● Moderate

Theme-token findings are mixed: recent hardcoded-color concerns were rejected, while related styling
consistency findings were accepted.

PR-#1684
PR-#1675

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3036653 permits only the listed bg-theme-* background utilities unless a design exception is
documented, but the added acknowledgement uses bg-red-500/10 with no such comment.

Rule 3036653: Use theme background tokens instead of hardcoded utility color classes
packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx[193-198]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added data-loss acknowledgement uses a hardcoded red Tailwind background without a documented design-spec exception.

## Fix Focus Areas
- packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx[193-198]

## Recommended Fix
Replace `bg-red-500/10` with the appropriate approved theme background token while retaining suitable themed text and warning semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Drain plans misstate budget blockers ✓ Resolved 🐞 Bug ≡ Correctness
Description
pdbBlocking treats every matching budget with zero disruptionsAllowed as current blocking
evidence without checking observedGeneration, unhealthyPodEvictionPolicy, or the pod's Ready
condition. Stale budget status is not authoritative, and a running unready pod covered by
AlwaysAllow can be evicted regardless of the budget, so either condition makes the plan show a
blocker that the live Eviction API may not enforce.
Code

pkg/k8score/drain_plan.go[R109-110]

+		if p.Namespace != pod.Namespace || p.Spec.Selector == nil || p.Status.DisruptionsAllowed > 0 {
+			continue
Relevance

●● Moderate

This is a substantive Kubernetes semantics correction; historical evidence supports correctness
fixes but lacks a close PDB precedent.

PR-#1298

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new predicate checks only namespace, selector, and DisruptionsAllowed, while the repository's
existing PDB helper explicitly treats an unobserved generation as unknown and its readiness checks
account for AlwaysAllow. Kubernetes documents both that status is valid only when the observed and
object generations match and that AlwaysAllow permits eviction of running unhealthy pods
regardless of whether budget criteria are met.

pkg/k8score/drain_plan.go[105-118]
pkg/k8score/pdb.go[15-28]
pkg/upgradereadiness/checks_evidenced.go[194-219]
🌐 Kubernetes states that PDB status is valid only when observedGeneration matches generation, and that AlwaysAllow permits running unhealthy pods to be evicted regardless of budget criteria.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The drain classifier interprets any matching PDB with zero `disruptionsAllowed` as blocking, even when its status is stale or its `AlwaysAllow` policy permits eviction of the specific unhealthy pod.

## Fix Focus Areas
- pkg/k8score/drain_plan.go[102-118]
- pkg/k8score/drain_plan_test.go[66-120]

## Recommended Fix
Only use `disruptionsAllowed` as blocking evidence when the PDB status has observed its current generation. For running pods that are not Ready, honor `unhealthyPodEvictionPolicy: AlwaysAllow`; represent stale status as unevaluated or uncertain rather than `may-block`, and add tests for both cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 44 rules
✅ Web pages:
  +10 more
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-hub-web (sha: 27838689)
  Explored: repo: skyhook-dev/radar-e2e (sha: e4918cc2)
  Explored: repo: skyhook-dev/skyhook-connector (sha: b2052d09)
Review mode: 🧠 Deep: This broad, behavior-changing backend/frontend feature spans many independent code paths, including destructive node draining, RBAC-sensitive APIs, Kubernetes classification/PDB logic, and UI gating, creating a dense set of subtle defects a redundant review could materially catch.

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/server/node_handlers.go Outdated
Comment thread pkg/k8score/drain_plan.go Outdated
Comment thread web/src/api/client.ts Outdated
Comment thread internal/server/node_handlers.go Outdated
Comment thread web/src/api/client.ts Outdated
Comment thread packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx Outdated
Comment thread pkg/k8score/drain_plan.go Outdated
Comment thread pkg/k8score/drain_plan.go

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

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/shared/DrainPlanDialog.tsx
…w-ups

Budget evaluation now reproduces the API server's eviction handler:
Pending and terminating pods bypass PodDisruptionBudgets; a pod covered by
more than one budget is reported as may-block (the eviction subresource
refuses it); an unready pod is evictable under unhealthyPodEvictionPolicy
AlwaysAllow, or under IfHealthyBudget when currentHealthy >= desiredHealthy;
a budget whose status is not reconciled yet (observedGeneration < generation)
is reported as may-block, because the API server answers 429 until it is.

PlanNodeDrain lists budgets only for namespaces holding a pod whose decision
depends on them, so a forbidden namespace with nothing but skipped pods no
longer marks the plan as unevaluated; pdbChecked is set only when a decision
reached the budget check.

Frontend: the drain stays disabled after a plan request error; the plan
request goes through apiUrl(); the plan mutation carries the toast metadata
the app's mutation handler expects; the emptyDir acknowledgement uses the
themed AlertBanner instead of a hardcoded colour. Comment and log-format
nits from review.

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ec1e2c7. Configure here.

Comment thread web/src/api/client.ts Outdated
The drain dialog is the only caller and shows a failed plan inline while
keeping Drain disabled; the global mutation toast reported the same failure
a second time on every unsuccessful refetch.
@hisco
hisco merged commit 672f967 into skyhook-io:main Sep 14, 2026
1 check passed
@hisco

hisco commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for this one, and sorry for the slow answers. Your PDB question needed real digging.

One small thing first: in risk item (a), a pod whose owner references are all non-controller, with no DaemonSet among them, was evicted by an ordinary drain before your change, not skipped. The DaemonSet case is exactly as you wrote it. The code does the right thing either way.

On the deleteEmptyDirData default, let's leave it as it is. Flipping it would silently change behaviour for anything already calling with an empty body. There's a docs line going in so the difference between the plan and the drain is at least written down somewhere. Separately, the second emptyDir checkbox is going away, which reverses you. Your snapshot argument was right, but with the option off by default and the warning naming the pods, the extra tick wasn't earning its click.

Same on the cluster-wide PDB listing, let's leave that one too. A single list pulls every budget in the cluster to use the handful on one node, so it's fewer big calls instead of more small ones rather than a clear win. It only runs on dialog open. If a slow dialog does get reported on a wide node, your version is the fix.

Last thing, and it's a question. You flagged that the plan doesn't model a budget this drain would exhaust: two replicas on the node under maxUnavailable: 1 both show as evict, when really one waits for the other. Having watched it run, that looks like the most likely reason a drain partly fails, and worth building. Do you want it? Roughly: count the pods on the node that would actually consume a disruption from each budget, compare with disruptionsAllowed, and report it per budget as evidence, without changing any pod's outcome. There's more to it, mainly which pods don't count, what to do when a budget's status is stale, and keeping the wording to evidence rather than a verdict. I'll write that out properly if you're interested.

@alexeymoskalev-devops

Copy link
Copy Markdown
Contributor Author

Yes — I'd like to take this on. Please write it out when you have a moment and I'll work from that.

Thanks for the detailed answers, and for the correction on risk item (a) — you're right, a pod whose owners are all non-controller was being evicted before, not skipped.

@alexeymoskalev-devops

Copy link
Copy Markdown
Contributor Author

@hisco no rush on this. I saw #1758 lists it as P3 with open decisions on your internal follow-up issue. If it saves you time, I can draft the spec from what you described: count the pods on the node that would actually consume a disruption from each budget, compare with disruptionsAllowed, and report it per budget as evidence without changing any pod's outcome. I've already checked against eviction.go which pods consume a disruption. Should I open an issue with that draft for you to correct, or would you rather post the open points here?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preview node drains with a read-only, honest drain plan

2 participants