Skip to content

Commit 2495c20

Browse files
authored
Merge branch 'main' into issues/1449
2 parents 740b264 + 9b5c1fd commit 2495c20

File tree

18 files changed

+502
-110
lines changed

18 files changed

+502
-110
lines changed

cmd/thv-operator/api/v1alpha1/mcpserver_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ type MCPServerSpec struct {
4848
// +optional
4949
Secrets []SecretRef `json:"secrets,omitempty"`
5050

51+
// ServiceAccount is the name of an already existing service account to use by the MCP server.
52+
// If not specified, a ServiceAccount will be created automatically and used by the MCP server.
53+
// +optional
54+
ServiceAccount *string `json:"serviceAccount,omitempty"`
55+
5156
// PermissionProfile defines the permission profile to use
5257
// +optional
5358
PermissionProfile *PermissionProfileRef `json:"permissionProfile,omitempty"`

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

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

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 33 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,7 @@ func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer
357357
return err
358358
}
359359

360-
// Ensure RoleBinding
361-
return r.ensureRBACResource(ctx, mcpServer, "RoleBinding", func() client.Object {
360+
if err := r.ensureRBACResource(ctx, mcpServer, "RoleBinding", func() client.Object {
362361
return &rbacv1.RoleBinding{
363362
ObjectMeta: metav1.ObjectMeta{
364363
Name: proxyRunnerNameForRBAC,
@@ -377,6 +376,25 @@ func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer
377376
},
378377
},
379378
}
379+
}); err != nil {
380+
return err
381+
}
382+
383+
// If a service account is specified, we don't need to create one
384+
if mcpServer.Spec.ServiceAccount != nil {
385+
return nil
386+
}
387+
388+
// otherwise, create a service account for the MCP server
389+
mcpServerServiceAccountName := mcpServerServiceAccountName(mcpServer.Name)
390+
return r.ensureRBACResource(ctx, mcpServer, "ServiceAccount", func() client.Object {
391+
mcpServer.Spec.ServiceAccount = &mcpServerServiceAccountName
392+
return &corev1.ServiceAccount{
393+
ObjectMeta: metav1.ObjectMeta{
394+
Name: mcpServerServiceAccountName,
395+
Namespace: mcpServer.Namespace,
396+
},
397+
}
380398
})
381399
}
382400

@@ -399,8 +417,11 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
399417
}
400418

401419
// Generate pod template patch for secrets and merge with user-provided patch
402-
finalPodTemplateSpec := generateAndMergePodTemplateSpecs(m.Spec.Secrets, m.Spec.PodTemplateSpec)
403420

