Skip to content
Closed
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
39 changes: 0 additions & 39 deletions go/api/utils/utils.go

This file was deleted.

39 changes: 0 additions & 39 deletions go/api/v1alpha3/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,12 @@ limitations under the License.
package v1alpha3

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kagent-dev/kagent/go/api/utils"
)

// ObjectReference identifies a resource; an omitted namespace means the owner's namespace.
Expand Down Expand Up @@ -147,21 +143,6 @@ type ValueSource struct {
Key string `json:"key"`
}

func (s *ValueSource) Resolve(ctx context.Context, client client.Client, namespace string) (string, error) {
if s == nil {
return "", fmt.Errorf("ValueSource cannot be nil")
}

switch s.Type {
case ConfigMapValueSource:
return utils.GetConfigMapValue(ctx, client, types.NamespacedName{Namespace: namespace, Name: s.Name}, s.Key)
case SecretValueSource:
return utils.GetSecretValue(ctx, client, types.NamespacedName{Namespace: namespace, Name: s.Name}, s.Key)
default:
return "", fmt.Errorf("unknown value source type: %s", s.Type)
}
}

// ValueRef represents a configuration value
// +kubebuilder:validation:ExactlyOneOf=value;valueFrom
type ValueRef struct {
Expand All @@ -173,23 +154,3 @@ type ValueRef struct {
// +optional
ValueFrom *ValueSource `json:"valueFrom,omitempty"`
}

func (r *ValueRef) Resolve(ctx context.Context, client client.Client, namespace string) (string, string, error) {
if r == nil {
return "", "", fmt.Errorf("ValueRef cannot be nil")
}

switch {
case r.Value != "":
return r.Name, r.Value, nil
case r.ValueFrom != nil:
value, err := r.ValueFrom.Resolve(ctx, client, namespace)
if err != nil {
return "", "", fmt.Errorf("failed to resolve value for ref %s: %v", r.Name, err)
}

return r.Name, value, nil
default:
return r.Name, "", nil
}
}
39 changes: 0 additions & 39 deletions go/api/v1alpha3/remotemcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,8 @@ limitations under the License.
package v1alpha3

import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// +kubebuilder:validation:Enum=SSE;STREAMABLE_HTTP
Expand Down Expand Up @@ -95,22 +88,6 @@ type RemoteMCPServerSpec struct {
TLS *TLSConfig `json:"tls,omitempty"`
}

var _ sql.Scanner = (*RemoteMCPServerSpec)(nil)

func (t *RemoteMCPServerSpec) Scan(src any) error {
switch v := src.(type) {
case []uint8:
return json.Unmarshal(v, t)
}
return nil
}

var _ driver.Valuer = (*RemoteMCPServerSpec)(nil)

func (t RemoteMCPServerSpec) Value() (driver.Value, error) {
return json.Marshal(t)
}

// RemoteMCPServerStatus defines the observed state of RemoteMCPServer.
type RemoteMCPServerStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
Expand Down Expand Up @@ -153,22 +130,6 @@ type RemoteMCPServer struct {
Status RemoteMCPServerStatus `json:"status,omitempty"`
}

// ResolveHeaders resolves all HeadersFrom entries using the object's namespace.
func (r *RemoteMCPServer) ResolveHeaders(ctx context.Context, client client.Client) (map[string]string, error) {
result := map[string]string{}

for _, h := range r.Spec.HeadersFrom {
k, v, err := h.Resolve(ctx, client, r.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to resolve header: %v", err)
}

result[k] = v
}

return result, nil
}

// +kubebuilder:object:root=true
// RemoteMCPServerList contains a list of RemoteMCPServer.
type RemoteMCPServerList struct {
Expand Down
49 changes: 48 additions & 1 deletion go/core/internal/service/tool/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/trace"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kagent-dev/kagent/go/api/v1alpha3"
Expand Down Expand Up @@ -135,7 +137,7 @@ func (c *RuntimeMCPClient) connect(ctx context.Context, ref MCPServerRef) (*mcp.
}
connectCtx, cancel := context.WithTimeout(ctx, timeout)

headers, err := server.ResolveHeaders(connectCtx, c.kubeClient)
headers, err := resolveHeaders(connectCtx, c.kubeClient, server.Namespace, server.Spec.HeadersFrom)
if err != nil {
cancel()
return nil, nil, fmt.Errorf("failed to resolve RemoteMCPServer headers: %w", err)
Expand Down Expand Up @@ -276,3 +278,48 @@ func (t *mcpAppsHeaderTransport) RoundTrip(request *http.Request) (*http.Respons
}
return t.base.RoundTrip(request)
}

// resolveHeaders reads RemoteMCPServer headers, loading ValueFrom entries from
// Secrets or ConfigMaps in the server's namespace.
func resolveHeaders(ctx context.Context, kube client.Reader, namespace string, refs []v1alpha3.ValueRef) (map[string]string, error) {
headers := make(map[string]string, len(refs))
for _, ref := range refs {
value, err := resolveHeaderValue(ctx, kube, namespace, ref)
if err != nil {
return nil, fmt.Errorf("failed to resolve header %s: %w", ref.Name, err)
}
headers[ref.Name] = value
}
return headers, nil
}

func resolveHeaderValue(ctx context.Context, kube client.Reader, namespace string, ref v1alpha3.ValueRef) (string, error) {
if ref.ValueFrom == nil {
return ref.Value, nil
}
key := types.NamespacedName{Namespace: namespace, Name: ref.ValueFrom.Name}
switch ref.ValueFrom.Type {
case v1alpha3.SecretValueSource:
secret := &corev1.Secret{}
if err := kube.Get(ctx, key, secret); err != nil {
return "", fmt.Errorf("failed to get Secret %s: %w", key, err)
}
value, ok := secret.Data[ref.ValueFrom.Key]
if !ok {
return "", fmt.Errorf("key %s not found in Secret %s", ref.ValueFrom.Key, key)
}
return string(value), nil
case v1alpha3.ConfigMapValueSource:
configMap := &corev1.ConfigMap{}
if err := kube.Get(ctx, key, configMap); err != nil {
return "", fmt.Errorf("failed to get ConfigMap %s: %w", key, err)
}
value, ok := configMap.Data[ref.ValueFrom.Key]
if !ok {
return "", fmt.Errorf("key %s not found in ConfigMap %s", ref.ValueFrom.Key, key)
}
return value, nil
default:
return "", fmt.Errorf("unknown value source type %q", ref.ValueFrom.Type)
}
}
70 changes: 70 additions & 0 deletions go/core/internal/service/tool/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -117,3 +118,72 @@ func TestRuntimeMCPClientResolveServerMatrix(t *testing.T) {
})
}
}

