Skip to content

[CASCL-1386] (6½/11) Implement shared node cordon and drain primitives#3207

Merged
L3n41c merged 1 commit into
mainfrom
lenaic/CASCL-1386-evict-06.5-cordon-drain
Jul 3, 2026
Merged

[CASCL-1386] (6½/11) Implement shared node cordon and drain primitives#3207
L3n41c merged 1 commit into
mainfrom
lenaic/CASCL-1386-evict-06.5-cordon-drain

Conversation

@L3n41c

@L3n41c L3n41c commented Jul 1, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Introduces the shared node-cordon and pod-eviction/drain primitives used by the evict-legacy-nodes command's per-manager evictors (EKS managed node groups, ASG, Karpenter, standalone):

  • cordonNodes/cordonNode — idempotent, conflict-retrying cordon that treats an already-gone node as a silent skip and returns the cordoned Node objects.
  • drainNode — evicts every evictable pod (skipping mirror, DaemonSet, completed and terminating pods), then waits for the node to empty.
  • evictPodWithRetry — PDB-aware eviction that retries on 429 and treats 404 as success.
  • listPodsOnNode, waitForNodeEmpty, and the pod-classification predicates (podOccupiesNode/shouldSkipEviction/isMirrorPod/isDaemonSetPod/isCompleted).

Motivation

Every per-manager evictor needs these primitives. Extracting them into a dedicated PR at the base of the stack keeps each downstream PR focused on its manager-specific logic and lets them all build on a single, reviewed implementation.

Additional Notes

  • Part of the CASCL-1386 stacked series. This PR sits at the base; the per-manager evictors (starting with EKS managed node groups) rebase on top and become the first production callers.
  • golangci-lint runs with [run] tests = false, so unit-test usage does not count toward the unused linter and there is no production caller yet in this PR. A small, documented var ( _ = drainNode; _ = cordonNodes ) block keeps unused quiet; it is removed by the next PR in the stack when evictEKSManagedNodeGroup becomes the first caller.

Minimum Agent Versions

N/A — kubectl-datadog plugin only; no Datadog Agent or Cluster Agent change.

Describe your test plan

Unit tests (go test ./cmd/kubectl-datadog/autoscaling/cluster/evict/) cover: cordon (idempotent / conflict-retry / already-gone / dry-run), pod eviction (429 retry, 404 success, non-retryable error, 429 exhaustion, zero-interval guard), node-empty polling (empty / becomes-empty / timeout / list-error), pod classification, and the spec.nodeName field-selector scoping of listPodsOnNode. make lint is clean.

Checklist

  • PR has at least one valid label: enhancement
  • PR has a milestone or the qa/skip-qa label
  • All commits are signed (see: signing commits)

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

🎯 Code Coverage (details)
Patch Coverage: 94.12%
Overall Coverage: 45.26% (+0.19%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 39bbf26 | Docs | Datadog PR Page | Give us feedback!

@L3n41c L3n41c changed the title [CASCL-1386] Implement shared node cordon and drain primitives [CASCL-1386] (6½/11) Implement shared node cordon and drain primitives Jul 1, 2026
Introduce the reusable node-cordon and pod-eviction/drain primitives that the
per-manager evictors (EKS managed node groups, ASG, Karpenter, standalone)
build on:

- cordonNodes/cordonNode: idempotent, conflict-retrying cordon that treats an
  already-gone node as a silent skip and returns the cordoned Node objects.
- drainNode: evicts every evictable pod (skipping mirror, DaemonSet, completed
  and terminating pods), then waits for the node to empty.
- evictPodWithRetry: PDB-aware eviction that retries on 429 and treats 404 as
  success.
- listPodsOnNode, waitForNodeEmpty and the pod-classification predicates
  (podOccupiesNode/shouldSkipEviction/isMirrorPod/isDaemonSetPod/isCompleted).

These primitives have no production caller yet; the first (evictEKSManagedNode-
Group) lands in the next PR in the stack. A temporary blank-identifier
reference keeps golangci-lint's `unused` quiet until then (the linter runs with
tests = false, so the unit tests do not count as use) and is removed by that PR.
@L3n41c
L3n41c force-pushed the lenaic/CASCL-1386-evict-06.5-cordon-drain branch from 47f7ed9 to 39bbf26 Compare July 2, 2026 09:43
@L3n41c
L3n41c marked this pull request as ready for review July 2, 2026 09:43
@L3n41c
L3n41c requested review from a team as code owners July 2, 2026 09:43
cordoned = append(cordoned, node)
}
}
return cordoned, errs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It’s actually convenient to keep a slice at this point as it allows to append other errors at the call site like here: https://github.com/DataDog/datadog-operator/pull/3174/changes#diff-6b899156345b292b05d4951862034449f4a5413e7b2506f88fb821db60bf90bdR27-R38
Thanks to that, we can have, at the end of the cordon+drain sequence a slice of errors with exactly one error per node which couldn’t be either cordoned or drained.
It can potentially be interesting in the future to know precisely how many nodes failed to be cordoned or drained.

Comment on lines +21 to +35
// initialUnschedulable is the node's starting `Spec.Unschedulable`.
initialUnschedulable bool
// conflictFirstUpdate forces a Conflict on the first Update so
// RetryOnConflict has to refetch and re-apply.
conflictFirstUpdate bool
// updateNotFound makes the Update return NotFound, simulating the node
// being deleted between the Get and the Update.
updateNotFound bool
dryRun bool
// wantUpdateCalls is the minimum number of Update invocations
// expected on the Nodes endpoint.
wantMinUpdateCalls int
wantUnschedulable bool
// wantNilNode expects cordonNode to return a nil Node (the node is gone).
wantNilNode bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: for test suites which vary greatly in setup/behaviour it might be better to use nested t.Runs instead of table tests.

if shouldSkipEviction(&p) {
continue
}
if err := evictPodWithRetry(ctx, clientset, &p, opts.EvictionTimeout, opts.PollInterval); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: here it would add up retry latency on each pod with pdb, the alternative approach could be to make several passes over the pod list to amortize that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed that the current algorithm is very naive and slow.
Processing node by node is also sub-optimal.
In a future version, I’d like to have a queue of “pods to evict” that would allow to constantly have x pods being evicted at a time even if they are spread on different nodes.

@L3n41c
L3n41c merged commit c9436c4 into main Jul 3, 2026
39 checks passed
@L3n41c
L3n41c deleted the lenaic/CASCL-1386-evict-06.5-cordon-drain branch July 3, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants