@@ -4,11 +4,13 @@ package vmcpconfig
44import (
55"context"
66"encoding/json"
7+ "fmt"
78"time"
89
910"sigs.k8s.io/controller-runtime/pkg/log"
1011
1112mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
13+ "github.com/stacklok/toolhive/cmd/thv-operator/pkg/oidc"
1214authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types"
1315vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
1416)
@@ -18,19 +20,31 @@ const (
1820authzLabelValueInline = "inline"
1921// conflictResolutionPrefix is the string value for prefix conflict resolution strategy
2022conflictResolutionPrefix = "prefix"
23+ // vmcpOIDCClientSecretEnvVar is the environment variable name for the OIDC client secret.
24+ // The deployment controller mounts secrets as environment variables with this name.
25+ //nolint:gosec // This is an environment variable name, not a credential
26+ vmcpOIDCClientSecretEnvVar = "VMCP_OIDC_CLIENT_SECRET"
2127)
2228
2329// Converter converts VirtualMCPServer CRD specs to vmcp Config
24- type Converter struct {}
30+ type Converter struct {
31+ oidcResolver oidc.Resolver
32+ }
2533
26- // NewConverter creates a new Converter instance
27- func NewConverter () * Converter {
28- return & Converter {}
34+ // NewConverter creates a new Converter instance.
35+ // oidcResolver is required and used to resolve OIDC configuration from various sources
36+ // (kubernetes, configMap, inline). Use a mock resolver in tests.
37+ // Returns an error if oidcResolver is nil.
38+ func NewConverter (oidcResolver oidc.Resolver ) (* Converter , error ) {
39+ if oidcResolver == nil {
40+ return nil , fmt .Errorf ("oidcResolver is required" )
41+ }
42+ return & Converter {
43+ oidcResolver : oidcResolver ,
44+ }, nil
2945}
3046
3147// Convert converts VirtualMCPServer CRD spec to vmcp Config
32- //
33- //nolint:unparam // error return reserved for future reference resolution
3448func (c * Converter ) Convert (
3549ctx context.Context ,
3650vmcp * mcpv1alpha1.VirtualMCPServer ,
@@ -42,7 +56,11 @@ func (c *Converter) Convert(
4256
4357// Convert IncomingAuth
4458if vmcp .Spec .IncomingAuth != nil {
45- config .IncomingAuth = c .convertIncomingAuth (ctx , vmcp )
59+ incomingAuth , err := c .convertIncomingAuth (ctx , vmcp )
60+ if err != nil {
61+ return nil , fmt .Errorf ("failed to convert incoming auth: %w" , err )
62+ }
63+ config .IncomingAuth = incomingAuth
4664}
4765
4866// Convert OutgoingAuth - always set with defaults if not specified
@@ -84,45 +102,34 @@ func (c *Converter) Convert(
84102return config , nil
85103}
86104
87- // convertIncomingAuth converts IncomingAuthConfig from CRD to vmcp config
88- func (* Converter ) convertIncomingAuth (
89- _ context.Context ,
105+ // convertIncomingAuth converts IncomingAuthConfig from CRD to vmcp config.
106+ func (c * Converter ) convertIncomingAuth (
107+ ctx context.Context ,
90108vmcp * mcpv1alpha1.VirtualMCPServer ,
91- ) * vmcpconfig.IncomingAuthConfig {
109+ ) (* vmcpconfig.IncomingAuthConfig , error ) {
110+ ctxLogger := log .FromContext (ctx )
111+
92112incoming := & vmcpconfig.IncomingAuthConfig {
93113Type : vmcp .Spec .IncomingAuth .Type ,
94114}
95115
96116// Convert OIDC configuration if present
97117if vmcp .Spec .IncomingAuth .OIDCConfig != nil {
98- // Handle inline OIDC configuration
99- if vmcp .Spec .IncomingAuth .OIDCConfig .Type == authzLabelValueInline && vmcp .Spec .IncomingAuth .OIDCConfig .Inline != nil {
100- inline := vmcp .Spec .IncomingAuth .OIDCConfig .Inline
101- oidcConfig := & vmcpconfig.OIDCConfig {
102- Issuer : inline .Issuer ,
103- ClientID : inline .ClientID , // Note: API uses clientId (camelCase) but config uses ClientID
104- Audience : inline .Audience ,
105- Resource : vmcp .Spec .IncomingAuth .OIDCConfig .ResourceURL ,
106- Scopes : nil , // TODO: Add scopes if needed
107- ProtectedResourceAllowPrivateIP : inline .ProtectedResourceAllowPrivateIP ,
108- InsecureAllowHTTP : inline .InsecureAllowHTTP ,
109- }
110-
111- // Handle client secret - always use environment variable reference for security
112- // Both ClientSecretRef (reference to existing secret) and ClientSecret (literal value)
113- // are mounted as environment variables by the deployment controller
114- if inline .ClientSecretRef != nil || inline .ClientSecret != "" {
115- // Generate environment variable name that will be mounted in the deployment
116- // The deployment controller will mount the secret (either from ClientSecretRef or
117- // from a generated secret for ClientSecret literal values)
118- oidcConfig .ClientSecretEnv = "VMCP_OIDC_CLIENT_SECRET"
119- }
120-
121- incoming .OIDC = oidcConfig
122- } else {
123- // TODO: Handle configMap and kubernetes types
124- // For now, create empty config to avoid nil pointer
125- incoming .OIDC = & vmcpconfig.OIDCConfig {}
118+ // Use the OIDC resolver to handle all OIDC types (kubernetes, configMap, inline)
119+ // VirtualMCPServer implements OIDCConfigurable, so the resolver can work with it directly
120+ resolvedConfig , err := c .oidcResolver .Resolve (ctx , vmcp )
121+ if err != nil {
122+ ctxLogger .Error (err , "failed to resolve OIDC config" ,
123+ "vmcp" , vmcp .Name ,
124+ "namespace" , vmcp .Namespace ,
125+ "oidcType" , vmcp .Spec .IncomingAuth .OIDCConfig .Type )
126+ // Fail closed: return error when OIDC is configured but resolution fails
127+ // This prevents deploying without authentication when OIDC is explicitly requested
128+ return nil , fmt .Errorf ("OIDC resolution failed for type %q: %w" ,
129+ vmcp .Spec .IncomingAuth .OIDCConfig .Type , err )
130+ }
131+ if resolvedConfig != nil {
132+ incoming .OIDC = mapResolvedOIDCToVmcpConfig (resolvedConfig , vmcp .Spec .IncomingAuth .OIDCConfig )
126133}
127134}
128135
@@ -146,7 +153,53 @@ func (*Converter) convertIncomingAuth(
146153// TODO: Load policies from ConfigMap if Type is "configMap"
147154}
148155
149- return incoming
156+ return incoming , nil
157+ }
158+
159+ // mapResolvedOIDCToVmcpConfig maps from oidc.OIDCConfig (resolved by the OIDC resolver)
160+ // to vmcpconfig.OIDCConfig (used by the vmcp runtime).
161+ // This keeps the vmcp config types separate from the operator's OIDC resolver types,
162+ // maintaining clean architectural boundaries while enabling unified OIDC resolution.
163+ func mapResolvedOIDCToVmcpConfig (
164+ resolved * oidc.OIDCConfig ,
165+ oidcConfigRef * mcpv1alpha1.OIDCConfigRef ,
166+ ) * vmcpconfig.OIDCConfig {
167+ if resolved == nil {
168+ return nil
169+ }
170+
171+ config := & vmcpconfig.OIDCConfig {
172+ Issuer : resolved .Issuer ,
173+ ClientID : resolved .ClientID ,
174+ Audience : resolved .Audience ,
175+ Resource : resolved .ResourceURL ,
176+ ProtectedResourceAllowPrivateIP : resolved .JWKSAllowPrivateIP ,
177+ InsecureAllowHTTP : resolved .InsecureAllowHTTP ,
178+ // Scopes are not currently in oidc.OIDCConfig - should be added later
179+ }
180+
181+ // Handle client secret - the deployment controller mounts secrets as environment variables
182+ // We need to set ClientSecretEnv for all OIDC config types that may have a client secret
183+ if oidcConfigRef != nil {
184+ switch oidcConfigRef .Type {
185+ case mcpv1alpha1 .OIDCConfigTypeInline :
186+ // Inline config: check if ClientSecretRef or ClientSecret is set
187+ if oidcConfigRef .Inline != nil {
188+ if oidcConfigRef .Inline .ClientSecretRef != nil || oidcConfigRef .Inline .ClientSecret != "" {
189+ config .ClientSecretEnv = vmcpOIDCClientSecretEnvVar
190+ }
191+ }
192+ case mcpv1alpha1 .OIDCConfigTypeConfigMap :
193+ // ConfigMap config: check if the resolved config has a client secret
194+ // Note: Storing secrets in ConfigMaps is not recommended; use inline with SecretRef instead
195+ if resolved .ClientSecret != "" {
196+ config .ClientSecretEnv = vmcpOIDCClientSecretEnvVar
197+ }
198+ // OIDCConfigTypeKubernetes does not use client secrets (uses service account tokens)
199+ }
200+ }
201+
202+ return config
150203}
151204
152205// convertOutgoingAuth converts OutgoingAuthConfig from CRD to vmcp config
0 commit comments