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
5 changes: 5 additions & 0 deletions api/v1alpha1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ type MCPServerDeployment struct {
// +optional
Image string `json:"image,omitempty"`

// ImagePullPolicy defines the pull policy for the container image.
// +optional
// +kubebuilder:validation:Enum=Always;Never;IfNotPresent
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`

// Port defines the port on which the MCP server will listen.
// +optional
// +kubebuilder:default=3000
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/kagent.dev_mcpservers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ spec:
description: Image defines the container image to to deploy the
MCP server.
type: string
imagePullPolicy:
description: ImagePullPolicy defines the pull policy for the container
image.
enum:
- Always
- Never
- IfNotPresent
type: string
initContainer:
description: |-
InitContainer defines the configuration for the init container that copies
Expand Down
196 changes: 196 additions & 0 deletions pkg/controller/mcpserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,202 @@ var _ = ginkgo.Describe("MCPServer Controller", func() {
gomega.Expect(err).NotTo(gomega.HaveOccurred())
})
})

ginkgo.Context("ImagePullPolicy", func() {
ctx := context.Background()

ginkgo.It("should set ImagePullPolicy to Always when specified for stdio transport", func() {
ginkgo.By("Creating MCPServer with ImagePullPolicy Always for stdio transport")
serverName := "test-stdio-imagepullpolicy"
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,
ImagePullPolicy: corev1.PullAlways,
},
},
}

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 ImagePullPolicy set to Always")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

container := deployment.Spec.Template.Spec.Containers[0]
gomega.Expect(container.ImagePullPolicy).To(gomega.Equal(corev1.PullAlways))

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

ginkgo.It("should set ImagePullPolicy to Always when specified for HTTP transport", func() {
ginkgo.By("Creating MCPServer with ImagePullPolicy Always for HTTP transport")
serverName := "test-http-imagepullpolicy"
server := &kagentdevv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: serverName,
Namespace: "default",
},
Spec: kagentdevv1alpha1.MCPServerSpec{
TransportType: kagentdevv1alpha1.TransportTypeHTTP,
Deployment: kagentdevv1alpha1.MCPServerDeployment{
Image: "test-image:latest",
Port: 3000,
ImagePullPolicy: corev1.PullAlways,
},
HTTPTransport: &kagentdevv1alpha1.HTTPTransport{
TargetPort: 8080,
},
},
}

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 ImagePullPolicy set to Always")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

container := deployment.Spec.Template.Spec.Containers[0]
gomega.Expect(container.ImagePullPolicy).To(gomega.Equal(corev1.PullAlways))

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

ginkgo.It("should default ImagePullPolicy to IfNotPresent when not specified for stdio transport", func() {
ginkgo.By("Creating MCPServer without ImagePullPolicy for stdio transport")
serverName := "test-stdio-default-imagepullpolicy"
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,
},
},
}

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 ImagePullPolicy defaulted to IfNotPresent")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

container := deployment.Spec.Template.Spec.Containers[0]
gomega.Expect(container.ImagePullPolicy).To(gomega.Equal(corev1.PullIfNotPresent))

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

ginkgo.It("should default ImagePullPolicy to IfNotPresent when not specified for HTTP transport", func() {
ginkgo.By("Creating MCPServer without ImagePullPolicy for HTTP transport")
serverName := "test-http-default-imagepullpolicy"
server := &kagentdevv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: serverName,
Namespace: "default",
},
Spec: kagentdevv1alpha1.MCPServerSpec{
TransportType: kagentdevv1alpha1.TransportTypeHTTP,
Deployment: kagentdevv1alpha1.MCPServerDeployment{
Image: "test-image:latest",
Port: 3000,
},
HTTPTransport: &kagentdevv1alpha1.HTTPTransport{
TargetPort: 8080,
},
},
}

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 ImagePullPolicy defaulted to IfNotPresent")
deployment := &appsv1.Deployment{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: serverName,
Namespace: "default",
}, deployment)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

container := deployment.Spec.Template.Spec.Containers[0]
gomega.Expect(container.ImagePullPolicy).To(gomega.Equal(corev1.PullIfNotPresent))

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

// Helper functions to reduce code duplication
Expand Down
10 changes: 8 additions & 2 deletions pkg/controller/transportadapter/transportadapter_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
}
}

// Determine the main container pull policy to use
mainContainerPullPolicy := server.Spec.Deployment.ImagePullPolicy
if mainContainerPullPolicy == "" {
mainContainerPullPolicy = corev1.PullIfNotPresent
}

var template corev1.PodSpec
switch server.Spec.TransportType {
case v1alpha1.TransportTypeStdio:
Expand All @@ -149,7 +155,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
Containers: []corev1.Container{{
Name: "mcp-server",
Image: image,
ImagePullPolicy: corev1.PullIfNotPresent,
ImagePullPolicy: mainContainerPullPolicy,
Command: []string{
"/adapterbin/agentgateway",
},
Expand Down Expand Up @@ -200,7 +206,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
{
Name: "mcp-server",
Image: image,
ImagePullPolicy: corev1.PullIfNotPresent,
ImagePullPolicy: mainContainerPullPolicy,
Command: cmd,
Args: server.Spec.Deployment.Args,
Env: convertEnvVars(server.Spec.Deployment.Env),
Expand Down
Loading