Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@
runner container images (`kong/mcp-server-init`, `kong/mcp-server-runner`) to
the versions returned by Konnect.
[#3943](https://github.com/Kong/kong-operator/pull/3943)
- `MCPServer`: provision the `ai-mcp-proxy` `KongPlugin` and attach it to the
owned `KongService` via a dedicated `KongPluginBinding`.
[#4188](https://github.com/Kong/kong-operator/pull/4188)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions controller/mcpserver/controller_rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ package mcpserver
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get
// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongservices,verbs=create;get;list;watch;update;patch;delete
// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes,verbs=create;get;list;watch;update;patch;delete
// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongplugins,verbs=create;get;list;watch;update;patch;delete
// +kubebuilder:rbac:groups=configuration.konghq.com,resources=kongpluginbindings,verbs=create;get;list;watch;update;patch;delete
3 changes: 3 additions & 0 deletions controller/mcpserver/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"

configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1"
configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1"
konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1"
sdkops "github.com/kong/kong-operator/v2/controller/konnect/ops/sdk"
Expand Down Expand Up @@ -74,6 +75,8 @@ func (r *MCPServerReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
Owns(&corev1.Service{}).
Owns(&configurationv1alpha1.KongService{}).
Owns(&configurationv1alpha1.KongRoute{}).
Owns(&configurationv1.KongPlugin{}).
Owns(&configurationv1alpha1.KongPluginBinding{}).
WatchesRawSource(
source.Channel(
r.ReconcileEventCh,
Expand Down
28 changes: 26 additions & 2 deletions controller/mcpserver/owned_kong_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

commonv1alpha1 "github.com/kong/kong-operator/v2/api/common/v1alpha1"
configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1"
configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1"
konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1"
sdkops "github.com/kong/kong-operator/v2/controller/konnect/ops/sdk"
Expand Down Expand Up @@ -99,6 +100,17 @@ func (r *MCPServerReconciler) ensureKongEntities(
return err
}

// ------------------------------------------------------------------
// Ensure KongPlugin and KongPluginBinding CRs
// ------------------------------------------------------------------
if _, err := r.ensureKongPlugins(ctx, mcpServer); err != nil {
return err
}

if err := r.ensureKongPluginBindings(ctx, mcpServer, desiredServiceNames); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -130,7 +142,7 @@ func (r *MCPServerReconciler) ensureKongService(
return op.Created, nn, nil
}

// TODO: enforce the KongService Spec
// TODO: enforce the KongService Spec https://github.com/Kong/kong-operator/issues/3979

return op.Noop, nn, nil
}
Expand Down Expand Up @@ -191,7 +203,7 @@ func (r *MCPServerReconciler) ensureKongRoute(
return op.Created, nn, nil
}

// TODO: enforce the KongRoute Spec
// TODO: enforce the KongRoute Spec https://github.com/Kong/kong-operator/issues/3979

return op.Noop, nn, nil
}
Expand Down Expand Up @@ -284,6 +296,18 @@ func extractItems(list client.ObjectList) []client.Object {
items[i] = &l.Items[i]
}
return items
case *configurationv1.KongPluginList:
items := make([]client.Object, len(l.Items))
for i := range l.Items {
items[i] = &l.Items[i]
}
return items
case *configurationv1alpha1.KongPluginBindingList:
items := make([]client.Object, len(l.Items))
for i := range l.Items {
items[i] = &l.Items[i]
}
return items
}
return nil
}
194 changes: 194 additions & 0 deletions controller/mcpserver/owned_plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package mcpserver

import (
"context"
"fmt"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

configurationv1 "github.com/kong/kong-operator/v2/api/configuration/v1"
configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1"
konnectv1alpha1 "github.com/kong/kong-operator/v2/api/konnect/v1alpha1"
"github.com/kong/kong-operator/v2/controller/pkg/log"
"github.com/kong/kong-operator/v2/controller/pkg/op"
k8sutils "github.com/kong/kong-operator/v2/pkg/utils/kubernetes"
k8sresources "github.com/kong/kong-operator/v2/pkg/utils/kubernetes/resources"
)

// builtinPlugin describes a Kong plugin that is automatically provisioned for
// every MCPServer deployment.
type builtinPlugin struct {
name string
config string
}

var builtinPlugins = []builtinPlugin{
{
name: "ai-mcp-proxy",
config: `{"mode":"passthrough-listener"}`,
},
}

// kongPluginName returns the deterministic name for the KongPlugin CR
// corresponding to the given builtin plugin and MCPServer.
func kongPluginName(mcpServer *konnectv1alpha1.MCPServer, plg builtinPlugin) string {
return fmt.Sprintf("%s-%s", generateWorkloadNN(mcpServer).Name, plg.name)
}

// ensureKongPlugins creates KongPlugin CRs for every builtinPlugin and deletes
// any stale KongPlugins owned by the MCPServer that are no longer expected.
// It returns the set of plugin names that were ensured.
func (r *MCPServerReconciler) ensureKongPlugins(
ctx context.Context,
mcpServer *konnectv1alpha1.MCPServer,
) (map[string]struct{}, error) {
logger := log.GetLogger(ctx, "mcpserver", r.LoggingMode)

desiredPluginNames := make(map[string]struct{}, len(builtinPlugins))
for _, plg := range builtinPlugins {
res, nn, err := r.ensureKongPlugin(ctx, mcpServer, plg)
if err != nil {
return nil, err
}
if res != op.Noop {
log.Info(logger, fmt.Sprintf("%s KongPlugin for MCPServer", res),
"namespace", mcpServer.Namespace, "name", mcpServer.Name, "plugin", nn.Name)
}
desiredPluginNames[nn.Name] = struct{}{}
}

if err := r.deleteStaleResources(ctx, mcpServer, &configurationv1.KongPluginList{}, desiredPluginNames); err != nil {
return nil, err
}

return desiredPluginNames, nil
}

func (r *MCPServerReconciler) ensureKongPlugin(
ctx context.Context,
mcpServer *konnectv1alpha1.MCPServer,
plg builtinPlugin,
) (op.Result, client.ObjectKey, error) {
desired := generateKongPlugin(mcpServer, plg)
nn := client.ObjectKeyFromObject(desired)

k8sutils.SetOwnerForObject(desired, mcpServer)
k8sresources.LabelObjectAsMCPServerManaged(desired)

existing := &configurationv1.KongPlugin{}
err := r.Get(ctx, nn, existing)
if err != nil {
if !apierrors.IsNotFound(err) {
return op.Noop, nn, fmt.Errorf("failed to get KongPlugin %s: %w", nn, err)
}

if err := r.Create(ctx, desired); err != nil {
return op.Noop, nn, fmt.Errorf("failed to create KongPlugin %s: %w", nn, err)
}
return op.Created, nn, nil
}

// TODO: enforce the KongPlugin Spec https://github.com/Kong/kong-operator/issues/3979

return op.Noop, nn, nil
}

func generateKongPlugin(mcpServer *konnectv1alpha1.MCPServer, plg builtinPlugin) *configurationv1.KongPlugin {
return &configurationv1.KongPlugin{
ObjectMeta: metav1.ObjectMeta{
Name: kongPluginName(mcpServer, plg),
Namespace: mcpServer.Namespace,
},
PluginName: plg.name,
Config: apiextensionsv1.JSON{
Raw: []byte(plg.config),
},
}
}

// ensureKongPluginBindings creates a KongPluginBinding CR for every
// (KongService, KongPlugin) pair, binding the plugin to the service. Stale
// bindings owned by the MCPServer are deleted.
func (r *MCPServerReconciler) ensureKongPluginBindings(
ctx context.Context,
mcpServer *konnectv1alpha1.MCPServer,
serviceNames map[string]struct{},
) error {
logger := log.GetLogger(ctx, "mcpserver", r.LoggingMode)

desiredBindingNames := make(map[string]struct{}, len(builtinPlugins)*len(serviceNames))
for _, plg := range builtinPlugins {
pluginName := kongPluginName(mcpServer, plg)
for svcName := range serviceNames {
res, nn, err := r.ensureKongPluginBinding(ctx, mcpServer, pluginName, svcName)
if err != nil {
return err
}
if res != op.Noop {
log.Info(logger, fmt.Sprintf("%s KongPluginBinding for MCPServer", res),
"namespace", mcpServer.Namespace, "name", mcpServer.Name,
"binding", nn.Name, "plugin", pluginName, "service", svcName)
}
desiredBindingNames[nn.Name] = struct{}{}
}
}

return r.deleteStaleResources(ctx, mcpServer, &configurationv1alpha1.KongPluginBindingList{}, desiredBindingNames)
}

func (r *MCPServerReconciler) ensureKongPluginBinding(
ctx context.Context,
mcpServer *konnectv1alpha1.MCPServer,
pluginName, serviceName string,
) (op.Result, client.ObjectKey, error) {
desired := generateKongPluginBinding(mcpServer, pluginName, serviceName)
nn := client.ObjectKeyFromObject(desired)

k8sutils.SetOwnerForObject(desired, mcpServer)
k8sresources.LabelObjectAsMCPServerManaged(desired)

existing := &configurationv1alpha1.KongPluginBinding{}
err := r.Get(ctx, nn, existing)
if err != nil {
if !apierrors.IsNotFound(err) {
return op.Noop, nn, fmt.Errorf("failed to get KongPluginBinding %s: %w", nn, err)
}

if err := r.Create(ctx, desired); err != nil {
return op.Noop, nn, fmt.Errorf("failed to create KongPluginBinding %s: %w", nn, err)
}
return op.Created, nn, nil
}

// TODO: enforce the KongPluginBinding Spec https://github.com/Kong/kong-operator/issues/3979

return op.Noop, nn, nil
}

func generateKongPluginBinding(
mcpServer *konnectv1alpha1.MCPServer,
pluginName, serviceName string,
) *configurationv1alpha1.KongPluginBinding {
return &configurationv1alpha1.KongPluginBinding{
ObjectMeta: metav1.ObjectMeta{
Name: pluginName,
Namespace: mcpServer.Namespace,
},
Spec: configurationv1alpha1.KongPluginBindingSpec{
PluginReference: configurationv1alpha1.PluginRef{
Name: pluginName,
},
Targets: &configurationv1alpha1.KongPluginBindingTargets{
ServiceReference: &configurationv1alpha1.TargetRefWithGroupKind{
Name: serviceName,
Group: configurationv1.GroupVersion.Group,
Kind: "KongService",
},
},
ControlPlaneRef: mcpServer.Spec.ControlPlaneRef,
},
}
}
Loading