Skip to content

Commit 5173a39

Browse files
authored
Merge pull request #64 from numberly/fix/storedataasync-detach-context
fix(vault): detach StoreDataAsync from caller context
2 parents c5cf7da + 48af1fc commit 5173a39

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

pkg/vault/handle_token.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,16 @@ func (c *Connector) StoreData(ctx context.Context, contextID string, vaultInform
9797
// Callers that require guaranteed persistence (e.g. integration tests, migration tools)
9898
// MUST use StoreData (synchronous) instead of this function.
9999
func (c *Connector) StoreDataAsync(ctx context.Context, contextID string, vaultInformation *KeyInfo, secretName, uuid, namespace, prefix string) {
100+
// Detach from the caller's context: the admission webhook handler's
101+
// ctx is cancelled as soon as the hot path returns a response to the
102+
// API server, which would kill the in-flight kv.Put with
103+
// "context canceled" and leave the KV bookkeeping entry missing —
104+
// breaking renewer/revoker management on the next cycle. We keep the
105+
// values (logger, tracing) but drop the cancellation signal.
106+
detached := context.WithoutCancel(ctx)
100107
go func() {
101108
start := time.Now()
102-
asyncCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
109+
asyncCtx, cancel := context.WithTimeout(detached, 60*time.Second)
103110
defer cancel()
104111

105112
if c.K8sSaVaultToken == "" {

pkg/vault/handle_token_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)