Skip to content

Inject Redis session storage config into VirtualMCPServer vMCP ConfigMap #4214

@yrobla

Description

@yrobla

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 to pkg/vmcp/config/config.go's Config struct with address, db, and keyPrefix sub-fields, marshaling correctly to YAML
  • ensureVmcpConfigConfigMap (or the converter it calls) populates the new session storage field when spec.sessionStorage.provider == "redis"
  • When provider == "redis", the marshaled config.yaml in the ConfigMap includes address, db, and keyPrefix values sourced from spec.sessionStorage
  • When provider == "memory" or spec.sessionStorage is nil, the session storage field is omitted from config.yaml (zero-value / omitempty behavior)
  • The Redis password (PasswordRef) is NOT written to config.yaml — it is injected as env var THV_SESSION_REDIS_PASSWORD by TASK-006 (verified by inspecting the generated ConfigMap)
  • The vpkg/vmcp/config/zz_generated.deepcopy.go is 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:

  1. ensureVmcpConfigConfigMap calls converter.Convert(ctx, vmcp) to produce a *vmcpconfig.Config
  2. The converter deep-copies vmcp.Spec.Config (an embedded vmcpconfig.Config) as the base
  3. The result is marshaled to YAML and stored in the config.yaml key of the ConfigMap

To propagate Redis config, the recommended approach is:

  1. Add a SessionStorage field to vmcpconfig.Config in pkg/vmcp/config/config.go. Define a new SessionStorageConfig struct in that package with Address, DB, and KeyPrefix fields (no password — that is an env var). Use omitempty tags so the field is absent from YAML when not set.

  2. Populate the field in the converter (cmd/thv-operator/pkg/vmcpconfig/converter.go) after the deep-copy of vmcp.Spec.Config. When vmcp.Spec.SessionStorage != nil && vmcp.Spec.SessionStorage.Provider == "redis", assign the corresponding fields to config.SessionStorage. This keeps the conversion logic co-located with all other CRD-to-config field mappings.

  3. Regenerate deepcopy for pkg/vmcp/config if the new SessionStorageConfig struct contains pointer fields. Run task operator-generate or 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 corresponding vmcpconfig.Config field.
  • YAML marshaling: gopkg.in/yaml.v3 is used in ensureVmcpConfigConfigMap. Fields tagged with yaml:"...,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(). Since vmcp.Spec.Config embeds vmcpconfig.Config, any new field in vmcpconfig.Config is 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.goensureVmcpConfigConfigMap is the entry point. The function calls converter.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.goConverter.Convert performs the CRD-to-config mapping. This is the preferred location to populate config.SessionStorage from vmcp.Spec.SessionStorage.
  • pkg/vmcp/config/config.goConfig struct definition. Add the new SessionStorage *SessionStorageConfig field here. Define SessionStorageConfig as a new struct in this file (or a separate file in the same package) with Address string, DB int32, and KeyPrefix string fields.
  • pkg/vmcp/config/zz_generated.deepcopy.go — Must be regenerated after adding pointer fields to Config or its new sub-struct.
  • cmd/thv-operator/api/v1alpha1/mcpserver_types.go — Source of the SessionStorageConfig CRD type (added in TASK-001). The Provider, Address, DB, and KeyPrefix fields map to the new vmcpconfig.SessionStorageConfig fields.
  • cmd/thv-operator/controllers/virtualmcpserver_deployment.gobuildEnvVarsForVmcp and buildOIDCEnvVars — 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.Convert with spec.sessionStorage.provider == "redis" sets config.SessionStorage with correct address, db, and keyPrefix
  • Test case: converter.Convert with spec.sessionStorage.provider == "memory" leaves config.SessionStorage nil
  • Test case: converter.Convert with spec.sessionStorage == nil leaves config.SessionStorage nil
  • Test case: marshaled config.yaml includes sessionStorage.address, sessionStorage.db, sessionStorage.keyPrefix when provider is redis
  • Test case: marshaled config.yaml does NOT include any password or credential field when provider is redis (verify the PasswordRef field from the CRD type is not propagated)
  • Test case: marshaled config.yaml omits sessionStorage entirely 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.keyPrefix is empty string — verify it is omitted from YAML (omitempty)
  • spec.sessionStorage.db is 0 — verify behavior with omitempty (zero value may be omitted; if vMCP requires explicit db: 0, the field tag may need to be yaml:"db" without omitempty)
  • Concurrent call to ensureVmcpConfigConfigMap while spec.sessionStorage is being updated — converter reads a consistent snapshot since it operates on the passed-in vmcp object

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestkubernetesItems related to KubernetesoperatorscalabilityItems related to scalabilityvmcpVirtual MCP Server related issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions