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
25 changes: 20 additions & 5 deletions internal/controller/appserver/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1032,16 +1032,26 @@ var _ = Describe("App server assets", func() {
Expect(openshiftMCPServerContainer.Name).To(Equal(utils.OpenShiftMCPServerContainerName))
Expect(openshiftMCPServerContainer.Image).To(Equal(utils.OpenShiftMCPServerImageDefault))
Expect(openshiftMCPServerContainer.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent))
Expect(openshiftMCPServerContainer.Command).To(Equal([]string{"/openshift-mcp-server", "--read-only", "--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort)}))
Expect(openshiftMCPServerContainer.SecurityContext).To(Equal(restrictedContainerSecurityContext()))
Expect(openshiftMCPServerContainer.Command).To(Equal([]string{
"/openshift-mcp-server",
"--read-only",
"--config", utils.GetOpenShiftMCPServerConfigPath(),
"--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort),
}))
Expect(openshiftMCPServerContainer.SecurityContext).To(Equal(utils.RestrictedContainerSecurityContext()))
Expect(openshiftMCPServerContainer.Resources).To(Equal(corev1.ResourceRequirements{
Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("200Mi")},
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("50m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
Claims: []corev1.ResourceClaim{},
}))

// Verify MCP server has the same volume mounts as other containers
Expect(openshiftMCPServerContainer.VolumeMounts).To(ConsistOf(get10RequiredVolumeMounts()))
// Verify MCP server has its own config volume mount
_, expectedConfigMount := utils.GetOpenShiftMCPServerConfigVolumeAndMount()
Expect(openshiftMCPServerContainer.VolumeMounts).To(ContainElement(expectedConfigMount))

// Verify MCP server config volume is in deployment
expectedConfigVolume, _ := utils.GetOpenShiftMCPServerConfigVolumeAndMount()
Expect(dep.Spec.Template.Spec.Volumes).To(ContainElement(expectedConfigVolume))

By("Disabling introspection")
cr.Spec.OLSConfig.IntrospectionEnabled = false
Expand Down Expand Up @@ -1128,7 +1138,12 @@ var _ = Describe("App server assets", func() {
// Verify MCP container configuration
mcpContainer := dep.Spec.Template.Spec.Containers[1]
Expect(mcpContainer.Image).To(Equal(utils.OpenShiftMCPServerImageDefault))
Expect(mcpContainer.Command).To(Equal([]string{"/openshift-mcp-server", "--read-only", "--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort)}))
Expect(mcpContainer.Command).To(Equal([]string{
"/openshift-mcp-server",
"--read-only",
"--config", utils.GetOpenShiftMCPServerConfigPath(),
"--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort),
}))
Expect(mcpContainer.Resources).To(Equal(corev1.ResourceRequirements{
Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("200Mi")},
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("50m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
Expand Down
38 changes: 16 additions & 22 deletions internal/controller/appserver/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,6 @@ import (
"github.com/openshift/lightspeed-operator/internal/controller/utils"
)

// restrictedContainerSecurityContext returns a pointer to a SecurityContext
// that conforms to the Pod Security "restricted" profile.
func restrictedContainerSecurityContext() *corev1.SecurityContext {
return &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
RunAsNonRoot: &[]bool{true}[0],
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
}
}

func getOLSServerResources(cr *olsv1alpha1.OLSConfig) *corev1.ResourceRequirements {
return utils.GetResourcesOrDefault(
Expand Down Expand Up @@ -405,7 +390,7 @@ func GenerateOLSDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (
Image: r.GetAppServerImage(),
ImagePullPolicy: corev1.PullAlways,
Ports: ports,
SecurityContext: restrictedContainerSecurityContext(),
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: volumeMounts,
Env: append(utils.GetProxyEnvVars(), corev1.EnvVar{
Name: "OLS_CONFIG_FILE",
Expand Down Expand Up @@ -481,7 +466,7 @@ func GenerateOLSDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (
Name: "lightspeed-to-dataverse-exporter",
Image: r.GetDataverseExporterImage(),
ImagePullPolicy: corev1.PullAlways,
SecurityContext: restrictedContainerSecurityContext(),
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: volumeMounts,
// running in openshift mode ensures that cluster_id is set
// as identity_id
Expand All @@ -500,17 +485,26 @@ func GenerateOLSDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, exporterContainer)
}

// Add OpenShift MCP server sidecar container if introspection is enabled
// Add OpenShift MCP server sidecar container if introspection is enabled.
// The sidecar is configured with a TOML config that denies access to Secret resources,
// preventing secret data from reaching the LLM.
if cr.Spec.OLSConfig.IntrospectionEnabled {
configVolume, configMount := utils.GetOpenShiftMCPServerConfigVolumeAndMount()
openshiftMCPServerSidecarContainer := corev1.Container{
Name: "openshift-mcp-server",
Image: r.GetOpenShiftMCPServerImage(),
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: restrictedContainerSecurityContext(),
VolumeMounts: volumeMounts,
Command: []string{"/openshift-mcp-server", "--read-only", "--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort)},
Resources: *mcp_server_resources,
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: []corev1.VolumeMount{configMount},
Command: []string{
"/openshift-mcp-server",
"--read-only",
"--config", utils.GetOpenShiftMCPServerConfigPath(),
"--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort),
},
Resources: *mcp_server_resources,
}
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, configVolume)
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, openshiftMCPServerSidecarContainer)
}

Expand Down
8 changes: 8 additions & 0 deletions internal/controller/appserver/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ func ReconcileAppServerResources(r reconciler.Reconciler, ctx context.Context, o
Name: "reconcile ImageStreams",
Task: reconcileImageStreams,
},
{
Name: "reconcile MCP Server ConfigMap",
Task: reconcileMCPServerConfigMap,
},
}

failedTasks := make(map[string]error)
Expand Down Expand Up @@ -247,6 +251,10 @@ func reconcileExporterConfigMap(r reconciler.Reconciler, ctx context.Context, cr
return nil
}

func reconcileMCPServerConfigMap(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
return utils.ReconcileOpenShiftMCPServerConfigMap(r, ctx, cr, utils.GenerateAppServerSelectorLabels())
}

func reconcileProxyCAConfigMap(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
if cr.Spec.OLSConfig.ProxyConfig == nil || cr.Spec.OLSConfig.ProxyConfig.ProxyCACertificateRef == nil {
// no proxy CA certs, skip
Expand Down
5 changes: 1 addition & 4 deletions internal/controller/console/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ func GenerateConsoleUIDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSCon
Protocol: corev1.ProtocolTCP,
},
},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
},
SecurityContext: utils.RestrictedContainerSecurityContext(),
ImagePullPolicy: corev1.PullAlways,
Env: utils.GetProxyEnvVars(),
Resources: *resources,
Expand Down
22 changes: 13 additions & 9 deletions internal/controller/lcore/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,27 +95,34 @@ func getOLSDataCollectorResources(cr *olsv1alpha1.OLSConfig) *corev1.ResourceReq

// addOpenShiftMCPServerSidecar adds the OpenShift MCP server sidecar container to the deployment
// if introspection is enabled in the CR. This modifies the deployment in place.
// The sidecar is configured with a TOML config that denies access to Secret resources,
// preventing secret data from reaching the LLM.
func addOpenShiftMCPServerSidecar(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig, deployment *appsv1.Deployment) {
if !cr.Spec.OLSConfig.IntrospectionEnabled {
return
}

configVolume, configMount := utils.GetOpenShiftMCPServerConfigVolumeAndMount()

openshiftMCPServerContainer := corev1.Container{
Name: utils.OpenShiftMCPServerContainerName,
Image: r.GetOpenShiftMCPServerImage(),
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
},
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: []corev1.VolumeMount{configMount},
Command: []string{
"/openshift-mcp-server",
"--read-only",
"--config", utils.GetOpenShiftMCPServerConfigPath(),
"--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort),
},
Resources: *getOLSMCPServerResources(cr),
}

deployment.Spec.Template.Spec.Volumes = append(
deployment.Spec.Template.Spec.Volumes,
configVolume,
)
deployment.Spec.Template.Spec.Containers = append(
deployment.Spec.Template.Spec.Containers,
openshiftMCPServerContainer,
Expand All @@ -138,11 +145,8 @@ func addDataCollectorSidecar(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig,
Name: "lightspeed-to-dataverse-exporter",
Image: r.GetDataverseExporterImage(),
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
},
VolumeMounts: volumeMounts,
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: volumeMounts,
// running in openshift mode ensures that cluster_id is set
// as identity_id
Args: []string{
Expand Down
55 changes: 45 additions & 10 deletions internal/controller/lcore/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ func TestGenerateLCoreDeploymentWithIntrospection(t *testing.T) {
t.Errorf("Expected ImagePullPolicy PullIfNotPresent, got %v", openshiftMCPContainer.ImagePullPolicy)
}

// Verify command includes port flag
// Verify command includes port, read-only, and config flags
if len(openshiftMCPContainer.Command) == 0 {
t.Error("OpenShift MCP server container has no command")
} else {
Expand All @@ -493,22 +493,50 @@ func TestGenerateLCoreDeploymentWithIntrospection(t *testing.T) {
if !strings.Contains(commandStr, "--read-only") {
t.Error("Expected command to include '--read-only' flag")
}
if !strings.Contains(commandStr, "--config") || !strings.Contains(commandStr, utils.GetOpenShiftMCPServerConfigPath()) {
t.Errorf("Expected command to include '--config %s', got: %s", utils.GetOpenShiftMCPServerConfigPath(), commandStr)
}
}

// Verify security context
if openshiftMCPContainer.SecurityContext == nil {
t.Error("OpenShift MCP server container has no security context")
// Verify MCP server config volume mount
if len(openshiftMCPContainer.VolumeMounts) == 0 {
t.Error("OpenShift MCP server container has no volume mounts")
} else {
if openshiftMCPContainer.SecurityContext.AllowPrivilegeEscalation == nil ||
*openshiftMCPContainer.SecurityContext.AllowPrivilegeEscalation != false {
t.Error("Expected AllowPrivilegeEscalation to be false")
hasMCPConfigMount := false
for _, mount := range openshiftMCPContainer.VolumeMounts {
if mount.Name == utils.OpenShiftMCPServerConfigVolumeName {
hasMCPConfigMount = true
if !mount.ReadOnly {
t.Error("MCP server config volume mount should be read-only")
}
}
}
if openshiftMCPContainer.SecurityContext.ReadOnlyRootFilesystem == nil ||
*openshiftMCPContainer.SecurityContext.ReadOnlyRootFilesystem != true {
t.Error("Expected ReadOnlyRootFilesystem to be true")
if !hasMCPConfigMount {
t.Error("Missing MCP server config volume mount in openshift-mcp-server container")
}
}

// Verify MCP server config volume is added to deployment
volumes := deployment.Spec.Template.Spec.Volumes
hasMCPConfigVolume := false
for _, vol := range volumes {
if vol.Name == utils.OpenShiftMCPServerConfigVolumeName {
hasMCPConfigVolume = true
if vol.ConfigMap == nil || vol.ConfigMap.Name != utils.OpenShiftMCPServerConfigCmName {
t.Errorf("MCP server config volume should reference ConfigMap '%s'", utils.OpenShiftMCPServerConfigCmName)
}
}
}
if !hasMCPConfigVolume {
t.Error("Missing MCP server config volume in deployment")
}

// Verify security context matches the restricted profile
expectedSC := utils.RestrictedContainerSecurityContext()
if !reflect.DeepEqual(openshiftMCPContainer.SecurityContext, expectedSC) {
t.Errorf("Expected restricted security context, got %+v", openshiftMCPContainer.SecurityContext)
}

// Verify resource requirements are set
if openshiftMCPContainer.Resources.Limits == nil || openshiftMCPContainer.Resources.Requests == nil {
t.Error("OpenShift MCP server container missing resource limits or requests")
Expand Down Expand Up @@ -679,6 +707,13 @@ func TestGenerateLCoreDeploymentWithoutIntrospection(t *testing.T) {
}
}

// Verify MCP server config volume is NOT present
for _, vol := range deployment.Spec.Template.Spec.Volumes {
if vol.Name == utils.OpenShiftMCPServerConfigVolumeName {
t.Error("MCP server config volume should not be present when introspection is disabled")
}
}

t.Logf("Successfully validated LCore Deployment without OpenShift MCP server sidecar")
}

Expand Down
8 changes: 8 additions & 0 deletions internal/controller/lcore/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func ReconcileLCoreResources(r reconciler.Reconciler, ctx context.Context, olsco
Name: "reconcile LCore NetworkPolicy",
Task: reconcileNetworkPolicy,
},
{
Name: "reconcile MCP Server ConfigMap",
Task: reconcileMCPServerConfigMap,
},
}

failedTasks := make(map[string]error)
Expand Down Expand Up @@ -351,6 +355,10 @@ func reconcileOLSAdditionalCAConfigMap(r reconciler.Reconciler, ctx context.Cont
return nil
}

func reconcileMCPServerConfigMap(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
return utils.ReconcileOpenShiftMCPServerConfigMap(r, ctx, cr, buildCommonLabels())
}

func reconcileProxyCAConfigMap(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
if cr.Spec.OLSConfig.ProxyConfig == nil || cr.Spec.OLSConfig.ProxyConfig.ProxyCACertificateRef == nil {
// no proxy CA certs, skip
Expand Down
5 changes: 1 addition & 4 deletions internal/controller/postgres/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,7 @@ func GeneratePostgresDeployment(r reconciler.Reconciler, ctx context.Context, cr
Protocol: corev1.ProtocolTCP,
},
},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
},
SecurityContext: utils.RestrictedContainerSecurityContext(),
VolumeMounts: volumeMounts,
Resources: *databaseResources,
Env: []corev1.EnvVar{
Expand Down
8 changes: 8 additions & 0 deletions internal/controller/utils/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt'
CLIENT_PLACEHOLDER = "client"
// MCPHeadersMountRoot is the directory hosting MCP headers in the container
MCPHeadersMountRoot = "/etc/mcp/headers"
// OpenShiftMCPServerConfigCmName is the name of the ConfigMap for openshift-mcp-server configuration
OpenShiftMCPServerConfigCmName = "openshift-mcp-server-config"
// OpenShiftMCPServerConfigFilename is the filename for the openshift-mcp-server TOML config
OpenShiftMCPServerConfigFilename = "config.toml"
// OpenShiftMCPServerConfigMountPath is the directory where the MCP server config is mounted
OpenShiftMCPServerConfigMountPath = "/etc/mcp-server"
// OpenShiftMCPServerConfigVolumeName is the volume name for the MCP server config
OpenShiftMCPServerConfigVolumeName = "mcp-server-config"
// Header Secret Data Path
MCPSECRETDATAPATH = "header"
/*** Data Exporter Constants ***/
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/utils/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,10 @@ const (
ErrGenerateLlamaStackConfigMap = "failed to generate Llama Stack configmap"
ErrGetLlamaStackConfigMap = "failed to get Llama Stack configmap"
ErrUpdateLlamaStackConfigMap = "failed to update Llama Stack configmap"

// OpenShift MCP server config errors
ErrCreateMCPServerConfigMap = "failed to create MCP server config configmap"
ErrDeleteMCPServerConfigMap = "failed to delete MCP server config configmap"
ErrGetMCPServerConfigMap = "failed to get MCP server config configmap"
ErrUpdateMCPServerConfigMap = "failed to update MCP server config configmap"
)
Loading