You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.// +optionalBackendReplicas*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".// +optionaltypeSessionRedisConfigstruct {
// Address is the Redis server address (host:port)Addressstring`json:"address,omitempty" yaml:"address,omitempty"`// DB is the Redis database numberDBint32`json:"db,omitempty" yaml:"db,omitempty"`// KeyPrefix is the prefix applied to all Redis keysKeyPrefixstring`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.// +optionalSessionRedis*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:
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.go — createRunConfigFromMCPServer 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.go — buildOIDCEnvVars 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"`
Post-builder addition in createRunConfigFromMCPServer (mcpserver_runconfig.go), inserted after the PopulateMiddlewareConfigs call:
// Set BackendReplicas from spec (nil-passthrough: only set when explicitly configured)ifm.Spec.BackendReplicas!=nil {
val:=*m.Spec.BackendReplicasrunConfig.BackendReplicas=&val
}
// Inject Redis session storage config (address/db/keyPrefix only — password is a pod env var)ifm.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
cmd/thv-operator/controllers/mcpserver_runconfig.go — createRunConfigFromMCPServer function
cmd/thv-operator/controllers/mcpserver_runconfig_test.go — Existing test patterns for RunConfig population
cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go — SecretKeyRef type definition (password remains as SecretKeyRef, not copied to RunConfig)
Description
Extend
createRunConfigFromMCPServerinmcpserver_runconfig.goto populateBackendReplicasfromspec.backendReplicasand, whensessionStorage.provider == redis, populate the Redis connection fields (address, db, keyPrefix) in the RunConfig. Add theBackendReplicas *int32field torunner.RunConfiginpkg/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
createRunConfigFromMCPServerto readspec.backendReplicasandspec.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_replicasMCPServer.spec.backendReplicasis non-nil,createRunConfigFromMCPServersetsRunConfig.BackendReplicasto the spec valueMCPServer.spec.backendReplicasis nil,RunConfig.BackendReplicasis nil (zero-value pointer; the field is omitted from the serialized ConfigMap JSON)MCPServer.spec.sessionStorage.provider == "redis", the RunConfig contains the Redis address, DB number, and key prefix fromSessionStorageConfigspec.sessionStorageis nil orprovider == "memory", no Redis fields are written to the RunConfiggo build ./cmd/thv-operator/... ./pkg/runner/...passes after the changego test ./cmd/thv-operator/controllers/... ./pkg/runner/...passes (including existing RunConfig tests)Technical Approach
Recommended Implementation
Step 1: Add
BackendReplicastorunner.RunConfigIn
pkg/runner/config.go, add a new optional field to theRunConfigstruct after the existing scaling-related fields (e.g., nearGroupor at the end of the struct):This is the only change outside
cmd/thv-operator/.Step 2: Add Redis session storage fields to
runner.RunConfigAlso in
pkg/runner/config.go, add aSessionRedisConfigstruct (or inline fields) to carry the Redis connection parameters:Step 3: Extend
createRunConfigFromMCPServerinmcpserver_runconfig.goAfter
runner.NewOperatorRunConfigBuilderreturns therunConfig, add direct field assignment for the new fields. This follows the same pattern used forPopulateMiddlewareConfigs— post-builder field setting on the returned struct:Alternatively, add a
WithBackendReplicasandWithSessionRedisbuilder option and pass them throughoptions. Either approach is acceptable — direct field assignment after the builder is simpler and matches thePopulateMiddlewareConfigspost-builder pattern already in the function.Do NOT include
PasswordRefin the RunConfig. The Redis password is injected asTHV_SESSION_REDIS_PASSWORDinto the pod viaSecretKeyRefby the deployment builder (TASK-004/TASK-006).Patterns & Frameworks
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.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".omitemptyon both JSON and YAML tags so that nil/zero-value fields are absent from the serialized ConfigMap, keeping the output clean.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.RunConfiginpkg/runner/config.go:New type in
pkg/runner/config.go:Post-builder addition in
createRunConfigFromMCPServer(mcpserver_runconfig.go), inserted after thePopulateMiddlewareConfigscall:Testing Strategy
Unit Tests (extend
mcpserver_runconfig_test.goor add a newmcpserver_runconfig_scaling_test.go):spec.backendReplicasis nil,runConfig.BackendReplicasis nilspec.backendReplicasis set to3,runConfig.BackendReplicaspoints toint32(3)spec.sessionStorageis nil,runConfig.SessionRedisis nilspec.sessionStorage.provider == "memory",runConfig.SessionRedisis nilspec.sessionStorage.provider == "redis"with address"redis.default.svc:6379", db2, keyPrefix"thv:",runConfig.SessionRedisequals{Address: "redis.default.svc:6379", DB: 2, KeyPrefix: "thv:"}spec.sessionStorage.provider == "redis"andpasswordRefis set,runConfig.SessionRedisdoes NOT contain any password field (assert the struct has only address/db/keyPrefix)json.Marshal) omitsbackend_replicasentirely when nilbackend_replicas: 3when set toint32(3)Integration Tests
Edge Cases
backendReplicasset to0— should be written to RunConfig (non-nil pointer to 0) since0is an explicit value that differs from "not configured" (nil)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 acceptabledbfield 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
ensureVmcpConfigConfigMapinvirtualmcpserver_vmcpconfig.go, covered in TASK-007BackendReplicasorSessionRedisinside proxyrunner at runtime — that is a separate epicterminationGracePeriodSecondson Deployments — that is in TASK-004 and TASK-006References
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)