421+
finalPodTemplateSpec := NewMCPServerPodTemplateSpecBuilder(m.Spec.PodTemplateSpec).
422+
WithServiceAccount(m.Spec.ServiceAccount).
423+
WithSecrets(m.Spec.Secrets).
424+
Build()
404425
// Add pod template patch if we have one
405426
if finalPodTemplateSpec != nil {
406427
podTemplatePatch, err := json.Marshal(finalPodTemplateSpec)
@@ -941,7 +962,10 @@ func deploymentNeedsUpdate(deployment *appsv1.Deployment, mcpServer *mcpv1alpha1
941962
}
942963

943964
// Check if the pod template spec has changed (including secrets)
944-
expectedPodTemplateSpec := generateAndMergePodTemplateSpecs(mcpServer.Spec.Secrets, mcpServer.Spec.PodTemplateSpec)
965+
expectedPodTemplateSpec := NewMCPServerPodTemplateSpecBuilder(mcpServer.Spec.PodTemplateSpec).
966+
WithServiceAccount(mcpServer.Spec.ServiceAccount).
967+
WithSecrets(mcpServer.Spec.Secrets).
968+
Build()
945969

946970
// Find the current pod template patch in the container args
947971
var currentPodTemplatePatch string
@@ -1089,6 +1113,11 @@ func proxyRunnerServiceAccountName(mcpServerName string) string {
10891113
return fmt.Sprintf("%s-proxy-runner", mcpServerName)
10901114
}
10911115

1116+
// mcpServerServiceAccountName returns the service account name for the mcp server
1117+
func mcpServerServiceAccountName(mcpServerName string) string {
1118+
return fmt.Sprintf("%s-sa", mcpServerName)
1119+
}
1120+
10921121
// labelsForMCPServer returns the labels for selecting the resources
10931122
// belonging to the given MCPServer CR name.
10941123
func labelsForMCPServer(name string) map[string]string {
@@ -1511,103 +1540,6 @@ func int32Ptr(i int32) *int32 {
15111540
return &i
15121541
}
15131542

1514-
// generateSecretsPodTemplatePatch generates a podTemplateSpec patch for secrets
1515-
func generateSecretsPodTemplatePatch(secrets []mcpv1alpha1.SecretRef) *corev1.PodTemplateSpec {
1516-
if len(secrets) == 0 {
1517-
return nil
1518-
}
1519-
1520-
envVars := make([]corev1.EnvVar, 0, len(secrets))
1521-
for _, secret := range secrets {
1522-
targetEnv := secret.Key
1523-
if secret.TargetEnvName != "" {
1524-
targetEnv = secret.TargetEnvName
1525-
}
1526-
1527-
envVars = append(envVars, corev1.EnvVar{
1528-
Name: targetEnv,
1529-
ValueFrom: &corev1.EnvVarSource{
1530-
SecretKeyRef: &corev1.SecretKeySelector{
1531-
LocalObjectReference: corev1.LocalObjectReference{
1532-
Name: secret.Name,
1533-
},
1534-
Key: secret.Key,
1535-
},
1536-
},
1537-
})
1538-
}
1539-
1540-
return &corev1.PodTemplateSpec{
1541-
Spec: corev1.PodSpec{
1542-
Containers: []corev1.Container{
1543-
{
1544-
Name: mcpContainerName,
1545-
Env: envVars,
1546-
},
1547-
},
1548-
},
1549-
}
1550-
}
1551-
1552-
// mergePodTemplateSpecs merges a secrets patch with a user-provided podTemplateSpec
1553-
func mergePodTemplateSpecs(secretsPatch, userPatch *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
1554-
// If no secrets, return user patch as-is
1555-
if secretsPatch == nil {
1556-
return userPatch
1557-
}
1558-
1559-
// If no user patch, return secrets patch
1560-
if userPatch == nil {
1561-
return secretsPatch
1562-
}
1563-
1564-
// Start with user patch as base (preserves all user customizations)
1565-
result := userPatch.DeepCopy()
1566-
1567-
// Find or create mcp container in result
1568-
mcpIndex := -1
1569-
for i, container := range result.Spec.Containers {
1570-
if container.Name == mcpContainerName {
1571-
mcpIndex = i
1572-
break
1573-
}
1574-
}
1575-
1576-
// Get secret env vars from secrets patch
1577-
var secretEnvVars []corev1.EnvVar
1578-
for _, container := range secretsPatch.Spec.Containers {
1579-
if container.Name == mcpContainerName {
1580-
secretEnvVars = container.Env
1581-
break
1582-
}
1583-
}
1584-
1585-
if mcpIndex >= 0 {
1586-
// Merge env vars into existing mcp container
1587-
result.Spec.Containers[mcpIndex].Env = append(
1588-
result.Spec.Containers[mcpIndex].Env,
1589-
secretEnvVars...,
1590-
)
1591-
} else {
1592-
// Add new mcp container with just env vars
1593-
result.Spec.Containers = append(result.Spec.Containers, corev1.Container{
1594-
Name: mcpContainerName,
1595-
Env: secretEnvVars,
1596-
})
1597-
}
1598-
1599-
return result
1600-
}
1601-
1602-
// generateAndMergePodTemplateSpecs generates secrets patch and merges with user patch
1603-
func generateAndMergePodTemplateSpecs(
1604-
secrets []mcpv1alpha1.SecretRef,
1605-
userPatch *corev1.PodTemplateSpec,
1606-
) *corev1.PodTemplateSpec {
1607-
secretsPatch := generateSecretsPodTemplatePatch(secrets)
1608-
return mergePodTemplateSpecs(secretsPatch, userPatch)
1609-
}
1610-
16111543
// SetupWithManager sets up the controller with the Manager.
16121544
func (r *MCPServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
16131545
return ctrl.NewControllerManagedBy(mgr).
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package controllers
2+
3+
import (
4+
corev1 "k8s.io/api/core/v1"
5+
6+
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
7+
)
8+
9+
// MCPServerPodTemplateSpecBuilder provides an interface for building PodTemplateSpec patches for MCP Servers
10+
type MCPServerPodTemplateSpecBuilder struct {
11+
spec *corev1.PodTemplateSpec
12+
}
13+
14+
// NewMCPServerPodTemplateSpecBuilder creates a new builder, optionally starting with a user-provided template
15+
func NewMCPServerPodTemplateSpecBuilder(userTemplate *corev1.PodTemplateSpec) *MCPServerPodTemplateSpecBuilder {
16+
var spec *corev1.PodTemplateSpec
17+
if userTemplate != nil {
18+
spec = userTemplate.DeepCopy()
19+
} else {
20+
spec = &corev1.PodTemplateSpec{
21+
Spec: corev1.PodSpec{
22+
Containers: []corev1.Container{},
23+
},
24+
}
25+
}
26+
27+
return &MCPServerPodTemplateSpecBuilder{spec: spec}
28+
}
29+
30+
// WithServiceAccount sets the service account name
31+
func (b *MCPServerPodTemplateSpecBuilder) WithServiceAccount(serviceAccount *string) *MCPServerPodTemplateSpecBuilder {
32+
if serviceAccount != nil && *serviceAccount != "" {
33+
b.spec.Spec.ServiceAccountName = *serviceAccount
34+
}
35+
return b
36+
}
37+
38+
// WithSecrets adds secret environment variables to the MCP container
39+
func (b *MCPServerPodTemplateSpecBuilder) WithSecrets(secrets []mcpv1alpha1.SecretRef) *MCPServerPodTemplateSpecBuilder {
40+
if len(secrets) == 0 {
41+
return b
42+
}
43+
44+
// Generate secret env vars
45+
secretEnvVars := make([]corev1.EnvVar, 0, len(secrets))
46+
for _, secret := range secrets {
47+
targetEnv := secret.Key
48+
if secret.TargetEnvName != "" {
49+
targetEnv = secret.TargetEnvName
50+
}
51+
52+
secretEnvVars = append(secretEnvVars, corev1.EnvVar{
53+
Name: targetEnv,
54+
ValueFrom: &corev1.EnvVarSource{
55+
SecretKeyRef: &corev1.SecretKeySelector{
56+
LocalObjectReference: corev1.LocalObjectReference{
57+
Name: secret.Name,
58+
},
59+
Key: secret.Key,
60+
},
61+
},
62+
})
63+
}
64+
65+
if len(secretEnvVars) == 0 {
66+
return b
67+
}
68+
69+
// add secret env vars to MCP container
70+
mcpIndex := -1
71+
for i, container := range b.spec.Spec.Containers {
72+
if container.Name == mcpContainerName {
73+
mcpIndex = i
74+
break
75+
}
76+
}
77+
78+
if mcpIndex >= 0 {
79+
// Merge env vars into existing MCP container
80+
b.spec.Spec.Containers[mcpIndex].Env = append(
81+
b.spec.Spec.Containers[mcpIndex].Env,
82+
secretEnvVars...,
83+
)
84+
} else {
85+
// Add new MCP container with env vars
86+
b.spec.Spec.Containers = append(b.spec.Spec.Containers, corev1.Container{
87+
Name: mcpContainerName,
88+
Env: secretEnvVars,
89+
})
90+
}
91+
return b
92+
}
93+
94+
// Build returns the final PodTemplateSpec, or nil if no customizations were made
95+
func (b *MCPServerPodTemplateSpecBuilder) Build() *corev1.PodTemplateSpec {
96+
// Return nil if the spec is effectively empty (no meaningful customizations)
97+
if b.isEmpty() {
98+
return nil
99+
}
100+
return b.spec
101+
}
102+
103+
// isEmpty checks if the builder contains any meaningful customizations
104+
func (b *MCPServerPodTemplateSpecBuilder) isEmpty() bool {
105+
return b.spec.Spec.ServiceAccountName == "" &&
106+
len(b.spec.Spec.Containers) == 0
107+
}

0 commit comments

Comments
 (0)