Skip to content

Commit 1e52ede

Browse files
authored
Merge pull request #87 from authzed/ensure-component-nil-fix
fix: bail from EnsureComponentByHash.Handle when newObj signals requeue
2 parents 4ea3a6d + 04b4b25 commit 1e52ede

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

component/ensure_component.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ func (e *EnsureComponentByHash[K, A]) Handle(ctx context.Context) {
5555
ownedObjs := e.List(ctx, e.nn.MustValue(ctx))
5656

5757
newObj := e.newObj(ctx)
58+
// newObj may have signaled a requeue (e.g. RequeueAPIErr) which cancels
59+
// the handler context; bail out before dereferencing the result.
60+
if ctx.Err() != nil {
61+
return
62+
}
5863
hash := e.Hash(newObj)
5964
newObj = newObj.WithAnnotations(map[string]string{e.HashAnnotationKey: hash})
6065

component/ensure_component_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package component
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"testing"
8+
"time"
79

810
"github.com/stretchr/testify/require"
911
corev1 "k8s.io/api/core/v1"
@@ -174,3 +176,83 @@ func TestEnsureServiceHandler(t *testing.T) {
174176
})
175177
}
176178
}
179+
180+
// TestEnsureComponentByHashStopsWhenNewObjSignalsRequeue guards against a panic
181+
// where a newObj factory both calls a queue operation (e.g. RequeueAPIErr) and
182+
// returns nil. RequeueAPIErr cancels the handler context but does not unwind
183+
// the goroutine, so without a ctx.Err() check the framework would dereference
184+
// the nil to attach a hash annotation and segfault. Reproduces the panic
185+
// observed at authzed/internal#10787.
186+
func TestEnsureComponentByHashStopsWhenNewObjSignalsRequeue(t *testing.T) {
187+
const (
188+
hashKey = "example.com/component-hash"
189+
ownerIndex = "owner"
190+
)
191+
192+
ctx, cancel := context.WithCancel(t.Context())
193+
defer cancel()
194+
195+
serviceGVR := corev1.SchemeGroupVersion.WithResource("services")
196+
scheme := runtime.NewScheme()
197+
require.NoError(t, corev1.AddToScheme(scheme))
198+
client := clientfake.NewSimpleDynamicClient(scheme)
199+
informerFactory := dynamicinformer.NewDynamicSharedInformerFactory(client, 0)
200+
require.NoError(t, informerFactory.ForResource(serviceGVR).Informer().AddIndexers(map[string]cache.IndexFunc{
201+
ownerIndex: func(_ interface{}) ([]string, error) {
202+
return []string{types.NamespacedName{Namespace: "test", Name: "owner"}.String()}, nil
203+
},
204+
}))
205+
informerFactory.Start(ctx.Done())
206+
informerFactory.WaitForCacheSync(ctx.Done())
207+
indexer := typed.NewIndexer[*corev1.Service](informerFactory.ForResource(serviceGVR).Informer().GetIndexer())
208+
ctxOwner := typedctx.WithDefault[types.NamespacedName](types.NamespacedName{Namespace: "test", Name: "owner"})
209+
210+
queueOps := queue.NewQueueOperationsCtx()
211+
212+
// Simulate the manager plumbing: a real Operations whose cancel func
213+
// cancels the handler-scoped context, so RequeueAPIErr propagates via
214+
// ctx.Err() the same way it does in production.
215+
handlerCtx, cancelHandler := context.WithCancel(ctx)
216+
defer cancelHandler()
217+
ops := queue.NewOperations(func() {}, func(time.Duration) {}, cancelHandler)
218+
handlerCtx = queueOps.WithValue(handlerCtx, ops)
219+
220+
simulatedErr := errors.New("simulated upstream failure")
221+
applyCalled := false
222+
deleteCalled := false
223+
224+
h := handler.NewHandler(NewEnsureComponentByHash(
225+
NewHashableComponent[*corev1.Service](
226+
NewIndexedComponent(
227+
indexer,
228+
ownerIndex,
229+
func(_ context.Context) labels.Selector {
230+
return labels.SelectorFromSet(map[string]string{
231+
"example.com/component": "the-main-service-component",
232+
})
233+
}),
234+
hash.NewObjectHash(), hashKey),
235+
ctxOwner,
236+
queueOps,
237+
func(_ context.Context, _ *applycorev1.ServiceApplyConfiguration) (*corev1.Service, error) {
238+
applyCalled = true
239+
return nil, nil
240+
},
241+
func(_ context.Context, _ types.NamespacedName) error {
242+
deleteCalled = true
243+
return nil
244+
},
245+
func(ctx context.Context) *applycorev1.ServiceApplyConfiguration {
246+
queueOps.RequeueAPIErr(ctx, simulatedErr)
247+
return nil
248+
}), "ensureService")
249+
250+
require.NotPanics(t, func() {
251+
h.Handle(handlerCtx)
252+
})
253+
254+
require.False(t, applyCalled, "must not apply when newObj signaled requeue")
255+
require.False(t, deleteCalled, "must not delete when newObj signaled requeue")
256+
require.Equal(t, simulatedErr, ops.Error(), "Operations must record the requeue error")
257+
require.Error(t, handlerCtx.Err(), "handler context must be cancelled by RequeueAPIErr")
258+
}

0 commit comments

Comments
 (0)