Skip to content

Commit 71e2934

Browse files
authored
Forces use of runconfig via configmap for Operator and ProxyRunner (#2196)
* hardcodes default to true Signed-off-by: ChrisJBurns <[email protected]> * removes uneeded tests Signed-off-by: ChrisJBurns <[email protected]> * amends tests for runconfig configmap Signed-off-by: ChrisJBurns <[email protected]> * removes local oject ref Signed-off-by: ChrisJBurns <[email protected]> --------- Signed-off-by: ChrisJBurns <[email protected]>
1 parent 61d04b4 commit 71e2934

File tree

4 files changed

+43
-263
lines changed

4 files changed

+43
-263
lines changed

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ var defaultRBACRules = []rbacv1.PolicyRule{
8484
// mcpContainerName is the name of the mcp container used in pod templates
8585
const mcpContainerName = "mcp"
8686

87-
// trueValue is the string value "true" used for environment variable comparisons
88-
const trueValue = "true"
89-
9087
// Restart annotation keys for triggering pod restart
9188
const (
9289
RestartedAtAnnotationKey = "mcpserver.toolhive.stacklok.dev/restarted-at"
@@ -752,7 +749,7 @@ func (r *MCPServerReconciler) deploymentForMCPServer(ctx context.Context, m *mcp
752749
volumes := []corev1.Volume{}
753750

754751
// Check if global ConfigMap mode is enabled via environment variable
755-
useConfigMap := os.Getenv("TOOLHIVE_USE_CONFIGMAP") == trueValue
752+
useConfigMap := true
756753
if useConfigMap {
757754
// Also add pod template patch for secrets and service account (same as regular flags approach)
758755
// If service account is not specified, use the default MCP server service account

cmd/thv-operator/controllers/mcpserver_pod_template_test.go

Lines changed: 0 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -294,112 +294,6 @@ func TestDeploymentForMCPServerWithSecrets(t *testing.T) {
294294
assert.NotContains(t, arg, "--secret=", "No secret CLI arguments should be present")
295295
}
296296
}
297-
298-
func TestDeploymentForMCPServerWithEnvVars(t *testing.T) {
299-
t.Parallel()
300-
// Create a test MCPServer with environment variables
301-
mcpServer := &mcpv1alpha1.MCPServer{
302-
ObjectMeta: metav1.ObjectMeta{
303-
Name: "test-mcp-server-env",
304-
Namespace: "default",
305-
},
306-
Spec: mcpv1alpha1.MCPServerSpec{
307-
Image: "test-image:latest",
308-
Transport: "stdio",
309-
Port: 8080,
310-
Env: []mcpv1alpha1.EnvVar{
311-
{
312-
Name: "API_KEY",
313-
Value: "secret-key-123",
314-
},
315-
{
316-
Name: "DEBUG_MODE",
317-
Value: "true",
318-
},
319-
},
320-
},
321-
}
322-
323-
// Create a new scheme for this test to avoid race conditions
324-
s := runtime.NewScheme()
325-
_ = scheme.AddToScheme(s)
326-
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServer{})
327-
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServerList{})
328-
329-
// Create a reconciler with the scheme
330-
r := &MCPServerReconciler{
331-
Scheme: s,
332-
}
333-
334-
// Generate the deployment
335-
ctx := context.Background()
336-
deployment := r.deploymentForMCPServer(ctx, mcpServer)
337-
require.NotNil(t, deployment, "Deployment should not be nil")
338-
339-
// Check that environment variables are passed as --env flags in the container args
340-
container := deployment.Spec.Template.Spec.Containers[0]
341-
342-
// Verify that the environment variables are NOT set as container environment variables
343-
for _, env := range container.Env {
344-
assert.NotEqual(t, "API_KEY", env.Name, "API_KEY should not be set as container env var")
345-
assert.NotEqual(t, "DEBUG_MODE", env.Name, "DEBUG_MODE should not be set as container env var")
346-
}
347-
348-
// Verify that the environment variables are passed as --env flags in the args
349-
apiKeyArgFound := false
350-
debugModeArgFound := false
351-
for _, arg := range container.Args {
352-
if arg == "--env=API_KEY=secret-key-123" {
353-
apiKeyArgFound = true
354-
}
355-
if arg == "--env=DEBUG_MODE=true" {
356-
debugModeArgFound = true
357-
}
358-
}
359-
assert.True(t, apiKeyArgFound, "API_KEY should be passed as --env flag")
360-
assert.True(t, debugModeArgFound, "DEBUG_MODE should be passed as --env flag")
361-
}
362-
363-
func TestDeploymentForMCPServerWithProxyMode(t *testing.T) {
364-
t.Parallel()
365-
366-
// Create a test MCPServer with ProxyMode
367-
mcpServer := &mcpv1alpha1.MCPServer{
368-
ObjectMeta: metav1.ObjectMeta{
369-
Name: "test-proxy-mode",
370-
Namespace: "default",
371-
},
372-
Spec: mcpv1alpha1.MCPServerSpec{
373-
Image: "test-image:latest",
374-
Transport: "stdio",
375-
Port: 8080,
376-
ProxyMode: "streamable-http",
377-
},
378-
}
379-
380-
// Create a reconciler
381-
s := runtime.NewScheme()
382-
_ = scheme.AddToScheme(s)
383-
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServer{})
384-
s.AddKnownTypes(mcpv1alpha1.GroupVersion, &mcpv1alpha1.MCPServerList{})
385-
r := &MCPServerReconciler{Scheme: s}
386-
387-
// Generate deployment and check for --proxy-mode flag
388-
deployment := r.deploymentForMCPServer(context.Background(), mcpServer)
389-
require.NotNil(t, deployment)
390-
391-
// Verify --proxy-mode flag is present
392-
container := deployment.Spec.Template.Spec.Containers[0]
393-
found := false
394-
for _, arg := range container.Args {
395-
if arg == "--proxy-mode=streamable-http" {
396-
found = true
397-
break
398-
}
399-
}
400-
assert.True(t, found, "--proxy-mode=streamable-http flag should be present in container args")
401-
}
402-
403297
func TestProxyRunnerSecurityContext(t *testing.T) {
404298
t.Parallel()
405299

cmd/thv-operator/controllers/mcpserver_resource_overrides_test.go

Lines changed: 0 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -320,29 +320,6 @@ func TestResourceOverrides(t *testing.T) {
320320
assert.Equal(t, tt.expectedServiceLabels, service.Labels)
321321
assert.Equal(t, tt.expectedServiceAnns, service.Annotations)
322322

323-
// For test case with Vault Agent Injection, verify --env-file-dir argument is present
324-
if tt.name == "with Vault Agent Injection pod template annotations" {
325-
require.Len(t, deployment.Spec.Template.Spec.Containers, 1)
326-
container := deployment.Spec.Template.Spec.Containers[0]
327-
328-
// Check that --env-file-dir=/vault/secrets argument is present
329-
found := false
330-
for _, arg := range container.Args {
331-
if arg == "--env-file-dir=/vault/secrets" {
332-
found = true
333-
break
334-
}
335-
}
336-
assert.True(t, found, "Expected --env-file-dir=/vault/secrets argument when vault.hashicorp.com/agent-inject=true")
337-
338-
// Verify pod template has the expected Vault annotations
339-
expectedTemplateAnnotations := map[string]string{
340-
"vault.hashicorp.com/agent-inject": "true",
341-
"vault.hashicorp.com/role": "toolhive-mcp-workloads",
342-
}
343-
assert.Equal(t, expectedTemplateAnnotations, deployment.Spec.Template.Annotations)
344-
}
345-
346323
// For test cases with environment variables, verify they are set correctly
347324
if tt.name == "with proxy environment variables" || tt.name == "with both metadata overrides and proxy environment variables" {
348325
require.Len(t, deployment.Spec.Template.Spec.Containers, 1)
@@ -428,41 +405,6 @@ func TestMergeStringMaps(t *testing.T) {
428405
}
429406
}
430407

431-
func TestDeploymentNeedsUpdateServiceAccount(t *testing.T) {
432-
t.Parallel()
433-
434-
scheme := runtime.NewScheme()
435-
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))
436-
437-
client := fake.NewClientBuilder().WithScheme(scheme).Build()
438-
r := &MCPServerReconciler{
439-
Client: client,
440-
Scheme: scheme,
441-
}
442-
443-
mcpServer := &mcpv1alpha1.MCPServer{
444-
ObjectMeta: metav1.ObjectMeta{
445-
Name: "test-server",
446-
Namespace: "default",
447-
},
448-
Spec: mcpv1alpha1.MCPServerSpec{
449-
Image: "test-image",
450-
Port: 8080,
451-
},
452-
}
453-
454-
// Create a deployment using the current implementation
455-
ctx := context.Background()
456-
deployment := r.deploymentForMCPServer(ctx, mcpServer)
457-
require.NotNil(t, deployment)
458-
459-
// Test with the current deployment - this should NOT need update
460-
needsUpdate := r.deploymentNeedsUpdate(context.Background(), deployment, mcpServer)
461-
462-
// With the service account bug fixed, this should now return false
463-
assert.False(t, needsUpdate, "deploymentNeedsUpdate should return false when deployment matches MCPServer spec")
464-
}
465-
466408
func TestDeploymentNeedsUpdateProxyEnv(t *testing.T) {
467409
t.Parallel()
468410

@@ -651,75 +593,3 @@ func TestDeploymentNeedsUpdateProxyEnv(t *testing.T) {
651593
})
652594
}
653595
}
654-
655-
func TestDeploymentNeedsUpdateToolsFilter(t *testing.T) {
656-
t.Parallel()
657-
658-
scheme := runtime.NewScheme()
659-
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))
660-
661-
client := fake.NewClientBuilder().WithScheme(scheme).Build()
662-
r := &MCPServerReconciler{
663-
Client: client,
664-
Scheme: scheme,
665-
}
666-
667-
tests := []struct {
668-
name string
669-
initialToolsFilter []string
670-
newToolsFilter []string
671-
expectEnvChange bool
672-
}{
673-
{
674-
name: "empty tools filter",
675-
initialToolsFilter: nil,
676-
newToolsFilter: nil,
677-
expectEnvChange: false,
678-
},
679-
{
680-
name: "tools filter not changed",
681-
initialToolsFilter: []string{"tool1", "tool2"},
682-
newToolsFilter: []string{"tool1", "tool2"},
683-
expectEnvChange: false,
684-
},
685-
{
686-
name: "tools filter changed",
687-
initialToolsFilter: []string{"tool1", "tool2"},
688-
newToolsFilter: []string{"tool2", "tool3"},
689-
expectEnvChange: true,
690-
},
691-
{
692-
name: "tools filter change order",
693-
initialToolsFilter: []string{"tool1", "tool2"},
694-
newToolsFilter: []string{"tool2", "tool1"},
695-
expectEnvChange: false,
696-
},
697-
}
698-
699-
for _, tt := range tests {
700-
t.Run(tt.name, func(t *testing.T) {
701-
t.Parallel()
702-
703-
mcpServer := &mcpv1alpha1.MCPServer{
704-
ObjectMeta: metav1.ObjectMeta{
705-
Name: "test-server",
706-
Namespace: "default",
707-
},
708-
Spec: mcpv1alpha1.MCPServerSpec{
709-
Image: "test-image",
710-
Port: 8080,
711-
ToolsFilter: tt.initialToolsFilter,
712-
},
713-
}
714-
715-
ctx := context.Background()
716-
deployment := r.deploymentForMCPServer(ctx, mcpServer)
717-
require.NotNil(t, deployment)
718-
719-
mcpServer.Spec.ToolsFilter = tt.newToolsFilter
720-
721-
needsUpdate := r.deploymentNeedsUpdate(context.Background(), deployment, mcpServer)
722-
assert.Equal(t, tt.expectEnvChange, needsUpdate)
723-
})
724-
}
725-
}

