Fix/applyset prune uid precondition#1040
Fix/applyset prune uid precondition#1040WHOIM1205 wants to merge 1 commit intokubernetes-sigs:mainfrom
Conversation
|
Hi @WHOIM1205. Thanks for your PR. I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions 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. |
23aa286 to
5c92048
Compare
|
/assign @a-hilaly |
a-hilaly
left a comment
There was a problem hiding this comment.
Two tiny suggestions, thanks @WHOIM1205 !
| // Simulate: between LIST and DELETE, the resource was recreated with a new UID. | ||
| // The fake client has the recreated object (new UID), so when DELETE comes in | ||
| // with precondition for the old UID, it should fail with 409 Conflict. | ||
| recreated := newConfigMap("orphan-cm", "default") | ||
| recreated.SetLabels(map[string]string{ | ||
| ApplysetPartOfLabel: applySetID, | ||
| }) | ||
| recreated.SetUID(types.UID("new-uid")) |
There was a problem hiding this comment.
looks like this resource isn't used
|
/ok-to-test |
|
/retest |
5c92048 to
ef0e8fd
Compare
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: WHOIM1205 The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@a-hilaly
Tests passing locally. |
| if err == nil { | ||
| mu.Lock() | ||
| results = append(results, PruneResultItem{Object: c.obj}) | ||
| mu.Unlock() | ||
|
|
||
| a.log.V(2).Info("pruned resource", | ||
| "name", c.obj.GetName(), | ||
| "namespace", c.obj.GetNamespace(), | ||
| "gvr", c.gvr.String(), | ||
| ) | ||
| a.log.V(2).Info("pruned resource", | ||
| "name", c.obj.GetName(), | ||
| "namespace", c.obj.GetNamespace(), | ||
| "gvr", c.gvr.String(), | ||
| ) | ||
| } | ||
| if apierrors.IsConflict(err) { | ||
| a.log.V(2).Info("skipped prune due to UID mismatch (resource recreated)", | ||
| "name", c.obj.GetName(), | ||
| "namespace", c.obj.GetNamespace(), | ||
| "gvr", c.gvr.String(), | ||
| ) | ||
| } |
There was a problem hiding this comment.
nit:
| if err == nil { | |
| mu.Lock() | |
| results = append(results, PruneResultItem{Object: c.obj}) | |
| mu.Unlock() | |
| a.log.V(2).Info("pruned resource", | |
| "name", c.obj.GetName(), | |
| "namespace", c.obj.GetNamespace(), | |
| "gvr", c.gvr.String(), | |
| ) | |
| a.log.V(2).Info("pruned resource", | |
| "name", c.obj.GetName(), | |
| "namespace", c.obj.GetNamespace(), | |
| "gvr", c.gvr.String(), | |
| ) | |
| } | |
| if apierrors.IsConflict(err) { | |
| a.log.V(2).Info("skipped prune due to UID mismatch (resource recreated)", | |
| "name", c.obj.GetName(), | |
| "namespace", c.obj.GetNamespace(), | |
| "gvr", c.gvr.String(), | |
| ) | |
| } | |
| if apierrors.IsConflict(err) { | |
| a.log.V(2).Info("skipped prune due to UID mismatch (resource recreated)", | |
| "name", c.obj.GetName(), | |
| "namespace", c.obj.GetNamespace(), | |
| "gvr", c.gvr.String(), | |
| ) | |
| } | |
| if err == nil { | |
| mu.Lock() | |
| results = append(results, PruneResultItem{Object: c.obj}) | |
| mu.Unlock() | |
| a.log.V(2).Info("pruned resource", | |
| "name", c.obj.GetName(), | |
| "namespace", c.obj.GetNamespace(), | |
| "gvr", c.gvr.String(), | |
| ) | |
| } |
|
@WHOIM1205 you haven't addressed all the provided feedback |
…race Signed-off-by: WHOIM1205 <rathourprateek8@gmail.com>
ef0e8fd to
0705e25
Compare
Refactored the post-delete handling into a switch so the conflict, success, and error paths are clearly mutually exclusive. No behavior change just cleaner structure per feedback. All tests passing. |
|
@a-hilaly , @jakobmoellerdev is there anything i can change in this pr |
| } | ||
|
|
||
| if err != nil && !apierrors.IsNotFound(err) { | ||
| switch { |
There was a problem hiding this comment.
this switch case is a bit hard to understand. My understanding is that if the err != nil and it is not found or conflict, we are no longer appending the item to prune result. is that correct?
|
@WHOIM1205 friendly reminder |
|
friendly reminder @WHOIM1205 |
applyset: prevent stale deletes during prune using UID preconditions
Summary
This PR fixes a time-of-check to time-of-act (TOCTOU) race in the ApplySet prune logic.
Between the LIST and DELETE steps, a resource can be deleted and recreated with the same name but a different UID. Without a UID precondition on DELETE, the newly recreated object could be incorrectly deleted.
This change adds a UID precondition to prune DELETE calls and correctly handles 409 Conflict responses.
Problem
The prune flow:
If a resource is recreated between these steps, the DELETE may remove the replacement object because it only matches by name.
When a UID precondition is used, the API server returns a 409 Conflict if the UID no longer matches. This is expected and safe behavior, but it must not cause prune to fail.
Changes
applyset.go
In the
prunemethod DELETE section:DeleteOptions.Preconditions.UID.IsNotFoundandIsConflictas safe-to-ignore.Prunedresults whenerr == nil(actual delete occurred).This ensures:
applyset_test.go
Added
TestPrune_UIDPreconditionwith two subtests:uid matches - resource pruneduid mismatch - resource not prunedThe test verifies that:
Verification
Run:
go test ./pkg/controller/instance/applyset/... -v -count=1