-
Notifications
You must be signed in to change notification settings - Fork 198
Inject Redis session storage config into VirtualMCPServer vMCP ConfigMap #4214
Description
Description
Extend ensureVmcpConfigConfigMap in virtualmcpserver_vmcpconfig.go to marshal Redis session storage connection parameters (address, db, keyPrefix) from VirtualMCPServerSpec.SessionStorage into the vMCP config.yaml ConfigMap when sessionStorage.provider == redis. This allows vMCP pods to discover Redis session storage configuration at startup without hardcoding credentials in the ConfigMap (the Redis password is injected separately as an env var by TASK-006). This is the operator-side change that completes the Redis session storage injection chain for VirtualMCPServer resources.
Context
Epic THV-0047 adds horizontal scaling support for vMCP and proxyrunner. When vMCP is scaled to multiple replicas, session state must be stored in a shared backend (Redis) rather than in-memory so that requests routed to different pods can share sessions. The operator's role is to configure the vMCP process with Redis connection details; vMCP reads these from its config.yaml at startup.
TASK-003 (#275) regenerated deepcopy and CRD manifests after TASK-001 and TASK-002 added the SessionStorageConfig struct, Replicas, and SessionStorage fields to both CRD specs. With the compilable API surface available, this task extends the vMCP ConfigMap generation pipeline to propagate Redis config from the CRD spec into the mounted config YAML.
The vMCP config.yaml is produced by marshaling a vmcpconfig.Config struct (pkg/vmcp/config/config.go). To carry Redis config to vMCP, a SessionStorage field must be added to vmcpconfig.Config, and the operator's converter or ConfigMap builder must populate it from VirtualMCPServerSpec.SessionStorage when provider is redis.
Dependencies: #275 (TASK-003 — Regenerate deepcopy and CRD manifests after scaling type changes)
Blocks: TASK-008 (Unit Tests)
Acceptance Criteria
- A
SessionStorageConfig(or equivalent) field is added topkg/vmcp/config/config.go'sConfigstruct withaddress,db, andkeyPrefixsub-fields, marshaling correctly to YAML -
ensureVmcpConfigConfigMap(or the converter it calls) populates the new session storage field whenspec.sessionStorage.provider == "redis" - When
provider == "redis", the marshaledconfig.yamlin the ConfigMap includesaddress,db, andkeyPrefixvalues sourced fromspec.sessionStorage - When
provider == "memory"orspec.sessionStorageis nil, the session storage field is omitted fromconfig.yaml(zero-value / omitempty behavior) - The Redis password (
PasswordRef) is NOT written toconfig.yaml— it is injected as env varTHV_SESSION_REDIS_PASSWORDby TASK-006 (verified by inspecting the generated ConfigMap) - The
vpkg/vmcp/config/zz_generated.deepcopy.gois regenerated if the new struct has pointer fields - All existing controller tests pass after the change (
go test ./cmd/thv-operator/...) - All existing vmcp config tests pass (
go test ./pkg/vmcp/config/...) - Code reviewed and approved
Technical Approach
Recommended Implementation
The vMCP config.yaml is built through the following pipeline:
ensureVmcpConfigConfigMapcallsconverter.Convert(ctx, vmcp)to produce a*vmcpconfig.Config- The converter deep-copies
vmcp.Spec.Config(an embeddedvmcpconfig.Config) as the base - The result is marshaled to YAML and stored in the
config.yamlkey of the ConfigMap
To propagate Redis config, the recommended approach is:
-
Add a
SessionStoragefield tovmcpconfig.Configinpkg/vmcp/config/config.go. Define a newSessionStorageConfigstruct in that package withAddress,DB, andKeyPrefixfields (no password — that is an env var). Useomitemptytags so the field is absent from YAML when not set. -
Populate the field in the converter (
cmd/thv-operator/pkg/vmcpconfig/converter.go) after the deep-copy ofvmcp.Spec.Config. Whenvmcp.Spec.SessionStorage != nil && vmcp.Spec.SessionStorage.Provider == "redis", assign the corresponding fields toconfig.SessionStorage. This keeps the conversion logic co-located with all other CRD-to-config field mappings. -
Regenerate deepcopy for
pkg/vmcp/configif the newSessionStorageConfigstruct contains pointer fields. Runtask operator-generateor the relevant generation command.
Note: vmcp.Spec.Config is a vmcpconfig.Config that the operator deep-copies as the initial state. If the SessionStorage field is added to vmcpconfig.Config with omitempty, leaving it nil when provider is memory or absent means no YAML output — which is the desired behavior.
Patterns & Frameworks
- Converter pattern:
cmd/thv-operator/pkg/vmcpconfig/converter.go— all CRD-to-config field mappings live here. New fields should follow the established pattern: check if the CRD spec field is set, then populate the correspondingvmcpconfig.Configfield. - YAML marshaling:
gopkg.in/yaml.v3is used inensureVmcpConfigConfigMap. Fields tagged withyaml:"...,omitempty"are omitted when nil/zero, ensuring clean config output when Redis is not configured. - Deep-copy base: The converter starts with
vmcp.Spec.Config.DeepCopy(). Sincevmcp.Spec.Configembedsvmcpconfig.Config, any new field invmcpconfig.Configis automatically included in the deep-copy — no further changes to the converter's initialization are needed beyond populating the new field. - No plaintext secrets in ConfigMap: Follow the principle established by OIDC and HMAC secret handling — only non-sensitive connection config (address, db, keyPrefix) goes in the ConfigMap. The Redis password is exclusively handled as an env var with
SecretKeyRef.
Code Pointers
cmd/thv-operator/controllers/virtualmcpserver_vmcpconfig.go—ensureVmcpConfigConfigMapis the entry point. The function callsconverter.Convert, then marshals the result to YAML for the ConfigMap. Session storage injection should be done before the marshal step.cmd/thv-operator/pkg/vmcpconfig/converter.go—Converter.Convertperforms the CRD-to-config mapping. This is the preferred location to populateconfig.SessionStoragefromvmcp.Spec.SessionStorage.pkg/vmcp/config/config.go—Configstruct definition. Add the newSessionStorage *SessionStorageConfigfield here. DefineSessionStorageConfigas a new struct in this file (or a separate file in the same package) withAddress string,DB int32, andKeyPrefix stringfields.pkg/vmcp/config/zz_generated.deepcopy.go— Must be regenerated after adding pointer fields toConfigor its new sub-struct.cmd/thv-operator/api/v1alpha1/mcpserver_types.go— Source of theSessionStorageConfigCRD type (added in TASK-001). TheProvider,Address,DB, andKeyPrefixfields map to the newvmcpconfig.SessionStorageConfigfields.cmd/thv-operator/controllers/virtualmcpserver_deployment.go—buildEnvVarsForVmcpandbuildOIDCEnvVars— reference pattern for how secrets are kept out of ConfigMaps and injected as env vars instead. The Redis password follows this pattern (handled in TASK-006).cmd/thv-operator/pkg/vmcpconfig/converter_test.go— Existing converter tests. New test cases for Redis session storage conversion should be added here.
Component Interfaces
New struct to add to pkg/vmcp/config/config.go:
// SessionStorageConfig configures the session storage backend for vMCP.
// Only connection parameters are stored here; the Redis password is
// injected separately as env var THV_SESSION_REDIS_PASSWORD.
// +kubebuilder:object:generate=true
type SessionStorageConfig struct {
// Address is the Redis server address (host:port).
// +optional
Address string `json:"address,omitempty" yaml:"address,omitempty"`
// DB is the Redis database number.
// +optional
DB int32 `json:"db,omitempty" yaml:"db,omitempty"`
// KeyPrefix is the prefix applied to all Redis keys.
// +optional
KeyPrefix string `json:"keyPrefix,omitempty" yaml:"keyPrefix,omitempty"`
}Field to add to Config in the same file:
// SessionStorage configures the session storage backend.
// When provider is "redis", Address, DB, and KeyPrefix are populated here.
// The Redis password is NOT stored here — it is injected as env var THV_SESSION_REDIS_PASSWORD.
// +optional
SessionStorage *SessionStorageConfig `json:"sessionStorage,omitempty" yaml:"sessionStorage,omitempty"`Conversion logic to add in converter.Convert (after the deep-copy):
// Populate session storage config when provider is redis.
// Password is excluded — it is injected as env var by the deployment builder.
if vmcp.Spec.SessionStorage != nil && vmcp.Spec.SessionStorage.Provider == "redis" {
config.SessionStorage = &vmcpconfig.SessionStorageConfig{
Address: vmcp.Spec.SessionStorage.Address,
DB: vmcp.Spec.SessionStorage.DB,
KeyPrefix: vmcp.Spec.SessionStorage.KeyPrefix,
}
}Testing Strategy
Unit Tests
- Test case:
converter.Convertwithspec.sessionStorage.provider == "redis"setsconfig.SessionStoragewith correctaddress,db, andkeyPrefix - Test case:
converter.Convertwithspec.sessionStorage.provider == "memory"leavesconfig.SessionStoragenil - Test case:
converter.Convertwithspec.sessionStorage == nilleavesconfig.SessionStoragenil - Test case: marshaled
config.yamlincludessessionStorage.address,sessionStorage.db,sessionStorage.keyPrefixwhen provider is redis - Test case: marshaled
config.yamldoes NOT include any password or credential field when provider is redis (verify thePasswordReffield from the CRD type is not propagated) - Test case: marshaled
config.yamlomitssessionStorageentirely when provider is memory or unset
Integration Tests
- Not in scope for this task; integration tests are covered in TASK-008
Edge Cases
-
spec.sessionStorage.keyPrefixis empty string — verify it is omitted from YAML (omitempty) -
spec.sessionStorage.dbis 0 — verify behavior withomitempty(zero value may be omitted; if vMCP requires explicitdb: 0, the field tag may need to beyaml:"db"without omitempty) - Concurrent call to
ensureVmcpConfigConfigMapwhilespec.sessionStorageis being updated — converter reads a consistent snapshot since it operates on the passed-invmcpobject
Out of Scope
- Application-level Redis session storage implementation inside the vMCP process (separate epic)
- Redis password injection into the pod spec — handled in TASK-006
- MCPServer RunConfig Redis injection — handled in TASK-005
- VirtualMCPServer Deployment Builder replica and termination grace period changes — handled in TASK-006
- Session storage warning condition emission in the reconciler — handled in TASK-004
- Horizontal Pod Autoscaler (HPA) object management
- Redis deployment or lifecycle management
References
- Epic THV-0047: Horizontal Scaling - CRD and Operator Changes — https://github.com/stacklok/stacklok-epics/issues/264
- Upstream TASK-003: Regenerate deepcopy and CRD manifests — https://github.com/stacklok/stacklok-epics/issues/275
- RFC THV-0047: Manual Horizontal Scaling for vMCP and Proxy Runner — RFC: Horizontal Scaling for vMCP and Proxy Runner toolhive-rfcs#47
- Related TASK-006: VirtualMCPServer Deployment Builder Updates (Redis password env var) — see DAG
- Related TASK-005: MCPServer RunConfig Redis and BackendReplicas Injection — see DAG
cmd/thv-operator/controllers/virtualmcpserver_vmcpconfig.go— file under modificationcmd/thv-operator/pkg/vmcpconfig/converter.go— conversion pipelinepkg/vmcp/config/config.go— vmcpconfig.Config struct definition