cmd/thv-operator/test-integration/mcp-server/mcpserver_controller_integration_test.go

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,29 @@ var _ = Describe("MCPServer Controller Integration Tests", func() {
141141
// Verify there's exactly one container (the toolhive proxy runner)
142142
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
143143

144+
templateSpec := deployment.Spec.Template.Spec
145+
146+
foundRunconfigVolume := false
147+
for _, v := range templateSpec.Volumes {
148+
if v.Name == "runconfig" && v.ConfigMap != nil && v.ConfigMap.Name == (mcpServerName+"-runconfig") {
149+
foundRunconfigVolume = true
150+
break
151+
}
152+
}
153+
Expect(foundRunconfigVolume).To(BeTrue(), "Deployment should have a volume sourced from runconfig ConfigMap")
154+
144155
container := deployment.Spec.Template.Spec.Containers[0]
145156

157+
// Verify that the runconfig ConfigMap is mounted as a volume
158+
foundRunconfigMount := false
159+
for _, vm := range container.VolumeMounts {
160+
if vm.Name == "runconfig" && vm.MountPath == "/etc/runconfig" {
161+
foundRunconfigMount = true
162+
break
163+
}
164+
}
165+
Expect(foundRunconfigMount).To(BeTrue(), "runconfig ConfigMap should be mounted at /etc/runconfig")
166+
146167
// Verify container name and image
147168
Expect(container.Name).To(Equal("toolhive"))
148169
Expect(container.Image).To(Equal(getExpectedRunnerImage()))
@@ -168,25 +189,8 @@ var _ = Describe("MCPServer Controller Integration Tests", func() {
168189
// Verify container args contain the required parameters
169190
Expect(container.Args).To(ContainElement("run"))
170191
Expect(container.Args).To(ContainElement("--foreground=true"))
171-
Expect(container.Args).To(ContainElement(fmt.Sprintf("--proxy-port=%d", mcpServer.Spec.Port)))
172-
Expect(container.Args).To(ContainElement(fmt.Sprintf("--name=%s", mcpServerName)))
173-
Expect(container.Args).To(ContainElement(fmt.Sprintf("--transport=%s", mcpServer.Spec.Transport)))
174-
Expect(container.Args).To(ContainElement(fmt.Sprintf("--proxy-mode=%s", mcpServer.Spec.ProxyMode)))
175192
Expect(container.Args).To(ContainElement(mcpServer.Spec.Image))
176193

177-
// Verify that user args are appended after "--"
178-
dashIndex := -1
179-
for i, arg := range container.Args {
180-
if arg == "--" {
181-
dashIndex = i
182-
break
183-
}
184-
}
185-
if len(mcpServer.Spec.Args) > 0 {
186-
Expect(dashIndex).To(BeNumerically(">=", 0))
187-
Expect(container.Args[dashIndex+1:]).To(ContainElement("--verbose"))
188-
}
189-
190194
// Verify container ports
191195
Expect(container.Ports).To(HaveLen(1))
192196
Expect(container.Ports[0].Name).To(Equal("http"))
@@ -208,6 +212,26 @@ var _ = Describe("MCPServer Controller Integration Tests", func() {
208212

209213
})
210214

215+
It("Should create the RunConfig ConfigMap", func() {
216+
217+
// Wait for Service to be created (using the correct naming pattern)
218+
configMap := &corev1.ConfigMap{}
219+
configMapName := mcpServerName + "-runconfig"
220+
Eventually(func() error {
221+
return k8sClient.Get(ctx, types.NamespacedName{
222+
Name: configMapName,
223+
Namespace: namespace,
224+
}, configMap)
225+
}, timeout, interval).Should(Succeed())
226+
227+
// Verify owner reference is set correctly
228+
verifyOwnerReference(configMap.OwnerReferences, createdMCPServer, "ConfigMap")
229+
230+
// Verify Service configuration
231+
Expect(configMap.Data).To(HaveKey("runconfig.json"))
232+
Expect(configMap.Annotations).To(HaveKey("toolhive.stacklok.dev/content-checksum"))
233+
})
234+
211235
It("Should create a Service for the MCPServer Proxy", func() {
212236

213237
// Wait for Service to be created (using the correct naming pattern)
@@ -306,7 +330,6 @@ var _ = Describe("MCPServer Controller Integration Tests", func() {
306330
return err
307331
}
308332
mcpServer.Spec.Image = "example/mcp-server:v2"
309-
mcpServer.Spec.Port = 9090
310333
return k8sClient.Update(ctx, mcpServer)
311334
}, timeout, interval).Should(Succeed())
312335

@@ -322,16 +345,12 @@ var _ = Describe("MCPServer Controller Integration Tests", func() {
322345
container := deployment.Spec.Template.Spec.Containers[0]
323346
// Check if the new image is in the args
324347
hasNewImage := false
325-
hasNewPort := false
326348
for _, arg := range container.Args {
327349
if arg == "example/mcp-server:v2" {
328350
hasNewImage = true
329351
}
330-
if arg == "--proxy-port=9090" {
331-
hasNewPort = true
332-
}
333352
}
334-
return hasNewImage && hasNewPort
353+
return hasNewImage
335354
}, timeout, interval).Should(BeTrue())
336355
})
337356
})

0 commit comments

Comments
 (0)