From 3d3dbf6f48b631e93a10ce72035eee74bc5c8cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Thu, 26 Sep 2024 14:44:33 +0200 Subject: [PATCH] feat(konnect): allow binding KongConsumer in KongPluginBinding (#652) --- CHANGELOG.md | 1 + ...luginbinding-kongservice-kongconsumer.yaml | 79 +++++++++++ .../konnect/constraints/entitytypename.go | 7 + .../konnect/ops/ops_kongpluginbinding.go | 60 ++++++--- .../kongpluginbinding_unmanaged_test.go | 125 ++++++++++++++++++ test/envtest/update_status.go | 17 +++ 6 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 config/samples/konnect-kongpluginbinding-kongservice-kongconsumer.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index e38eaf043..79cea83a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ the creation of a managed `KongPluginBinding` resource: - `KongService` [#550](https://github.com/Kong/gateway-operator/pull/550) - `KongRoute` [#644](https://github.com/Kong/gateway-operator/pull/644) + - `KongConsumer` [#652](https://github.com/Kong/gateway-operator/pull/652) These `KongPluginBinding`s are taken by the `KongPluginBinding` reconciler to create the corresponding plugin objects in Konnect. - `KongConsumer` associated with `ConsumerGroups` is now reconciled in Konnect by removing/adding diff --git a/config/samples/konnect-kongpluginbinding-kongservice-kongconsumer.yaml b/config/samples/konnect-kongpluginbinding-kongservice-kongconsumer.yaml new file mode 100644 index 000000000..f8da8bbd7 --- /dev/null +++ b/config/samples/konnect-kongpluginbinding-kongservice-kongconsumer.yaml @@ -0,0 +1,79 @@ +--- +kind: KonnectAPIAuthConfiguration +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: demo-auth + namespace: default +spec: + type: token + token: kpat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + serverURL: eu.api.konghq.tech +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: demo-cp + namespace: default +spec: + name: demo-cp + labels: + app: demo-cp + key1: demo-cp + konnect: + authRef: + name: demo-auth + # namespace not required if APIAuthConfiguration is in the same namespace +--- +kind: KongService +apiVersion: configuration.konghq.com/v1alpha1 +metadata: + name: service-1 + namespace: default +spec: + name: service-1 + host: example.com + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: demo-cp +--- +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: rate-limit-5-min +config: + minute: 5 + policy: local +plugin: rate-limiting +--- +kind: KongConsumer +apiVersion: configuration.konghq.com/v1 +metadata: + name: consumer-for-service-plugin-binding + namespace: default +username: consumer-for-service-plugin-binding +custom_id: 08433C12-2B81-4738-B61D-3AA2136F0102 +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: demo-cp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: plugin-binding-kongservice-kongroute +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: demo-cp + pluginRef: + name: rate-limit-5-min + targets: + serviceRef: + name: service-1 + kind: KongService + group: configuration.konghq.com + consumerRef: + name: consumer-for-service-plugin-binding diff --git a/controller/konnect/constraints/entitytypename.go b/controller/konnect/constraints/entitytypename.go index 4dfc22b33..bc96d42f1 100644 --- a/controller/konnect/constraints/entitytypename.go +++ b/controller/konnect/constraints/entitytypename.go @@ -5,3 +5,10 @@ func EntityTypeName[T SupportedKonnectEntityType]() string { var e T return e.GetTypeName() } + +// EntityTypeNameForObj returns the name of the provided entity. +func EntityTypeNameForObj[T interface { + GetTypeName() string +}](obj T) string { + return obj.GetTypeName() +} diff --git a/controller/konnect/ops/ops_kongpluginbinding.go b/controller/konnect/ops/ops_kongpluginbinding.go index 318e81322..df2f5f773 100644 --- a/controller/konnect/ops/ops_kongpluginbinding.go +++ b/controller/konnect/ops/ops_kongpluginbinding.go @@ -14,6 +14,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/constraints" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" "github.com/kong/kubernetes-configuration/pkg/metadata" @@ -166,37 +168,47 @@ func getPluginBindingTargets( ctx context.Context, cl client.Client, pluginBinding *configurationv1alpha1.KongPluginBinding, -) ([]client.Object, error) { +) ([]pluginTarget, error) { targets := pluginBinding.Spec.Targets - targetObjects := []client.Object{} - if targets.ServiceReference != nil { - if targets.ServiceReference.Kind != "KongService" { - return nil, fmt.Errorf("unsupported service target kind %q", targets.ServiceReference.Kind) + targetObjects := []pluginTarget{} + if ref := targets.ServiceReference; ref != nil { + ref := targets.ServiceReference + if ref.Kind != "KongService" { + return nil, fmt.Errorf("unsupported service target kind %q", ref.Kind) } kongService := configurationv1alpha1.KongService{} - kongService.SetName(targets.ServiceReference.Name) + kongService.SetName(ref.Name) kongService.SetNamespace(pluginBinding.GetNamespace()) if err := cl.Get(ctx, client.ObjectKeyFromObject(&kongService), &kongService); err != nil { return nil, err } targetObjects = append(targetObjects, &kongService) } - if targets.RouteReference != nil { - if targets.RouteReference.Kind != "KongRoute" { - return nil, fmt.Errorf("unsupported route target kind %q", targets.RouteReference.Kind) + if ref := targets.RouteReference; ref != nil { + if ref.Kind != "KongRoute" { + return nil, fmt.Errorf("unsupported route target kind %q", ref.Kind) } kongRoute := configurationv1alpha1.KongRoute{} - kongRoute.SetName(targets.RouteReference.Name) + kongRoute.SetName(ref.Name) kongRoute.SetNamespace(pluginBinding.GetNamespace()) if err := cl.Get(ctx, client.ObjectKeyFromObject(&kongRoute), &kongRoute); err != nil { return nil, err } targetObjects = append(targetObjects, &kongRoute) } + if ref := targets.ConsumerReference; ref != nil { + + kongConsumer := configurationv1.KongConsumer{} + kongConsumer.SetName(ref.Name) + kongConsumer.SetNamespace(pluginBinding.GetNamespace()) + if err := cl.Get(ctx, client.ObjectKeyFromObject(&kongConsumer), &kongConsumer); err != nil { + return nil, err + } + targetObjects = append(targetObjects, &kongConsumer) + } - // TODO: https://github.com/Kong/gateway-operator/issues/526 add support for KongConsumer // TODO: https://github.com/Kong/gateway-operator/issues/527 add support for KongConsumerGroup return targetObjects, nil @@ -216,11 +228,17 @@ func getReferencedPlugin(ctx context.Context, cl client.Client, pluginBinding *c return &plugin, nil } +type pluginTarget interface { + client.Object + GetKonnectID() string + GetTypeName() string +} + // kongPluginWithTargetsToKongPluginInput converts a KongPlugin configuration along with KongPluginBinding's targets and // tags to an SKD PluginInput. func kongPluginWithTargetsToKongPluginInput( plugin *configurationv1.KongPlugin, - targets []client.Object, + targets []pluginTarget, tags []string, ) (*sdkkonnectcomp.PluginInput, error) { if len(targets) == 0 { @@ -245,24 +263,24 @@ func kongPluginWithTargetsToKongPluginInput( // TODO(mlavacca): check all the entities reference the same KonnectGatewayControlPlane for _, t := range targets { + id := t.GetKonnectID() + if id == "" { + return nil, fmt.Errorf("%s %s is not configured in Konnect yet", constraints.EntityTypeNameForObj(t), client.ObjectKeyFromObject(t)) + } + switch t := t.(type) { case *configurationv1alpha1.KongService: - id := t.GetKonnectID() - if id == "" { - return nil, fmt.Errorf("KongService %s is not configured in Konnect yet", client.ObjectKeyFromObject(t)) - } pluginInput.Service = &sdkkonnectcomp.PluginService{ ID: lo.ToPtr(id), } case *configurationv1alpha1.KongRoute: - id := t.GetKonnectID() - if id == "" { - return nil, fmt.Errorf("KongRoute %s is not configured in Konnect yet", client.ObjectKeyFromObject(t)) - } pluginInput.Route = &sdkkonnectcomp.PluginRoute{ ID: lo.ToPtr(id), } - // TODO: https://github.com/Kong/gateway-operator/issues/526 add support for KongConsumer + case *configurationv1.KongConsumer: + pluginInput.Consumer = &sdkkonnectcomp.PluginConsumer{ + ID: lo.ToPtr(id), + } // TODO: https://github.com/Kong/gateway-operator/issues/527 add support for KongConsumerGroup default: return nil, fmt.Errorf("unsupported target type %T", t) diff --git a/test/envtest/kongpluginbinding_unmanaged_test.go b/test/envtest/kongpluginbinding_unmanaged_test.go index 978d9cc24..160d0c27a 100644 --- a/test/envtest/kongpluginbinding_unmanaged_test.go +++ b/test/envtest/kongpluginbinding_unmanaged_test.go @@ -394,4 +394,129 @@ func TestKongPluginBindingUnmanaged(t *testing.T) { assert.True(c, sdk.PluginSDK.AssertExpectations(t)) }, waitTime, tickTime) }) + + t.Run("binding to KongService and KongConsumer", func(t *testing.T) { + proxyCacheKongPlugin := deployProxyCachePlugin(t, ctx, clientNamespaced) + + serviceID := uuid.NewString() + consumerID := uuid.NewString() + pluginID := uuid.NewString() + username := "test-user" + uuid.NewString() + + kongService := deployKongServiceAttachedToCP(t, ctx, clientNamespaced, cp) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(clientNamespaced.Delete(ctx, kongService))) + }) + updateKongServiceStatusWithProgrammed(t, ctx, clientNamespaced, kongService, serviceID, cp.GetKonnectStatus().GetKonnectID()) + kongConsumer := deployKongConsumerAttachedToCP(t, ctx, clientNamespaced, username, cp) + t.Cleanup(func() { + require.NoError(t, client.IgnoreNotFound(clientNamespaced.Delete(ctx, kongConsumer))) + }) + updateKongConsumerStatusWithKonnectID(t, ctx, clientNamespaced, kongConsumer, consumerID, cp.GetKonnectStatus().GetKonnectID()) + + wKongPlugin := setupWatch[configurationv1.KongPluginList](t, ctx, clientWithWatch, client.InNamespace(ns.Name)) + sdk.PluginSDK.EXPECT(). + CreatePlugin( + mock.Anything, + cp.GetKonnectStatus().GetKonnectID(), + mock.MatchedBy(func(pi sdkkonnectcomp.PluginInput) bool { + return pi.Consumer != nil && pi.Consumer.ID != nil && *pi.Consumer.ID == consumerID && + pi.Service != nil && pi.Service.ID != nil && *pi.Service.ID == serviceID + })). + Return( + &sdkkonnectops.CreatePluginResponse{ + Plugin: &sdkkonnectcomp.Plugin{ + ID: lo.ToPtr(pluginID), + }, + }, + nil, + ) + kpb := deployKongPluginBinding(t, ctx, clientNamespaced, + &configurationv1alpha1.KongPluginBinding{ + Spec: configurationv1alpha1.KongPluginBindingSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: cp.Name, + }, + }, + PluginReference: configurationv1alpha1.PluginRef{ + Name: proxyCacheKongPlugin.Name, + }, + Targets: configurationv1alpha1.KongPluginBindingTargets{ + ConsumerReference: &configurationv1alpha1.TargetRef{ + Name: kongConsumer.Name, + }, + ServiceReference: &configurationv1alpha1.TargetRefWithGroupKind{ + Group: configurationv1alpha1.GroupVersion.Group, + Kind: "KongService", + Name: kongService.Name, + }, + }, + }, + }, + ) + t.Logf( + "wait for the controller to pick the new unmanaged KongPluginBinding %s and put a %s finalizer on the referenced plugin %s", + client.ObjectKeyFromObject(kpb), + consts.PluginInUseFinalizer, + client.ObjectKeyFromObject(proxyCacheKongPlugin), + ) + _ = watchFor(t, ctx, wKongPlugin, watch.Modified, + func(kp *configurationv1.KongPlugin) bool { + return kp.Name == proxyCacheKongPlugin.Name && + controllerutil.ContainsFinalizer(kp, consts.PluginInUseFinalizer) + }, + "KongPlugin wasn't updated to get the plugin-in-use finalizer", + ) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, sdk.PluginSDK.AssertExpectations(t)) + }, waitTime, tickTime) + + sdk.PluginSDK.EXPECT(). + DeletePlugin(mock.Anything, cp.GetKonnectStatus().GetKonnectID(), mock.Anything). + Return( + &sdkkonnectops.DeletePluginResponse{ + StatusCode: 200, + }, + nil, + ) + + t.Logf("delete the KongPlugin %s, then check it does not get collected", client.ObjectKeyFromObject(proxyCacheKongPlugin)) + require.NoError(t, clientNamespaced.Delete(ctx, proxyCacheKongPlugin)) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, k8serrors.IsNotFound( + clientNamespaced.Get(ctx, client.ObjectKeyFromObject(proxyCacheKongPlugin), proxyCacheKongPlugin), + )) + assert.True(c, proxyCacheKongPlugin.DeletionTimestamp != nil) + assert.True(c, controllerutil.ContainsFinalizer(proxyCacheKongPlugin, consts.PluginInUseFinalizer)) + }, waitTime, tickTime) + + t.Logf("delete the unmanaged KongPluginBinding %s, then check the proxy-cache KongPlugin %s gets collected", + client.ObjectKeyFromObject(kpb), + client.ObjectKeyFromObject(proxyCacheKongPlugin), + ) + require.NoError(t, clientNamespaced.Delete(ctx, kpb)) + _ = watchFor(t, ctx, wKongPlugin, watch.Deleted, + func(kp *configurationv1.KongPlugin) bool { + return kp.Name == proxyCacheKongPlugin.Name + }, + "KongPlugin did not get deleted but should have", + ) + + t.Logf( + "delete the KongConsumer %s and check it gets collected, as the KongPluginBinding finalizer should have been removed", + client.ObjectKeyFromObject(kongConsumer), + ) + require.NoError(t, clientNamespaced.Delete(ctx, kongConsumer)) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, k8serrors.IsNotFound( + clientNamespaced.Get(ctx, client.ObjectKeyFromObject(kongConsumer), kongConsumer), + )) + }, waitTime, tickTime) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, sdk.PluginSDK.AssertExpectations(t)) + }, waitTime, tickTime) + }) } diff --git a/test/envtest/update_status.go b/test/envtest/update_status.go index 5cb1dc7f1..ef518b2d4 100644 --- a/test/envtest/update_status.go +++ b/test/envtest/update_status.go @@ -11,10 +11,27 @@ import ( "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) +func updateKongConsumerStatusWithKonnectID( + t *testing.T, + ctx context.Context, + cl client.Client, + obj *configurationv1.KongConsumer, + id string, + cpID string, +) { + obj.Status.Konnect = &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: cpID, + KonnectEntityStatus: konnectEntityStatus(id), + } + + require.NoError(t, cl.Status().Update(ctx, obj)) +} + func updateKongServiceStatusWithProgrammed( t *testing.T, ctx context.Context,