diff --git a/CHANGELOG.md b/CHANGELOG.md index c77e0f9d8c..9bc47c642c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,10 +127,11 @@ Adding a new version? You'll need three changes: updates. The controller will now send configuration to such Gateways and will actively monitor their readiness for accepting configuration updates. [#4368](https://github.com/Kong/kubernetes-ingress-controller/pull/4368 -- `KongConsumer` CRD was extended with `Status.Conditions` field. It will now - contain the `Programmed` condition describing whether an object was successfully - translated into Kong entities and sent to Kong. - [#4409](https://github.com/Kong/kubernetes-ingress-controller/pull/4409 +- `KongConsumer`, `KongPlugin`, and `KongClusterPlugin` CRDs were extended with + `Status.Conditions` field. It will contain the `Programmed` condition describing + whether an object was successfully translated into Kong entities and sent to Kong. + [#4409](https://github.com/Kong/kubernetes-ingress-controller/pull/4409) + [#4412](https://github.com/Kong/kubernetes-ingress-controller/pull/4412) ### Changed @@ -184,6 +185,7 @@ Adding a new version? You'll need three changes: This was caused by a bug in `NodeAgent` that was sending the updates despite the fact that the configuration status was not changed. [#4324](https://github.com/Kong/kubernetes-ingress-controller/pull/4324) + ## [2.10.2] > Release date: 2023-07-07 diff --git a/hack/generators/controllers/networking/main.go b/hack/generators/controllers/networking/main.go index c0a373b46f..18a2b12c06 100644 --- a/hack/generators/controllers/networking/main.go +++ b/hack/generators/controllers/networking/main.go @@ -114,6 +114,8 @@ var inputControllersNeeded = &typesNeeded{ Plural: "kongplugins", CacheType: "Plugin", NeedsStatusPermissions: true, + ConfigStatusNotificationsEnabled: true, + ProgrammedConditionUpdatesEnabled: true, AcceptsIngressClassNameAnnotation: false, AcceptsIngressClassNameSpec: false, NeedsUpdateReferences: true, @@ -129,6 +131,8 @@ var inputControllersNeeded = &typesNeeded{ Plural: "kongclusterplugins", CacheType: "ClusterPlugin", NeedsStatusPermissions: true, + ConfigStatusNotificationsEnabled: true, + ProgrammedConditionUpdatesEnabled: true, AcceptsIngressClassNameAnnotation: true, AcceptsIngressClassNameSpec: false, NeedsUpdateReferences: true, diff --git a/internal/controllers/configuration/zz_generated_controllers.go b/internal/controllers/configuration/zz_generated_controllers.go index 7ae33db7d2..8982fdb02c 100644 --- a/internal/controllers/configuration/zz_generated_controllers.go +++ b/internal/controllers/configuration/zz_generated_controllers.go @@ -619,6 +619,7 @@ type KongV1KongPluginReconciler struct { Scheme *runtime.Scheme DataplaneClient controllers.DataPlane CacheSyncTimeout time.Duration + StatusQueue *status.Queue ReferenceIndexers ctrlref.CacheIndexers } @@ -636,6 +637,19 @@ func (r *KongV1KongPluginReconciler) SetupWithManager(mgr ctrl.Manager) error { if err != nil { return err } + // if configured, start the status updater controller + if r.StatusQueue != nil { + if err := c.Watch( + &source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Group: "configuration.konghq.com", + Version: "v1", + Kind: "KongPlugin", + })}, + &handler.EnqueueRequestForObject{}, + ); err != nil { + return err + } + } return c.Watch( source.Kind(mgr.GetCache(), &kongv1.KongPlugin{}), &handler.EnqueueRequestForObject{}, @@ -698,6 +712,17 @@ func (r *KongV1KongPluginReconciler) Reconcile(ctx context.Context, req ctrl.Req if err := r.DataplaneClient.UpdateObject(obj); err != nil { return ctrl.Result{}, err } + // if status updates are enabled report the status for the object + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + log.V(util.DebugLevel).Info("updating programmed condition status", "namespace", req.Namespace, "name", req.Name) + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + log.V(util.DebugLevel).Info("status update not needed", "namespace", req.Namespace, "name", req.Name) + } // update reference relationship from the KongPlugin to other objects. if err := updateReferredObjects(ctx, r.Client, r.ReferenceIndexers, r.DataplaneClient, obj); err != nil { if apierrors.IsNotFound(err) { @@ -724,6 +749,7 @@ type KongV1KongClusterPluginReconciler struct { Scheme *runtime.Scheme DataplaneClient controllers.DataPlane CacheSyncTimeout time.Duration + StatusQueue *status.Queue IngressClassName string DisableIngressClassLookups bool @@ -744,6 +770,19 @@ func (r *KongV1KongClusterPluginReconciler) SetupWithManager(mgr ctrl.Manager) e if err != nil { return err } + // if configured, start the status updater controller + if r.StatusQueue != nil { + if err := c.Watch( + &source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Group: "configuration.konghq.com", + Version: "v1", + Kind: "KongClusterPlugin", + })}, + &handler.EnqueueRequestForObject{}, + ); err != nil { + return err + } + } if !r.DisableIngressClassLookups { err = c.Watch( source.Kind(mgr.GetCache(), &netv1.IngressClass{}), @@ -859,6 +898,17 @@ func (r *KongV1KongClusterPluginReconciler) Reconcile(ctx context.Context, req c if err := r.DataplaneClient.UpdateObject(obj); err != nil { return ctrl.Result{}, err } + // if status updates are enabled report the status for the object + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + log.V(util.DebugLevel).Info("updating programmed condition status", "namespace", req.Namespace, "name", req.Name) + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + log.V(util.DebugLevel).Info("status update not needed", "namespace", req.Namespace, "name", req.Name) + } // update reference relationship from the KongClusterPlugin to other objects. if err := updateReferredObjects(ctx, r.Client, r.ReferenceIndexers, r.DataplaneClient, obj); err != nil { if apierrors.IsNotFound(err) { diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go index e49257cac1..3305b9bd97 100644 --- a/internal/dataplane/kongstate/kongstate.go +++ b/internal/dataplane/kongstate/kongstate.go @@ -280,7 +280,7 @@ func buildPlugins(log logrus.FieldLogger, s store.Storer, pluginRels map[string] } for _, rel := range relations.GetCombinations() { - plugin := *plugin.DeepCopy() + plugin := plugin.DeepCopy() // ID is populated because that is read by decK and in_memory // translator too if rel.Service != "" { @@ -292,7 +292,7 @@ func buildPlugins(log logrus.FieldLogger, s store.Storer, pluginRels map[string] if rel.Consumer != "" { plugin.Consumer = &kong.Consumer{ID: kong.String(rel.Consumer)} } - plugins = append(plugins, Plugin{plugin}) + plugins = append(plugins, plugin) } } @@ -347,9 +347,7 @@ func globalPlugins(log logrus.FieldLogger, s store.Storer) ([]Plugin, error) { continue } if plugin, err := kongPluginFromK8SClusterPlugin(s, k8sPlugin); err == nil { - res[pluginName] = Plugin{ - Plugin: plugin, - } + res[pluginName] = plugin } else { log.WithFields(logrus.Fields{ "kongclusterplugin_name": k8sPlugin.Name, diff --git a/internal/dataplane/kongstate/types.go b/internal/dataplane/kongstate/types.go index 0536184590..8aabe0ab9f 100644 --- a/internal/dataplane/kongstate/types.go +++ b/internal/dataplane/kongstate/types.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/kong/go-kong/kong" + "sigs.k8s.io/controller-runtime/pkg/client" ) type PortMode int @@ -78,4 +79,12 @@ func (c *Certificate) SanitizedCopy() *Certificate { // Plugin represents a plugin Object in Kong. type Plugin struct { kong.Plugin + K8sParent client.Object +} + +func (p Plugin) DeepCopy() Plugin { + return Plugin{ + Plugin: *p.Plugin.DeepCopy(), + K8sParent: p.K8sParent, + } } diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go index 61ddccfa00..af4df6850f 100644 --- a/internal/dataplane/kongstate/util.go +++ b/internal/dataplane/kongstate/util.go @@ -79,8 +79,8 @@ func getKongIngressFromObjAnnotations( } // getPlugin constructs a plugins from a KongPlugin resource. -func getPlugin(s store.Storer, namespace, name string) (kong.Plugin, error) { - var plugin kong.Plugin +func getPlugin(s store.Storer, namespace, name string) (Plugin, error) { + var plugin Plugin k8sPlugin, err := s.GetKongPlugin(namespace, name) if err != nil { // if no namespaced plugin definition, then @@ -114,15 +114,15 @@ func getPlugin(s store.Storer, namespace, name string) (kong.Plugin, error) { func kongPluginFromK8SClusterPlugin( s store.Storer, k8sPlugin configurationv1.KongClusterPlugin, -) (kong.Plugin, error) { +) (Plugin, error) { var config kong.Configuration config, err := RawConfigToConfiguration(k8sPlugin.Config) if err != nil { - return kong.Plugin{}, fmt.Errorf("could not parse KongPlugin %v/%v config: %w", + return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w", k8sPlugin.Namespace, k8sPlugin.Name, err) } if k8sPlugin.ConfigFrom != nil && len(config) > 0 { - return kong.Plugin{}, + return Plugin{}, fmt.Errorf("KongClusterPlugin '/%v' has both "+ "Config and ConfigFrom set", k8sPlugin.Name) } @@ -132,23 +132,26 @@ func kongPluginFromK8SClusterPlugin( s, (*k8sPlugin.ConfigFrom).SecretValue) if err != nil { - return kong.Plugin{}, - fmt.Errorf("error parsing config for KongClusterPlugin %v: %w", + return Plugin{}, + fmt.Errorf("error parsing config for KongClusterPlugin %s: %w", k8sPlugin.Name, err) } } - kongPlugin := plugin{ - Name: k8sPlugin.PluginName, - Config: config, - RunOn: k8sPlugin.RunOn, - Ordering: k8sPlugin.Ordering, - InstanceName: k8sPlugin.InstanceName, - Disabled: k8sPlugin.Disabled, - Protocols: protocolsToStrings(k8sPlugin.Protocols), - Tags: util.GenerateTagsForObject(&k8sPlugin), - }.toKongPlugin() - return kongPlugin, nil + return Plugin{ + Plugin: plugin{ + Name: k8sPlugin.PluginName, + Config: config, + + RunOn: k8sPlugin.RunOn, + Ordering: k8sPlugin.Ordering, + InstanceName: k8sPlugin.InstanceName, + Disabled: k8sPlugin.Disabled, + Protocols: protocolsToStrings(k8sPlugin.Protocols), + Tags: util.GenerateTagsForObject(&k8sPlugin), + }.toKongPlugin(), + K8sParent: &k8sPlugin, + }, nil } func protocolPointersToStringPointers(protocols []*configurationv1.KongProtocol) (res []*string) { @@ -168,17 +171,16 @@ func protocolsToStrings(protocols []configurationv1.KongProtocol) (res []string) func kongPluginFromK8SPlugin( s store.Storer, k8sPlugin configurationv1.KongPlugin, -) (kong.Plugin, error) { +) (Plugin, error) { var config kong.Configuration config, err := RawConfigToConfiguration(k8sPlugin.Config) if err != nil { - return kong.Plugin{}, fmt.Errorf("could not parse KongPlugin %v/%v config: %w", + return Plugin{}, fmt.Errorf("could not parse KongPlugin %s/%s config: %w", k8sPlugin.Namespace, k8sPlugin.Name, err) } if k8sPlugin.ConfigFrom != nil && len(config) > 0 { - return kong.Plugin{}, - fmt.Errorf("KongPlugin '%v/%v' has both "+ - "Config and ConfigFrom set", + return Plugin{}, + fmt.Errorf("KongPlugin '%s/%s' has both Config and ConfigFrom set", k8sPlugin.Namespace, k8sPlugin.Name) } if k8sPlugin.ConfigFrom != nil { @@ -186,23 +188,26 @@ func kongPluginFromK8SPlugin( config, err = SecretToConfiguration(s, (*k8sPlugin.ConfigFrom).SecretValue, k8sPlugin.Namespace) if err != nil { - return kong.Plugin{}, - fmt.Errorf("error parsing config for KongPlugin '%v/%v': %w", + return Plugin{}, + fmt.Errorf("error parsing config for KongPlugin '%s/%s': %w", k8sPlugin.Name, k8sPlugin.Namespace, err) } } - kongPlugin := plugin{ - Name: k8sPlugin.PluginName, - Config: config, - RunOn: k8sPlugin.RunOn, - Ordering: k8sPlugin.Ordering, - InstanceName: k8sPlugin.InstanceName, - Disabled: k8sPlugin.Disabled, - Protocols: protocolsToStrings(k8sPlugin.Protocols), - Tags: util.GenerateTagsForObject(&k8sPlugin), - }.toKongPlugin() - return kongPlugin, nil + return Plugin{ + Plugin: plugin{ + Name: k8sPlugin.PluginName, + Config: config, + + RunOn: k8sPlugin.RunOn, + Ordering: k8sPlugin.Ordering, + InstanceName: k8sPlugin.InstanceName, + Disabled: k8sPlugin.Disabled, + Protocols: protocolsToStrings(k8sPlugin.Protocols), + Tags: util.GenerateTagsForObject(&k8sPlugin), + }.toKongPlugin(), + K8sParent: &k8sPlugin, + }, nil } func RawConfigToConfiguration(config apiextensionsv1.JSON) (kong.Configuration, error) { diff --git a/internal/dataplane/kongstate/util_test.go b/internal/dataplane/kongstate/util_test.go index 3070dfd43a..62abef81a6 100644 --- a/internal/dataplane/kongstate/util_test.go +++ b/internal/dataplane/kongstate/util_test.go @@ -151,7 +151,8 @@ func TestKongPluginFromK8SClusterPlugin(t *testing.T) { t.Errorf("kongPluginFromK8SClusterPlugin error = %v, wantErr %v", err, tt.wantErr) return } - assert.Equal(tt.want, got) + assert.Equal(tt.want, got.Plugin) + assert.NotEmpty(t, got.K8sParent) }) } } @@ -294,7 +295,8 @@ func TestKongPluginFromK8SPlugin(t *testing.T) { } // don't care about tags in this test got.Tags = nil - assert.Equal(tt.want, got) + assert.Equal(tt.want, got.Plugin) + assert.NotEmpty(t, got.K8sParent) }) } } diff --git a/internal/dataplane/parser/parser.go b/internal/dataplane/parser/parser.go index 91c8273d75..f65b93af81 100644 --- a/internal/dataplane/parser/parser.go +++ b/internal/dataplane/parser/parser.go @@ -234,6 +234,9 @@ func (p *Parser) BuildKongConfig() KongConfigBuildingResult { // process annotation plugins result.FillPlugins(p.logger, p.storer) + for _, pl := range result.Plugins { + p.registerSuccessfullyParsedObject(pl.K8sParent) + } // generate Certificates and SNIs ingressCerts := p.getCerts(ingressRules.SecretNameToSNIs) diff --git a/internal/dataplane/parser/parser_test.go b/internal/dataplane/parser/parser_test.go index 47850a6534..dc802a993e 100644 --- a/internal/dataplane/parser/parser_test.go +++ b/internal/dataplane/parser/parser_test.go @@ -5384,6 +5384,69 @@ func TestParser_ConfiguredKubernetesObjects(t *testing.T) { {Name: "consumer", Namespace: "bar"}, }, }, + { + name: "KongPlugin with KongConsumer", + objectsInStore: store.FakeObjects{ + KongPlugins: []*configurationv1.KongPlugin{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "plugin", + Namespace: "bar", + Annotations: map[string]string{annotations.IngressClassKey: annotations.DefaultIngressClass}, + }, + PluginName: "plugin", + }, + }, + KongConsumers: []*configurationv1.KongConsumer{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "consumer", + Namespace: "bar", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + annotations.AnnotationPrefix + annotations.PluginsKey: "plugin", + }, + }, + Username: "foo", + }, + }, + }, + expectedObjectsToBeConfigured: []k8stypes.NamespacedName{ + {Name: "plugin", Namespace: "bar"}, + {Name: "consumer", Namespace: "bar"}, + }, + }, + { + name: "KongClusterPlugin with KongConsumer", + objectsInStore: store.FakeObjects{ + KongClusterPlugins: []*configurationv1.KongClusterPlugin{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "plugin", + Annotations: map[string]string{annotations.IngressClassKey: annotations.DefaultIngressClass}, + }, + PluginName: "plugin", + }, + }, + KongConsumers: []*configurationv1.KongConsumer{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "consumer", + Namespace: "bar", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + annotations.AnnotationPrefix + annotations.PluginsKey: "plugin", + }, + }, + Username: "foo", + }, + }, + }, + expectedObjectsToBeConfigured: []k8stypes.NamespacedName{ + {Name: "plugin"}, + {Name: "consumer", Namespace: "bar"}, + }, + }, } for _, tc := range testCases { diff --git a/internal/manager/controllerdef.go b/internal/manager/controllerdef.go index 32acdb2c67..168b000e35 100644 --- a/internal/manager/controllerdef.go +++ b/internal/manager/controllerdef.go @@ -250,6 +250,7 @@ func setupControllers( DataplaneClient: dataplaneClient, CacheSyncTimeout: c.CacheSyncTimeout, ReferenceIndexers: referenceIndexers, + StatusQueue: kubernetesStatusQueue, }, }, { @@ -311,6 +312,7 @@ func setupControllers( DisableIngressClassLookups: !c.IngressClassNetV1Enabled, CacheSyncTimeout: c.CacheSyncTimeout, ReferenceIndexers: referenceIndexers, + StatusQueue: kubernetesStatusQueue, }, }, // --------------------------------------------------------------------------- diff --git a/test/envtest/programmed_condition_envtest_test.go b/test/envtest/programmed_condition_envtest_test.go index 01daaf703a..e26010b238 100644 --- a/test/envtest/programmed_condition_envtest_test.go +++ b/test/envtest/programmed_condition_envtest_test.go @@ -16,7 +16,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v2/test/helpers/conditions" ) -func TestKongConsumer_ProgrammedCondition(t *testing.T) { +func TestKongCRDs_ProgrammedCondition(t *testing.T) { t.Parallel() scheme := Scheme(t, WithKong) @@ -33,9 +33,10 @@ func TestKongConsumer_ProgrammedCondition(t *testing.T) { ns := CreateNamespace(ctx, t, ctrlClient) testCases := []struct { - name string - objects []client.Object - test func(t *testing.T, ctrlClient client.Client) + name string + objects []client.Object + getExpectedObjectConditions func(ctrlClient client.Client) ([]metav1.Condition, error) + expectedProgrammedStatus metav1.ConditionStatus }{ { name: "valid KongConsumer", @@ -51,29 +52,18 @@ func TestKongConsumer_ProgrammedCondition(t *testing.T) { Username: "username", }, }, - test: func(t *testing.T, ctrlClient client.Client) { - require.Eventually(t, func() bool { - var consumer kongv1.KongConsumer - err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ - Name: "consumer", - Namespace: ns.Name, - }, &consumer) - if err != nil { - t.Logf("error getting consumer: %v", err) - return false - } - - if !conditions.Contain( - consumer.Status.Conditions, - conditions.WithType("Programmed"), - conditions.WithStatus(metav1.ConditionTrue), - ) { - t.Logf("Programmed condition not found, actual: %v", consumer.Status.Conditions) - return false - } - return true - }, 10*time.Second, 50*time.Millisecond) + getExpectedObjectConditions: func(ctrlClient client.Client) ([]metav1.Condition, error) { + var consumer kongv1.KongConsumer + err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ + Name: "consumer", + Namespace: ns.Name, + }, &consumer) + if err != nil { + return nil, err + } + return consumer.Status.Conditions, nil }, + expectedProgrammedStatus: metav1.ConditionTrue, }, { name: "KongConsumer referencing non-existent secret", @@ -90,30 +80,88 @@ func TestKongConsumer_ProgrammedCondition(t *testing.T) { Credentials: []string{"non-existent-secret"}, }, }, - test: func(t *testing.T, ctrlClient client.Client) { - require.Eventually(t, func() bool { - var consumer kongv1.KongConsumer - err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ - Name: "consumer-with-secret", + getExpectedObjectConditions: func(ctrlClient client.Client) ([]metav1.Condition, error) { + var consumer kongv1.KongConsumer + err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ + Name: "consumer-with-secret", + Namespace: ns.Name, + }, &consumer) + if err != nil { + return nil, err + } + return consumer.Status.Conditions, nil + }, + expectedProgrammedStatus: metav1.ConditionFalse, + }, + { + name: "valid KongPlugin", + objects: []client.Object{ + &kongv1.KongPlugin{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kong-plugin", + Namespace: ns.Name, + Annotations: map[string]string{annotations.IngressClassKey: annotations.DefaultIngressClass}, + }, + PluginName: "plugin", + }, + &kongv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consumer-for-plugin", Namespace: ns.Name, - }, &consumer) - if err != nil { - t.Logf("error getting consumer: %v", err) - return false - } - - if !conditions.Contain( - consumer.Status.Conditions, - conditions.WithType("Programmed"), - conditions.WithStatus(metav1.ConditionFalse), - conditions.WithReason(string(kongv1.ReasonInvalid)), - ) { - t.Logf("Programmed condition not found, actual: %v", consumer.Status.Conditions) - return false - } - return true - }, 10*time.Second, 50*time.Millisecond) + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + annotations.AnnotationPrefix + annotations.PluginsKey: "kong-plugin", + }, + }, + Username: "foo", + }, }, + getExpectedObjectConditions: func(ctrlClient client.Client) ([]metav1.Condition, error) { + var plugin kongv1.KongPlugin + err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ + Name: "kong-plugin", + Namespace: ns.Name, + }, &plugin) + if err != nil { + return nil, err + } + return plugin.Status.Conditions, nil + }, + expectedProgrammedStatus: metav1.ConditionTrue, + }, + { + name: "valid KongClusterPlugin", + objects: []client.Object{ + &kongv1.KongClusterPlugin{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kong-cluster-plugin", + Annotations: map[string]string{annotations.IngressClassKey: annotations.DefaultIngressClass}, + }, + PluginName: "plugin", + }, + &kongv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consumer-for-cluster-plugin", + Namespace: ns.Name, + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + annotations.AnnotationPrefix + annotations.PluginsKey: "kong-cluster-plugin", + }, + }, + Username: "foo", + }, + }, + getExpectedObjectConditions: func(ctrlClient client.Client) ([]metav1.Condition, error) { + var clusterPlugin kongv1.KongClusterPlugin + err := ctrlClient.Get(ctx, k8stypes.NamespacedName{ + Name: "kong-cluster-plugin", + }, &clusterPlugin) + if err != nil { + return nil, err + } + return clusterPlugin.Status.Conditions, nil + }, + expectedProgrammedStatus: metav1.ConditionTrue, }, } @@ -123,7 +171,23 @@ func TestKongConsumer_ProgrammedCondition(t *testing.T) { require.NoError(t, ctrlClient.Create(ctx, obj)) t.Cleanup(func() { _ = ctrlClient.Delete(ctx, obj) }) } - tc.test(t, ctrlClient) + + require.Eventually(t, func() bool { + cs, err := tc.getExpectedObjectConditions(ctrlClient) + if err != nil { + t.Logf("error getting expected object conditions: %v", err) + return false + } + if !conditions.Contain( + cs, + conditions.WithType(string(kongv1.ConditionProgrammed)), + conditions.WithStatus(tc.expectedProgrammedStatus), + ) { + t.Logf("Programmed condition not found, actual: %v", cs) + return false + } + return true + }, 10*time.Second, 50*time.Millisecond) }) } }