@@ -332,6 +332,64 @@ func TestStoreDataAsync_UsesK8sSaVaultTokenDirectly(t *testing.T) {
332332 }
333333}
334334
335+ // TestStoreDataAsync_SurvivesParentContextCancel guarantees that the async KV
336+ // write is detached from the caller's context. The admission webhook handler
337+ // returns to the API server long before the Vault round-trip completes, which
338+ // cancels the parent ctx. If StoreDataAsync re-uses that ctx, kv.Put fails
339+ // with "context canceled" and the bookkeeping entry is never written —
340+ // breaking renewer/revoker management on the next cycle.
341+ func TestStoreDataAsync_SurvivesParentContextCancel (t * testing.T ) {
342+ const bookkeepingToken = "s.bookkeeping-token"
343+
344+ kvPutDone := make (chan struct {})
345+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
346+ if r .Method == http .MethodPut {
347+ // Simulate Vault round-trip latency so the parent ctx has time
348+ // to be cancelled while the request is in flight.
349+ time .Sleep (150 * time .Millisecond )
350+ w .Header ().Set ("Content-Type" , "application/json" )
351+ w .WriteHeader (http .StatusOK )
352+ _ , _ = w .Write ([]byte (`{"data":{"version":1}}` ))
353+ close (kvPutDone )
354+ return
355+ }
356+ http .NotFound (w , r )
357+ }))
358+ t .Cleanup (srv .Close )
359+
360+ log := logrus .New ()
361+ log .SetLevel (logrus .PanicLevel )
362+
363+ vaultCfg := vaultapi .DefaultConfig ()
364+ vaultCfg .Address = srv .URL
365+ client , err := vaultapi .NewClient (vaultCfg )
366+ require .NoError (t , err )
367+ client .SetToken (bookkeepingToken )
368+
369+ c := & Connector {
370+ address : srv .URL ,
371+ client : client ,
372+ vaultToken : bookkeepingToken ,
373+ K8sSaVaultToken : bookkeepingToken ,
374+ Log : log ,
375+ }
376+
377+ ki := NewKeyInfo ("pod-uid" , "lease-1" , "tok-1" , "default" , "sa" , "pod" , "node" )
378+
379+ parentCtx , cancel := context .WithCancel (context .Background ())
380+ c .StoreDataAsync (parentCtx , "ctx-id" , ki , "vault-injector" , "pod-uid" , "default" , "prefix" )
381+ // Cancel immediately — mirroring the webhook handler returning before
382+ // the goroutine's kv.Put completes.
383+ cancel ()
384+
385+ select {
386+ case <- kvPutDone :
387+ // success: write completed despite parent cancellation
388+ case <- time .After (2 * time .Second ):
389+ t .Fatal ("KV put never completed — async write is still bound to the cancelled parent context" )
390+ }
391+ }
392+
335393// TestSyncAndCleanupTokens_EmptyKeys verifies that an empty keysInformations slice
336394// returns true (no-op success). No Vault client is required: the orphan-token
337395// dance was removed; SyncAndCleanupTokens now uses the renewer's own login token
0 commit comments