Skip to content

Commit 20ced20

Browse files
reyortiz3claude
andauthored
Add Redis Cluster mode support to auth server storage (#5153)
* Add Redis Cluster mode support to auth server storage Managed Redis services like GCP Memorystore Cluster and AWS ElastiCache Serverless use the Redis Cluster protocol rather than standalone or Sentinel connections, making it impossible to use them without this third mode. - Add ClusterConfig/ClusterRunConfig types to storage and runner layers - Wire cluster client creation via redis.NewClusterClient in NewRedisStorage - Update validateConfig, convertRedisRunConfig, and buildStorageRunConfig to enforce three-way mutual exclusion (addr / sentinel / cluster) - Add RedisClusterConfig CRD type with MinItems=1 validation; update CEL rule to require exactly one of the three modes - Regenerate deepcopy, CRD YAML, and Helm chart templates - Add unit tests for all new validation and happy-path code paths Closes #5010 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Simplify Redis Cluster mode to addr + clusterMode bool ClusterConfig{Addrs []string} was a misleading abstraction: GCP Memorystore Cluster and AWS ElastiCache cluster mode both expose a single discovery endpoint, so the slice always held exactly one address and was redundant with the existing Addr field. Replace ClusterConfig with a ClusterMode bool flag that is set alongside the existing Addr field. go-redis NewClusterClient still receives the single endpoint as []string{addr} and auto-discovers the full cluster topology from there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Regenerate Swagger docs for cluster_mode field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Regenerate CRD API reference docs for clusterMode field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6a6b2b2 commit 20ced20

16 files changed

Lines changed: 320 additions & 82 deletions

cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,18 +660,28 @@ type AuthServerStorageConfig struct {
660660
}
661661

662662
// RedisStorageConfig configures Redis connection for auth server storage.
663-
// Exactly one of addr (standalone) or sentinelConfig (Sentinel) must be set.
663+
// Exactly one of addr or sentinelConfig must be set. Set clusterMode to true when
664+
// addr points to a Redis Cluster discovery endpoint (GCP Memorystore Cluster,
665+
// AWS ElastiCache cluster mode enabled).
664666
//
665-
// +kubebuilder:validation:XValidation:rule="(self.addr.size() > 0) != has(self.sentinelConfig)",message="exactly one of addr (standalone) or sentinelConfig (Sentinel) must be set"
667+
// +kubebuilder:validation:XValidation:rule="(self.addr.size() > 0) != has(self.sentinelConfig)",message="exactly one of addr or sentinelConfig must be set"
668+
// +kubebuilder:validation:XValidation:rule="!self.clusterMode || self.addr.size() > 0",message="clusterMode requires addr to be set"
666669
//
667670
//nolint:lll // CEL validation rules exceed line length limit
668671
type RedisStorageConfig struct {
669-
// Addr is the Redis server address for standalone mode (e.g., "host:port").
670-
// Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
671-
// a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
672+
// Addr is the Redis server address (host:port). Required for standalone and cluster modes.
673+
// Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
674+
// AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
675+
// Mutually exclusive with sentinelConfig.
672676
// +optional
673677
Addr string `json:"addr,omitempty"`
674678

679+
// ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
680+
// Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
681+
// cluster mode enabled). Requires addr to be set.
682+
// +optional
683+
ClusterMode bool `json:"clusterMode,omitempty"`
684+
675685
// SentinelConfig holds Redis Sentinel configuration.
676686
// Use for self-managed Redis with Sentinel-based HA. Mutually exclusive with addr.
677687
// +optional

cmd/thv-operator/pkg/controllerutil/authserver.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -604,12 +604,11 @@ func buildStorageRunConfig(
604604
return nil, fmt.Errorf("redis config is required when storage type is redis")
605605
}
606606

607-
if redisConfig.Addr == "" && redisConfig.SentinelConfig == nil {
608-
return nil, fmt.Errorf("either addr (standalone) or sentinel config is required for Redis storage")
609-
}
610-
611607
if redisConfig.Addr != "" && redisConfig.SentinelConfig != nil {
612-
return nil, fmt.Errorf("addr and sentinel config are mutually exclusive for Redis storage")
608+
return nil, fmt.Errorf("addr and sentinelConfig are mutually exclusive for Redis storage")
609+
}
610+
if redisConfig.Addr == "" && redisConfig.SentinelConfig == nil {
611+
return nil, fmt.Errorf("one of addr (standalone or cluster) or sentinelConfig (Sentinel) is required for Redis storage")
613612
}
614613

615614
if redisConfig.ACLUserConfig == nil ||
@@ -629,6 +628,7 @@ func buildStorageRunConfig(
629628

630629
rc := &storage.RedisRunConfig{
631630
Addr: redisConfig.Addr,
631+
ClusterMode: redisConfig.ClusterMode,
632632
AuthType: storage.AuthTypeACLUser,
633633
ACLUserConfig: aclRunConfig,
634634
KeyPrefix: keyPrefix,

cmd/thv-operator/pkg/controllerutil/authserver_test.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,7 +1980,7 @@ func TestBuildStorageRunConfig(t *testing.T) {
19801980
errContains: "redis config is required",
19811981
},
19821982
{
1983-
name: "Redis storage missing both addr and sentinelConfig returns error",
1983+
name: "Redis storage missing addr and sentinelConfig returns error",
19841984
authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
19851985
Issuer: "https://auth.example.com",
19861986
Storage: &mcpv1beta1.AuthServerStorageConfig{
@@ -1994,7 +1994,7 @@ func TestBuildStorageRunConfig(t *testing.T) {
19941994
},
19951995
},
19961996
wantErr: true,
1997-
errContains: "either addr (standalone) or sentinel config is required",
1997+
errContains: "one of addr (standalone or cluster) or sentinelConfig (Sentinel) is required",
19981998
},
19991999
{
20002000
name: "Redis storage with both addr and sentinelConfig returns error",
@@ -2016,7 +2016,37 @@ func TestBuildStorageRunConfig(t *testing.T) {
20162016
},
20172017
},
20182018
wantErr: true,
2019-
errContains: "addr and sentinel config are mutually exclusive",
2019+
errContains: "mutually exclusive",
2020+
},
2021+
{
2022+
name: "Redis cluster mode builds correctly",
2023+
authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
2024+
Issuer: "https://auth.example.com",
2025+
Storage: &mcpv1beta1.AuthServerStorageConfig{
2026+
Type: mcpv1beta1.AuthServerStorageTypeRedis,
2027+
Redis: &mcpv1beta1.RedisStorageConfig{
2028+
Addr: "discovery.example.com:6379",
2029+
ClusterMode: true,
2030+
ACLUserConfig: &mcpv1beta1.RedisACLUserConfig{
2031+
UsernameSecretRef: &mcpv1beta1.SecretKeyRef{Name: "redis-secret", Key: "username"},
2032+
PasswordSecretRef: &mcpv1beta1.SecretKeyRef{Name: "redis-secret", Key: "password"},
2033+
},
2034+
},
2035+
},
2036+
},
2037+
checkFunc: func(t *testing.T, cfg *storage.RunConfig) {
2038+
t.Helper()
2039+
assert.Equal(t, string(storage.TypeRedis), cfg.Type)
2040+
require.NotNil(t, cfg.RedisConfig)
2041+
assert.Equal(t, "discovery.example.com:6379", cfg.RedisConfig.Addr)
2042+
assert.True(t, cfg.RedisConfig.ClusterMode)
2043+
assert.Nil(t, cfg.RedisConfig.SentinelConfig)
2044+
assert.Equal(t, storage.AuthTypeACLUser, cfg.RedisConfig.AuthType)
2045+
require.NotNil(t, cfg.RedisConfig.ACLUserConfig)
2046+
assert.Equal(t, authrunner.RedisUsernameEnvVar, cfg.RedisConfig.ACLUserConfig.UsernameEnvVar)
2047+
assert.Equal(t, authrunner.RedisPasswordEnvVar, cfg.RedisConfig.ACLUserConfig.PasswordEnvVar)
2048+
assert.Equal(t, "thv:auth:{default:test-server}:", cfg.RedisConfig.KeyPrefix)
2049+
},
20202050
},
20212051
{
20222052
name: "Redis storage with standalone addr builds correctly",

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,17 @@ spec:
316316
type: object
317317
addr:
318318
description: |-
319-
Addr is the Redis server address for standalone mode (e.g., "host:port").
320-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
321-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
319+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
320+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
321+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
322+
Mutually exclusive with sentinelConfig.
322323
type: string
324+
clusterMode:
325+
description: |-
326+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
327+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
328+
cluster mode enabled). Requires addr to be set.
329+
type: boolean
323330
dialTimeout:
324331
default: 5s
325332
description: |-
@@ -442,9 +449,10 @@ spec:
442449
- aclUserConfig
443450
type: object
444451
x-kubernetes-validations:
445-
- message: exactly one of addr (standalone) or sentinelConfig
446-
(Sentinel) must be set
452+
- message: exactly one of addr or sentinelConfig must be set
447453
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
454+
- message: clusterMode requires addr to be set
455+
rule: '!self.clusterMode || self.addr.size() > 0'
448456
type:
449457
default: memory
450458
description: |-
@@ -1478,10 +1486,17 @@ spec:
14781486
type: object
14791487
addr:
14801488
description: |-
1481-
Addr is the Redis server address for standalone mode (e.g., "host:port").
1482-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
1483-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
1489+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
1490+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
1491+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
1492+
Mutually exclusive with sentinelConfig.
14841493
type: string
1494+
clusterMode:
1495+
description: |-
1496+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
1497+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
1498+
cluster mode enabled). Requires addr to be set.
1499+
type: boolean
14851500
dialTimeout:
14861501
default: 5s
14871502
description: |-
@@ -1604,9 +1619,10 @@ spec:
16041619
- aclUserConfig
16051620
type: object
16061621
x-kubernetes-validations:
1607-
- message: exactly one of addr (standalone) or sentinelConfig
1608-
(Sentinel) must be set
1622+
- message: exactly one of addr or sentinelConfig must be set
16091623
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
1624+
- message: clusterMode requires addr to be set
1625+
rule: '!self.clusterMode || self.addr.size() > 0'
16101626
type:
16111627
default: memory
16121628
description: |-

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,17 @@ spec:
189189
type: object
190190
addr:
191191
description: |-
192-
Addr is the Redis server address for standalone mode (e.g., "host:port").
193-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
194-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
192+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
193+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
194+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
195+
Mutually exclusive with sentinelConfig.
195196
type: string
197+
clusterMode:
198+
description: |-
199+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
200+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
201+
cluster mode enabled). Requires addr to be set.
202+
type: boolean
196203
dialTimeout:
197204
default: 5s
198205
description: |-
@@ -315,9 +322,10 @@ spec:
315322
- aclUserConfig
316323
type: object
317324
x-kubernetes-validations:
318-
- message: exactly one of addr (standalone) or sentinelConfig
319-
(Sentinel) must be set
325+
- message: exactly one of addr or sentinelConfig must be set
320326
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
327+
- message: clusterMode requires addr to be set
328+
rule: '!self.clusterMode || self.addr.size() > 0'
321329
type:
322330
default: memory
323331
description: |-
@@ -2799,10 +2807,17 @@ spec:
27992807
type: object
28002808
addr:
28012809
description: |-
2802-
Addr is the Redis server address for standalone mode (e.g., "host:port").
2803-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
2804-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
2810+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
2811+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
2812+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
2813+
Mutually exclusive with sentinelConfig.
28052814
type: string
2815+
clusterMode:
2816+
description: |-
2817+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
2818+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
2819+
cluster mode enabled). Requires addr to be set.
2820+
type: boolean
28062821
dialTimeout:
28072822
default: 5s
28082823
description: |-
@@ -2925,9 +2940,10 @@ spec:
29252940
- aclUserConfig
29262941
type: object
29272942
x-kubernetes-validations:
2928-
- message: exactly one of addr (standalone) or sentinelConfig
2929-
(Sentinel) must be set
2943+
- message: exactly one of addr or sentinelConfig must be set
29302944
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
2945+
- message: clusterMode requires addr to be set
2946+
rule: '!self.clusterMode || self.addr.size() > 0'
29312947
type:
29322948
default: memory
29332949
description: |-

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,17 @@ spec:
319319
type: object
320320
addr:
321321
description: |-
322-
Addr is the Redis server address for standalone mode (e.g., "host:port").
323-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
324-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
322+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
323+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
324+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
325+
Mutually exclusive with sentinelConfig.
325326
type: string
327+
clusterMode:
328+
description: |-
329+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
330+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
331+
cluster mode enabled). Requires addr to be set.
332+
type: boolean
326333
dialTimeout:
327334
default: 5s
328335
description: |-
@@ -445,9 +452,10 @@ spec:
445452
- aclUserConfig
446453
type: object
447454
x-kubernetes-validations:
448-
- message: exactly one of addr (standalone) or sentinelConfig
449-
(Sentinel) must be set
455+
- message: exactly one of addr or sentinelConfig must be set
450456
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
457+
- message: clusterMode requires addr to be set
458+
rule: '!self.clusterMode || self.addr.size() > 0'
451459
type:
452460
default: memory
453461
description: |-
@@ -1481,10 +1489,17 @@ spec:
14811489
type: object
14821490
addr:
14831491
description: |-
1484-
Addr is the Redis server address for standalone mode (e.g., "host:port").
1485-
Use for managed Redis services (GCP Memorystore, AWS ElastiCache) that present
1486-
a single endpoint and manage HA internally. Mutually exclusive with sentinelConfig.
1492+
Addr is the Redis server address (host:port). Required for standalone and cluster modes.
1493+
Use for managed Redis services that expose a single endpoint (GCP Memorystore basic tier,
1494+
AWS ElastiCache without cluster mode, or cluster-mode services when clusterMode is true).
1495+
Mutually exclusive with sentinelConfig.
14871496
type: string
1497+
clusterMode:
1498+
description: |-
1499+
ClusterMode enables the Redis Cluster protocol. Set to true when addr points to a
1500+
Redis Cluster discovery endpoint (e.g., GCP Memorystore Cluster, AWS ElastiCache
1501+
cluster mode enabled). Requires addr to be set.
1502+
type: boolean
14881503
dialTimeout:
14891504
default: 5s
14901505
description: |-
@@ -1607,9 +1622,10 @@ spec:
16071622
- aclUserConfig
16081623
type: object
16091624
x-kubernetes-validations:
1610-
- message: exactly one of addr (standalone) or sentinelConfig
1611-
(Sentinel) must be set
1625+
- message: exactly one of addr or sentinelConfig must be set
16121626
rule: (self.addr.size() > 0) != has(self.sentinelConfig)
1627+
- message: clusterMode requires addr to be set
1628+
rule: '!self.clusterMode || self.addr.size() > 0'
16131629
type:
16141630
default: memory
16151631
description: |-

0 commit comments

Comments
 (0)