diff --git a/api/v1alpha1/mcpserver_types.go b/api/v1alpha1/mcpserver_types.go index 4fae477..13bd033 100644 --- a/api/v1alpha1/mcpserver_types.go +++ b/api/v1alpha1/mcpserver_types.go @@ -231,6 +231,25 @@ type MCPServerDeployment struct { // This allows for custom volume configurations beyond just secrets and configmaps. // +optional Volumes []corev1.Volume `json:"volumes,omitempty"` + + // InitContainer defines the configuration for the init container that copies + // the transport adapter binary. This is used for stdio transport type. + // +optional + InitContainer *InitContainerConfig `json:"initContainer,omitempty"` +} + +// InitContainerConfig defines the configuration for the init container. +type InitContainerConfig struct { + // Image defines the full image reference for the init container. + // If specified, this overrides the default transport adapter image. + // Example: "myregistry.com/agentgateway/agentgateway:0.9.0-musl" + // +optional + Image string `json:"image,omitempty"` + + // ImagePullPolicy defines the pull policy for the init container image. + // +optional + // +kubebuilder:validation:Enum=Always;Never;IfNotPresent + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index da1200e..41fef23 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -41,6 +41,21 @@ func (in *HTTPTransport) DeepCopy() *HTTPTransport { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InitContainerConfig) DeepCopyInto(out *InitContainerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InitContainerConfig. +func (in *InitContainerConfig) DeepCopy() *InitContainerConfig { + if in == nil { + return nil + } + out := new(InitContainerConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MCPServer) DeepCopyInto(out *MCPServer) { *out = *in @@ -107,6 +122,11 @@ func (in *MCPServerDeployment) DeepCopyInto(out *MCPServerDeployment) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.InitContainer != nil { + in, out := &in.InitContainer, &out.InitContainer + *out = new(InitContainerConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPServerDeployment. diff --git a/config/crd/bases/kagent.dev_mcpservers.yaml b/config/crd/bases/kagent.dev_mcpservers.yaml index a7a4928..d9c7317 100644 --- a/config/crd/bases/kagent.dev_mcpservers.yaml +++ b/config/crd/bases/kagent.dev_mcpservers.yaml @@ -92,6 +92,26 @@ spec: description: Image defines the container image to to deploy the MCP server. type: string + initContainer: + description: |- + InitContainer defines the configuration for the init container that copies + the transport adapter binary. This is used for stdio transport type. + properties: + image: + description: |- + Image defines the full image reference for the init container. + If specified, this overrides the default transport adapter image. + Example: "myregistry.com/agentgateway/agentgateway:0.9.0-musl" + type: string + imagePullPolicy: + description: ImagePullPolicy defines the pull policy for the + init container image. + enum: + - Always + - Never + - IfNotPresent + type: string + type: object port: default: 3000 description: Port defines the port on which the MCP server will diff --git a/pkg/controller/transportadapter/transportadapter_translator.go b/pkg/controller/transportadapter/transportadapter_translator.go index 38afbde..91389fe 100644 --- a/pkg/controller/transportadapter/transportadapter_translator.go +++ b/pkg/controller/transportadapter/transportadapter_translator.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "os" + "regexp" "sort" "go.uber.org/multierr" @@ -23,9 +24,14 @@ import ( ) const ( - defaultTransportAdapterContainerImage = "ghcr.io/agentgateway/agentgateway:0.9.0-musl" + transportAdapterRepository = "ghcr.io/agentgateway/agentgateway" + defaultTransportAdapterVersion = "0.9.0" ) +// versionRegex validates that version strings contain only allowed characters +// (alphanumeric, dots, hyphens) to prevent potential image injection attacks +var versionRegex = regexp.MustCompile(`^[a-zA-Z0-9.\-]+$`) + // Translator is the interface for translating MCPServer objects to TransportAdapter objects. type Translator interface { TranslateTransportAdapterOutputs( @@ -103,10 +109,19 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment( // Create volume mounts from the MCPServer spec volumeMounts := t.createVolumeMounts(server.Spec.Deployment) - transportAdapterContainerImage := defaultTransportAdapterContainerImage - transportAdapterVersion := os.Getenv("TRANSPORT_ADAPTER_VERSION") - if transportAdapterVersion != "" { - transportAdapterContainerImage = fmt.Sprintf("ghcr.io/agentgateway/agentgateway:%s-musl", transportAdapterVersion) + // Determine the init container image and pull policy to use + // Start with the default transport adapter image + transportAdapterContainerImage := getTransportAdapterImage() + + initContainerPullPolicy := corev1.PullIfNotPresent + + // Override with custom image if specified in the MCPServer spec + if server.Spec.Deployment.InitContainer != nil && server.Spec.Deployment.InitContainer.Image != "" { + transportAdapterContainerImage = server.Spec.Deployment.InitContainer.Image + initContainerPullPolicy = server.Spec.Deployment.InitContainer.ImagePullPolicy + if initContainerPullPolicy == "" { + initContainerPullPolicy = corev1.PullIfNotPresent + } } var template corev1.PodSpec @@ -118,7 +133,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment( InitContainers: []corev1.Container{{ Name: "copy-binary", Image: transportAdapterContainerImage, - ImagePullPolicy: corev1.PullIfNotPresent, + ImagePullPolicy: initContainerPullPolicy, Command: []string{}, Args: []string{ "--copy-self", @@ -544,3 +559,28 @@ func (t *transportAdapterTranslator) runPlugins( return objects, errs } + +// validateVersion validates that a version string contains only allowed characters +// to prevent potential image injection attacks +func validateVersion(version string) error { + if !versionRegex.MatchString(version) { + return fmt.Errorf("invalid version format: %s (only alphanumeric characters, dots, and hyphens are allowed)", version) + } + return nil +} + +// getTransportAdapterImage returns the transport adapter container image, +// using the environment variable if provided and valid, otherwise using the default +func getTransportAdapterImage() string { + transportAdapterVersion := os.Getenv("TRANSPORT_ADAPTER_VERSION") + if transportAdapterVersion == "" { + return fmt.Sprintf("%s:%s-musl", transportAdapterRepository, defaultTransportAdapterVersion) + } + + if err := validateVersion(transportAdapterVersion); err != nil { + klog.Warningf("Invalid TRANSPORT_ADAPTER_VERSION: %v, fallback to %s", err, defaultTransportAdapterVersion) + return fmt.Sprintf("%s:%s-musl", transportAdapterRepository, defaultTransportAdapterVersion) + } + + return fmt.Sprintf("%s:%s-musl", transportAdapterRepository, transportAdapterVersion) +}