Skip to content

Inject BackendReplicas and Redis session config into MCPServer RunConfig #4218

@yrobla

Description

@yrobla

Description

Extend createRunConfigFromMCPServer in mcpserver_runconfig.go to populate BackendReplicas from spec.backendReplicas and, when sessionStorage.provider == redis, populate the Redis connection fields (address, db, keyPrefix) in the RunConfig. Add the BackendReplicas *int32 field to runner.RunConfig in pkg/runner/config.go. The Redis password is not stored in the ConfigMap — it is injected as a pod env var (THV_SESSION_REDIS_PASSWORD) by the deployment builder; this task is responsible only for the ConfigMap-level fields.

Context

Epic THV-0047 adds horizontal scaling support to MCPServer and VirtualMCPServer CRDs. Proxyrunner needs to know how many backend (MCP server) replicas to distribute connections across; this is communicated via the RunConfig ConfigMap that the operator manages. When Redis is configured as the session storage backend, proxyrunner also needs the Redis address, database number, and key prefix at startup so it can connect to the shared session store.

TASK-003 (#275) completed the regeneration of deepcopy and CRD manifests after TASK-001 and TASK-002 added the new CRD fields. The compiled API surface is now available for createRunConfigFromMCPServer to read spec.backendReplicas and spec.sessionStorage.

Dependencies: #275 (TASK-003 — Regenerate deepcopy and CRD manifests after scaling type changes)
Blocks: TASK-008 (Unit Tests)

Acceptance Criteria

  • runner.RunConfig in pkg/runner/config.go has a new BackendReplicas *int32 field with JSON/YAML tags backend_replicas
  • When MCPServer.spec.backendReplicas is non-nil, createRunConfigFromMCPServer sets RunConfig.BackendReplicas to the spec value
  • When MCPServer.spec.backendReplicas is nil, RunConfig.BackendReplicas is nil (zero-value pointer; the field is omitted from the serialized ConfigMap JSON)
  • When MCPServer.spec.sessionStorage.provider == "redis", the RunConfig contains the Redis address, DB number, and key prefix from SessionStorageConfig
  • When spec.sessionStorage is nil or provider == "memory", no Redis fields are written to the RunConfig
  • The Redis password is NOT written into the RunConfig or the ConfigMap (it is handled as a pod env var in TASK-004/TASK-006)
  • go build ./cmd/thv-operator/... ./pkg/runner/... passes after the change
  • go test ./cmd/thv-operator/controllers/... ./pkg/runner/... passes (including existing RunConfig tests)
  • All tests pass
  • Code reviewed and approved

Technical Approach

Recommended Implementation

Step 1: Add BackendReplicas to runner.RunConfig

In pkg/runner/config.go, add a new optional field to the RunConfig struct after the existing scaling-related fields (e.g., near Group or at the end of the struct):

// BackendReplicas is the number of backend replicas, passed from MCPServer.spec.backendReplicas.
// Used by proxyrunner for connection pool sizing and load-balancing hints.
// +optional
BackendReplicas *int32 `json:"backend_replicas,omitempty" yaml:"backend_replicas,omitempty"`

This is the only change outside cmd/thv-operator/.

Step 2: Add Redis session storage fields to runner.RunConfig

Also in pkg/runner/config.go, add a SessionRedisConfig struct (or inline fields) to carry the Redis connection parameters:

// SessionRedisConfig holds the Redis connection parameters for session storage.
// Populated by the operator when sessionStorage.provider == "redis".
// +optional
type SessionRedisConfig struct {
    // Address is the Redis server address (host:port)
    Address string `json:"address,omitempty" yaml:"address,omitempty"`

    // DB is the Redis database number
    DB int32 `json:"db,omitempty" yaml:"db,omitempty"`

    // KeyPrefix is the prefix applied to all Redis keys
    KeyPrefix string `json:"key_prefix,omitempty" yaml:"key_prefix,omitempty"`
}

// In RunConfig:
// SessionRedis holds Redis connection parameters for session storage when
// sessionStorage.provider == "redis". The password is not included here —
// it is injected as env var THV_SESSION_REDIS_PASSWORD by the operator.
// +optional
SessionRedis *SessionRedisConfig `json:"session_redis,omitempty" yaml:"session_redis,omitempty"`

Step 3: Extend createRunConfigFromMCPServer in mcpserver_runconfig.go

After runner.NewOperatorRunConfigBuilder returns the runConfig, add direct field assignment for the new fields. This follows the same pattern used for PopulateMiddlewareConfigs — post-builder field setting on the returned struct:

// Populate BackendReplicas from spec
if m.Spec.BackendReplicas != nil {
    val := *m.Spec.BackendReplicas
    runConfig.BackendReplicas = &val
}

// Populate Redis session storage config
if m.Spec.SessionStorage != nil && m.Spec.SessionStorage.Provider == "redis" {
    runConfig.SessionRedis = &runner.SessionRedisConfig{
        Address:   m.Spec.SessionStorage.Address,
        DB:        m.Spec.SessionStorage.DB,
        KeyPrefix: m.Spec.SessionStorage.KeyPrefix,
    }
}

Alternatively, add a WithBackendReplicas and WithSessionRedis builder option and pass them through options. Either approach is acceptable — direct field assignment after the builder is simpler and matches the PopulateMiddlewareConfigs post-builder pattern already in the function.

Do NOT include PasswordRef in the RunConfig. The Redis password is injected as THV_SESSION_REDIS_PASSWORD into the pod via SecretKeyRef by the deployment builder (TASK-004/TASK-006).

Patterns & Frameworks

  • Post-builder field assignment: createRunConfigFromMCPServer already calls runner.PopulateMiddlewareConfigs(runConfig) after the builder returns. Setting BackendReplicas and SessionRedis directly on the returned *runner.RunConfig follows this established pattern and avoids adding builder options for fields that are purely operator-injected.
  • Nil-passthrough for optional pointer fields: BackendReplicas *int32 must use the same nil-passthrough as spec.replicas — only set the RunConfig field when the spec field is non-nil; never substitute a default. This ensures proxyrunner can distinguish "not configured" from "configured as 0".
  • Omitempty JSON tags: all new fields must use omitempty on both JSON and YAML tags so that nil/zero-value fields are absent from the serialized ConfigMap, keeping the output clean.
  • Password never in ConfigMap: consistent with how OIDC client secrets are handled (SecretKeyRef → env var, not ConfigMap), the Redis password must not appear in RunConfig. Only address, db, and keyPrefix go in the ConfigMap.

Code Pointers

  • pkg/runner/config.go — Primary file for the BackendReplicas field and new SessionRedisConfig type. Read the existing struct layout (lines 46–213) to find the right insertion point; place new fields near the end of the struct or logically grouped with scaling configuration.
  • cmd/thv-operator/controllers/mcpserver_runconfig.gocreateRunConfigFromMCPServer function (lines 84–246). The post-builder section starting at line 239 (PopulateMiddlewareConfigs) is the correct insertion point for the new field-assignment blocks.
  • cmd/thv-operator/api/v1alpha1/mcpserver_types.go — Contains MCPServerSpec.BackendReplicas *int32 and MCPServerSpec.SessionStorage *SessionStorageConfig fields added in TASK-001. Read this to confirm the exact field names and types before writing the population logic.
  • cmd/thv-operator/controllers/mcpserver_runconfig_test.go — Existing test file for createRunConfigFromMCPServer. The createTestMCPServerWithConfig helper at line 45 and test structure in TestCreateRunConfigFromMCPServer are the patterns to follow when adding new test cases.
  • cmd/thv-operator/controllers/mcpserver_replicas_test.go — Shows the table-driven test pattern used in this package (TestReplicaBehavior). Use the same t.Parallel() and testify/assert style for new RunConfig tests.
  • cmd/thv-operator/controllers/virtualmcpserver_deployment.gobuildOIDCEnvVars and buildHMACSecretEnvVar — reference for why the password is NOT in the RunConfig (it goes into the pod spec as corev1.EnvVar with ValueFrom.SecretKeyRef instead).

Component Interfaces

New field on runner.RunConfig in pkg/runner/config.go:

// BackendReplicas is the number of backend replicas to inform proxyrunner's
// connection pool sizing. Set from MCPServer.spec.backendReplicas.
// Omitted when nil (not configured); never defaults to 0.
BackendReplicas *int32 `json:"backend_replicas,omitempty" yaml:"backend_replicas,omitempty"`

// SessionRedis holds Redis connection parameters for distributed session storage.
// Populated only when MCPServer.spec.sessionStorage.provider == "redis".
// The Redis password is NOT included — it is injected as env var THV_SESSION_REDIS_PASSWORD.
SessionRedis *SessionRedisConfig `json:"session_redis,omitempty" yaml:"session_redis,omitempty"`

New type in pkg/runner/config.go:

// SessionRedisConfig holds non-sensitive Redis connection parameters for session storage.
type SessionRedisConfig struct {
    Address   string `json:"address,omitempty" yaml:"address,omitempty"`
    DB        int32  `json:"db,omitempty" yaml:"db,omitempty"`
    KeyPrefix string `json:"key_prefix,omitempty" yaml:"key_prefix,omitempty"`
}

Post-builder addition in createRunConfigFromMCPServer (mcpserver_runconfig.go), inserted after the PopulateMiddlewareConfigs call:

// Set BackendReplicas from spec (nil-passthrough: only set when explicitly configured)
if m.Spec.BackendReplicas != nil {
    val := *m.Spec.BackendReplicas
    runConfig.BackendReplicas = &val
}

// Inject Redis session storage config (address/db/keyPrefix only — password is a pod env var)
if m.Spec.SessionStorage != nil && m.Spec.SessionStorage.Provider == "redis" {
    runConfig.SessionRedis = &runner.SessionRedisConfig{
        Address:   m.Spec.SessionStorage.Address,
        DB:        m.Spec.SessionStorage.DB,
        KeyPrefix: m.Spec.SessionStorage.KeyPrefix,
    }
}

Testing Strategy

Unit Tests (extend mcpserver_runconfig_test.go or add a new mcpserver_runconfig_scaling_test.go):

  • When spec.backendReplicas is nil, runConfig.BackendReplicas is nil
  • When spec.backendReplicas is set to 3, runConfig.BackendReplicas points to int32(3)
  • When spec.sessionStorage is nil, runConfig.SessionRedis is nil
  • When spec.sessionStorage.provider == "memory", runConfig.SessionRedis is nil
  • When spec.sessionStorage.provider == "redis" with address "redis.default.svc:6379", db 2, keyPrefix "thv:", runConfig.SessionRedis equals {Address: "redis.default.svc:6379", DB: 2, KeyPrefix: "thv:"}
  • When spec.sessionStorage.provider == "redis" and passwordRef is set, runConfig.SessionRedis does NOT contain any password field (assert the struct has only address/db/keyPrefix)
  • The serialized ConfigMap JSON (via json.Marshal) omits backend_replicas entirely when nil
  • The serialized ConfigMap JSON includes backend_replicas: 3 when set to int32(3)

Integration Tests

  • Not in scope for this task; integration-level tests covering the full reconcile loop are in TASK-008

Edge Cases

  • backendReplicas set to 0 — should be written to RunConfig (non-nil pointer to 0) since 0 is an explicit value that differs from "not configured" (nil)
  • Redis address set to empty string when provider is redis — the CEL validation on the CRD should prevent this from reaching the controller; the RunConfig population code does not need to re-validate, but a test confirming pass-through of an empty address is acceptable
  • db field at its +kubebuilder:default=0 default — should appear as 0 in SessionRedisConfig.DB (not omitted, since int32 zero value and the struct field uses omitempty only on the JSON tag; verify the serialized output)

Out of Scope

  • Redis password injection into pods — that is part of TASK-004 (MCPServer deployment builder) and TASK-006 (VirtualMCPServer deployment builder)
  • VirtualMCPServer Redis config injection — the vMCP config YAML is managed by ensureVmcpConfigConfigMap in virtualmcpserver_vmcpconfig.go, covered in TASK-007
  • Reading or consuming BackendReplicas or SessionRedis inside proxyrunner at runtime — that is a separate epic
  • terminationGracePeriodSeconds on Deployments — that is in TASK-004 and TASK-006
  • Session storage warning condition on the reconciler — that is in TASK-004
  • Stdio replica cap enforcement — that is in TASK-004
  • Any changes to the vMCP ConfigMap or VirtualMCPServer types
  • HPA integration, Redis deployment, or Redis 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