Skip to content

Commit 3da9d52

Browse files
committed
discover and mount ExternalAuthConfig from MCPServers for VirtualMCPServer
1 parent 7420133 commit 3da9d52

File tree

9 files changed

+2005
-86
lines changed

9 files changed

+2005
-86
lines changed

cmd/thv-operator/controllers/virtualmcpserver_controller.go

Lines changed: 194 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ import (
3030
"github.com/stacklok/toolhive/pkg/groups"
3131
vmcptypes "github.com/stacklok/toolhive/pkg/vmcp"
3232
"github.com/stacklok/toolhive/pkg/vmcp/aggregator"
33+
"github.com/stacklok/toolhive/pkg/vmcp/auth/converters"
34+
authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types"
35+
vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
3336
"github.com/stacklok/toolhive/pkg/vmcp/workloads"
3437
)
3538

39+
const (
40+
// OutgoingAuthSourceDiscovered indicates that auth configs should be automatically discovered from MCPServers
41+
OutgoingAuthSourceDiscovered = "discovered"
42+
// OutgoingAuthSourceInline indicates that auth configs should be explicitly specified
43+
OutgoingAuthSourceInline = "inline"
44+
)
45+
3646
// VirtualMCPServerReconciler reconciles a VirtualMCPServer object
3747
//
3848
// Resource Cleanup Strategy:
@@ -896,6 +906,169 @@ func createVmcpServiceURL(vmcpName, namespace string, port int32) string {
896906
return fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, namespace, port)
897907
}
898908

