diff --git a/pkg/readiness/list.go b/pkg/readiness/list.go index 514c8692eba..5b929c99d8a 100644 --- a/pkg/readiness/list.go +++ b/pkg/readiness/list.go @@ -82,6 +82,11 @@ func retryAll(_ error) bool { return true } +// retryNone is a retryPredicate that will never retry an error. +func retryNone(_ error) bool { + return false +} + // retryUnlessUnregistered is a retryPredicate that retries all errors except // *NoResourceMatchError, *NoKindMatchError, e.g. a resource was not registered to // the RESTMapper. diff --git a/pkg/readiness/ready_tracker.go b/pkg/readiness/ready_tracker.go index 6d61d0f3286..e5208d7c1f7 100644 --- a/pkg/readiness/ready_tracker.go +++ b/pkg/readiness/ready_tracker.go @@ -78,21 +78,23 @@ type Tracker struct { constraints *trackerMap data *trackerMap - ready chan struct{} - constraintTrackers *syncutil.SingleRunner - dataTrackers *syncutil.SingleRunner - statsEnabled syncutil.SyncBool - mutationEnabled bool - externalDataEnabled bool - expansionEnabled bool + ready chan struct{} + constraintTrackers *syncutil.SingleRunner + dataTrackers *syncutil.SingleRunner + statsEnabled syncutil.SyncBool + mutationEnabled bool + externalDataEnabled bool + expansionEnabled bool + crashOnFailure bool + trackListerPredicateOverride retryPredicate } // NewTracker creates a new Tracker and initializes the internal trackers. func NewTracker(lister Lister, mutationEnabled, externalDataEnabled, expansionEnabled bool) *Tracker { - return newTracker(lister, mutationEnabled, externalDataEnabled, expansionEnabled, nil) + return newTracker(lister, mutationEnabled, externalDataEnabled, expansionEnabled, *crashOnFailureFetchingExpectations, nil, nil) } -func newTracker(lister Lister, mutationEnabled, externalDataEnabled, expansionEnabled bool, fn objDataFactory) *Tracker { +func newTracker(lister Lister, mutationEnabled, externalDataEnabled, expansionEnabled bool, crashOnFailure bool, trackListerPredicateOverride retryPredicate, fn objDataFactory) *Tracker { tracker := Tracker{ lister: lister, templates: newObjTracker(v1beta1.SchemeGroupVersion.WithKind("ConstraintTemplate"), fn), @@ -104,9 +106,11 @@ func newTracker(lister Lister, mutationEnabled, externalDataEnabled, expansionEn constraintTrackers: &syncutil.SingleRunner{}, dataTrackers: &syncutil.SingleRunner{}, - mutationEnabled: mutationEnabled, - externalDataEnabled: externalDataEnabled, - expansionEnabled: expansionEnabled, + mutationEnabled: mutationEnabled, + externalDataEnabled: externalDataEnabled, + expansionEnabled: expansionEnabled, + crashOnFailure: crashOnFailure, + trackListerPredicateOverride: trackListerPredicateOverride, } if mutationEnabled { tracker.assignMetadata = newObjTracker(mutationv1.GroupVersion.WithKind("AssignMetadata"), fn) @@ -325,7 +329,7 @@ func (t *Tracker) Run(ctx context.Context) error { var grp *errgroup.Group grp = &errgroup.Group{} gctx := ctx - if *crashOnFailureFetchingExpectations { + if t.crashOnFailure { grp, gctx = errgroup.WithContext(ctx) } t.constraintTrackers = syncutil.RunnerWithContext(gctx) @@ -365,14 +369,11 @@ func (t *Tracker) Run(ctx context.Context) error { return t.trackConfigAndSyncSets(gctx) }) - grp.Go(func() error { - t.statsPrinter(ctx) - return nil - }) + go t.statsPrinter(ctx) // start deleted object polling. Periodically collects // objects that are expected by the Tracker, but are deleted - grp.Go(func() error { + go func() error { // wait before proceeding, hoping // that the tracker will be satisfied by then timer := time.NewTimer(2000 * time.Millisecond) @@ -396,17 +397,18 @@ func (t *Tracker) Run(ctx context.Context) error { t.collectInvalidExpectations(ctx) } } - }) + }() - if err := grp.Wait(); err != nil && *crashOnFailureFetchingExpectations { + if err := grp.Wait(); err != nil && t.crashOnFailure { return err } - if err := t.constraintTrackers.Wait(); err != nil && *crashOnFailureFetchingExpectations { + // constraintTrackers & dataTrackers Wait() must appear after grp.Wait() - allows trackConstraintTemplates() & trackConfigAndSyncSets() time to schedule their sub-tasks. + if err := t.constraintTrackers.Wait(); err != nil && t.crashOnFailure { return err } - if err := t.dataTrackers.Wait(); err != nil && *crashOnFailureFetchingExpectations { + if err := t.dataTrackers.Wait(); err != nil && t.crashOnFailure { return err } @@ -563,7 +565,7 @@ func (t *Tracker) trackAssignMetadata(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.assignMetadata.ExpectationsDone() log.V(logging.DebugLevel).Info("AssignMetadata expectations populated") } @@ -574,7 +576,11 @@ func (t *Tracker) trackAssignMetadata(ctx context.Context) error { } assignMetadataList := &mutationv1.AssignMetadataList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, assignMetadataList); err != nil { hadError = true log.Error(err, "listing AssignMetadata") @@ -593,7 +599,7 @@ func (t *Tracker) trackAssign(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.assign.ExpectationsDone() log.V(logging.DebugLevel).Info("Assign expectations populated") } @@ -604,7 +610,11 @@ func (t *Tracker) trackAssign(ctx context.Context) error { } assignList := &mutationv1.AssignList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, assignList); err != nil { hadError = true log.Error(err, "listing Assign") @@ -623,7 +633,7 @@ func (t *Tracker) trackModifySet(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.modifySet.ExpectationsDone() log.V(logging.DebugLevel).Info("ModifySet expectations populated") } @@ -634,7 +644,11 @@ func (t *Tracker) trackModifySet(ctx context.Context) error { } modifySetList := &mutationv1.ModifySetList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, modifySetList); err != nil { hadError = true log.Error(err, "listing ModifySet") @@ -653,7 +667,7 @@ func (t *Tracker) trackAssignImage(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.assignImage.ExpectationsDone() log.V(logging.DebugLevel).Info("AssignImage expectations populated") } @@ -664,7 +678,11 @@ func (t *Tracker) trackAssignImage(ctx context.Context) error { } assignImageList := &mutationsv1alpha1.AssignImageList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, assignImageList); err != nil { hadError = true log.Error(err, "listing AssignImage") @@ -683,7 +701,7 @@ func (t *Tracker) trackExpansionTemplates(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.expansions.ExpectationsDone() log.V(logging.DebugLevel).Info("ExpansionTemplate expectations populated") } @@ -694,7 +712,11 @@ func (t *Tracker) trackExpansionTemplates(ctx context.Context) error { } expansionList := &expansionv1alpha1.ExpansionTemplateList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, expansionList); err != nil { hadError = true log.Error(err, "listing ExpansionTemplate") @@ -713,7 +735,7 @@ func (t *Tracker) trackExternalDataProvider(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.externalDataProvider.ExpectationsDone() log.V(logging.DebugLevel).Info("Provider expectations populated") } @@ -724,7 +746,11 @@ func (t *Tracker) trackExternalDataProvider(ctx context.Context) error { } providerList := &externaldatav1beta1.ProviderList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, providerList); err != nil { hadError = true log.Error(err, "listing Provider") @@ -743,14 +769,18 @@ func (t *Tracker) trackConstraintTemplates(ctx context.Context) error { hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { t.templates.ExpectationsDone() log.V(logging.DebugLevel).Info("template expectations populated") } }() templates := &v1beta1.ConstraintTemplateList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, templates); err != nil { hadError = true log.Error(err, "listing templates") @@ -784,7 +814,7 @@ func (t *Tracker) trackConstraintTemplates(ctx context.Context) error { err := t.trackConstraints(ctx, gvk, ot) if err != nil { log.Error(err, "aborted trackConstraints", "gvk", gvk) - if *crashOnFailureFetchingExpectations { + if t.crashOnFailure { return err } } @@ -801,7 +831,7 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { var retErr error defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || retErr == nil { + if !t.crashOnFailure || retErr == nil { t.config.ExpectationsDone() log.V(logging.DebugLevel).Info("config expectations populated") @@ -813,9 +843,9 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { dataGVKs := make(map[schema.GroupVersionKind]struct{}) cfg, err := t.getConfigResource(ctx) if err != nil { - log.Error(err, "fetching config resource") - retErr = fmt.Errorf("fetching config resource: %w", err) - if *crashOnFailureFetchingExpectations { + log.Error(err, "fetching config resources") + retErr = fmt.Errorf("fetching config resources: %w", err) + if t.crashOnFailure { return retErr } } else { @@ -841,11 +871,15 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { } syncsets := &syncsetv1alpha1.SyncSetList{} - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, syncsets); err != nil { log.Error(err, "fetching syncset resources") retErr = fmt.Errorf("fetching syncset resources: %w", err) - if *crashOnFailureFetchingExpectations { + if t.crashOnFailure { return retErr } } else { @@ -878,7 +912,7 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { err := t.trackData(ctx, gvkCpy, dt) if err != nil { log.Error(err, "aborted trackData", "gvk", gvkCpy) - if *crashOnFailureFetchingExpectations { + if t.crashOnFailure { return err } } @@ -886,10 +920,6 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { }) } - if retErr == nil { - return nil - } - return retErr } @@ -897,8 +927,13 @@ func (t *Tracker) trackConfigAndSyncSets(ctx context.Context) error { // Returns a nil reference if it is not found. func (t *Tracker) getConfigResource(ctx context.Context) (*configv1alpha1.Config, error) { lst := &configv1alpha1.ConfigList{} - lister := retryLister(t.lister, nil) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, lst); err != nil { + log.Error(err, "listing config") return nil, fmt.Errorf("listing config: %w", err) } @@ -922,7 +957,7 @@ func (t *Tracker) trackData(ctx context.Context, gvk schema.GroupVersionKind, dt hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { dt.ExpectationsDone() log.V(logging.DebugLevel).Info("data expectations populated", "gvk", gvk) } @@ -936,12 +971,16 @@ func (t *Tracker) trackData(ctx context.Context, gvk schema.GroupVersionKind, dt Kind: gvk.Kind + "List", }) // NoKindMatchError is non-recoverable, otherwise we'll retry. - lister := retryLister(t.lister, retryUnlessUnregistered) + retryPred := retryUnlessUnregistered + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) err := lister.List(ctx, u) if err != nil { hadError = true log.Error(err, "listing data", "gvk", gvk) - return err + return fmt.Errorf("listing data: %w", err) } for i := range u.Items { @@ -958,7 +997,7 @@ func (t *Tracker) trackConstraints(ctx context.Context, gvk schema.GroupVersionK hadError := false defer func() { // If we are ignoring errors when tracking expecations, we need to set expectations to done to prevent readiness tracker never being satisfied - if !*crashOnFailureFetchingExpectations || !hadError { + if !t.crashOnFailure || !hadError { constraints.ExpectationsDone() log.V(logging.DebugLevel).Info("constraint expectations populated", "gvk", gvk) } @@ -966,11 +1005,15 @@ func (t *Tracker) trackConstraints(ctx context.Context, gvk schema.GroupVersionK u := unstructured.UnstructuredList{} u.SetGroupVersionKind(gvk) - lister := retryLister(t.lister, retryAll) + retryPred := retryAll + if t.trackListerPredicateOverride != nil { + retryPred = t.trackListerPredicateOverride + } + lister := retryLister(t.lister, retryPred) if err := lister.List(ctx, &u); err != nil { hadError = true log.Error(err, "listing constraints") - return err + return fmt.Errorf("listing constraints: %w", err) } for i := range u.Items { diff --git a/pkg/readiness/ready_tracker_unit_test.go b/pkg/readiness/ready_tracker_unit_test.go index 8cc084440ad..c8157d9f97b 100644 --- a/pkg/readiness/ready_tracker_unit_test.go +++ b/pkg/readiness/ready_tracker_unit_test.go @@ -18,18 +18,28 @@ package readiness import ( "context" "fmt" + "strings" "sync" "testing" "time" + externaldatav1beta1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/externaldata/v1beta1" "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1beta1" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + configv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/config/v1alpha1" + expansionv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/expansion/v1alpha1" + mutationv1 "github.com/open-policy-agent/gatekeeper/v3/apis/mutations/v1" + mutationsv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/mutations/v1alpha1" syncsetv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/syncset/v1alpha1" "github.com/open-policy-agent/gatekeeper/v3/pkg/fakes" + "github.com/open-policy-agent/gatekeeper/v3/pkg/syncutil" "github.com/stretchr/testify/require" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) const ( @@ -64,6 +74,67 @@ var ( }, } + testConfig = configv1alpha1.Config{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-config", + }, + Spec: configv1alpha1.ConfigSpec{}, + } + + testAssignMetadata = mutationsv1alpha1.AssignMetadata{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-assign-metadata", + }, + Spec: mutationsv1alpha1.AssignMetadataSpec{ + Location: "", + }, + } + + testAssign = mutationsv1alpha1.Assign{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-assign", + }, + Spec: mutationsv1alpha1.AssignSpec{ + Location: "", + }, + } + + testModifySet = mutationsv1alpha1.ModifySet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-modify-set", + }, + Spec: mutationsv1alpha1.ModifySetSpec{ + Location: "", + }, + } + + testAssignImage = mutationsv1alpha1.AssignImage{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-assign-image", + }, + Spec: mutationsv1alpha1.AssignImageSpec{ + Location: "", + }, + } + + testExternalDataProvider = externaldatav1beta1.Provider{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-external-data-provider", + }, + Spec: externaldatav1beta1.ProviderSpec{ + URL: "", + }, + } + + testExpansionTemplate = expansionv1alpha1.ExpansionTemplate{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-expansion-templates", + }, + Spec: expansionv1alpha1.ExpansionTemplateSpec{ + TemplateSource: "", + }, + } + podGVK = schema.GroupVersionKind{Version: "v1", Kind: "Pod"} ) @@ -75,10 +146,22 @@ func init() { } } +func getTestConstraint() *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetName("test-constraint") + gvk := schema.GroupVersionKind{ + Group: "constraints.gatekeeper.sh", + Version: "v1beta1", + Kind: "FooKind", + } + u.SetGroupVersionKind(gvk) + return u +} + // Verify that TryCancelTemplate functions the same as regular CancelTemplate if readinessRetries is set to 0. func Test_ReadyTracker_TryCancelTemplate_No_Retries(t *testing.T) { lister := fake.NewClientBuilder().WithRuntimeObjects(convertedTemplate.DeepCopyObject()).Build() - rt := newTracker(lister, false, false, false, func() objData { + rt := newTracker(lister, false, false, false, false, nil, func() objData { return objData{retries: 0} }) @@ -118,7 +201,7 @@ func Test_ReadyTracker_TryCancelTemplate_No_Retries(t *testing.T) { // Verify that TryCancelTemplate must be called enough times to remove all retries before canceling a template. func Test_ReadyTracker_TryCancelTemplate_Retries(t *testing.T) { lister := fake.NewClientBuilder().WithRuntimeObjects(convertedTemplate.DeepCopyObject()).Build() - rt := newTracker(lister, false, false, false, func() objData { + rt := newTracker(lister, false, false, false, false, nil, func() objData { return objData{retries: 2} }) @@ -184,7 +267,7 @@ func Test_Tracker_TryCancelData(t *testing.T) { objDataFn := func() objData { return objData{retries: tc.retries} } - rt := newTracker(lister, false, false, false, objDataFn) + rt := newTracker(lister, false, false, false, false, nil, objDataFn) ctx, cancel := context.WithCancel(context.Background()) var runErr error @@ -225,3 +308,646 @@ func Test_Tracker_TryCancelData(t *testing.T) { }) } } + +func Test_ReadyTracker_TrackAssignMetadata(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackAssignMetadata fail close", + failClose: true, + }, + { + name: "TrackAssignMetadata fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*mutationv1.AssignMetadataList); ok { + return fmt.Errorf("Force Test AssignMetadataList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testAssignMetadata).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, true, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackAssignMetadata(ctx) + cancel() + if err == nil { + t.Fatal("trackAssignMetadata should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.assignMetadata.Populated() != expectPopulated { + t.Fatalf("assignMetadata object tracker's populated field is marked as %v but should be %v", rt.assignMetadata.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackAssign(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackAssign fail close", + failClose: true, + }, + { + name: "TrackAssign fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*mutationv1.AssignList); ok { + return fmt.Errorf("Force Test AssignList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testAssign).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, true, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackAssign(ctx) + cancel() + if err == nil { + t.Fatal("trackAssign should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.assign.Populated() != expectPopulated { + t.Fatalf("assign object tracker's populated field is marked as %v but should be %v", rt.assign.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackModifySet(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackModifySet fail close", + failClose: true, + }, + { + name: "TrackModifySet fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*mutationv1.ModifySetList); ok { + return fmt.Errorf("Force Test TrackModifySetList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testModifySet).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, true, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackModifySet(ctx) + cancel() + if err == nil { + t.Fatal("trackModifySet should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.modifySet.Populated() != expectPopulated { + t.Fatalf("modifySet object tracker's populated field is marked as %v but should be %v", rt.modifySet.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackAssignImage(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackAssignImage fail close", + failClose: true, + }, + { + name: "TrackAssignImage fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*mutationsv1alpha1.AssignImageList); ok { + return fmt.Errorf("Force Test AssignImageList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testAssignImage).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, true, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackAssignImage(ctx) + cancel() + if err == nil { + t.Fatal("trackAssignImage should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.assignImage.Populated() != expectPopulated { + t.Fatalf("assignImage object tracker's populated field is marked as %v but should be %v", rt.assignImage.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackExternalDataProvider(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackExternalDataProvider fail close", + failClose: true, + }, + { + name: "TrackExternalDataProvider fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*externaldatav1beta1.ProviderList); ok { + return fmt.Errorf("Force Test ProviderList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testExternalDataProvider).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, true, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackExternalDataProvider(ctx) + cancel() + if err == nil { + t.Fatal("trackExternalDataProvider should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.externalDataProvider.Populated() != expectPopulated { + t.Fatalf("externalDataProvider object tracker's populated field is marked as %v but should be %v", rt.externalDataProvider.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackExpansionTemplates(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackExpansionTemplates fail close", + failClose: true, + }, + { + name: "TrackExpansionTemplates fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*expansionv1alpha1.ExpansionTemplateList); ok { + return fmt.Errorf("Force Test ExpansionTemplateList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testExpansionTemplate).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, true, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + err := rt.trackExpansionTemplates(ctx) + cancel() + if err == nil { + t.Fatal("trackExpansionTemplates should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.expansions.Populated() != expectPopulated { + t.Fatalf("expansions object tracker's populated field is marked as %v but should be %v", rt.expansions.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackConstraintTemplates(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackConstraintTemplates fail close", + failClose: true, + }, + { + name: "TrackConstraintTemplates fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*v1beta1.ConstraintTemplateList); ok { + return fmt.Errorf("Force Test ConstraintTemplateList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(convertedTemplate.DeepCopyObject()).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + rt.constraintTrackers = syncutil.RunnerWithContext(ctx) + err := rt.trackConstraintTemplates(ctx) + cancel() + if err == nil { + t.Fatal("trackConstraintTemplates should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.templates.Populated() != expectPopulated { + t.Fatalf("templates object tracker's populated field is marked as %v but should be %v", rt.templates.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackConfigAndSyncSets(t *testing.T) { + tcs := []struct { + name string + configForceErr bool + syncsetForceErr bool + failClose bool + }{ + { + name: "TrackConfigAndSyncSets config err fail close", + configForceErr: true, + failClose: true, + }, + { + name: "TrackConfigAndSyncSets config err fail open", + configForceErr: true, + failClose: false, + }, + { + name: "TrackConfigAndSyncSets syncset err fail close", + syncsetForceErr: true, + failClose: true, + }, + { + name: "TrackConfigAndSyncSets syncset err fail open", + syncsetForceErr: true, + failClose: false, + }, + { + name: "TrackConfigAndSyncSets both err fail close", + configForceErr: true, + syncsetForceErr: true, + failClose: true, + }, + { + name: "TrackConfigAndSyncSets both err fail open", + configForceErr: true, + syncsetForceErr: true, + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*configv1alpha1.ConfigList); ok && tc.configForceErr { + return fmt.Errorf("Force Test ConfigList Failure") + } + + if _, ok := list.(*syncsetv1alpha1.SyncSetList); ok && tc.syncsetForceErr { + return fmt.Errorf("Force Test ConfigList Failure") + } + + return client.List(ctx, list, opts...) + } + lister := fake.NewClientBuilder().WithRuntimeObjects(&testSyncSet, &testConfig).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + rt.dataTrackers = syncutil.RunnerWithContext(ctx) + err := rt.trackConfigAndSyncSets(ctx) + cancel() + if err == nil { + t.Fatal("trackConfigAndSyncSets should have returned an error") + } + if tc.failClose { + expectPopulated := !tc.configForceErr && !tc.syncsetForceErr + if rt.config.Populated() != expectPopulated || rt.syncsets.Populated() != expectPopulated { + t.Fatalf("config & syncset object trackers' populated fields are marked as config: %v & syncset: %v, but both should be %v", rt.config.Populated(), rt.syncsets.Populated(), expectPopulated) + } + } else { + if !rt.config.Populated() || !rt.syncsets.Populated() { + t.Fatalf("config & syncset object trackers' populated fields are marked as config: %v & syncset: %v, but both should be true", rt.config.Populated(), rt.syncsets.Populated()) + } + } + }) + } +} + +func Test_ReadyTracker_TrackConstraint(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackConstraint fail close", + failClose: true, + }, + { + name: "TrackConstraint fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if v, ok := list.(*unstructured.UnstructuredList); ok && v.GroupVersionKind().Group == "constraints.gatekeeper.sh" { + return fmt.Errorf("Force Test constraint list Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(getTestConstraint()).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + gvk := schema.GroupVersionKind{ + Group: constraintGroup, + Version: v1beta1.SchemeGroupVersion.Version, + Kind: "FooKind", + } + ot := rt.constraints.Get(gvk) + err := rt.trackConstraints(ctx, gvk, ot) + cancel() + if err == nil { + t.Fatal("trackConstraints should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.constraints.Get(gvk).Populated() != expectPopulated { + t.Fatalf("constraints(%v) object tracker's populated field is marked as %v but should be %v", gvk, rt.templates.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_TrackData(t *testing.T) { + tcs := []struct { + name string + failClose bool + }{ + { + name: "TrackData fail close", + failClose: true, + }, + { + name: "TrackData fail open", + failClose: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if v, ok := list.(*unstructured.UnstructuredList); ok && v.GroupVersionKind().Kind == "PodList" { + return fmt.Errorf("Force Test data list Failure") + } + + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(fakes.UnstructuredFor(podGVK, "", "pod1-name")).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, false, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + ctx, cancel := context.WithCancel(context.Background()) + gvk := testSyncSet.Spec.GVKs[0].ToGroupVersionKind() + ot := rt.data.Get(gvk) + err := rt.trackData(ctx, gvk, ot) + cancel() + if err == nil { + t.Fatal("trackAssignImage should have returned an error") + } + + expectPopulated := !tc.failClose + if rt.data.Get(gvk).Populated() != expectPopulated { + t.Fatalf("data(%v) object tracker's populated field is marked as %v but should be %v", gvk, rt.templates.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_Run_GRP_Wait(t *testing.T) { + tcs := []struct { + name string + expectedErr string + failClose bool + }{ + { + name: "Ready Tracker Run GRP.Wait() fail close", + expectedErr: "listing templates", + failClose: true, + }, + { + name: "Ready Tracker Run GRP.Wait() fail open", + failClose: false, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if _, ok := list.(*v1beta1.ConstraintTemplateList); ok { + return fmt.Errorf("Force Test ConstraintTemplateList Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testExpansionTemplate, convertedTemplate.DeepCopyObject(), getTestConstraint(), &testSyncSet, fakes.UnstructuredFor(podGVK, "", "pod1-name")).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, true, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + // Run kicks off all the tracking + ctx, cancel := context.WithCancel(context.Background()) + err := rt.Run(ctx) + cancel() + expectError := tc.failClose + gotError := (err != nil) + if gotError != expectError || gotError && !strings.Contains(err.Error(), tc.expectedErr) { + t.Fatalf("Run should have returned an error with %v, but got %v", tc.expectedErr, err) + } + + expectPopulated := !tc.failClose + if rt.Populated() != expectPopulated { + t.Fatalf("templates object tracker's populated field is marked as %v but should be %v", rt.templates.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_Run_ConstraintTrackers_Wait(t *testing.T) { + tcs := []struct { + name string + expectedErr string + failClose bool + }{ + { + name: "Ready Tracker Run GRP.Wait() fail close", + expectedErr: "listing constraints", + failClose: true, + }, + { + name: "Ready Tracker Run GRP.Wait() fail open", + failClose: false, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if v, ok := list.(*unstructured.UnstructuredList); ok && v.GroupVersionKind().Kind == "test-constraint" { + return fmt.Errorf("Force Test constraint list Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testExpansionTemplate, convertedTemplate.DeepCopyObject(), getTestConstraint(), &testSyncSet, fakes.UnstructuredFor(podGVK, "", "pod1-name")).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, true, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + // Run kicks off all the tracking + ctx, cancel := context.WithCancel(context.Background()) + err := rt.Run(ctx) + cancel() + expectError := tc.failClose + gotError := (err != nil) + if gotError != expectError || gotError && !strings.Contains(err.Error(), tc.expectedErr) { + t.Fatalf("Run should have returned an error with %v, but got %v", tc.expectedErr, err) + } + + expectPopulated := !tc.failClose + if rt.Populated() != expectPopulated { + t.Fatalf("templates object tracker's populated field is marked as %v but should be %v", rt.templates.Populated(), expectPopulated) + } + }) + } +} + +func Test_ReadyTracker_Run_DataTrackers_Wait(t *testing.T) { + tcs := []struct { + name string + expectedErr string + failClose bool + }{ + { + name: "Ready Tracker Run GRP.Wait() fail close", + expectedErr: "listing data", + failClose: true, + }, + { + name: "Ready Tracker Run GRP.Wait() fail open", + failClose: false, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + funcs := &interceptor.Funcs{} + funcs.List = func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { + if v, ok := list.(*unstructured.UnstructuredList); ok && v.GroupVersionKind().Kind == "PodList" { + return fmt.Errorf("Force Test pod list Failure") + } + return client.List(ctx, list, opts...) + } + + lister := fake.NewClientBuilder().WithRuntimeObjects(&testExpansionTemplate, convertedTemplate.DeepCopyObject(), getTestConstraint(), &testSyncSet, fakes.UnstructuredFor(podGVK, "", "pod1-name")).WithInterceptorFuncs(*funcs).Build() + rt := newTracker(lister, false, false, true, tc.failClose, retryNone, func() objData { + return objData{retries: 0} + }) + + // Run kicks off all the tracking + ctx, cancel := context.WithCancel(context.Background()) + err := rt.Run(ctx) + cancel() + expectError := tc.failClose + gotError := (err != nil) + if gotError != expectError || gotError && !strings.Contains(err.Error(), tc.expectedErr) { + t.Fatalf("Run should have returned an error with %v, but got %v", tc.expectedErr, err) + } + + expectPopulated := !tc.failClose + if rt.Populated() != expectPopulated { + t.Fatalf("templates object tracker's populated field is marked as %v but should be %v", rt.templates.Populated(), expectPopulated) + } + }) + } +}