Skip to content

[CASCL-1386] (11/11) Create temporary PodDisruptionBudgets during eviction#3178

Merged
L3n41c merged 10 commits into
mainfrom
lenaic/CASCL-1386-evict-11-pdb
Jul 6, 2026
Merged

[CASCL-1386] (11/11) Create temporary PodDisruptionBudgets during eviction#3178
L3n41c merged 10 commits into
mainfrom
lenaic/CASCL-1386-evict-11-pdb

Conversation

@L3n41c

@L3n41c L3n41c commented Jun 18, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Implements creation and label-based cleanup of temporary PodDisruptionBudgets (maxUnavailable: 1) that protect cross-group workloads during the drain.

Motivation

PR #3026 is too large to review as a single change. This is part 11 of 11 of a stack that splits it into small, individually-reviewable pieces that each build and pass tests on their own. See #3026 for the full feature context and end-to-end QA.

Additional Notes

Stacked PR — the base branch is lenaic/CASCL-1386-evict-10-karpenter-user, not main, so the diff shows only this step's change. Lands last because its controller discovery (discoverControllers) reuses the shouldSkipEviction predicate introduced in part 8. The code is taken verbatim from #3026 (rebased onto main); there are no logic changes versus #3026.

Minimum Agent Versions

N/A — kubectl-datadog plugin only.

Describe your test plan

go test ./cmd/kubectl-datadog/autoscaling/cluster/... passes at this commit. Full end-to-end QA is tracked on #3026.

Checklist

@datadog-official

datadog-official Bot commented Jun 18, 2026

Copy link
Copy Markdown

Code Coverage  Pipelines

Fix all issues with BitsAI

🛑 Gate Violations

🎯 1 Code Coverage issue detected

A Patch coverage percentage gate may be blocking this PR.

Patch coverage: 74.71% (threshold: 80.00%)

⚠️ Warnings

🚦 2 Pipeline jobs failed

DataDog/datadog-operator | e2e_autoscaling   View in Datadog   GitLab

DataDog/datadog-operator | build_operator_image_fips_arm64   View in Datadog   GitLab

ℹ️ Info

🎯 Code Coverage (details)
Patch Coverage: 74.71%
Overall Coverage: 45.78% (+0.18%)

Useful? React with 👍 / 👎

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

@codecov-commenter

codecov-commenter commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.23077% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.57%. Comparing base (e6ebd52) to head (6a978ac).

Files with missing lines Patch % Lines
...d/kubectl-datadog/autoscaling/cluster/evict/pdb.go 69.23% 44 Missing and 8 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                              Coverage Diff                              @@
##           lenaic/CASCL-1386-evict-10-karpenter-user    #3178      +/-   ##
=============================================================================
+ Coverage                                      44.41%   44.57%   +0.15%     
=============================================================================
  Files                                            391      391              
  Lines                                          31316    31483     +167     
=============================================================================
+ Hits                                           13910    14033     +123     
- Misses                                         16508    16544      +36     
- Partials                                         898      906       +8     
Flag Coverage Δ
unittests 44.57% <69.23%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...d/kubectl-datadog/autoscaling/cluster/evict/pdb.go 69.00% <69.23%> (+69.00%) ⬆️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e6ebd52...6a978ac. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 6a978ac to 1a070c5 Compare June 19, 2026 14:21
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from e6ebd52 to c0b814f Compare June 19, 2026 14:21
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 1a070c5 to 3cdcaf0 Compare June 19, 2026 14:45
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch 2 times, most recently from 08904e2 to 43a0d78 Compare June 22, 2026 13:25
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 3cdcaf0 to 2b49dd2 Compare June 22, 2026 13:25
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 2b49dd2 to d88c10a Compare June 23, 2026 08:49
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from 43a0d78 to 1505efe Compare June 23, 2026 08:49
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from d88c10a to 6b94424 Compare June 25, 2026 08:18
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch 2 times, most recently from 045f39b to f21be8d Compare June 26, 2026 12:16
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 6b94424 to d391f26 Compare June 26, 2026 12:16
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from f21be8d to 4fbbbac Compare June 26, 2026 14:45
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch 2 times, most recently from 8b7a293 to 9f7e339 Compare June 29, 2026 15:41
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch 2 times, most recently from 4ba4925 to e5f9e42 Compare July 1, 2026 21:50
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 9f7e339 to ee84e12 Compare July 1, 2026 21:53
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from e5f9e42 to bc86be8 Compare July 1, 2026 22:58
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from ee84e12 to 8d436ee Compare July 1, 2026 22:58
L3n41c added 2 commits July 2, 2026 11:38
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.
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 8d436ee to e6bca70 Compare July 2, 2026 09:43
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from bc86be8 to a6e1990 Compare July 2, 2026 09:43
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from a6e1990 to 768f105 Compare July 2, 2026 12:53
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from e6bca70 to 7564579 Compare July 2, 2026 12:53
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from 768f105 to af9257f Compare July 3, 2026 09:17
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 7564579 to 2b67827 Compare July 3, 2026 09:17
L3n41c added 2 commits July 3, 2026 11:31
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 2b67827 to c79a29e Compare July 3, 2026 09:32
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-10-karpenter-user branch from af9257f to 72d803c Compare July 3, 2026 09:32
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from c79a29e to 071f5d7 Compare July 3, 2026 09:52
@L3n41c L3n41c marked this pull request as ready for review July 3, 2026 09:52
@L3n41c L3n41c requested review from a team as code owners July 3, 2026 09:52

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 071f5d7ed5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

