You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/internal/controller/controller_test.go

741 lines
21 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// SPDX-License-Identifier: BUSL-1.1
package controller_test
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
controller "github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/controller/cache"
"github.com/hashicorp/consul/internal/controller/cache/index"
"github.com/hashicorp/consul/internal/controller/cache/indexers"
"github.com/hashicorp/consul/internal/controller/dependency"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/resource/resourcetest"
"github.com/hashicorp/consul/proto-public/pbresource"
pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1"
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
var injectedError = errors.New("injected error")
func errQuery(_ cache.ReadOnlyCache, _ ...any) (cache.ResourceIterator, error) {
return nil, injectedError
}
func TestController_API(t *testing.T) {
t.Parallel()
idx := indexers.DecodedSingleIndexer("genre", index.SingleValueFromArgs(func(value string) ([]byte, error) {
var b index.Builder
b.String(value)
return b.Bytes(), nil
}), func(res *resource.DecodedResource[*pbdemov2.Artist]) (bool, []byte, error) {
var b index.Builder
b.String(res.Data.Genre.String())
return true, b.Bytes(), nil
})
rec := newTestReconciler()
init := newTestInitializer(1)
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
concertsChan := make(chan controller.Event)
defer close(concertsChan)
concertSource := &controller.Source{Source: concertsChan}
concertMapper := func(ctx context.Context, rt controller.Runtime, event controller.Event) ([]controller.Request, error) {
artistID := event.Obj.(*Concert).artistID
var requests []controller.Request
requests = append(requests, controller.Request{ID: artistID})
return requests, nil
}
ctrl := controller.
NewController("artist", pbdemov2.ArtistType, idx).
WithWatch(pbdemov2.AlbumType, dependency.MapOwner, indexers.OwnerIndex("owner")).
WithQuery("some-query", errQuery).
WithCustomWatch(concertSource, concertMapper).
WithBackoff(10*time.Millisecond, 100*time.Millisecond).
WithReconciler(rec).
WithInitializer(init)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
mgr.SetRaftLeader(true)
go mgr.Run(testContext(t))
// Wait for initialization to complete
init.wait(t)
t.Run("managed resource type", func(t *testing.T) {
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
rt, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// ensure that the cache index is being properly managed
dec := resourcetest.MustDecode[*pbdemov2.Artist](t, res)
resources, err := rt.Cache.List(pbdemov2.ArtistType, "genre", dec.Data.Genre.String())
require.NoError(t, err)
prototest.AssertElementsMatch(t, []*pbresource.Resource{rsp.Resource}, resources)
// ensure that the query was successfully registered - as we should not do equality
// checks on functions we are using a constant error return query to ensure it was
// registered properly.
iter, err := rt.Cache.Query("some-query", "irrelevant")
require.ErrorIs(t, err, injectedError)
require.Nil(t, iter)
})
t.Run("watched resource type", func(t *testing.T) {
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
rec.expectNoRequest(t, 500*time.Millisecond)
album, err := demo.GenerateV2Album(rsp.Resource.Id)
require.NoError(t, err)
albumRsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: album})
require.NoError(t, err)
_, req = rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
album2, err := demo.GenerateV2Album(rsp.Resource.Id)
require.NoError(t, err)
albumRsp2, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: album2})
require.NoError(t, err)
rt, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// ensure that the watched type cache is being updated
resources, err := rt.Cache.List(pbdemov2.AlbumType, "owner", rsp.Resource.Id)
require.NoError(t, err)
prototest.AssertElementsMatch(t, []*pbresource.Resource{albumRsp1.Resource, albumRsp2.Resource}, resources)
})
t.Run("custom watched resource type", func(t *testing.T) {
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
rec.expectNoRequest(t, 500*time.Millisecond)
concertsChan <- controller.Event{Obj: &Concert{name: "test-concert", artistID: rsp.Resource.Id}}
_, watchedReq := rec.wait(t)
prototest.AssertDeepEqual(t, req.ID, watchedReq.ID)
otherArtist, err := demo.GenerateV2Artist()
require.NoError(t, err)
concertsChan <- controller.Event{Obj: &Concert{name: "test-concert", artistID: otherArtist.Id}}
_, watchedReq = rec.wait(t)
prototest.AssertDeepEqual(t, otherArtist.Id, watchedReq.ID)
})
t.Run("error retries", func(t *testing.T) {
rec.failNext(errors.New("KABOOM"))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Reconciler should be called with the same request again.
_, req = rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
t.Run("panic retries", func(t *testing.T) {
rec.panicNext("KABOOM")
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Reconciler should be called with the same request again.
_, req = rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
t.Run("defer", func(t *testing.T) {
rec.failNext(controller.RequeueAfter(1 * time.Second))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
rec.expectNoRequest(t, 750*time.Millisecond)
_, req = rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
}
func TestController_API_InitializeRetry(t *testing.T) {
t.Parallel()
// Configure initializer to error initially in order to test retries
expectedInitAttempts := 2
init := newTestInitializer(expectedInitAttempts)
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
rec := newTestReconciler()
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithBackoff(10*time.Millisecond, 100*time.Millisecond).
WithReconciler(rec).
WithInitializer(init)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
mgr.SetRaftLeader(true)
go mgr.Run(testContext(t))
// Wait for initialization attempts to complete
for i := 0; i < expectedInitAttempts; i++ {
init.wait(t)
}
// Create a resource and expect it to reconcile now that initialization is complete
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
rec.wait(t)
}
func waitForAtomicBoolValue(t testutil.TestingTB, actual *atomic.Bool, expected bool) {
t.Helper()
retry.Run(t, func(r *retry.R) {
require.Equal(r, expected, actual.Load())
})
}
func TestController_WithForceReconcileEvery_UpsertSuccess(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
// Given a controller
// When the controller reconciles a resource due to an upsert and succeeds
// Then the controller manager should scheduled a forced reconcile after forceReconcileEvery
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
// Create sizeable gap between reconcile #1 and forced reconcile #2 to ensure the delay occurs
forceReconcileEvery := 5 * time.Second
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithLogger(testutil.Logger(t)).
WithForceReconcileEvery(forceReconcileEvery).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
mgr.SetRaftLeader(true)
go mgr.Run(testContext(t))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
// Verify reconcile #1 happens immediately
_, req := rec.waitFor(t, time.Second)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Verify no reconciles occur between reconcile #1 and forced reconcile #2.
// Remove a second for max jitter (20% of 5s) and one more second to be safe.
rec.expectNoRequest(t, forceReconcileEvery-time.Second-time.Second)
// Verify forced reconcile #2 occurred (forceReconcileEvery - 1s - 1s + 3s > forceReconcileEvery)
_, req = rec.waitFor(t, time.Second*3)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
}
func TestController_WithForceReconcileEvery_SkipOnReconcileError(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
// Given a controller configured with a forceReconcileEvery duration
// When the controller reconciles a resource due to an upsert and returns an error
// Then the controller manager should not schedule a forced reconcile and allow
// the existing error handling to schedule a rate-limited retry
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
// Large enough gap to test for a period of no-reconciles
forceReconcileEvery := 5 * time.Second
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithLogger(testutil.Logger(t)).
WithForceReconcileEvery(forceReconcileEvery).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
mgr.SetRaftLeader(true)
go mgr.Run(testContext(t))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
// Setup reconcile #1 to fail
rec.failNext(errors.New("reconcile #1 error"))
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
// Observe failed reconcile #1
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Observe successful (rate-limited retry) reconcile #2. By not failNext'ing it,
// we're expecting it now'ish.
_, req = rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Observe no forced reconcile for gap after last successful reconcile
// -1s for 20% jitter reduction
// -1s for just to be safe
rec.expectNoRequest(t, forceReconcileEvery-time.Second-time.Second)
// Finally observe forced reconcile #3 up to 1 sec past (5-1-1+3) accumulated forceReconcileEvery delay
_, req = rec.waitFor(t, 3*time.Second)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
}
func TestController_WithForceReconcileEvery_SkipOnDelete(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
// Given a controller configured with a forceReconcileEvery duration
// When the controller reconciles a resource due to a delete and succeeds
// Then the controller manager should not schedule a forced reconcile
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
// Large enough gap to test for a period of no-reconciles
forceReconcileEvery := 5 * time.Second
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithLogger(testutil.Logger(t)).
WithForceReconcileEvery(forceReconcileEvery).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
mgr.SetRaftLeader(true)
go mgr.Run(testContext(t))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
// Account for reconcile #1 due to initial write
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
// Perform a delete
_, err = client.Delete(testContext(t), &pbresource.DeleteRequest{Id: rsp.Resource.Id})
require.NoError(t, err)
// Account for the reconcile #2 due to the delete
_, req = rec.wait(t)
// Account for the deferred forced reconcile #3 from the original write event since deferred
// reconciles don't seem to be de-duped against non-deferred reconciles.
_, req = rec.waitFor(t, forceReconcileEvery)
// Verify no further reconciles occur
rec.expectNoRequest(t, forceReconcileEvery)
}
func TestController_Placement(t *testing.T) {
t.Parallel()
t.Run("singleton", func(t *testing.T) {
var running atomic.Bool
running.Store(false)
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
WithCloningDisabled().
Run(t)
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithWatch(pbdemov2.AlbumType, dependency.MapOwner).
WithPlacement(controller.PlacementSingleton).
WithNotifyStart(func(context.Context, controller.Runtime) {
running.Store(true)
}).
WithNotifyStop(func(context.Context, controller.Runtime) {
running.Store(false)
}).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
go mgr.Run(testContext(t))
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
// Reconciler should not be called until we're the Raft leader.
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
rec.expectNoRequest(t, 500*time.Millisecond)
// Become the leader and check the reconciler is called.
mgr.SetRaftLeader(true)
waitForAtomicBoolValue(t, &running, true)
_, _ = rec.wait(t)
// Should not be called after losing leadership.
mgr.SetRaftLeader(false)
waitForAtomicBoolValue(t, &running, false)
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
rec.expectNoRequest(t, 500*time.Millisecond)
})
t.Run("each server", func(t *testing.T) {
var running atomic.Bool
running.Store(false)
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithWatch(pbdemov2.AlbumType, dependency.MapOwner).
WithPlacement(controller.PlacementEachServer).
WithNotifyStart(func(context.Context, controller.Runtime) {
running.Store(true)
}).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.Register(ctrl)
go mgr.Run(testContext(t))
waitForAtomicBoolValue(t, &running, true)
res, err := demo.GenerateV2Artist()
require.NoError(t, err)
// Reconciler should be called even though we're not the Raft leader.
_, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, _ = rec.wait(t)
})
}
func TestController_String(t *testing.T) {
ctrl := controller.
NewController("artist", pbdemov2.ArtistType).
WithWatch(pbdemov2.AlbumType, dependency.MapOwner).
WithBackoff(5*time.Second, 1*time.Hour).
WithPlacement(controller.PlacementEachServer)
require.Equal(t,
`<Controller managed_type=demo.v2.Artist, watched_types=[demo.v2.Album], backoff=<base=5s, max=1h0m0s>, placement=each-server>`,
ctrl.String(),
)
}
func TestController_NoReconciler(t *testing.T) {
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
mgr := controller.NewManager(client, testutil.Logger(t))
ctrl := controller.NewController("artist", pbdemov2.ArtistType)
require.PanicsWithValue(t,
fmt.Sprintf("cannot register controller without a reconciler %s", ctrl.String()),
func() { mgr.Register(ctrl) })
}
func TestController_Watch(t *testing.T) {
t.Parallel()
t.Run("partitioned scoped resources", func(t *testing.T) {
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
ctrl := controller.
NewController("labels", pbdemov1.RecordLabelType).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.SetRaftLeader(true)
mgr.Register(ctrl)
ctx := testContext(t)
go mgr.Run(ctx)
res, err := demo.GenerateV1RecordLabel("test")
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
t.Run("cluster scoped resources", func(t *testing.T) {
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
ctrl := controller.
NewController("executives", pbdemov1.ExecutiveType).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.SetRaftLeader(true)
mgr.Register(ctrl)
go mgr.Run(testContext(t))
exec, err := demo.GenerateV1Executive("test", "CEO")
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: exec})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
t.Run("namespace scoped resources", func(t *testing.T) {
rec := newTestReconciler()
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(demo.RegisterTypes).
Run(t)
ctrl := controller.
NewController("artists", pbdemov2.ArtistType).
WithReconciler(rec)
mgr := controller.NewManager(client, testutil.Logger(t))
mgr.SetRaftLeader(true)
mgr.Register(ctrl)
go mgr.Run(testContext(t))
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: artist})
require.NoError(t, err)
_, req := rec.wait(t)
prototest.AssertDeepEqual(t, rsp.Resource.Id, req.ID)
})
}
func newTestReconciler() *testReconciler {
return &testReconciler{
calls: make(chan requestArgs),
errors: make(chan error, 1),
panics: make(chan any, 1),
}
}
type requestArgs struct {
req controller.Request
rt controller.Runtime
}
type testReconciler struct {
calls chan requestArgs
errors chan error
panics chan any
}
func (r *testReconciler) Reconcile(_ context.Context, rt controller.Runtime, req controller.Request) error {
r.calls <- requestArgs{req: req, rt: rt}
select {
case err := <-r.errors:
return err
case p := <-r.panics:
panic(p)
default:
return nil
}
}
func (r *testReconciler) failNext(err error) {
r.errors <- err
}
func (r *testReconciler) panicNext(p any) {
r.panics <- p
}
func (r *testReconciler) expectNoRequest(t *testing.T, duration time.Duration) {
t.Helper()
started := time.Now()
select {
case args := <-r.calls:
t.Fatalf("expected no request for %s, but got: %s after %s", duration, args.req.ID, time.Since(started))
case <-time.After(duration):
}
}
func (r *testReconciler) wait(t *testing.T) (controller.Runtime, controller.Request) {
t.Helper()
return r.waitFor(t, 500*time.Millisecond)
}
func (r *testReconciler) waitFor(t *testing.T, duration time.Duration) (controller.Runtime, controller.Request) {
t.Helper()
var args requestArgs
select {
case args = <-r.calls:
case <-time.After(duration):
t.Fatalf("Reconcile was not called after %v", duration)
}
return args.rt, args.req
}
func testContext(t *testing.T) context.Context {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
return ctx
}
type Concert struct {
name string
artistID *pbresource.ID
}
func (c Concert) Key() string {
return c.name
}
func newTestInitializer(errorCount int) *testInitializer {
return &testInitializer{
calls: make(chan error, 1),
expectedAttempts: errorCount,
}
}
type testInitializer struct {
expectedAttempts int // number of times the initializer should run to test retries
attempts int // running count of times initialize is called
calls chan error
}
func (i *testInitializer) Initialize(_ context.Context, _ controller.Runtime) error {
i.attempts++
if i.attempts < i.expectedAttempts {
// Return an error to cause a retry
err := errors.New("initialization error")
i.calls <- err
return err
} else {
i.calls <- nil
return nil
}
}
func (i *testInitializer) wait(t *testing.T) {
t.Helper()
select {
case err := <-i.calls:
if err == nil {
// Initialize did not error, no more calls should be expected
close(i.calls)
}
return
case <-time.After(1000 * time.Millisecond):
t.Fatal("Initialize was not called after 1000ms")
}
}