909+
// convertExternalAuthConfigToStrategy converts an MCPExternalAuthConfig to a BackendAuthStrategy.
910+
// This uses the converter registry to support all auth types (token exchange, header injection, etc.).
911+
// For ConfigMap mode (inline), secrets are referenced as environment variables that will be
912+
// mounted in the deployment.
913+
func (r *VirtualMCPServerReconciler) convertExternalAuthConfigToStrategy(
914+
ctx context.Context,
915+
namespace string,
916+
externalAuthConfig *mcpv1alpha1.MCPExternalAuthConfig,
917+
) (*authtypes.BackendAuthStrategy, error) {
918+
// Use the converter registry to convert to typed strategy
919+
registry := converters.DefaultRegistry()
920+
converter, err := registry.GetConverter(externalAuthConfig.Spec.Type)
921+
if err != nil {
922+
return nil, err
923+
}
924+
925+
// Convert to typed BackendAuthStrategy (this will use env var references for secrets)
926+
strategy, err := converter.ConvertToStrategy(externalAuthConfig)
927+
if err != nil {
928+
return nil, fmt.Errorf("failed to convert external auth config to strategy: %w", err)
929+
}
930+
931+
// For header injection, resolve secrets from Kubernetes
932+
if externalAuthConfig.Spec.Type == mcpv1alpha1.ExternalAuthTypeHeaderInjection {
933+
strategy, err = converter.ResolveSecrets(ctx, externalAuthConfig, r.Client, namespace, strategy)
934+
if err != nil {
935+
return nil, fmt.Errorf("failed to resolve secrets for header injection: %w", err)
936+
}
937+
}
938+
939+
return strategy, nil
940+
}
941+
942+
// convertBackendAuthConfigToVMCP converts a BackendAuthConfig from CRD to vmcp config.
943+
func (r *VirtualMCPServerReconciler) convertBackendAuthConfigToVMCP(
944+
ctx context.Context,
945+
namespace string,
946+
crdConfig *mcpv1alpha1.BackendAuthConfig,
947+
) (*authtypes.BackendAuthStrategy, error) {
948+
// For type="discovered", return a minimal strategy (will be populated by discovery)
949+
if crdConfig.Type == mcpv1alpha1.BackendAuthTypeDiscovered {
950+
return &authtypes.BackendAuthStrategy{
951+
Type: crdConfig.Type,
952+
}, nil
953+
}
954+
955+
// For type="external_auth_config_ref", fetch and convert the referenced config
956+
if crdConfig.ExternalAuthConfigRef != nil {
957+
// Fetch the MCPExternalAuthConfig and convert it
958+
externalAuthConfig, err := ctrlutil.GetExternalAuthConfigByName(
959+
ctx, r.Client, namespace, crdConfig.ExternalAuthConfigRef.Name)
960+
if err != nil {
961+
return nil, fmt.Errorf("failed to get MCPExternalAuthConfig %s: %w", crdConfig.ExternalAuthConfigRef.Name, err)
962+
}
963+
964+
// Convert the external auth config to strategy
965+
return r.convertExternalAuthConfigToStrategy(ctx, namespace, externalAuthConfig)
966+
}
967+
968+
// Fallback: return minimal strategy
969+
return &authtypes.BackendAuthStrategy{
970+
Type: crdConfig.Type,
971+
}, nil
972+
}
973+
974+
// discovers ExternalAuthConfig from MCPServers and adds them to the outgoing config
975+
func (r *VirtualMCPServerReconciler) discoverExternalAuthConfigs(
976+
ctx context.Context,
977+
vmcp *mcpv1alpha1.VirtualMCPServer,
978+
workloadNames []string,
979+
outgoing *vmcpconfig.OutgoingAuthConfig,
980+
) {
981+
ctxLogger := log.FromContext(ctx)
982+
983+
for _, workloadName := range workloadNames {
984+
mcpServer := &mcpv1alpha1.MCPServer{}
985+
if err := r.Get(ctx, types.NamespacedName{Name: workloadName, Namespace: vmcp.Namespace}, mcpServer); err != nil {
986+
// Skip if MCPServer not found (might be a different workload type)
987+
continue
988+
}
989+
990+
// Only process if MCPServer has ExternalAuthConfigRef
991+
if mcpServer.Spec.ExternalAuthConfigRef == nil {
992+
continue
993+
}
994+
995+
// Fetch the MCPExternalAuthConfig
996+
externalAuthConfig, err := ctrlutil.GetExternalAuthConfigByName(
997+
ctx, r.Client, vmcp.Namespace, mcpServer.Spec.ExternalAuthConfigRef.Name)
998+
if err != nil {
999+
ctxLogger.V(1).Info("Failed to get MCPExternalAuthConfig for backend, skipping",
1000+
"backend", workloadName,
1001+
"externalAuthConfig", mcpServer.Spec.ExternalAuthConfigRef.Name,
1002+
"error", err)
1003+
continue
1004+
}
1005+
1006+
// Convert MCPExternalAuthConfig to BackendAuthStrategy
1007+
strategy, err := r.convertExternalAuthConfigToStrategy(ctx, vmcp.Namespace, externalAuthConfig)
1008+
if err != nil {
1009+
ctxLogger.V(1).Info("Failed to convert MCPExternalAuthConfig to strategy, skipping",
1010+
"backend", workloadName,
1011+
"externalAuthConfig", externalAuthConfig.Name,
1012+
"error", err)
1013+
continue
1014+
}
1015+
1016+
// Only add if not already overridden in inline config
1017+
if vmcp.Spec.OutgoingAuth == nil || vmcp.Spec.OutgoingAuth.Backends == nil {
1018+
outgoing.Backends[workloadName] = strategy
1019+
} else if _, exists := vmcp.Spec.OutgoingAuth.Backends[workloadName]; !exists {
1020+
// Only add discovered config if not explicitly overridden
1021+
outgoing.Backends[workloadName] = strategy
1022+
}
1023+
}
1024+
}
1025+
1026+
// buildOutgoingAuthConfig builds an OutgoingAuthConfig from the VirtualMCPServer spec,
1027+
// discovering ExternalAuthConfig from MCPServers when source is "discovered".
1028+
func (r *VirtualMCPServerReconciler) buildOutgoingAuthConfig(
1029+
ctx context.Context,
1030+
vmcp *mcpv1alpha1.VirtualMCPServer,
1031+
workloadNames []string,
1032+
) (*vmcpconfig.OutgoingAuthConfig, error) {
1033+
// Determine source - default to "discovered" if not specified
1034+
source := OutgoingAuthSourceDiscovered
1035+
if vmcp.Spec.OutgoingAuth != nil && vmcp.Spec.OutgoingAuth.Source != "" {
1036+
source = vmcp.Spec.OutgoingAuth.Source
1037+
}
1038+
1039+
outgoing := &vmcpconfig.OutgoingAuthConfig{
1040+
Source: source,
1041+
Backends: make(map[string]*authtypes.BackendAuthStrategy),
1042+
}
1043+
1044+
// Convert Default if specified
1045+
if vmcp.Spec.OutgoingAuth != nil && vmcp.Spec.OutgoingAuth.Default != nil {
1046+
defaultStrategy, err := r.convertBackendAuthConfigToVMCP(ctx, vmcp.Namespace, vmcp.Spec.OutgoingAuth.Default)
1047+
if err != nil {
1048+
return nil, fmt.Errorf("failed to convert default auth config: %w", err)
1049+
}
1050+
outgoing.Default = defaultStrategy
1051+
}
1052+
1053+
// Discover ExternalAuthConfig from MCPServers if source is "discovered"
1054+
if source == OutgoingAuthSourceDiscovered {
1055+
r.discoverExternalAuthConfigs(ctx, vmcp, workloadNames, outgoing)
1056+
}
1057+
1058+
// Apply inline overrides (works for all source modes)
1059+
if vmcp.Spec.OutgoingAuth != nil && vmcp.Spec.OutgoingAuth.Backends != nil {
1060+
for backendName, backendAuth := range vmcp.Spec.OutgoingAuth.Backends {
1061+
strategy, err := r.convertBackendAuthConfigToVMCP(ctx, vmcp.Namespace, &backendAuth)
1062+
if err != nil {
1063+
return nil, fmt.Errorf("failed to convert backend auth config for %s: %w", backendName, err)
1064+
}
1065+
outgoing.Backends[backendName] = strategy
1066+
}
1067+
}
1068+
1069+
return outgoing, nil
1070+
}
1071+
8991072
// discoverBackends discovers all MCPServers in the referenced MCPGroup and returns
9001073
// a list of DiscoveredBackend objects with their current status.
9011074
// This reuses the existing workload discovery code from pkg/vmcp/workloads.
@@ -911,22 +1084,35 @@ func (r *VirtualMCPServerReconciler) discoverBackends(
9111084
// Create K8S workload discoverer for the VirtualMCPServer's namespace
9121085
workloadDiscoverer := workloads.NewK8SDiscovererWithClient(r.Client, vmcp.Namespace)
9131086

1087+
// Get all workload names in the group
1088+
workloadNames, err := workloadDiscoverer.ListWorkloadsInGroup(ctx, vmcp.Spec.GroupRef.Name)
1089+
if err != nil {
1090+
return nil, fmt.Errorf("failed to list workloads in group: %w", err)
1091+
}
1092+
1093+
// Build outgoing auth config only if OutgoingAuth is explicitly configured
1094+
// This allows the aggregator to apply auth config to backends based on source mode
1095+
var authConfig *vmcpconfig.OutgoingAuthConfig
1096+
if vmcp.Spec.OutgoingAuth != nil {
1097+
var err error
1098+
authConfig, err = r.buildOutgoingAuthConfig(ctx, vmcp, workloadNames)
1099+
if err != nil {
1100+
ctxLogger.V(1).Info("Failed to build outgoing auth config, continuing without auth",
1101+
"error", err)
1102+
// Continue without auth config rather than failing
1103+
authConfig = nil
1104+
}
1105+
}
1106+
9141107
// Use the aggregator's unified backend discoverer to reuse discovery logic
915-
// Pass nil for authConfig since we'll extract auth config from MCPServer directly
916-
backendDiscoverer := aggregator.NewUnifiedBackendDiscoverer(workloadDiscoverer, groupsManager, nil)
1108+
backendDiscoverer := aggregator.NewUnifiedBackendDiscoverer(workloadDiscoverer, groupsManager, authConfig)
9171109

9181110
// Discover backends using the aggregator
9191111
backends, err := backendDiscoverer.Discover(ctx, vmcp.Spec.GroupRef.Name)
9201112
if err != nil {
9211113
return nil, fmt.Errorf("failed to discover backends: %w", err)
9221114
}
9231115

924-
// Get all workload names to track backends that weren't accessible
925-
workloadNames, err := workloadDiscoverer.ListWorkloadsInGroup(ctx, vmcp.Spec.GroupRef.Name)
926-
if err != nil {
927-
return nil, fmt.Errorf("failed to list workloads in group: %w", err)
928-
}
929-
9301116
// Create a map of discovered backend names for quick lookup
9311117
discoveredBackendMap := make(map[string]*vmcptypes.Backend, len(backends))
9321118
for i := range backends {

0 commit comments

Comments
 (0)