Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/v1alpha1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ type MCPServerStatus struct {
}

// MCPServerDeployment
// +kubebuilder:validation:XValidation:rule="!(has(self.serviceAccount) && has(self.serviceAccountName))",message="serviceAccount and serviceAccountName are mutually exclusive"
type MCPServerDeployment struct {
// Image defines the container image to to deploy the MCP server.
// +optional
Expand Down Expand Up @@ -263,10 +264,14 @@ type MCPServerDeployment struct {
// +optional
InitContainer *InitContainerConfig `json:"initContainer,omitempty"`

// ServiceAccount defines the configuration for the ServiceAccount.
// ServiceAccount defines the configuration for the ServiceAccount to be created.
// +optional
ServiceAccount *ServiceAccountConfig `json:"serviceAccount,omitempty"`

// ServiceAccountName is the name of an existing ServiceAccount to use.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// Sidecars defines additional containers to run alongside the MCP server container.
// These containers will share the same pod and can share volumes with the main container.
// +optional
Expand Down
8 changes: 5 additions & 3 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion config/crd/bases/kagent.dev_mcpservers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1887,7 +1887,7 @@ spec:
type: object
serviceAccount:
description: ServiceAccount defines the configuration for the
ServiceAccount.
ServiceAccount to be created.
properties:
annotations:
additionalProperties:
Expand All @@ -1904,6 +1904,10 @@ spec:
description: Labels to add to the ServiceAccount.
type: object
type: object
serviceAccountName:
description: ServiceAccountName is the name of an existing ServiceAccount
to use.
type: string
sidecars:
description: |-
Sidecars defines additional containers to run alongside the MCP server container.
Expand Down Expand Up @@ -5239,6 +5243,9 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: serviceAccount and serviceAccountName are mutually exclusive
rule: '!(has(self.serviceAccount) && has(self.serviceAccountName))'
httpTransport:
description: HTTPTransport defines the configuration for a Streamable
HTTP transport.
Expand Down
114 changes: 114 additions & 0 deletions pkg/controller/mcpserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,120 @@ var _ = ginkgo.Describe("MCPServer Controller", func() {
gomega.Expect(err).NotTo(gomega.HaveOccurred())
})
})

ginkgo.Context("Service Account Configuration", func() {
ctx := context.Background()

ginkgo.It("should use existing service account name when provided", func() {
ginkgo.By("Creating MCPServer with serviceAccountName")
serverName := "test-existing-sa"
existingSAName := "my-existing-sa"
server := &kagentdevv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: serverName,
Namespace: "default",
},
Spec: kagentdevv1alpha1.MCPServerSpec{
TransportType: kagentdevv1alpha1.TransportTypeStdio,
Deployment: kagentdevv1alpha1.MCPServerDeployment{
Image: "test-image:latest",
Port: 3000,
ServiceAccountName: existingSAName,
},
},
}

err := k8sClient.Create(ctx, server)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("Reconciling the MCPServer")
controllerReconciler := setupController()
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: serverName,
Namespace: "default",
},
})
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("Verifying deployment has the correct serviceAccountName")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(deployment.Spec.Template.Spec.ServiceAccountName).To(gomega.Equal(existingSAName))

ginkgo.By("Verifying no ServiceAccount was created for the MCPServer")
sa := &corev1.ServiceAccount{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, sa)
gomega.Expect(errors.IsNotFound(err)).To(gomega.BeTrue(), "ServiceAccount should not have been created")

// Cleanup
err = k8sClient.Delete(ctx, server)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
})

ginkgo.It("should create a new service account when serviceAccountName is not provided", func() {
ginkgo.By("Creating MCPServer with serviceAccountConfig")
serverName := "test-create-sa"
server := &kagentdevv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: serverName,
Namespace: "default",
},
Spec: kagentdevv1alpha1.MCPServerSpec{
TransportType: kagentdevv1alpha1.TransportTypeStdio,
Deployment: kagentdevv1alpha1.MCPServerDeployment{
Image: "test-image:latest",
Port: 3000,
ServiceAccount: &kagentdevv1alpha1.ServiceAccountConfig{
Annotations: map[string]string{"foo": "bar"},
},
},
},
}

err := k8sClient.Create(ctx, server)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("Reconciling the MCPServer")
controllerReconciler := setupController()
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: serverName,
Namespace: "default",
},
})
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("Verifying deployment has the correct serviceAccountName (defaulting to MCPServer name)")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(deployment.Spec.Template.Spec.ServiceAccountName).To(gomega.Equal(serverName))

ginkgo.By("Verifying the ServiceAccount was created")
sa := &corev1.ServiceAccount{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, sa)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(sa.Annotations).To(gomega.HaveKeyWithValue("foo", "bar"))

// Cleanup
err = k8sClient.Delete(ctx, server)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
})
})
})

// Helper functions to reduce code duplication
Expand Down
33 changes: 23 additions & 10 deletions pkg/controller/transportadapter/transportadapter_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ func (t *transportAdapterTranslator) TranslateTransportAdapterOutputs(
ctx context.Context,
server *v1alpha1.MCPServer,
) ([]client.Object, error) {
serviceAccount, err := t.translateTransportAdapterServiceAccount(server)
if err != nil {
return nil, fmt.Errorf("failed to translate TransportAdapter service account: %w", err)
}
deployment, err := t.translateTransportAdapterDeployment(server)
if err != nil {
return nil, fmt.Errorf("failed to translate TransportAdapter deployment: %w", err)
Expand All @@ -78,14 +74,25 @@ func (t *transportAdapterTranslator) TranslateTransportAdapterOutputs(
}
configMap, err := t.translateTransportAdapterConfigMap(server)
if err != nil {
return nil, fmt.Errorf("failed to translate TransportAdapter config map: %w", err)
return nil, fmt.Errorf("failed to marshal MCP server config to YAML: %w", err)
}
return t.runPlugins(ctx, server, []client.Object{
serviceAccount,

objects := []client.Object{
deployment,
service,
configMap,
})
}

// Create new service account only when service account name is not specified
if server.Spec.Deployment.ServiceAccountName == "" {
serviceAccount, err := t.translateTransportAdapterServiceAccount(server)
if err != nil {
return nil, fmt.Errorf("failed to translate TransportAdapter service account: %w", err)
}
objects = append(objects, serviceAccount)
}

return t.runPlugins(ctx, server, objects)
}

func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
Expand Down Expand Up @@ -154,12 +161,18 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
mainContainerResources = *server.Spec.Deployment.Resources
}

// Determine ServiceAccountName to use
serviceAccountName := server.Name
if server.Spec.Deployment.ServiceAccountName != "" {
serviceAccountName = server.Spec.Deployment.ServiceAccountName
}

var template corev1.PodSpec
switch server.Spec.TransportType {
case v1alpha1.TransportTypeStdio:
// copy the binary into the container when running with stdio
template = corev1.PodSpec{
ServiceAccountName: server.Name,
ServiceAccountName: serviceAccountName,
SecurityContext: server.Spec.Deployment.PodSecurityContext,
ImagePullSecrets: server.Spec.Deployment.ImagePullSecrets,
Tolerations: server.Spec.Deployment.Tolerations,
Expand Down Expand Up @@ -232,7 +245,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
cmd = []string{server.Spec.Deployment.Cmd}
}
template = corev1.PodSpec{
ServiceAccountName: server.Name,
ServiceAccountName: serviceAccountName,
SecurityContext: server.Spec.Deployment.PodSecurityContext,
ImagePullSecrets: server.Spec.Deployment.ImagePullSecrets,
Tolerations: server.Spec.Deployment.Tolerations,
Expand Down
Loading