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..d231666b215 100644 --- a/pkg/readiness/ready_tracker.go +++ b/pkg/readiness/ready_tracker.go @@ -45,7 +45,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" ) -var crashOnFailureFetchingExpectations = flag.Bool("crash-on-failure-fetching-expectations", false, "When set (defaults to false), gatekeeper will ignore errors that occur when gathering expectations. This prevents bootstrapping errors from crashing Gatekeeper at the cost of increasing the risk Gatekeeper will under-enforce policy by serving before it has loaded in all policies. Enabling this will help prevent under-enforcement at the risk of crashing during startup for issues like network errors.") +var crashOnFailureFetchingExpectations = flag.Bool("crash-on-failure-fetching-expectations", false, "When set (defaults to false), gatekeeper will ignore errors that occur when gathering expectations. This prevents bootstrapping errors from crashing Gatekeeper at the cost of increasing the risk Gatekeeper will under-enforce policy by serving before it has loaded in all policies. Enabling this will help prevent under-enforcement at the risk of crashing during startup for issues like network errors. Note that enabling this flag currently does not achieve the aforementioned effect since fetching expectations are set to retry until success so failures during fetching expectations currently do not occurr.") var log = logf.Log.WithName("readiness-tracker") @@ -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) + } + }) + } +}