info, err := resolveTopLevelController(ctx, clientset, pod, depCache, rsCache, stsCache)
if err != nil {
log.Printf("Warning: cannot resolve controller for pod %s/%s: %v", pod.Namespace, pod.Name, err)
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not ignore controller lookup failures

When a supported controller lookup fails for reasons other than a deletion race, such as a Forbidden from missing get RBAC on Deployments/ReplicaSets/StatefulSets or a transient API error, this branch only logs and returns nil to the pager. ensureTempPDBs can then succeed while leaving that workload without the requested temporary PDB, and the later drain evicts it as if protection was in place. Propagate non-NotFound lookup errors so users either get the PDBs they asked for or the command fails before draining.

Useful? React with 👍 / 👎.

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.

Addressed. discoverControllers now distinguishes the deletion race from real failures: an IsNotFound (a Pod's owner deleted mid-scan) is logged and skipped, but any other error is propagated — a Forbidden (missing get RBAC on Deployments/ReplicaSets/StatefulSets) or a transient API error now fails ensureTempPDBs before the drain, rather than silently leaving that workload without its temporary PDB and evicting it as if protected.

if err != nil {
    if apierrors.IsNotFound(err) {
        log.Printf("Warning: controller for pod %s/%s not found (deleted mid-scan); skipping", pod.Namespace, pod.Name)
        return nil
    }
    return fmt.Errorf("resolve controller for pod %s/%s: %w", pod.Namespace, pod.Name, err)
}

Added a test (TestDiscoverControllers_LookupErrors) covering both the NotFound-skip and the Forbidden-propagate paths.

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.

Pushed in 38c7019.

Note a refinement from a follow-up review pass: the NotFound handling now distinguishes which lookup failed. An IsNotFound on a Pod's direct owner is still skipped (genuine deletion race), but if the owning ReplicaSet's grandparent Deployment returns NotFound (e.g. the Deployment was deleted with --cascade=orphan while its ReplicaSet keeps running), we now fall back to protecting the still-live ReplicaSet rather than skipping the workload. Non-NotFound errors (Forbidden/RBAC, transient, …) still propagate and fail the command before draining, as you asked.

func tempPDBName(kind, controllerName string) string {
prefix := strings.ToLower(kind) + "-" + controllerName
if len(prefix)+len(pdbNameSuffix) > validation.DNS1123LabelMaxLength {
prefix = prefix[:validation.DNS1123LabelMaxLength-len(pdbNameSuffix)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid colliding truncated temporary PDB names

For two controllers of the same kind in the same namespace whose names exceed the truncation point and share the same leading characters, this produces the same PDB name. The first one creates a temporary PDB, and createTempPDB treats that existing temp PDB as idempotent for the second controller without checking the selector, so one workload is drained without its own PDB. Include a hash or otherwise disambiguate truncated names before appending the suffix.

Useful? React with 👍 / 👎.

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.

Addressed. tempPDBName now appends a short SHA-256 hash of the full base when the name must be truncated, so two controllers of the same kind + namespace whose long names share a truncated prefix no longer collide on the same PDB name (previously the second was treated as idempotent by createTempPDB and drained without its own selector-matching PDB). Names that fit are unchanged.

base := strings.ToLower(kind) + "-" + controllerName
if len(base)+len(pdbNameSuffix) <= validation.DNS1123LabelMaxLength {
    return base + pdbNameSuffix
}
const hashLen = 8
sum := sha256.Sum256([]byte(base))
hash := hex.EncodeToString(sum[:])[:hashLen]
keep := validation.DNS1123LabelMaxLength - len(pdbNameSuffix) - hashLen - 1
return base[:keep] + "-" + hash + pdbNameSuffix

Added a test asserting two long names sharing a truncated prefix produce distinct, DNS1123-valid names.

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.

Pushed in 38c7019 — truncated names now get a short SHA-256 hash suffix, so two controllers of the same kind + namespace sharing a truncated prefix no longer collide on the same PDB name.

Comment on lines +34 to +35
pdbTempLabelKey = "autoscaling.datadoghq.com/temporary-pdb"
pdbTempLabelValue = "true"

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.

This is used for selector so maybe its better to encode the scope of PDB, e.g. autoscaling.datadoghq.com/kubectl-datadog-command=evict-legacy-nodes

Comment on lines +211 to +212
// DaemonSet (skipped before reaching here), Job (TTL), CronJob,
// custom controllers — none get a temporary PDB.

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.

Does it make sense to log skipped owners?

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.

I fear it could be very verbose.

return prefix + pdbNameSuffix
}

func formatSelector(s *metav1.LabelSelector) string {

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.

Base automatically changed from lenaic/CASCL-1386-evict-10-karpenter-user to main July 3, 2026 12:19
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
@L3n41c L3n41c force-pushed the lenaic/CASCL-1386-evict-11-pdb branch from 071f5d7 to 38c7019 Compare July 3, 2026 13:39
@L3n41c

L3n41c commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38c7019f49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
}
if !drainOpts.DryRun && len(errs) == 0 {
log.Printf("Drained %d node(s) from user NodePool %s; Karpenter will terminate their NodeClaims once empty.", len(cordoned), nodePoolName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Explicitly remove user Karpenter NodeClaims

When the target is a user-managed Karpenter NodePool whose consolidation is disabled or blocked by disruption settings, draining and cordoning the nodes is not enough to make Karpenter terminate the backing NodeClaims/instances. This path can therefore return success and let Run print the final success message while the legacy NodePool still contains cordoned empty nodes; a rerun just drains the same empty nodes again because nothing here deletes or waits on the NodeClaims/Nodes disappearing.

Useful? React with 👍 / 👎.

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.

@codex This file isn’t touched by this PR, is it?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No — cmd/kubectl-datadog/autoscaling/cluster/evict/karpenter_user.go is not touched by this PR’s diff.

I checked the PR base/head diff (8cf0aa8...f650f70), and the only changed files are:

  • cmd/kubectl-datadog/autoscaling/cluster/evict/pdb.go
  • cmd/kubectl-datadog/autoscaling/cluster/evict/pdb_test.go
  • cmd/kubectl-datadog/autoscaling/cluster/evict/run.go
  • cmd/kubectl-datadog/autoscaling/cluster/evict/run_test.go

The referenced line in karpenter_user.go is existing code: it logs that drained user NodePool nodes will be terminated by Karpenter once empty. cmd/kubectl-datadog/autoscaling/cluster/evict/karpenter_user.goL12-L22

Testing

  • git diff --name-status 8cf0aa852ecd296d09c7d5a89edc5bea336683a3...f650f7006ae89284d1a14111810672faf1dd7777 --
  • git diff --stat 8cf0aa852ecd296d09c7d5a89edc5bea336683a3...f650f7006ae89284d1a14111810672faf1dd7777 -- cmd/kubectl-datadog/autoscaling/cluster/evict/karpenter_user.go
  • nl -ba cmd/kubectl-datadog/autoscaling/cluster/evict/karpenter_user.go | sed -n '1,60p'

View task →

Comment thread cmd/kubectl-datadog/autoscaling/cluster/evict/evict_pods.go

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f650f7006a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/kubectl-datadog/autoscaling/cluster/evict/pdb.go
L3n41c and others added 3 commits July 3, 2026 17:43
Part of a stack splitting #3026 (too large to review in one piece) into
small pieces that each build and pass tests on their own. The command is
fully functional only once the whole stack lands.
@L3n41c

L3n41c commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5300c5ac10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +72 to +73
if hasUserPDB(existing, c.PodLabels) {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear leaked temp PDBs before relying on user PDBs

When a previous interrupted run left our temporary PDB behind and the user has since added their own PDB for the same workload, hasUserPDB returns true here because it ignores temporary PDBs, so this continue leaves the leaked temp PDB in place. The subsequent drain then sees the pod selected by both the user PDB and our stale temp PDB, which the eviction path is explicitly trying to avoid, so crash recovery can get stuck until the user manually deletes our labeled PDB. Clean up or replace our matching temp PDB before skipping creation because of a user PDB.

Useful? React with 👍 / 👎.

@L3n41c L3n41c merged commit 5d97771 into main Jul 6, 2026
37 of 39 checks passed
@L3n41c L3n41c deleted the lenaic/CASCL-1386-evict-11-pdb branch July 6, 2026 16:09
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.

3 participants