Skip to content

Commit 036c5fc

Browse files
committed
add tests and examples
1 parent f6f4e9f commit 036c5fc

30 files changed

+1566
-6
lines changed

adopt/adopt_test.go

+398
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
package adopt
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
corev1 "k8s.io/api/core/v1"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/apimachinery/pkg/types"
14+
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
15+
"k8s.io/client-go/tools/cache"
16+
"k8s.io/client-go/tools/record"
17+
18+
"github.com/authzed/controller-idioms/handler"
19+
"github.com/authzed/controller-idioms/queue"
20+
"github.com/authzed/controller-idioms/queue/fake"
21+
"github.com/authzed/controller-idioms/typed"
22+
"github.com/authzed/controller-idioms/typedctx"
23+
)
24+
25+
const (
26+
ManagedLabelKey = "example.com/managed-by"
27+
ManagedLabelValue = "example-controller"
28+
OwnerAnnotationPrefix = "example.com/owner-obj-"
29+
IndexName = "owner-index"
30+
31+
EventSecretAdopted = "SecretAdopedByOwner"
32+
)
33+
34+
var (
35+
QueueOps = queue.NewQueueOperationsCtx()
36+
CtxSecretNN = typedctx.WithDefault[types.NamespacedName](types.NamespacedName{})
37+
CtxOwnerNN = typedctx.WithDefault[types.NamespacedName](types.NamespacedName{})
38+
CtxSecret = typedctx.WithDefault[*corev1.Secret](nil)
39+
)
40+
41+
func TestSecretAdopterHandler(t *testing.T) {
42+
type applyCall struct {
43+
called bool
44+
input *applycorev1.SecretApplyConfiguration
45+
result *corev1.Secret
46+
err error
47+
}
48+
49+
secretNotFound := func(name string) error {
50+
return apierrors.NewNotFound(
51+
corev1.SchemeGroupVersion.WithResource("secrets").GroupResource(),
52+
"test")
53+
}
54+
55+
testSecret := &corev1.Secret{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "secret",
58+
Namespace: "test",
59+
Labels: map[string]string{
60+
ManagedLabelKey: ManagedLabelValue,
61+
},
62+
Annotations: map[string]string{
63+
OwnerAnnotationPrefix + "test": "owned",
64+
},
65+
},
66+
}
67+
tests := []struct {
68+
name string
69+
secretName string
70+
cluster types.NamespacedName
71+
secretInCache *corev1.Secret
72+
cacheErr error
73+
secretsInIndex []*corev1.Secret
74+
applyCalls []*applyCall
75+
expectEvents []string
76+
expectNext bool
77+
expectRequeueErr error
78+
expectRequeueAPIErr error
79+
expectRequeue bool
80+
expectCtxSecret *corev1.Secret
81+
}{
82+
{
83+
name: "no secret",
84+
cluster: types.NamespacedName{
85+
Namespace: "test",
86+
Name: "test",
87+
},
88+
secretName: "",
89+
applyCalls: []*applyCall{},
90+
expectNext: true,
91+
},
92+
{
93+
name: "secret needs adopting",
94+
secretName: "secret",
95+
cluster: types.NamespacedName{
96+
Namespace: "test",
97+
Name: "test",
98+
},
99+
cacheErr: secretNotFound("test"),
100+
secretsInIndex: []*corev1.Secret{},
101+
applyCalls: []*applyCall{
102+
{
103+
input: applycorev1.Secret("secret", "test").
104+
WithLabels(map[string]string{
105+
ManagedLabelKey: ManagedLabelValue,
106+
}),
107+
result: &corev1.Secret{
108+
ObjectMeta: metav1.ObjectMeta{
109+
Name: "secret",
110+
Namespace: "test",
111+
Labels: map[string]string{
112+
ManagedLabelKey: ManagedLabelValue,
113+
},
114+
},
115+
},
116+
},
117+
{
118+
input: applycorev1.Secret("secret", "test").
119+
WithAnnotations(map[string]string{
120+
OwnerAnnotationPrefix + "test": "owned",
121+
}),
122+
result: &corev1.Secret{
123+
ObjectMeta: metav1.ObjectMeta{
124+
Name: "secret",
125+
Namespace: "test",
126+
Labels: map[string]string{
127+
ManagedLabelKey: ManagedLabelValue,
128+
},
129+
Annotations: map[string]string{
130+
OwnerAnnotationPrefix + "test": "owned",
131+
},
132+
},
133+
},
134+
},
135+
},
136+
expectEvents: []string{"Normal SecretAdopedByOwner Secret was referenced by test/test; it has been labelled to mark it as part of the configuration for that controller."},
137+
expectCtxSecret: testSecret,
138+
expectNext: true,
139+
},
140+
{
141+
name: "secret already adopted",
142+
cluster: types.NamespacedName{
143+
Namespace: "test",
144+
Name: "test",
145+
},
146+
secretName: "secret",
147+
secretInCache: testSecret,
148+
secretsInIndex: []*corev1.Secret{testSecret},
149+
expectEvents: []string{},
150+
expectNext: true,
151+
expectCtxSecret: testSecret,
152+
},
153+
{
154+
name: "secret adopted by a second cluster",
155+
cluster: types.NamespacedName{
156+
Name: "test2",
157+
Namespace: "test",
158+
},
159+
secretName: "secret",
160+
secretInCache: testSecret,
161+
secretsInIndex: []*corev1.Secret{testSecret},
162+
applyCalls: []*applyCall{
163+
{
164+
input: applycorev1.Secret("secret", "test").
165+
WithAnnotations(map[string]string{
166+
OwnerAnnotationPrefix + "test2": "owned",
167+
}),
168+
result: &corev1.Secret{
169+
ObjectMeta: metav1.ObjectMeta{
170+
Name: "secret",
171+
Namespace: "test",
172+
Labels: map[string]string{
173+
ManagedLabelKey: ManagedLabelValue,
174+
},
175+
Annotations: map[string]string{
176+
OwnerAnnotationPrefix + "test": "owned",
177+
OwnerAnnotationPrefix + "test2": "owned",
178+
},
179+
},
180+
},
181+
},
182+
},
183+
expectEvents: []string{"Normal SecretAdopedByOwner Secret was referenced by test/test2; it has been labelled to mark it as part of the configuration for that controller."},
184+
expectCtxSecret: &corev1.Secret{
185+
ObjectMeta: metav1.ObjectMeta{
186+
Name: "secret",
187+
Namespace: "test",
188+
Labels: map[string]string{
189+
ManagedLabelKey: ManagedLabelValue,
190+
},
191+
Annotations: map[string]string{
192+
OwnerAnnotationPrefix + "test": "owned",
193+
OwnerAnnotationPrefix + "test2": "owned",
194+
},
195+
},
196+
},
197+
expectNext: true,
198+
},
199+
{
200+
name: "transient error adopting secret",
201+
cluster: types.NamespacedName{
202+
Namespace: "test",
203+
Name: "test",
204+
},
205+
secretName: "secret",
206+
cacheErr: secretNotFound("test"),
207+
applyCalls: []*applyCall{
208+
{
209+
input: applycorev1.Secret("secret", "test").
210+
WithLabels(map[string]string{
211+
ManagedLabelKey: ManagedLabelValue,
212+
}),
213+
err: apierrors.NewTooManyRequestsError("server having issues"),
214+
},
215+
},
216+
expectRequeueAPIErr: apierrors.NewTooManyRequestsError("server having issues"),
217+
},
218+
{
219+
name: "old secret still in index",
220+
cluster: types.NamespacedName{
221+
Namespace: "test",
222+
Name: "test",
223+
},
224+
secretName: "secret",
225+
secretsInIndex: []*corev1.Secret{testSecret, {
226+
ObjectMeta: metav1.ObjectMeta{
227+
Name: "secret2",
228+
Namespace: "test",
229+
Labels: map[string]string{
230+
ManagedLabelKey: ManagedLabelValue,
231+
},
232+
Annotations: map[string]string{
233+
OwnerAnnotationPrefix + "test": "owned",
234+
},
235+
},
236+
}},
237+
applyCalls: []*applyCall{
238+
{
239+
input: applycorev1.Secret("secret2", "test").
240+
WithLabels(map[string]string{}),
241+
result: &corev1.Secret{
242+
ObjectMeta: metav1.ObjectMeta{
243+
Name: "secret2",
244+
Namespace: "test",
245+
Annotations: map[string]string{
246+
OwnerAnnotationPrefix + "test": "owned",
247+
},
248+
},
249+
},
250+
},
251+
{
252+
input: applycorev1.Secret("secret2", "test").
253+
WithAnnotations(map[string]string{}),
254+
result: &corev1.Secret{
255+
ObjectMeta: metav1.ObjectMeta{
256+
Name: "secret2",
257+
Namespace: "test",
258+
},
259+
},
260+
},
261+
},
262+
expectCtxSecret: testSecret,
263+
expectNext: true,
264+
},
265+
{
266+
name: "old secret still in index, still has other owners",
267+
cluster: types.NamespacedName{
268+
Namespace: "test",
269+
Name: "test",
270+
},
271+
secretName: "secret",
272+
secretsInIndex: []*corev1.Secret{testSecret, {
273+
ObjectMeta: metav1.ObjectMeta{
274+
Name: "secret2",
275+
Namespace: "test",
276+
Labels: map[string]string{
277+
ManagedLabelKey: ManagedLabelValue,
278+
},
279+
Annotations: map[string]string{
280+
OwnerAnnotationPrefix + "test2": "owned",
281+
OwnerAnnotationPrefix + "test": "owned",
282+
},
283+
},
284+
}},
285+
applyCalls: []*applyCall{
286+
{
287+
input: applycorev1.Secret("secret2", "test").
288+
WithAnnotations(map[string]string{}),
289+
result: &corev1.Secret{
290+
ObjectMeta: metav1.ObjectMeta{
291+
Name: "secret2",
292+
Namespace: "test",
293+
},
294+
},
295+
},
296+
},
297+
expectCtxSecret: testSecret,
298+
expectNext: true,
299+
},
300+
}
301+
for _, tt := range tests {
302+
t.Run(tt.name, func(t *testing.T) {
303+
ctrls := &fake.FakeOperations{}
304+
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{IndexName: OwnerKeysFromMeta(OwnerAnnotationPrefix)})
305+
IndexAddUnstructured(t, indexer, tt.secretsInIndex)
306+
307+
recorder := record.NewFakeRecorder(1)
308+
nextCalled := false
309+
applyCallIndex := 0
310+
s := NewSecretAdoptionHandler(
311+
recorder,
312+
func(ctx context.Context) (*corev1.Secret, error) {
313+
return tt.secretInCache, tt.cacheErr
314+
},
315+
typed.NewIndexer[*corev1.Secret](indexer),
316+
func(ctx context.Context, secret *applycorev1.SecretApplyConfiguration, opts metav1.ApplyOptions) (result *corev1.Secret, err error) {
317+
defer func() { applyCallIndex++ }()
318+
call := tt.applyCalls[applyCallIndex]
319+
call.called = true
320+
require.Equal(t, call.input, secret, "error on call %d", applyCallIndex)
321+
return call.result, call.err
322+
},
323+
handler.NewHandlerFromFunc(func(ctx context.Context) {
324+
nextCalled = true
325+
require.Equal(t, tt.expectCtxSecret, CtxSecret.Value(ctx))
326+
}, "testnext"),
327+
)
328+
ctx := CtxOwnerNN.WithValue(context.Background(), tt.cluster)
329+
ctx = CtxSecretNN.WithValue(ctx, types.NamespacedName{Namespace: "test", Name: tt.secretName})
330+
ctx = QueueOps.WithValue(ctx, ctrls)
331+
s.Handle(ctx)
332+
for _, call := range tt.applyCalls {
333+
require.True(t, call.called)
334+
}
335+
ExpectEvents(t, recorder, tt.expectEvents)
336+
require.Equal(t, tt.expectNext, nextCalled)
337+
if tt.expectRequeueErr != nil {
338+
require.Equal(t, 1, ctrls.RequeueErrCallCount())
339+
require.Equal(t, tt.expectRequeueErr, ctrls.RequeueErrArgsForCall(0))
340+
}
341+
if tt.expectRequeueAPIErr != nil {
342+
require.Equal(t, 1, ctrls.RequeueAPIErrCallCount())
343+
require.Equal(t, tt.expectRequeueAPIErr, ctrls.RequeueAPIErrArgsForCall(0))
344+
}
345+
require.Equal(t, tt.expectRequeue, ctrls.RequeueCallCount() == 1)
346+
})
347+
}
348+
}
349+
350+
func NewSecretAdoptionHandler(recorder record.EventRecorder, getFromCache func(ctx context.Context) (*corev1.Secret, error), secretIndexer *typed.Indexer[*corev1.Secret], secretApplyFunc ApplyFunc[*corev1.Secret, *applycorev1.SecretApplyConfiguration], next handler.Handler) handler.Handler {
351+
return handler.NewHandler(&AdoptionHandler[*corev1.Secret, *applycorev1.SecretApplyConfiguration]{
352+
OperationsContext: QueueOps,
353+
ControllerFieldManager: "test-controller",
354+
AdopteeCtx: CtxSecretNN,
355+
OwnerCtx: CtxOwnerNN,
356+
AdoptedCtx: CtxSecret,
357+
ObjectAdoptedFunc: func(ctx context.Context, secret *corev1.Secret) {
358+
recorder.Eventf(secret, corev1.EventTypeNormal, EventSecretAdopted, "Secret was referenced by %s; it has been labelled to mark it as part of the configuration for that controller.", CtxOwnerNN.MustValue(ctx).String())
359+
},
360+
GetFromCache: getFromCache,
361+
Indexer: secretIndexer,
362+
IndexName: IndexName,
363+
Labels: map[string]string{ManagedLabelKey: ManagedLabelValue},
364+
NewPatch: func(nn types.NamespacedName) *applycorev1.SecretApplyConfiguration {
365+
return applycorev1.Secret(nn.Name, nn.Namespace)
366+
},
367+
OwnerAnnotationPrefix: OwnerAnnotationPrefix,
368+
OwnerAnnotationKeyFunc: func(owner types.NamespacedName) string {
369+
return OwnerAnnotationPrefix + owner.Name
370+
},
371+
OwnerFieldManagerFunc: func(owner types.NamespacedName) string {
372+
return "my-owner-" + owner.Namespace + "-" + owner.Name
373+
},
374+
ApplyFunc: secretApplyFunc,
375+
Next: next,
376+
}, "adoptSecret")
377+
}
378+
379+
func ExampleAdoptionHandler_Handle() {
380+
381+
}
382+
383+
func ExpectEvents(t *testing.T, recorder *record.FakeRecorder, expected []string) {
384+
close(recorder.Events)
385+
events := make([]string, 0)
386+
for e := range recorder.Events {
387+
events = append(events, e)
388+
}
389+
require.ElementsMatch(t, expected, events)
390+
}
391+
392+
func IndexAddUnstructured[K runtime.Object](t *testing.T, indexer cache.Indexer, objs []K) {
393+
for _, s := range objs {
394+
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
395+
require.NoError(t, err)
396+
require.NoError(t, indexer.Add(&unstructured.Unstructured{Object: u}))
397+
}
398+
}

0 commit comments

Comments
 (0)