diff --git a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go index d1e13fc5f..0daff44c6 100644 --- a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go +++ b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types.go @@ -34,10 +34,6 @@ type VirtualMCPServerSpec struct { // +optional CompositeToolRefs []CompositeToolDefinitionRef `json:"compositeToolRefs,omitempty"` - // TokenCache configures token caching behavior - // +optional - TokenCache *TokenCacheConfig `json:"tokenCache,omitempty"` - // Operational defines operational settings like timeouts and health checks // +optional Operational *OperationalConfig `json:"operational,omitempty"` @@ -283,64 +279,6 @@ type ErrorHandling struct { RetryDelay string `json:"retryDelay,omitempty"` } -// TokenCacheConfig configures token caching behavior -type TokenCacheConfig struct { - // Provider defines the cache provider type - // +kubebuilder:validation:Enum=memory;redis - // +kubebuilder:default=memory - // +optional - Provider string `json:"provider,omitempty"` - - // Memory configures in-memory token caching - // Only used when Provider is "memory" - // +optional - Memory *MemoryCacheConfig `json:"memory,omitempty"` - - // Redis configures Redis token caching - // Only used when Provider is "redis" - // +optional - Redis *RedisCacheConfig `json:"redis,omitempty"` -} - -// MemoryCacheConfig configures in-memory token caching -type MemoryCacheConfig struct { - // MaxEntries is the maximum number of cache entries - // +kubebuilder:default=1000 - // +optional - MaxEntries int `json:"maxEntries,omitempty"` - - // TTLOffset is the duration before token expiry to refresh - // +kubebuilder:default="5m" - // +optional - TTLOffset string `json:"ttlOffset,omitempty"` -} - -// RedisCacheConfig configures Redis token caching -type RedisCacheConfig struct { - // Address is the Redis server address - // +kubebuilder:validation:Required - Address string `json:"address"` - - // DB is the Redis database number - // +kubebuilder:default=0 - // +optional - DB int `json:"db,omitempty"` - - // KeyPrefix is the prefix for cache keys - // +kubebuilder:default="vmcp:tokens:" - // +optional - KeyPrefix string `json:"keyPrefix,omitempty"` - - // PasswordRef references a secret containing the Redis password - // +optional - PasswordRef *SecretKeyRef `json:"passwordRef,omitempty"` - - // TLS enables TLS for Redis connections - // +kubebuilder:default=false - // +optional - TLS bool `json:"tls,omitempty"` -} - // OperationalConfig defines operational settings type OperationalConfig struct { // Timeouts configures timeout settings diff --git a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go index e87e368ff..b662908f1 100644 --- a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go +++ b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_types_test.go @@ -147,9 +147,6 @@ func TestVirtualMCPServerDefaultValues(t *testing.T) { Aggregation: &AggregationConfig{ ConflictResolution: "", // Should default to "prefix" }, - TokenCache: &TokenCacheConfig{ - Provider: "", // Should default to "memory" - }, }, } @@ -157,7 +154,6 @@ func TestVirtualMCPServerDefaultValues(t *testing.T) { // but we document expected values here assert.NotNil(t, vmcp.Spec.OutgoingAuth) assert.NotNil(t, vmcp.Spec.Aggregation) - assert.NotNil(t, vmcp.Spec.TokenCache) } func TestVirtualMCPServerNamespaceIsolation(t *testing.T) { diff --git a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook.go b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook.go index 0786b92bf..0e2107b6d 100644 --- a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook.go +++ b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook.go @@ -74,13 +74,6 @@ func (r *VirtualMCPServer) Validate() error { } } - // Validate TokenCache configuration - if r.Spec.TokenCache != nil { - if err := r.validateTokenCache(); err != nil { - return err - } - } - return nil } @@ -355,39 +348,3 @@ func validateStepErrorHandling(toolIndex, stepIndex int, step WorkflowStep) erro return nil } - -// validateTokenCache validates token cache configuration -func (r *VirtualMCPServer) validateTokenCache() error { - cache := r.Spec.TokenCache - - // Validate provider - if cache.Provider != "" { - validProviders := map[string]bool{ - "memory": true, - "redis": true, - } - if !validProviders[cache.Provider] { - return fmt.Errorf("spec.tokenCache.provider must be memory or redis") - } - } - - // Validate provider-specific configuration - if cache.Provider == "redis" || (cache.Provider == "" && cache.Redis != nil) { - if cache.Redis == nil { - return fmt.Errorf("spec.tokenCache.redis is required when provider is redis") - } - if cache.Redis.Address == "" { - return fmt.Errorf("spec.tokenCache.redis.address is required") - } - if cache.Redis.PasswordRef != nil { - if cache.Redis.PasswordRef.Name == "" { - return fmt.Errorf("spec.tokenCache.redis.passwordRef.name is required when passwordRef is specified") - } - if cache.Redis.PasswordRef.Key == "" { - return fmt.Errorf("spec.tokenCache.redis.passwordRef.key is required when passwordRef is specified") - } - } - } - - return nil -} diff --git a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook_test.go b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook_test.go index 149c63580..bffbd28c3 100644 --- a/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook_test.go +++ b/cmd/thv-operator/api/v1alpha1/virtualmcpserver_webhook_test.go @@ -369,67 +369,6 @@ func TestVirtualMCPServerValidate(t *testing.T) { wantErr: true, errMsg: "spec.compositeTools[0].steps[1].id \"step1\" is duplicated", }, - { - name: "valid token cache - memory", - vmcp: &VirtualMCPServer{ - Spec: VirtualMCPServerSpec{ - GroupRef: GroupRef{Name: "test-group"}, - TokenCache: &TokenCacheConfig{ - Provider: "memory", - Memory: &MemoryCacheConfig{ - MaxEntries: 1000, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "valid token cache - redis with password", - vmcp: &VirtualMCPServer{ - Spec: VirtualMCPServerSpec{ - GroupRef: GroupRef{Name: "test-group"}, - TokenCache: &TokenCacheConfig{ - Provider: "redis", - Redis: &RedisCacheConfig{ - Address: "redis:6379", - PasswordRef: &SecretKeyRef{ - Name: "redis-secret", - Key: "password", - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "invalid token cache - redis without address", - vmcp: &VirtualMCPServer{ - Spec: VirtualMCPServerSpec{ - GroupRef: GroupRef{Name: "test-group"}, - TokenCache: &TokenCacheConfig{ - Provider: "redis", - Redis: &RedisCacheConfig{}, - }, - }, - }, - wantErr: true, - errMsg: "spec.tokenCache.redis.address is required", - }, - { - name: "invalid token cache - invalid provider", - vmcp: &VirtualMCPServer{ - Spec: VirtualMCPServerSpec{ - GroupRef: GroupRef{Name: "test-group"}, - TokenCache: &TokenCacheConfig{ - Provider: "invalid", - }, - }, - }, - wantErr: true, - errMsg: "spec.tokenCache.provider must be memory or redis", - }, } for _, tt := range tests { diff --git a/cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go b/cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go index 77190e5ef..b35be175f 100644 --- a/cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -1289,21 +1289,6 @@ func (in *MCPToolConfigStatus) DeepCopy() *MCPToolConfigStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MemoryCacheConfig) DeepCopyInto(out *MemoryCacheConfig) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemoryCacheConfig. -func (in *MemoryCacheConfig) DeepCopy() *MemoryCacheConfig { - if in == nil { - return nil - } - out := new(MemoryCacheConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NameFilter) DeepCopyInto(out *NameFilter) { *out = *in @@ -1602,26 +1587,6 @@ func (in *ProxyDeploymentOverrides) DeepCopy() *ProxyDeploymentOverrides { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RedisCacheConfig) DeepCopyInto(out *RedisCacheConfig) { - *out = *in - if in.PasswordRef != nil { - in, out := &in.PasswordRef, &out.PasswordRef - *out = new(SecretKeyRef) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisCacheConfig. -func (in *RedisCacheConfig) DeepCopy() *RedisCacheConfig { - if in == nil { - return nil - } - out := new(RedisCacheConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistryFilter) DeepCopyInto(out *RegistryFilter) { *out = *in @@ -1913,31 +1878,6 @@ func (in *TimeoutConfig) DeepCopy() *TimeoutConfig { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TokenCacheConfig) DeepCopyInto(out *TokenCacheConfig) { - *out = *in - if in.Memory != nil { - in, out := &in.Memory, &out.Memory - *out = new(MemoryCacheConfig) - **out = **in - } - if in.Redis != nil { - in, out := &in.Redis, &out.Redis - *out = new(RedisCacheConfig) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCacheConfig. -func (in *TokenCacheConfig) DeepCopy() *TokenCacheConfig { - if in == nil { - return nil - } - out := new(TokenCacheConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenExchangeConfig) DeepCopyInto(out *TokenExchangeConfig) { *out = *in @@ -2201,11 +2141,6 @@ func (in *VirtualMCPServerSpec) DeepCopyInto(out *VirtualMCPServerSpec) { *out = make([]CompositeToolDefinitionRef, len(*in)) copy(*out, *in) } - if in.TokenCache != nil { - in, out := &in.TokenCache, &out.TokenCache - *out = new(TokenCacheConfig) - (*in).DeepCopyInto(*out) - } if in.Operational != nil { in, out := &in.Operational, &out.Operational *out = new(OperationalConfig) diff --git a/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go b/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go index a0136a486..d385ed20c 100644 --- a/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go +++ b/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go @@ -819,80 +819,6 @@ func TestVirtualMCPServerAuthConfiguredCondition(t *testing.T) { expectedAuthReason: mcpv1alpha1.ConditionReasonAuthValid, expectError: false, }, - { - name: "Redis token cache with missing password", - vmcp: &mcpv1alpha1.VirtualMCPServer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-vmcp", - Namespace: "default", - }, - Spec: mcpv1alpha1.VirtualMCPServerSpec{ - GroupRef: mcpv1alpha1.GroupRef{ - Name: "test-group", - }, - IncomingAuth: &mcpv1alpha1.IncomingAuthConfig{ - Type: "anonymous", - }, - TokenCache: &mcpv1alpha1.TokenCacheConfig{ - Provider: "redis", - Redis: &mcpv1alpha1.RedisCacheConfig{ - Address: "redis:6379", - PasswordRef: &mcpv1alpha1.SecretKeyRef{ - Name: "missing-redis-secret", - Key: "password", - }, - }, - }, - }, - }, - secrets: []client.Object{}, - expectAuthCondition: true, - expectedAuthStatus: metav1.ConditionFalse, - expectedAuthReason: mcpv1alpha1.ConditionReasonAuthInvalid, - expectError: true, - }, - { - name: "Redis token cache with valid password", - vmcp: &mcpv1alpha1.VirtualMCPServer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-vmcp", - Namespace: "default", - }, - Spec: mcpv1alpha1.VirtualMCPServerSpec{ - GroupRef: mcpv1alpha1.GroupRef{ - Name: "test-group", - }, - IncomingAuth: &mcpv1alpha1.IncomingAuthConfig{ - Type: "anonymous", - }, - TokenCache: &mcpv1alpha1.TokenCacheConfig{ - Provider: "redis", - Redis: &mcpv1alpha1.RedisCacheConfig{ - Address: "redis:6379", - PasswordRef: &mcpv1alpha1.SecretKeyRef{ - Name: "redis-secret", - Key: "password", - }, - }, - }, - }, - }, - secrets: []client.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-secret", - Namespace: "default", - }, - Data: map[string][]byte{ - "password": []byte("redis-password"), - }, - }, - }, - expectAuthCondition: true, - expectedAuthStatus: metav1.ConditionTrue, - expectedAuthReason: mcpv1alpha1.ConditionReasonAuthValid, - expectError: false, - }, { name: "OIDC secret exists but missing required key", vmcp: &mcpv1alpha1.VirtualMCPServer{ @@ -936,48 +862,6 @@ func TestVirtualMCPServerAuthConfiguredCondition(t *testing.T) { expectedAuthReason: mcpv1alpha1.ConditionReasonAuthInvalid, expectError: true, }, - { - name: "Redis secret exists but missing required key", - vmcp: &mcpv1alpha1.VirtualMCPServer{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-vmcp", - Namespace: "default", - }, - Spec: mcpv1alpha1.VirtualMCPServerSpec{ - GroupRef: mcpv1alpha1.GroupRef{ - Name: "test-group", - }, - IncomingAuth: &mcpv1alpha1.IncomingAuthConfig{ - Type: "anonymous", - }, - TokenCache: &mcpv1alpha1.TokenCacheConfig{ - Provider: "redis", - Redis: &mcpv1alpha1.RedisCacheConfig{ - Address: "redis:6379", - PasswordRef: &mcpv1alpha1.SecretKeyRef{ - Name: "redis-secret", - Key: "password", - }, - }, - }, - }, - }, - secrets: []client.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-secret", - Namespace: "default", - }, - Data: map[string][]byte{ - "not-password": []byte("redis-password"), - }, - }, - }, - expectAuthCondition: true, - expectedAuthStatus: metav1.ConditionFalse, - expectedAuthReason: mcpv1alpha1.ConditionReasonAuthInvalid, - expectError: true, - }, } for _, tt := range tests { diff --git a/cmd/thv-operator/controllers/virtualmcpserver_deployment.go b/cmd/thv-operator/controllers/virtualmcpserver_deployment.go index be5fbacaa..196d6862e 100644 --- a/cmd/thv-operator/controllers/virtualmcpserver_deployment.go +++ b/cmd/thv-operator/controllers/virtualmcpserver_deployment.go @@ -385,7 +385,6 @@ func getVmcpImage() string { // Validated secrets include: // - OIDC client secrets (IncomingAuth.OIDCConfig.Inline.ClientSecretRef) // - Service account credentials (OutgoingAuth.*.ServiceAccount.CredentialsRef) -// - Redis passwords (TokenCache.Redis.PasswordRef) // // This follows the pattern from ctrlutil.GenerateOIDCClientSecretEnvVar() which validates secrets // exist before pod creation. @@ -423,17 +422,6 @@ func (r *VirtualMCPServerReconciler) validateSecretReferences( } } - // Validate Redis password if configured - if vmcp.Spec.TokenCache != nil && - vmcp.Spec.TokenCache.Redis != nil && - vmcp.Spec.TokenCache.Redis.PasswordRef != nil { - if err := r.validateSecretKeyRef(ctx, vmcp.Namespace, - vmcp.Spec.TokenCache.Redis.PasswordRef, - "Redis password"); err != nil { - return err - } - } - return nil } diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter.go b/cmd/thv-operator/pkg/vmcpconfig/converter.go index 14b76a1a8..bf0bcec4e 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter.go @@ -72,11 +72,6 @@ func (c *Converter) Convert( config.CompositeTools = c.convertCompositeTools(ctx, vmcp) } - // Convert TokenCache - if vmcp.Spec.TokenCache != nil { - config.TokenCache = c.convertTokenCache(ctx, vmcp) - } - // Convert Operational if vmcp.Spec.Operational != nil { config.Operational = c.convertOperational(ctx, vmcp) @@ -339,42 +334,6 @@ func convertArguments(args map[string]string) map[string]any { return result } -// convertTokenCache converts TokenCacheConfig from CRD to vmcp config -func (*Converter) convertTokenCache( - _ context.Context, - vmcp *mcpv1alpha1.VirtualMCPServer, -) *vmcpconfig.TokenCacheConfig { - cache := &vmcpconfig.TokenCacheConfig{ - Provider: vmcp.Spec.TokenCache.Provider, - } - - if vmcp.Spec.TokenCache.Memory != nil { - cache.Memory = &vmcpconfig.MemoryCacheConfig{ - MaxEntries: vmcp.Spec.TokenCache.Memory.MaxEntries, - } - if vmcp.Spec.TokenCache.Memory.TTLOffset != "" { - if duration, err := time.ParseDuration(vmcp.Spec.TokenCache.Memory.TTLOffset); err == nil { - cache.Memory.TTLOffset = vmcpconfig.Duration(duration) - } - } - } - - if vmcp.Spec.TokenCache.Redis != nil { - cache.Redis = &vmcpconfig.RedisCacheConfig{ - Address: vmcp.Spec.TokenCache.Redis.Address, - DB: vmcp.Spec.TokenCache.Redis.DB, - KeyPrefix: vmcp.Spec.TokenCache.Redis.KeyPrefix, - // TODO: Resolve password from secret reference when PasswordRef is set - } - //nolint:staticcheck // Empty branch reserved for future password reference resolution - if vmcp.Spec.TokenCache.Redis.PasswordRef != nil { - // Password will be resolved at runtime by vmcp binary via secret reference - } - } - - return cache -} - // convertOperational converts OperationalConfig from CRD to vmcp config func (*Converter) convertOperational( _ context.Context, diff --git a/cmd/vmcp/app/commands.go b/cmd/vmcp/app/commands.go index cf405f4b1..eb87d8718 100644 --- a/cmd/vmcp/app/commands.go +++ b/cmd/vmcp/app/commands.go @@ -160,10 +160,6 @@ This command checks: cfg.OutgoingAuth.Source) logger.Infof(" Conflict Resolution: %s", cfg.Aggregation.ConflictResolution) - if cfg.TokenCache != nil { - logger.Infof(" Token Cache: %s", cfg.TokenCache.Provider) - } - if len(cfg.CompositeTools) > 0 { logger.Infof(" Composite Tools: %d defined", len(cfg.CompositeTools)) } diff --git a/deploy/charts/operator-crds/Chart.yaml b/deploy/charts/operator-crds/Chart.yaml index 0401ec855..a23859333 100644 --- a/deploy/charts/operator-crds/Chart.yaml +++ b/deploy/charts/operator-crds/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: toolhive-operator-crds description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes. type: application -version: 0.0.69 +version: 0.0.70 appVersion: "0.0.1" diff --git a/deploy/charts/operator-crds/README.md b/deploy/charts/operator-crds/README.md index 070fa77f9..fc1aea3a3 100644 --- a/deploy/charts/operator-crds/README.md +++ b/deploy/charts/operator-crds/README.md @@ -1,6 +1,6 @@ # ToolHive Operator CRDs Helm Chart -![Version: 0.0.69](https://img.shields.io/badge/Version-0.0.69-informational?style=flat-square) +![Version: 0.0.70](https://img.shields.io/badge/Version-0.0.70-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) A Helm chart for installing the ToolHive Operator CRDs into Kubernetes. diff --git a/deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml index 96c292ad1..7d84f2f1b 100644 --- a/deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -670,69 +670,6 @@ spec: - NodePort - LoadBalancer type: string - tokenCache: - description: TokenCache configures token caching behavior - properties: - memory: - description: |- - Memory configures in-memory token caching - Only used when Provider is "memory" - properties: - maxEntries: - default: 1000 - description: MaxEntries is the maximum number of cache entries - type: integer - ttlOffset: - default: 5m - description: TTLOffset is the duration before token expiry - to refresh - type: string - type: object - provider: - default: memory - description: Provider defines the cache provider type - enum: - - memory - - redis - type: string - redis: - description: |- - Redis configures Redis token caching - Only used when Provider is "redis" - properties: - address: - description: Address is the Redis server address - type: string - db: - default: 0 - description: DB is the Redis database number - type: integer - keyPrefix: - default: 'vmcp:tokens:' - description: KeyPrefix is the prefix for cache keys - type: string - passwordRef: - description: PasswordRef references a secret containing the - Redis password - properties: - key: - description: Key is the key within the secret - type: string - name: - description: Name is the name of the secret - type: string - required: - - key - - name - type: object - tls: - default: false - description: TLS enables TLS for Redis connections - type: boolean - required: - - address - type: object - type: object required: - groupRef type: object diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index dbb538143..220e03973 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -1143,23 +1143,6 @@ _Appears in:_ | `referencingServers` _string array_ | ReferencingServers is a list of MCPServer resources that reference this MCPToolConfig
This helps track which servers need to be reconciled when this config changes | | | -#### MemoryCacheConfig - - - -MemoryCacheConfig configures in-memory token caching - - - -_Appears in:_ -- [TokenCacheConfig](#tokencacheconfig) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `maxEntries` _integer_ | MaxEntries is the maximum number of cache entries | 1000 | | -| `ttlOffset` _string_ | TTLOffset is the duration before token expiry to refresh | 5m | | - - #### NameFilter @@ -1379,26 +1362,6 @@ _Appears in:_ | `env` _[EnvVar](#envvar) array_ | Env are environment variables to set in the proxy container (thv run process)
These affect the toolhive proxy itself, not the MCP server it manages
Use TOOLHIVE_DEBUG=true to enable debug logging in the proxy | | | -#### RedisCacheConfig - - - -RedisCacheConfig configures Redis token caching - - - -_Appears in:_ -- [TokenCacheConfig](#tokencacheconfig) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `address` _string_ | Address is the Redis server address | | Required: \{\}
| -| `db` _integer_ | DB is the Redis database number | 0 | | -| `keyPrefix` _string_ | KeyPrefix is the prefix for cache keys | vmcp:tokens: | | -| `passwordRef` _[SecretKeyRef](#secretkeyref)_ | PasswordRef references a secret containing the Redis password | | | -| `tls` _boolean_ | TLS enables TLS for Redis connections | false | | - - #### RegistryFilter @@ -1518,7 +1481,6 @@ SecretKeyRef is a reference to a key within a Secret _Appears in:_ - [HeaderInjectionConfig](#headerinjectionconfig) - [InlineOIDCConfig](#inlineoidcconfig) -- [RedisCacheConfig](#rediscacheconfig) - [TokenExchangeConfig](#tokenexchangeconfig) | Field | Description | Default | Validation | @@ -1674,24 +1636,6 @@ _Appears in:_ | `perWorkload` _object (keys:string, values:string)_ | PerWorkload defines per-workload timeout overrides | | | -#### TokenCacheConfig - - - -TokenCacheConfig configures token caching behavior - - - -_Appears in:_ -- [VirtualMCPServerSpec](#virtualmcpserverspec) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `provider` _string_ | Provider defines the cache provider type | memory | Enum: [memory redis]
| -| `memory` _[MemoryCacheConfig](#memorycacheconfig)_ | Memory configures in-memory token caching
Only used when Provider is "memory" | | | -| `redis` _[RedisCacheConfig](#rediscacheconfig)_ | Redis configures Redis token caching
Only used when Provider is "redis" | | | - - #### TokenExchangeConfig @@ -1942,7 +1886,6 @@ _Appears in:_ | `aggregation` _[AggregationConfig](#aggregationconfig)_ | Aggregation defines tool aggregation and conflict resolution strategies | | | | `compositeTools` _[CompositeToolSpec](#compositetoolspec) array_ | CompositeTools defines inline composite tool definitions
For complex workflows, reference VirtualMCPCompositeToolDefinition resources instead | | | | `compositeToolRefs` _[CompositeToolDefinitionRef](#compositetooldefinitionref) array_ | CompositeToolRefs references VirtualMCPCompositeToolDefinition resources
for complex, reusable workflows | | | -| `tokenCache` _[TokenCacheConfig](#tokencacheconfig)_ | TokenCache configures token caching behavior | | | | `operational` _[OperationalConfig](#operationalconfig)_ | Operational defines operational settings like timeouts and health checks | | | | `serviceType` _string_ | ServiceType specifies the Kubernetes service type for the Virtual MCP server | ClusterIP | Enum: [ClusterIP NodePort LoadBalancer]
| | `podTemplateSpec` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#rawextension-runtime-pkg)_ | PodTemplateSpec defines the pod template to use for the Virtual MCP server
This allows for customizing the pod configuration beyond what is provided by the other fields.
Note that to modify the specific container the Virtual MCP server runs in, you must specify
the 'vmcp' container name in the PodTemplateSpec.
This field accepts a PodTemplateSpec object as JSON/YAML. | | Type: object
| diff --git a/pkg/vmcp/config/config.go b/pkg/vmcp/config/config.go index 28f761a79..c4b36042e 100644 --- a/pkg/vmcp/config/config.go +++ b/pkg/vmcp/config/config.go @@ -13,14 +13,6 @@ import ( "github.com/stacklok/toolhive/pkg/vmcp" ) -// Token cache provider types -const ( - // CacheProviderMemory represents in-memory token cache provider - CacheProviderMemory = "memory" - // CacheProviderRedis represents Redis token cache provider - CacheProviderRedis = "redis" -) - // Duration is a wrapper around time.Duration that marshals/unmarshals as a duration string. // This ensures duration values are serialized as "30s", "1m", etc. instead of nanosecond integers. type Duration time.Duration @@ -89,9 +81,6 @@ type Config struct { // For Kubernetes, complex workflows can also reference VirtualMCPCompositeToolDefinition CRDs. CompositeTools []*CompositeToolConfig `json:"composite_tools,omitempty" yaml:"composite_tools,omitempty"` - // TokenCache configures token caching. - TokenCache *TokenCacheConfig `json:"token_cache,omitempty" yaml:"token_cache,omitempty"` - // Operational configures operational settings. Operational *OperationalConfig `json:"operational,omitempty" yaml:"operational,omitempty"` @@ -248,45 +237,6 @@ type ToolOverride struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` } -// TokenCacheConfig configures token caching. -type TokenCacheConfig struct { - // Provider is the cache provider: "memory", "redis" - Provider string `json:"provider" yaml:"provider"` - - // Memory contains memory cache config (when Provider = "memory"). - Memory *MemoryCacheConfig `json:"memory,omitempty" yaml:"memory,omitempty"` - - // Redis contains Redis cache config (when Provider = "redis"). - Redis *RedisCacheConfig `json:"redis,omitempty" yaml:"redis,omitempty"` -} - -// MemoryCacheConfig configures in-memory token caching. -type MemoryCacheConfig struct { - // MaxEntries is the maximum number of cached tokens. - MaxEntries int `json:"max_entries" yaml:"max_entries"` - - // TTLOffset is how long before expiry to refresh tokens. - TTLOffset Duration `json:"ttl_offset" yaml:"ttl_offset"` -} - -// RedisCacheConfig configures Redis token caching. -type RedisCacheConfig struct { - // Address is the Redis server address. - Address string `json:"address" yaml:"address"` - - // DB is the Redis database number. - DB int `json:"db" yaml:"db"` - - // KeyPrefix is the prefix for cache keys. - KeyPrefix string `json:"key_prefix,omitempty" yaml:"key_prefix,omitempty"` - - // Password is the Redis password (or secret reference). - Password string `json:"password,omitempty" yaml:"password,omitempty"` - - // TTLOffset is how long before expiry to refresh tokens. - TTLOffset Duration `json:"ttl_offset" yaml:"ttl_offset"` -} - // OperationalConfig contains operational settings. type OperationalConfig struct { // Timeouts configures request timeouts. diff --git a/pkg/vmcp/config/validator.go b/pkg/vmcp/config/validator.go index 3c8750bc6..2a076d764 100644 --- a/pkg/vmcp/config/validator.go +++ b/pkg/vmcp/config/validator.go @@ -44,11 +44,6 @@ func (v *DefaultValidator) Validate(cfg *Config) error { errors = append(errors, err.Error()) } - // Validate token cache configuration - if err := v.validateTokenCache(cfg.TokenCache); err != nil { - errors = append(errors, err.Error()) - } - // Validate operational configuration if err := v.validateOperational(cfg.Operational); err != nil { errors = append(errors, err.Error()) @@ -282,43 +277,6 @@ func (*DefaultValidator) validateToolOverrides(overrides map[string]*ToolOverrid return nil } -func (*DefaultValidator) validateTokenCache(cache *TokenCacheConfig) error { - if cache == nil { - return nil // Token cache is optional - } - - validProviders := []string{CacheProviderMemory, CacheProviderRedis} - if !contains(validProviders, cache.Provider) { - return fmt.Errorf("token_cache.provider must be one of: %s", strings.Join(validProviders, ", ")) - } - - switch cache.Provider { - case CacheProviderMemory: - if cache.Memory == nil { - return fmt.Errorf("token_cache.memory is required when provider is 'memory'") - } - if cache.Memory.MaxEntries <= 0 { - return fmt.Errorf("token_cache.memory.max_entries must be positive") - } - if cache.Memory.TTLOffset < 0 { - return fmt.Errorf("token_cache.memory.ttl_offset cannot be negative") - } - - case CacheProviderRedis: - if cache.Redis == nil { - return fmt.Errorf("token_cache.redis is required when provider is 'redis'") - } - if cache.Redis.Address == "" { - return fmt.Errorf("token_cache.redis.address is required") - } - if cache.Redis.TTLOffset < 0 { - return fmt.Errorf("token_cache.redis.ttl_offset cannot be negative") - } - } - - return nil -} - func (v *DefaultValidator) validateOperational(ops *OperationalConfig) error { if ops == nil { return nil // Operational config is optional (defaults apply) diff --git a/pkg/vmcp/config/validator_test.go b/pkg/vmcp/config/validator_test.go index 1ab7dd8f4..dd0ad814c 100644 --- a/pkg/vmcp/config/validator_test.go +++ b/pkg/vmcp/config/validator_test.go @@ -386,82 +386,6 @@ func TestValidator_ValidateAggregation(t *testing.T) { } } -func TestValidator_ValidateTokenCache(t *testing.T) { - t.Parallel() - tests := []struct { - name string - cache *TokenCacheConfig - wantErr bool - errMsg string - }{ - { - name: "nil cache (optional)", - cache: nil, - wantErr: false, - }, - { - name: "valid memory cache", - cache: &TokenCacheConfig{ - Provider: CacheProviderMemory, - Memory: &MemoryCacheConfig{ - MaxEntries: 1000, - TTLOffset: Duration(5 * time.Minute), - }, - }, - wantErr: false, - }, - { - name: "valid redis cache", - cache: &TokenCacheConfig{ - Provider: "redis", - Redis: &RedisCacheConfig{ - Address: "localhost:6379", - TTLOffset: Duration(5 * time.Minute), - }, - }, - wantErr: false, - }, - { - name: "invalid provider", - cache: &TokenCacheConfig{ - Provider: "invalid", - }, - wantErr: true, - errMsg: "token_cache.provider must be one of", - }, - { - name: "memory cache with negative max entries", - cache: &TokenCacheConfig{ - Provider: CacheProviderMemory, - Memory: &MemoryCacheConfig{ - MaxEntries: -1, - }, - }, - wantErr: true, - errMsg: "max_entries must be positive", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - v := NewValidator() - err := v.validateTokenCache(tt.cache) - - if (err != nil) != tt.wantErr { - t.Errorf("validateTokenCache() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if tt.wantErr && err != nil && tt.errMsg != "" { - if !strings.Contains(err.Error(), tt.errMsg) { - t.Errorf("validateTokenCache() error message = %v, want to contain %v", err.Error(), tt.errMsg) - } - } - }) - } -} - func TestValidator_ValidateCompositeTools(t *testing.T) { t.Parallel() tests := []struct { diff --git a/pkg/vmcp/config/yaml_loader.go b/pkg/vmcp/config/yaml_loader.go index f1d3564b7..3386f8fcc 100644 --- a/pkg/vmcp/config/yaml_loader.go +++ b/pkg/vmcp/config/yaml_loader.go @@ -55,7 +55,6 @@ type rawConfig struct { IncomingAuth rawIncomingAuth `yaml:"incoming_auth"` OutgoingAuth rawOutgoingAuth `yaml:"outgoing_auth"` Aggregation rawAggregation `yaml:"aggregation"` - TokenCache *rawTokenCache `yaml:"token_cache"` Operational *rawOperational `yaml:"operational"` CompositeTools []*rawCompositeTool `yaml:"composite_tools"` @@ -128,18 +127,6 @@ type rawToolOverride struct { Description string `yaml:"description"` } -type rawTokenCache struct { - Provider string `yaml:"provider"` - Config struct { - MaxEntries int `yaml:"max_entries"` - TTLOffset string `yaml:"ttl_offset"` - Address string `yaml:"address"` - DB int `yaml:"db"` - KeyPrefix string `yaml:"key_prefix"` - Password string `yaml:"password"` - } `yaml:"config"` -} - type rawOperational struct { Timeouts struct { Default string `yaml:"default"` @@ -232,15 +219,6 @@ func (l *YAMLLoader) transformToConfig(raw *rawConfig) (*Config, error) { } cfg.Aggregation = aggregation - // Transform token cache - if raw.TokenCache != nil { - tokenCache, err := l.transformTokenCache(raw.TokenCache) - if err != nil { - return nil, fmt.Errorf("token_cache: %w", err) - } - cfg.TokenCache = tokenCache - } - // Transform operational if raw.Operational != nil { operational, err := l.transformOperational(raw.Operational) @@ -421,41 +399,6 @@ func (*YAMLLoader) transformAggregation(raw *rawAggregation) (*AggregationConfig return cfg, nil } -func (*YAMLLoader) transformTokenCache(raw *rawTokenCache) (*TokenCacheConfig, error) { - cfg := &TokenCacheConfig{ - Provider: raw.Provider, - } - - switch raw.Provider { - case CacheProviderMemory: - ttlOffset, err := time.ParseDuration(raw.Config.TTLOffset) - if err != nil { - return nil, fmt.Errorf("invalid ttl_offset: %w", err) - } - - cfg.Memory = &MemoryCacheConfig{ - MaxEntries: raw.Config.MaxEntries, - TTLOffset: Duration(ttlOffset), - } - - case CacheProviderRedis: - ttlOffset, err := time.ParseDuration(raw.Config.TTLOffset) - if err != nil { - return nil, fmt.Errorf("invalid ttl_offset: %w", err) - } - - cfg.Redis = &RedisCacheConfig{ - Address: raw.Config.Address, - DB: raw.Config.DB, - KeyPrefix: raw.Config.KeyPrefix, - Password: raw.Config.Password, - TTLOffset: Duration(ttlOffset), - } - } - - return cfg, nil -} - func (*YAMLLoader) transformOperational(raw *rawOperational) (*OperationalConfig, error) { cfg := &OperationalConfig{} diff --git a/pkg/vmcp/config/yaml_loader_test.go b/pkg/vmcp/config/yaml_loader_test.go index 7df5e1caf..52fc01c9a 100644 --- a/pkg/vmcp/config/yaml_loader_test.go +++ b/pkg/vmcp/config/yaml_loader_test.go @@ -127,51 +127,6 @@ aggregation: }, wantErr: false, }, - { - name: "valid configuration with token cache", - yaml: ` -name: test-vmcp -group: test-group - -incoming_auth: - type: anonymous - -outgoing_auth: - source: inline - default: - type: unauthenticated - -aggregation: - conflict_resolution: prefix - conflict_resolution_config: - prefix_format: "{workload}_" - -token_cache: - provider: memory - config: - max_entries: 1000 - ttl_offset: 5m -`, - want: func(t *testing.T, cfg *Config) { - t.Helper() - if cfg.TokenCache == nil { - t.Fatal("TokenCache is nil") - } - if cfg.TokenCache.Provider != CacheProviderMemory { - t.Errorf("TokenCache.Provider = %v, want memory", cfg.TokenCache.Provider) - } - if cfg.TokenCache.Memory == nil { - t.Fatal("TokenCache.Memory is nil") - } - if cfg.TokenCache.Memory.MaxEntries != 1000 { - t.Errorf("Memory.MaxEntries = %v, want 1000", cfg.TokenCache.Memory.MaxEntries) - } - if cfg.TokenCache.Memory.TTLOffset != Duration(5*time.Minute) { - t.Errorf("Memory.TTLOffset = %v, want 5m", cfg.TokenCache.Memory.TTLOffset) - } - }, - wantErr: false, - }, { name: "valid configuration with composite tools", yaml: ` @@ -279,34 +234,6 @@ aggregation: }, wantErr: false, }, - { - name: "invalid duration format", - yaml: ` -name: test-vmcp -group: test-group - -incoming_auth: - type: anonymous - -outgoing_auth: - source: inline - default: - type: unauthenticated - -aggregation: - conflict_resolution: prefix - conflict_resolution_config: - prefix_format: "{workload}_" - -token_cache: - provider: memory - config: - max_entries: 1000 - ttl_offset: invalid-duration -`, - wantErr: true, - errMsg: "invalid ttl_offset", - }, { name: "composite tool with missing parameter type", yaml: ` diff --git a/pkg/vmcp/config/yaml_loader_transform_test.go b/pkg/vmcp/config/yaml_loader_transform_test.go index b3fe17aab..14a23d02e 100644 --- a/pkg/vmcp/config/yaml_loader_transform_test.go +++ b/pkg/vmcp/config/yaml_loader_transform_test.go @@ -197,114 +197,6 @@ func TestYAMLLoader_transformBackendAuthStrategy(t *testing.T) { } } -// TestYAMLLoader_transformTokenCache tests duration parsing which can fail. -func TestYAMLLoader_transformTokenCache(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - raw *rawTokenCache - wantErr bool - errMsg string - wantOffset time.Duration - }{ - { - name: "memory cache parses duration correctly", - raw: &rawTokenCache{ - Provider: CacheProviderMemory, - Config: struct { - MaxEntries int `yaml:"max_entries"` - TTLOffset string `yaml:"ttl_offset"` - Address string `yaml:"address"` - DB int `yaml:"db"` - KeyPrefix string `yaml:"key_prefix"` - Password string `yaml:"password"` - }{ - MaxEntries: 1000, - TTLOffset: "5m", - }, - }, - wantOffset: 5 * time.Minute, - }, - { - name: "redis cache parses duration correctly", - raw: &rawTokenCache{ - Provider: CacheProviderRedis, - Config: struct { - MaxEntries int `yaml:"max_entries"` - TTLOffset string `yaml:"ttl_offset"` - Address string `yaml:"address"` - DB int `yaml:"db"` - KeyPrefix string `yaml:"key_prefix"` - Password string `yaml:"password"` - }{ - TTLOffset: "10m", - }, - }, - wantOffset: 10 * time.Minute, - }, - { - name: "invalid duration returns error", - raw: &rawTokenCache{ - Provider: CacheProviderMemory, - Config: struct { - MaxEntries int `yaml:"max_entries"` - TTLOffset string `yaml:"ttl_offset"` - Address string `yaml:"address"` - DB int `yaml:"db"` - KeyPrefix string `yaml:"key_prefix"` - Password string `yaml:"password"` - }{ - TTLOffset: "invalid", - }, - }, - wantErr: true, - errMsg: "invalid ttl_offset", - }, - { - name: "complex duration like 1h30m parses correctly", - raw: &rawTokenCache{ - Provider: CacheProviderMemory, - Config: struct { - MaxEntries int `yaml:"max_entries"` - TTLOffset string `yaml:"ttl_offset"` - Address string `yaml:"address"` - DB int `yaml:"db"` - KeyPrefix string `yaml:"key_prefix"` - Password string `yaml:"password"` - }{ - TTLOffset: "1h30m", - }, - }, - wantOffset: 90 * time.Minute, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - loader := &YAMLLoader{} - cfg, err := loader.transformTokenCache(tt.raw) - - if tt.wantErr { - require.Error(t, err) - if tt.errMsg != "" { - assert.Contains(t, err.Error(), tt.errMsg) - } - } else { - require.NoError(t, err) - require.NotNil(t, cfg) - // Verify duration was parsed correctly - if cfg.Memory != nil { - assert.Equal(t, Duration(tt.wantOffset), cfg.Memory.TTLOffset) - } else if cfg.Redis != nil { - assert.Equal(t, Duration(tt.wantOffset), cfg.Redis.TTLOffset) - } - } - }) - } -} - // TestYAMLLoader_transformOperational tests duration parsing in multiple fields. func TestYAMLLoader_transformOperational(t *testing.T) { t.Parallel()