func TestResolveHeaders(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "creds", Namespace: "team"},
Data: map[string][]byte{"token": []byte("secret-token")},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "settings", Namespace: "team"},
Data: map[string]string{"tenant": "acme"},
}

t.Run("Given inline, Secret, and ConfigMap headers, when the server headers are resolved, assert that every value is loaded from the server namespace", func(t *testing.T) {
kube := toolTestKube(t, secret, configMap)

headers, err := resolveHeaders(t.Context(), kube, "team", []v1alpha3.ValueRef{
{Name: "X-Static", Value: "static"},
{Name: "Authorization", ValueFrom: &v1alpha3.ValueSource{Type: v1alpha3.SecretValueSource, Name: "creds", Key: "token"}},
{Name: "X-Tenant", ValueFrom: &v1alpha3.ValueSource{Type: v1alpha3.ConfigMapValueSource, Name: "settings", Key: "tenant"}},
})

require.NoError(t, err)
assert.Equal(t, map[string]string{
"X-Static": "static",
"Authorization": "secret-token",
"X-Tenant": "acme",
}, headers)
})

scenarios := []struct {
name string
objects []client.Object
ref v1alpha3.ValueRef
wantError string
}{
{
name: "Given the Secret lives in another namespace, when a Secret header is resolved, assert that resolution fails",
objects: []client.Object{&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "creds", Namespace: "other"}, Data: secret.Data}},
ref: v1alpha3.ValueRef{Name: "Authorization", ValueFrom: &v1alpha3.ValueSource{Type: v1alpha3.SecretValueSource, Name: "creds", Key: "token"}},
wantError: "failed to get Secret team/creds",
},
{
name: "Given the Secret lacks the key, when a Secret header is resolved, assert that resolution fails",
objects: []client.Object{secret},
ref: v1alpha3.ValueRef{Name: "Authorization", ValueFrom: &v1alpha3.ValueSource{Type: v1alpha3.SecretValueSource, Name: "creds", Key: "missing"}},
wantError: "key missing not found in Secret team/creds",
},
{
name: "Given the ConfigMap lacks the key, when a ConfigMap header is resolved, assert that resolution fails",
objects: []client.Object{configMap},
ref: v1alpha3.ValueRef{Name: "X-Tenant", ValueFrom: &v1alpha3.ValueSource{Type: v1alpha3.ConfigMapValueSource, Name: "settings", Key: "missing"}},
wantError: "key missing not found in ConfigMap team/settings",
},
{
name: "Given an unknown source type, when the header is resolved, assert that resolution fails",
ref: v1alpha3.ValueRef{Name: "X-Other", ValueFrom: &v1alpha3.ValueSource{Type: "Vault", Name: "x", Key: "y"}},
wantError: `unknown value source type "Vault"`,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
kube := toolTestKube(t, scenario.objects...)

headers, err := resolveHeaders(t.Context(), kube, "team", []v1alpha3.ValueRef{scenario.ref})

require.ErrorContains(t, err, scenario.wantError)
assert.Nil(t, headers)
})
}
}
Loading