Skip to content

fix: send propagationPolicy at the top level of DeleteOptions - #28

Open
abnegate wants to merge 5 commits into
mainfrom
fix/delete-propagation-policy
Open

abnegate wants to merge 5 commits into
mainfrom
fix/delete-propagation-policy

Conversation

@abnegate

Copy link
Copy Markdown
Member

Problem

RunsClusterOperations::delete() claims to support propagationPolicy, but it never reaches the Kubernetes API. Two stacked bugs:

  1. propagationPolicy and gracePeriodSeconds were nested inside the preconditions map, where the API ignores them (preconditions only accepts uid and resourceVersion).
  2. The $this->refresh() call that followed setAttribute('preconditions', ...) replaces the resource attributes entirely (syncWith() assigns $this->attributes), so even the misplaced fields were wiped before serialization. The actual body sent was the refreshed resource relabelled as kind: DeleteOptions:
{
    "apiVersion": "batch/v1",
    "kind": "DeleteOptions",
    "metadata": {
        "name": "reap",
        "uid": "...",
        "resourceVersion": "12345",
        "namespace": "default"
    }
}

No propagationPolicy anywhere, so Job deletes fall back to the API default for batch/v1 (Orphan). This is the root cause of the DAT-1869 reopen: edge's resize reap calls $job->delete(propagationPolicy: 'Foreground'), the delete orphans the Completed pods (no ownerRef, no TTL), pvc-protection never releases the claim, and the resize wedges permanently.

Fix

Build the DeleteOptions payload explicitly instead of mutating the resource. New body:

{
    "apiVersion": "v1",
    "kind": "DeleteOptions",
    "propagationPolicy": "Foreground",
    "preconditions": {
        "uid": "..."
    }
}
  • propagationPolicy and gracePeriodSeconds (when set) are top-level DeleteOptions fields.
  • preconditions carries only the resource uid, guarding against deleting a recreated resource of the same name.
  • The resourceVersion precondition is dropped deliberately: it never took effect before (see bug 2), and enforcing it now would introduce conflict failures into every consumer's delete path whenever a controller bumps the resource between refresh() and the DELETE call.
  • The payload builder is extracted to deleteOptions() so the shape is unit-testable without a cluster.

Tests

tests/DeleteOptionsTest.php asserts the payload shape: propagationPolicy at the top level (and not in preconditions), gracePeriodSeconds at the top level when set, preconditions limited to uid and omitted when the uid is unknown, and no resource attributes leaking into the body.

Consumers

Edge's dedicated-database resize reap depends on this fix; the edge-side bump should happen after this merges and a release is tagged.

🤖 Generated with Claude Code

delete() nested propagationPolicy and gracePeriodSeconds inside the
preconditions map, where the Kubernetes API ignores them. Worse, the
refresh() call that followed replaced the resource attributes entirely,
so the DeleteOptions body sent to the API was just the refreshed
resource relabelled as kind DeleteOptions with no propagationPolicy
anywhere. Job deletes therefore fell back to the API default (Orphan),
leaving completed pods with no ownerRef and no TTL, which pins PVCs via
pvc-protection and permanently wedges dedicated database resizes
(DAT-1869).

Build the DeleteOptions payload explicitly instead of mutating the
resource: propagationPolicy and gracePeriodSeconds at the top level,
and preconditions carrying only the resource uid. The resourceVersion
precondition is dropped deliberately: it never took effect before, and
enforcing it now would introduce conflict failures into every
consumer's delete path whenever a controller bumps the resource between
refresh and delete. The uid precondition still guards against deleting
a recreated resource of the same name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 15, 2026 05:48

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the latest changes correctly fix ordered response handling in the request-recording test helper, and no outstanding findings remain.

Summary

This PR fixes Kubernetes deletion options so propagation policy and grace period are transmitted as top-level fields while UID remains the sole precondition.

  • Adds a validated PropagationPolicy enum while preserving compatible string inputs.
  • Builds an explicit Kubernetes DeleteOptions payload rather than mutating resource attributes.
  • Adds request-level regression coverage for the actual DELETE body.
  • Makes integration deletion checks wait for foreground deletion to complete.
  • Updates the recording test cluster to preserve response ordering across multiple requests.
  • Documents the updated deletion contract and foreground behavior.

Reviews (4) · Last reviewed commit: "test: keep one response queue per record..."

Comment thread src/Traits/RunsClusterOperations.php Outdated
Comment thread tests/DeleteOptionsTest.php Outdated
abnegate and others added 2 commits September 22, 2026 13:16
The propagation policy was an unchecked string, so a typo reached the API
and came back as a server-side rejection. It is now a PropagationPolicy
enum, accepted either as the case or as its string value, and validated
before the request with the valid values in the message.

Four tests asserted an immediate 404 after delete(), which stopped being
true once the policy defaulted to Foreground: the resource stays readable
until its dependents are gone. They now wait for it to disappear through
a bounded helper that fails with the kind and name rather than hanging.

DeleteOptionsTest no longer round-trips the payload through json_encode
to assert its shape; deleteOptions() already returns a plain array.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread tests/DeleteOptionsTest.php
The suite only called deleteOptions() and asserted the array it returns,
so it could have passed while delete() sent something else entirely -
which is the failure this fixes. A RecordingCluster now answers from
canned responses and keeps the requests, and the new test drives
delete() and reads propagationPolicy, gracePeriodSeconds and
preconditions out of the DELETE body.

Confirmed against the original bug: nesting the fields back under
preconditions fails the new test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread tests/RecordingCluster.php Outdated
getClient() is called once per request, so building a MockHandler from
the full response list each time replayed the first response to every
call: delete()'s DELETE was answered with the refresh GET's body and the
queued Status response was never reached. The client and its handler are
now built once, and the test asserts both requests in order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants