@ -22,6 +22,7 @@ import (
"fmt"
"log/slog"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@ -375,6 +376,158 @@ func TestStatusWaitForDeleteNonExistentObject(t *testing.T) {
assert . NoError ( t , statusWaiter . WaitForDelete ( resourceList , timeout ) )
}
func TestWaitForDeleteWithMissedWatchEvent ( t * testing . T ) {
t . Parallel ( )
c := newTestClient ( t )
fakeClient := dynamicfake . NewSimpleDynamicClient ( scheme . Scheme )
fakeMapper := testutil . NewFakeRESTMapper (
v1 . SchemeGroupVersion . WithKind ( "Pod" ) ,
)
// Return a watcher with no events to simulate a missed deletion notification.
// The watch starts after the initial list, so signal that the watcher has
// seen the resource before deleting it.
watchStarted := make ( chan struct { } )
var once sync . Once
fakeClient . PrependWatchReactor ( "pods" , func ( _ clienttesting . Action ) ( bool , watch . Interface , error ) {
once . Do ( func ( ) { close ( watchStarted ) } )
return true , watch . NewFake ( ) , nil
} )
sw := statusWaiter {
restMapper : fakeMapper ,
client : fakeClient ,
}
sw . SetLogger ( slog . Default ( ) . Handler ( ) )
objs := getRuntimeObjFromManifests ( t , [ ] string { podCurrentManifest } )
gvrs := make ( [ ] schema . GroupVersionResource , 0 , len ( objs ) )
for _ , obj := range objs {
u := obj . ( * unstructured . Unstructured )
gvr := getGVR ( t , fakeMapper , u )
err := fakeClient . Tracker ( ) . Create ( gvr , u , u . GetNamespace ( ) )
require . NoError ( t , err )
gvrs = append ( gvrs , gvr )
}
// Delete the resource once the watch has started. The watcher will miss
// this deletion because watch events are suppressed, but a live GET should
// confirm the resource is gone.
deleteErrs := make ( chan error , 1 )
go func ( ) {
<- watchStarted
var errs [ ] error
for i , obj := range objs {
u := obj . ( * unstructured . Unstructured )
errs = append ( errs , fakeClient . Tracker ( ) . Delete ( gvrs [ i ] , u . GetNamespace ( ) , u . GetName ( ) ) )
}
deleteErrs <- errors . Join ( errs ... )
} ( )
resourceList := getResourceListFromRuntimeObjs ( t , c , objs )
err := sw . WaitForDelete ( resourceList , 500 * time . Millisecond )
require . NoError ( t , <- deleteErrs )
assert . NoError ( t , err )
}
func TestWaitForDeleteWithMissedWatchEventGetError ( t * testing . T ) {
t . Parallel ( )
c := newTestClient ( t )
fakeClient := dynamicfake . NewSimpleDynamicClient ( scheme . Scheme )
fakeMapper := testutil . NewFakeRESTMapper (
v1 . SchemeGroupVersion . WithKind ( "Pod" ) ,
)
fakeClient . PrependWatchReactor ( "pods" , func ( _ clienttesting . Action ) ( bool , watch . Interface , error ) {
return true , watch . NewFake ( ) , nil
} )
// The live GET cannot verify the deletion, so the resource must not be
// reported as simply still existing, nor as deleted.
fakeClient . PrependReactor ( "get" , "pods" , func ( _ clienttesting . Action ) ( bool , runtime . Object , error ) {
return true , nil , apierrors . NewForbidden ( schema . GroupResource { Resource : "pods" } , "current-pod" , errors . New ( "get not allowed" ) )
} )
sw := statusWaiter {
restMapper : fakeMapper ,
client : fakeClient ,
}
sw . SetLogger ( slog . Default ( ) . Handler ( ) )
objs := getRuntimeObjFromManifests ( t , [ ] string { podCurrentManifest } )
for _ , obj := range objs {
u := obj . ( * unstructured . Unstructured )
gvr := getGVR ( t , fakeMapper , u )
err := fakeClient . Tracker ( ) . Create ( gvr , u , u . GetNamespace ( ) )
require . NoError ( t , err )
}
resourceList := getResourceListFromRuntimeObjs ( t , c , objs )
err := sw . WaitForDelete ( resourceList , 500 * time . Millisecond )
require . Error ( t , err )
assert . True ( t , apierrors . IsForbidden ( err ) , "expected the GET error to be wrapped, got: %v" , err )
require . ErrorIs ( t , err , context . DeadlineExceeded )
assert . Contains ( t , err . Error ( ) , "resource Pod/ns/current-pod may still exist. status: Current" )
assert . Contains ( t , err . Error ( ) , "unable to verify deletion" )
}
func TestIsResourceGone ( t * testing . T ) {
t . Parallel ( )
tests := [ ] struct {
name string
getErr error
exists bool
expected bool
expectErr func ( error ) bool
} {
{
name : "not found" ,
expected : true ,
} ,
{
name : "still exists" ,
exists : true ,
} ,
{
name : "forbidden" ,
getErr : apierrors . NewForbidden ( schema . GroupResource { Resource : "pods" } , "current-pod" , errors . New ( "get not allowed" ) ) ,
expectErr : apierrors . IsForbidden ,
} ,
{
name : "timeout" ,
getErr : context . DeadlineExceeded ,
expectErr : func ( err error ) bool { return errors . Is ( err , context . DeadlineExceeded ) } ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
t . Parallel ( )
fakeClient := dynamicfake . NewSimpleDynamicClient ( scheme . Scheme )
fakeMapper := testutil . NewFakeRESTMapper (
v1 . SchemeGroupVersion . WithKind ( "Pod" ) ,
)
if tt . getErr != nil {
fakeClient . PrependReactor ( "get" , "pods" , func ( _ clienttesting . Action ) ( bool , runtime . Object , error ) {
return true , nil , tt . getErr
} )
}
sw := statusWaiter {
restMapper : fakeMapper ,
client : fakeClient ,
}
sw . SetLogger ( slog . Default ( ) . Handler ( ) )
if tt . exists {
u := getRuntimeObjFromManifests ( t , [ ] string { podCurrentManifest } ) [ 0 ] . ( * unstructured . Unstructured )
err := fakeClient . Tracker ( ) . Create ( getGVR ( t , fakeMapper , u ) , u , u . GetNamespace ( ) )
require . NoError ( t , err )
}
id := object . ObjMetadata {
GroupKind : v1 . SchemeGroupVersion . WithKind ( "Pod" ) . GroupKind ( ) ,
Namespace : "ns" ,
Name : "current-pod" ,
}
gone , err := sw . isResourceGone ( t . Context ( ) , id )
if tt . expectErr != nil {
require . Error ( t , err )
assert . True ( t , tt . expectErr ( err ) , "unexpected error: %v" , err )
} else {
require . NoError ( t , err )
}
assert . Equal ( t , tt . expected , gone )
} )
}
}
func TestStatusWait ( t * testing . T ) {
t . Parallel ( )
tests := [ ] struct {