-
Notifications
You must be signed in to change notification settings - Fork 198
Inject BackendReplicas and Redis session config into MCPServer RunConfig #4218
Description
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.RunConfiginpkg/runner/config.gohas a newBackendReplicas *int32field with JSON/YAML tagsbackend_replicas - When
MCPServer.spec.backendReplicasis non-nil,createRunConfigFromMCPServersetsRunConfig.BackendReplicasto the spec value - When
MCPServer.spec.backendReplicasis nil,RunConfig.BackendReplicasis 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 fromSessionStorageConfig - When
spec.sessionStorageis nil orprovider == "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:
createRunConfigFromMCPServeralready callsrunner.PopulateMiddlewareConfigs(runConfig)after the builder returns. SettingBackendReplicasandSessionRedisdirectly on the returned*runner.RunConfigfollows this established pattern and avoids adding builder options for fields that are purely operator-injected. - Nil-passthrough for optional pointer fields:
BackendReplicas *int32must use the same nil-passthrough asspec.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
omitemptyon 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 theBackendReplicasfield and newSessionRedisConfigtype. 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.go—createRunConfigFromMCPServerfunction (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— ContainsMCPServerSpec.BackendReplicas *int32andMCPServerSpec.SessionStorage *SessionStorageConfigfields 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 forcreateRunConfigFromMCPServer. ThecreateTestMCPServerWithConfighelper at line 45 and test structure inTestCreateRunConfigFromMCPServerare 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 samet.Parallel()andtestify/assertstyle for new RunConfig tests.cmd/thv-operator/controllers/virtualmcpserver_deployment.go—buildOIDCEnvVarsandbuildHMACSecretEnvVar— reference for why the password is NOT in the RunConfig (it goes into the pod spec ascorev1.EnvVarwithValueFrom.SecretKeyRefinstead).
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.backendReplicasis nil,runConfig.BackendReplicasis nil - When
spec.backendReplicasis set to3,runConfig.BackendReplicaspoints toint32(3) - When
spec.sessionStorageis nil,runConfig.SessionRedisis nil - When
spec.sessionStorage.provider == "memory",runConfig.SessionRedisis nil - When
spec.sessionStorage.provider == "redis"with address"redis.default.svc:6379", db2, keyPrefix"thv:",runConfig.SessionRedisequals{Address: "redis.default.svc:6379", DB: 2, KeyPrefix: "thv:"} - When
spec.sessionStorage.provider == "redis"andpasswordRefis set,runConfig.SessionRedisdoes NOT contain any password field (assert the struct has only address/db/keyPrefix) - The serialized ConfigMap JSON (via
json.Marshal) omitsbackend_replicasentirely when nil - The serialized ConfigMap JSON includes
backend_replicas: 3when set toint32(3)
Integration Tests
- Not in scope for this task; integration-level tests covering the full reconcile loop are in TASK-008
Edge Cases
-
backendReplicasset to0— should be written to RunConfig (non-nil pointer to 0) since0is an explicit value that differs from "not configured" (nil) - Redis
addressset to empty string when provider isredis— 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 -
dbfield at its+kubebuilder:default=0default — should appear as0inSessionRedisConfig.DB(not omitted, sinceint32zero value and the struct field usesomitemptyonly 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
ensureVmcpConfigConfigMapinvirtualmcpserver_vmcpconfig.go, covered in TASK-007 - Reading or consuming
BackendReplicasorSessionRedisinside proxyrunner at runtime — that is a separate epic terminationGracePeriodSecondson 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
- 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 after scaling type changes — 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
pkg/runner/config.go—RunConfigstruct definitioncmd/thv-operator/controllers/mcpserver_runconfig.go—createRunConfigFromMCPServerfunctioncmd/thv-operator/controllers/mcpserver_runconfig_test.go— Existing test patterns for RunConfig populationcmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go—SecretKeyReftype definition (password remains as SecretKeyRef, not copied to RunConfig)