Skip to content

Commit

Permalink
feat: add support for global KongPluginBindings (#1052)
Browse files Browse the repository at this point in the history
  • Loading branch information
czeslavo authored Jan 21, 2025
1 parent 6c02f7d commit deb1ed1
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 19 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
- Added support for `ControlPlaneRef`s with `type` equal to `konnectID` for
all Konnect entities that refer to a `ControlPlane`.
[#985](https://github.com/Kong/gateway-operator/pull/985)
- Added support for global plugins with `KongPluginBinding`'s `scope` field.
The default value is `OnlyTargets` which means that the plugin will be
applied only to the targets specified in the `targets` field. The new
alternative is `GlobalInControlPlane` that will make the plugin apply
globally in a control plane.
[#1052](https://github.com/Kong/gateway-operator/pull/1052)

### Changed

Expand Down
47 changes: 47 additions & 0 deletions config/samples/konnect-kongpluginbinding-global.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
kind: KonnectAPIAuthConfiguration
apiVersion: konnect.konghq.com/v1alpha1
metadata:
name: demo-auth
namespace: default
spec:
type: token
token: kpat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
serverURL: us.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
---
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: rate-limit-5-min
config:
minute: 5
policy: local
plugin: rate-limiting
---
apiVersion: configuration.konghq.com/v1alpha1
kind: KongPluginBinding
metadata:
name: global-plugin-binding
spec:
scope: GlobalInControlPlane
controlPlaneRef:
type: konnectNamespacedRef
konnectNamespacedRef:
name: demo-cp
pluginRef:
name: rate-limit-5-min
10 changes: 6 additions & 4 deletions controller/konnect/index_kongpluginbinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func kongServiceReferencesFromKongPluginBinding(obj client.Object) []string {
if !ok {
return nil
}
if binding.Spec.Targets.ServiceReference == nil ||
if binding.Spec.Targets == nil ||
binding.Spec.Targets.ServiceReference == nil ||
binding.Spec.Targets.ServiceReference.Group != configurationv1alpha1.GroupVersion.Group ||
binding.Spec.Targets.ServiceReference.Kind != "KongService" {
return nil
Expand All @@ -89,7 +90,8 @@ func kongRouteReferencesFromKongPluginBinding(obj client.Object) []string {
if !ok {
return nil
}
if binding.Spec.Targets.RouteReference == nil ||
if binding.Spec.Targets == nil ||
binding.Spec.Targets.RouteReference == nil ||
binding.Spec.Targets.RouteReference.Group != configurationv1alpha1.GroupVersion.Group ||
binding.Spec.Targets.RouteReference.Kind != "KongRoute" {
return nil
Expand All @@ -103,7 +105,7 @@ func kongConsumerReferencesFromKongPluginBinding(obj client.Object) []string {
if !ok {
return nil
}
if binding.Spec.Targets.ConsumerReference == nil {
if binding.Spec.Targets == nil || binding.Spec.Targets.ConsumerReference == nil {
return nil
}
return []string{binding.Spec.Targets.ConsumerReference.Name}
Expand All @@ -115,7 +117,7 @@ func kongConsumerGroupReferencesFromKongPluginBinding(obj client.Object) []strin
if !ok {
return nil
}
if binding.Spec.Targets.ConsumerGroupReference == nil {
if binding.Spec.Targets == nil || binding.Spec.Targets.ConsumerGroupReference == nil {
return nil
}
return []string{binding.Spec.Targets.ConsumerGroupReference.Name}
Expand Down
18 changes: 18 additions & 0 deletions controller/konnect/kongpluginbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func (b *KongPluginBindingBuilder) WithControlPlaneRefKonnectNamespaced(name str

// WithConsumerTarget sets the consumer target of the KongPluginBinding.
func (b *KongPluginBindingBuilder) WithConsumerTarget(consumer string) *KongPluginBindingBuilder {
if b.binding.Spec.Targets == nil {
b.binding.Spec.Targets = &configurationv1alpha1.KongPluginBindingTargets{}
}
b.binding.Spec.Targets.ConsumerReference = &configurationv1alpha1.TargetRef{
Name: consumer,
}
Expand All @@ -78,6 +81,9 @@ func (b *KongPluginBindingBuilder) WithConsumerTarget(consumer string) *KongPlug

// WithConsumerGroupTarget sets the consumer group target of the KongPluginBinding.
func (b *KongPluginBindingBuilder) WithConsumerGroupTarget(cg string) *KongPluginBindingBuilder {
if b.binding.Spec.Targets == nil {
b.binding.Spec.Targets = &configurationv1alpha1.KongPluginBindingTargets{}
}
b.binding.Spec.Targets.ConsumerGroupReference = &configurationv1alpha1.TargetRef{
Name: cg,
}
Expand All @@ -86,6 +92,9 @@ func (b *KongPluginBindingBuilder) WithConsumerGroupTarget(cg string) *KongPlugi

// WithServiceTarget sets the service target of the KongPluginBinding.
func (b *KongPluginBindingBuilder) WithServiceTarget(serviceName string) *KongPluginBindingBuilder {
if b.binding.Spec.Targets == nil {
b.binding.Spec.Targets = &configurationv1alpha1.KongPluginBindingTargets{}
}
b.binding.Spec.Targets.ServiceReference = &configurationv1alpha1.TargetRefWithGroupKind{
Group: configurationv1alpha1.GroupVersion.Group,
Kind: "KongService",
Expand All @@ -96,6 +105,9 @@ func (b *KongPluginBindingBuilder) WithServiceTarget(serviceName string) *KongPl

// WithRouteTarget sets the route target of the KongPluginBinding.
func (b *KongPluginBindingBuilder) WithRouteTarget(routeName string) *KongPluginBindingBuilder {
if b.binding.Spec.Targets == nil {
b.binding.Spec.Targets = &configurationv1alpha1.KongPluginBindingTargets{}
}
b.binding.Spec.Targets.RouteReference = &configurationv1alpha1.TargetRefWithGroupKind{
Group: configurationv1alpha1.GroupVersion.Group,
Kind: "KongRoute",
Expand All @@ -116,6 +128,12 @@ func (b *KongPluginBindingBuilder) WithOwnerReference(owner client.Object, schem
return b, nil
}

// WithScope sets the scope of the KongPluginBinding.
func (b *KongPluginBindingBuilder) WithScope(scope configurationv1alpha1.KongPluginBindingScope) *KongPluginBindingBuilder {
b.binding.Spec.Scope = scope
return b
}

// Build returns the KongPluginBinding.
func (b *KongPluginBindingBuilder) Build() *configurationv1alpha1.KongPluginBinding {
return b.binding
Expand Down
14 changes: 7 additions & 7 deletions controller/konnect/ops/ops_kongpluginbinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func kongPluginBindingToSDKPluginInput(
}

tags := GenerateTagsForObject(pluginBinding, metadata.ExtractTags(plugin)...)
return kongPluginWithTargetsToKongPluginInput(plugin, targets, tags)
return kongPluginWithTargetsToKongPluginInput(pluginBinding, plugin, targets, tags)
}

// getPluginBindingTargets returns the list of client objects referenced
Expand All @@ -169,6 +169,10 @@ func getPluginBindingTargets(
pluginBinding *configurationv1alpha1.KongPluginBinding,
) ([]pluginTarget, error) {
targets := pluginBinding.Spec.Targets
if targets == nil {
return nil, nil
}

targetObjects := []pluginTarget{}
if ref := targets.ServiceReference; ref != nil {
ref := targets.ServiceReference
Expand Down Expand Up @@ -241,12 +245,8 @@ type pluginTarget interface {

// kongPluginWithTargetsToKongPluginInput converts a KongPlugin configuration along with KongPluginBinding's targets and
// tags to an SKD PluginInput.
func kongPluginWithTargetsToKongPluginInput(
plugin *configurationv1.KongPlugin,
targets []pluginTarget,
tags []string,
) (*sdkkonnectcomp.PluginInput, error) {
if len(targets) == 0 {
func kongPluginWithTargetsToKongPluginInput(binding *configurationv1alpha1.KongPluginBinding, plugin *configurationv1.KongPlugin, targets []pluginTarget, tags []string) (*sdkkonnectcomp.PluginInput, error) {
if binding.Spec.Scope == configurationv1alpha1.KongPluginBindingScopeOnlyTargets && len(targets) == 0 {
return nil, fmt.Errorf("no targets found for KongPluginBinding %s", client.ObjectKeyFromObject(plugin))
}

Expand Down
2 changes: 1 addition & 1 deletion controller/konnect/ops/ops_kongpluginbinding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestKongPluginBindingToSDKPluginInput_Tags(t *testing.T) {
Name: "plugin-1",
Kind: lo.ToPtr("KongPlugin"),
},
Targets: configurationv1alpha1.KongPluginBindingTargets{
Targets: &configurationv1alpha1.KongPluginBindingTargets{
ServiceReference: &configurationv1alpha1.TargetRefWithGroupKind{
Name: "service-1",
Kind: "KongService",
Expand Down
10 changes: 6 additions & 4 deletions controller/konnect/reconciler_generic_pluginbindingfinalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,31 @@ func (r *KonnectEntityPluginBindingFinalizerReconciler[T, TEnt]) enqueueObjectRe

switch any(ent).(type) {
case *configurationv1alpha1.KongService:
if kpb.Spec.Targets.ServiceReference == nil ||
if kpb.Spec.Targets == nil ||
kpb.Spec.Targets.ServiceReference == nil ||
kpb.Spec.Targets.ServiceReference.Kind != "KongService" ||
kpb.Spec.Targets.ServiceReference.Group != configurationv1alpha1.GroupVersion.Group {
return nil
}
name = kpb.Spec.Targets.ServiceReference.Name

case *configurationv1alpha1.KongRoute:
if kpb.Spec.Targets.RouteReference == nil ||
if kpb.Spec.Targets == nil ||
kpb.Spec.Targets.RouteReference == nil ||
kpb.Spec.Targets.RouteReference.Kind != "KongRoute" ||
kpb.Spec.Targets.RouteReference.Group != configurationv1alpha1.GroupVersion.Group {
return nil
}
name = kpb.Spec.Targets.RouteReference.Name

case *configurationv1.KongConsumer:
if kpb.Spec.Targets.ConsumerReference == nil {
if kpb.Spec.Targets == nil || kpb.Spec.Targets.ConsumerReference == nil {
return nil
}
name = kpb.Spec.Targets.ConsumerReference.Name

case *configurationv1beta1.KongConsumerGroup:
if kpb.Spec.Targets.ConsumerGroupReference == nil {
if kpb.Spec.Targets == nil || kpb.Spec.Targets.ConsumerGroupReference == nil {
return nil
}
name = kpb.Spec.Targets.ConsumerGroupReference.Name
Expand Down
5 changes: 5 additions & 0 deletions controller/konnect/reconciler_kongplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ func deleteUnusedKongPluginBindings(
continue
}

// If there's no targets in the KongPluginBinding, skip it.
if pb.Spec.Targets == nil {
continue
}

// If the konghq.com/plugins annotation is not present, it doesn't contain
// the plugin in question or the object referring to the plugin has a non zero deletion timestamp,
// we need to delete all the managed KongPluginBindings that reference the object.
Expand Down
1 change: 1 addition & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ KongPluginBindingSpec defines specification of a KongPluginBinding.
| `pluginRef` _[PluginRef](#pluginref)_ | PluginReference is a reference to the KongPlugin or KongClusterPlugin resource. |
| `targets` _[KongPluginBindingTargets](#kongpluginbindingtargets)_ | Targets contains the targets references. It is possible to set multiple combinations of references, as described in https://docs.konghq.com/gateway/latest/key-concepts/plugins/#precedence The complete set of allowed combinations and their order of precedence for plugins configured to multiple entities is:<br /><br /> 1. Consumer + route + service 2. Consumer group + service + route 3. Consumer + route 4. Consumer + service 5. Consumer group + route 6. Consumer group + service 7. Route + service 8. Consumer 9. Consumer group 10. Route 11. Service |
| `controlPlaneRef` _[ControlPlaneRef](#controlplaneref)_ | ControlPlaneRef is a reference to a ControlPlane this KongPluginBinding is associated with. |
| `scope` _[KongPluginBindingScope](#kongpluginbindingscope)_ | Scope defines the scope of the plugin binding. |


_Appears in:_
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/google/go-containerregistry v0.20.3
github.com/google/uuid v1.6.0
github.com/gruntwork-io/terratest v0.48.1
github.com/kong/kubernetes-configuration v1.0.7-0.20250117152851-64a09d770d57
github.com/kong/kubernetes-configuration v1.0.7-0.20250120092720-c985e30e5dd8
github.com/kong/kubernetes-telemetry v0.1.8
github.com/kong/kubernetes-testing-framework v0.47.2
github.com/kong/semver/v4 v4.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IX
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kong/go-kong v0.63.0 h1:8ECLgkgDqON61qCMq/M0gTwZKYxg55Oy692dRDOOBiU=
github.com/kong/go-kong v0.63.0/go.mod h1:ma9GWnhkxtrXZlLFfED955HjVzmUojYEHet3lm+PDik=
github.com/kong/kubernetes-configuration v1.0.7-0.20250117152851-64a09d770d57 h1:lakePerOiyrrakL5KEV3zgR/UJxqSzMlZ0CXxNhfVhg=
github.com/kong/kubernetes-configuration v1.0.7-0.20250117152851-64a09d770d57/go.mod h1:YzSyKf/Dg1TIPkVlzSHofk/RGchuDzpxBivfPDkQ2Do=
github.com/kong/kubernetes-configuration v1.0.7-0.20250120092720-c985e30e5dd8 h1:SOex4KtF+p+D+hipv461M2Ueu4qQGBf8C5H1cuK1xfo=
github.com/kong/kubernetes-configuration v1.0.7-0.20250120092720-c985e30e5dd8/go.mod h1:YzSyKf/Dg1TIPkVlzSHofk/RGchuDzpxBivfPDkQ2Do=
github.com/kong/kubernetes-telemetry v0.1.8 h1:nbtUmXW9xkzRO7dgvrgVrJZiRksATk4XHrqX+78g/5k=
github.com/kong/kubernetes-telemetry v0.1.8/go.mod h1:ZEQY/4DddKoe5XA7UTOIbdI/4d6ZRcrzh2ezRxnuyl0=
github.com/kong/kubernetes-testing-framework v0.47.2 h1:+2Z9anTpbV/hwNeN+NFQz53BMU+g3QJydkweBp3tULo=
Expand Down
60 changes: 60 additions & 0 deletions test/envtest/kongpluginbinding_unmanaged_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,64 @@ func TestKongPluginBindingUnmanaged(t *testing.T) {
assert.True(c, sdk.PluginSDK.AssertExpectations(t))
}, waitTime, tickTime)
})

t.Run("binding globally", func(t *testing.T) {
proxyCacheKongPlugin := deploy.ProxyCachePlugin(t, ctx, clientNamespaced)
pluginID := uuid.NewString()

sdk.PluginSDK.EXPECT().
CreatePlugin(
mock.Anything,
cp.GetKonnectStatus().GetKonnectID(),
mock.MatchedBy(func(pi sdkkonnectcomp.PluginInput) bool {
return pi.Consumer == nil && pi.ConsumerGroup == nil && pi.Route == nil && pi.Service == nil
})).
Return(
&sdkkonnectops.CreatePluginResponse{
Plugin: &sdkkonnectcomp.Plugin{
ID: lo.ToPtr(pluginID),
},
},
nil,
)
kpb := deploy.KongPluginBinding(t, ctx, clientNamespaced,
konnect.NewKongPluginBindingBuilder().
WithControlPlaneRefKonnectNamespaced(cp.Name).
WithPluginRef(proxyCacheKongPlugin.Name).
WithScope(configurationv1alpha1.KongPluginBindingScopeGlobalInControlPlane).
Build(),
)
t.Logf(
"wait for the controller to pick the new unmanaged global KongPluginBinding %s and create it in Konnect",
client.ObjectKeyFromObject(kpb),
)
assert.EventuallyWithT(t,
assertCollectObjectExistsAndHasKonnectID(t, ctx, clientNamespaced, kpb, pluginID),
waitTime, tickTime,
"KongPluginBinding wasn't created using Konnect API or its KonnectID wasn't set",
)

sdk.PluginSDK.EXPECT().
DeletePlugin(mock.Anything, cp.GetKonnectStatus().GetKonnectID(), mock.Anything).
Return(
&sdkkonnectops.DeletePluginResponse{
StatusCode: 200,
},
nil,
)

t.Logf("delete the unmanaged KongPluginBinding %s, the check it gets collected",
client.ObjectKeyFromObject(kpb),
)
require.NoError(t, clientNamespaced.Delete(ctx, kpb))
assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.True(c, k8serrors.IsNotFound(
clientNamespaced.Get(ctx, client.ObjectKeyFromObject(kpb), kpb),
))
}, waitTime, tickTime, "KongPluginBinding did not get deleted but should have")

assert.EventuallyWithT(t, func(c *assert.CollectT) {
assert.True(c, sdk.PluginSDK.AssertExpectations(t))
}, waitTime, tickTime)
})
}
16 changes: 16 additions & 0 deletions test/integration/test_konnect_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ func TestKonnectEntities(t *testing.T) {
assertKonnectEntityProgrammed(t, kpb)
}, testutils.ObjectUpdateTimeout, testutils.ObjectUpdateTick)

globalKPB := deploy.KongPluginBinding(t, ctx, clientNamespaced,
konnect.NewKongPluginBindingBuilder().
WithPluginRef(kp.Name).
WithControlPlaneRefKonnectNamespaced(cp.Name).
WithScope(configurationv1alpha1.KongPluginBindingScopeGlobalInControlPlane).
Build(),
deploy.WithTestIDLabel(testID),
)

t.Logf("Waiting for KongPluginBinding to be updated with Konnect ID")
require.EventuallyWithT(t, func(t *assert.CollectT) {
err := GetClients().MgrClient.Get(GetCtx(), types.NamespacedName{Name: globalKPB.Name, Namespace: globalKPB.Namespace}, globalKPB)
require.NoError(t, err)
assertKonnectEntityProgrammed(t, globalKPB)
}, testutils.ObjectUpdateTimeout, testutils.ObjectUpdateTick)

kup := deploy.KongUpstreamAttachedToCP(t, ctx, clientNamespaced, cp,
deploy.WithTestIDLabel(testID),
func(obj client.Object) {
Expand Down

0 comments on commit deb1ed1

Please sign in to comment.