Skip to content

Commit

Permalink
feat(konnect): allow binding KongConsumer in KongPluginBinding (#652)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmalek authored Sep 26, 2024
1 parent bd7bbaf commit 3d3dbf6
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions controller/konnect/constraints/entitytypename.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
60 changes: 39 additions & 21 deletions controller/konnect/ops/ops_kongpluginbinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
125 changes: 125 additions & 0 deletions test/envtest/kongpluginbinding_unmanaged_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
17 changes: 17 additions & 0 deletions test/envtest/update_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 3d3dbf6

Please sign in to comment.