Skip to content

Commit dcc2331

Browse files
committed
moved onto config.Config as spec.config.rateLimiting
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
1 parent 330ddd3 commit dcc2331

10 files changed

Lines changed: 826 additions & 573 deletions

File tree

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
15+
vmcpconfig "github.com/stacklok/toolhive/pkg/vmcp/config"
1416
)
1517

1618
func TestSessionStorageConfigJSONRoundtrip(t *testing.T) {
@@ -126,18 +128,20 @@ func TestVirtualMCPServerSpecRateLimitingJSONRoundtrip(t *testing.T) {
126128
Provider: "redis",
127129
Address: "redis.default.svc.cluster.local:6379",
128130
},
129-
RateLimiting: &RateLimitConfig{
130-
Shared: &RateLimitBucket{MaxTokens: 10, RefillPeriod: metav1.Duration{Duration: time.Minute}},
131-
PerUser: &RateLimitBucket{
132-
MaxTokens: 2,
133-
RefillPeriod: metav1.Duration{Duration: time.Minute},
134-
},
135-
Tools: []ToolRateLimitConfig{
136-
{
137-
Name: "backend_a_echo",
138-
Shared: &RateLimitBucket{
139-
MaxTokens: 5,
140-
RefillPeriod: metav1.Duration{Duration: 30 * time.Second},
131+
Config: vmcpconfig.Config{
132+
RateLimiting: &vmcpconfig.RateLimitConfig{
133+
Shared: &vmcpconfig.RateLimitBucket{MaxTokens: 10, RefillPeriod: metav1.Duration{Duration: time.Minute}},
134+
PerUser: &vmcpconfig.RateLimitBucket{
135+
MaxTokens: 2,
136+
RefillPeriod: metav1.Duration{Duration: time.Minute},
137+
},
138+
Tools: []vmcpconfig.ToolRateLimitConfig{
139+
{
140+
Name: "backend_a_echo",
141+
Shared: &vmcpconfig.RateLimitBucket{
142+
MaxTokens: 5,
143+
RefillPeriod: metav1.Duration{Duration: 30 * time.Second},
144+
},
141145
},
142146
},
143147
},
@@ -151,7 +155,7 @@ func TestVirtualMCPServerSpecRateLimitingJSONRoundtrip(t *testing.T) {
151155
assert.Contains(t, out, `"shared"`)
152156
assert.Contains(t, out, `"perUser"`)
153157
assert.Contains(t, out, `"backend_a_echo"`)
154-
assert.NotContains(t, out, `"config":{"rateLimiting"`)
158+
assert.Contains(t, out, `"config":{"rateLimiting"`)
155159
}
156160

157161
func TestMCPServerSpecScalingFieldsJSONRoundtrip(t *testing.T) {

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import (
1616

1717
// VirtualMCPServerSpec defines the desired state of VirtualMCPServer
1818
//
19-
// +kubebuilder:validation:XValidation:rule="!has(self.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')",message="rateLimiting requires sessionStorage with provider 'redis'"
20-
// +kubebuilder:validation:XValidation:rule="!(has(self.rateLimiting) && has(self.rateLimiting.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="rateLimiting.perUser requires incomingAuth.type oidc"
21-
// +kubebuilder:validation:XValidation:rule="!has(self.rateLimiting) || !has(self.rateLimiting.tools) || self.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="per-tool perUser rate limiting requires incomingAuth.type oidc"
19+
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')",message="config.rateLimiting requires sessionStorage with provider 'redis'"
20+
// +kubebuilder:validation:XValidation:rule="!(has(self.config) && has(self.config.rateLimiting) && has(self.config.rateLimiting.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="config.rateLimiting.perUser requires incomingAuth.type oidc"
21+
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools) || self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="per-tool perUser rate limiting requires incomingAuth.type oidc"
2222
//
2323
//nolint:lll // CEL validation rules exceed line length limit
2424
type VirtualMCPServerSpec struct {
@@ -147,11 +147,6 @@ type VirtualMCPServerSpec struct {
147147
// +listType=atomic
148148
// +optional
149149
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
150-
151-
// RateLimiting defines rate limiting configuration for the Virtual MCP server.
152-
// Requires Redis session storage to be configured for distributed rate limiting.
153-
// +optional
154-
RateLimiting *RateLimitConfig `json:"rateLimiting,omitempty"`
155150
}
156151

157152
// EmbeddingServerRef references an existing EmbeddingServer resource by name.

cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/pkg/vmcpconfig/converter_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,51 @@ func TestConverter_SessionStorage(t *testing.T) {
16011601
}
16021602
}
16031603

1604+
func TestConverter_RateLimitingPassThrough(t *testing.T) {
1605+
t.Parallel()
1606+
1607+
vmcpServer := &mcpv1beta1.VirtualMCPServer{
1608+
ObjectMeta: metav1.ObjectMeta{
1609+
Name: "test-vmcp",
1610+
Namespace: "default",
1611+
},
1612+
Spec: mcpv1beta1.VirtualMCPServerSpec{
1613+
GroupRef: &mcpv1beta1.MCPGroupRef{Name: "test-group"},
1614+
Config: vmcpconfig.Config{
1615+
RateLimiting: &vmcpconfig.RateLimitConfig{
1616+
PerUser: &vmcpconfig.RateLimitBucket{
1617+
MaxTokens: 2,
1618+
RefillPeriod: metav1.Duration{Duration: time.Minute},
1619+
},
1620+
Tools: []vmcpconfig.ToolRateLimitConfig{
1621+
{
1622+
Name: "backend_a_echo",
1623+
Shared: &vmcpconfig.RateLimitBucket{
1624+
MaxTokens: 5,
1625+
RefillPeriod: metav1.Duration{Duration: 30 * time.Second},
1626+
},
1627+
},
1628+
},
1629+
},
1630+
},
1631+
},
1632+
}
1633+
1634+
converter := newTestConverter(t, newNoOpMockResolver(t))
1635+
ctx := log.IntoContext(context.Background(), logr.Discard())
1636+
1637+
config, _, err := converter.Convert(ctx, vmcpServer, nil)
1638+
require.NoError(t, err)
1639+
require.NotNil(t, config)
1640+
require.NotNil(t, config.RateLimiting)
1641+
1642+
assert.EqualValues(t, 2, config.RateLimiting.PerUser.MaxTokens)
1643+
require.Len(t, config.RateLimiting.Tools, 1)
1644+
assert.Equal(t, "backend_a_echo", config.RateLimiting.Tools[0].Name)
1645+
require.NotNil(t, config.RateLimiting.Tools[0].Shared)
1646+
assert.EqualValues(t, 5, config.RateLimiting.Tools[0].Shared.MaxTokens)
1647+
}
1648+
16041649
func TestDeriveAllowedAudiences(t *testing.T) {
16051650
t.Parallel()
16061651

cmd/thv-operator/test-integration/virtualmcp/virtualmcpserver_sessionstorage_cel_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,33 +112,33 @@ var _ = Describe("CEL Validation for SessionStorageConfig on VirtualMCPServer",
112112
Context("rateLimiting", func() {
113113
It("should reject rate limiting without redis session storage", func() {
114114
vmcp := newVirtualMCPServerWithSessionStorage("vmcp-rl-no-redis", nil)
115-
vmcp.Spec.RateLimiting = &mcpv1beta1.RateLimitConfig{
116-
Shared: &mcpv1beta1.RateLimitBucket{
115+
vmcp.Spec.Config.RateLimiting = &vmcpconfig.RateLimitConfig{
116+
Shared: &vmcpconfig.RateLimitBucket{
117117
MaxTokens: 1,
118118
RefillPeriod: metav1.Duration{Duration: time.Minute},
119119
},
120120
}
121121

122122
err := k8sClient.Create(ctx, vmcp)
123123
Expect(err).To(HaveOccurred())
124-
Expect(err.Error()).To(ContainSubstring("rateLimiting requires sessionStorage with provider 'redis'"))
124+
Expect(err.Error()).To(ContainSubstring("config.rateLimiting requires sessionStorage with provider 'redis'"))
125125
})
126126

127127
It("should reject perUser rate limiting with anonymous auth", func() {
128128
vmcp := newVirtualMCPServerWithSessionStorage("vmcp-rl-peruser-anon", &mcpv1beta1.SessionStorageConfig{
129129
Provider: "redis",
130130
Address: "redis:6379",
131131
})
132-
vmcp.Spec.RateLimiting = &mcpv1beta1.RateLimitConfig{
133-
PerUser: &mcpv1beta1.RateLimitBucket{
132+
vmcp.Spec.Config.RateLimiting = &vmcpconfig.RateLimitConfig{
133+
PerUser: &vmcpconfig.RateLimitBucket{
134134
MaxTokens: 1,
135135
RefillPeriod: metav1.Duration{Duration: time.Minute},
136136
},
137137
}
138138

139139
err := k8sClient.Create(ctx, vmcp)
140140
Expect(err).To(HaveOccurred())
141-
Expect(err.Error()).To(ContainSubstring("rateLimiting.perUser requires incomingAuth.type oidc"))
141+
Expect(err.Error()).To(ContainSubstring("config.rateLimiting.perUser requires incomingAuth.type oidc"))
142142
})
143143

144144
It("should accept perUser rate limiting with oidc auth and redis session storage", func() {
@@ -153,8 +153,8 @@ var _ = Describe("CEL Validation for SessionStorageConfig on VirtualMCPServer",
153153
Audience: "test-audience",
154154
},
155155
}
156-
vmcp.Spec.RateLimiting = &mcpv1beta1.RateLimitConfig{
157-
PerUser: &mcpv1beta1.RateLimitBucket{
156+
vmcp.Spec.Config.RateLimiting = &vmcpconfig.RateLimitConfig{
157+
PerUser: &vmcpconfig.RateLimitBucket{
158158
MaxTokens: 1,
159159
RefillPeriod: metav1.Duration{Duration: time.Minute},
160160
},

0 commit comments

Comments
 (0)