diff --git a/api/v1alpha1/olsconfig_types.go b/api/v1alpha1/olsconfig_types.go index 1555c98f2..5371bc512 100644 --- a/api/v1alpha1/olsconfig_types.go +++ b/api/v1alpha1/olsconfig_types.go @@ -453,6 +453,10 @@ type MCPServerStreamableHTTPTransport struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="SSE Read Timeout in seconds" SSEReadTimeout int `json:"sseReadTimeout,omitempty"` // Headers to send to the MCP server + // the map contains the header name and the name of the secret with the content of the header. This secret + // should contain a header path in the data containing a header value. + // A special case is usage of the kubernetes token in the header. to specify this use + // a string "kubernetes" instead of the secret name // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Headers" Headers map[string]string `json:"headers,omitempty"` // Enable Server Sent Events diff --git a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml index 789cd53af..36b4ccd89 100644 --- a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml +++ b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml @@ -135,7 +135,12 @@ spec: - description: Enable Server Sent Events displayName: Enable Server Sent Events path: mcpServers[0].streamableHTTP.enableSSE - - description: Headers to send to the MCP server + - description: |- + Headers to send to the MCP server + the map contains the header name and the name of the secret with the content of the header. This secret + should contain a header path in the data containing a header value. + A special case is usage of the kubernetes token in the header. to specify this use + a string "kubernetes" instead of the secret name displayName: Headers path: mcpServers[0].streamableHTTP.headers - description: SSE Read Timeout, default is 10 seconds diff --git a/bundle/manifests/ols.openshift.io_olsconfigs.yaml b/bundle/manifests/ols.openshift.io_olsconfigs.yaml index 872393e9b..8abe23ff8 100644 --- a/bundle/manifests/ols.openshift.io_olsconfigs.yaml +++ b/bundle/manifests/ols.openshift.io_olsconfigs.yaml @@ -362,7 +362,12 @@ spec: headers: additionalProperties: type: string - description: Headers to send to the MCP server + description: |- + Headers to send to the MCP server + the map contains the header name and the name of the secret with the content of the header. This secret + should contain a header path in the data containing a header value. + A special case is usage of the kubernetes token in the header. to specify this use + a string "kubernetes" instead of the secret name type: object sseReadTimeout: default: 10 diff --git a/config/crd/bases/ols.openshift.io_olsconfigs.yaml b/config/crd/bases/ols.openshift.io_olsconfigs.yaml index 7cd81a033..01896ee3b 100644 --- a/config/crd/bases/ols.openshift.io_olsconfigs.yaml +++ b/config/crd/bases/ols.openshift.io_olsconfigs.yaml @@ -362,7 +362,12 @@ spec: headers: additionalProperties: type: string - description: Headers to send to the MCP server + description: |- + Headers to send to the MCP server + the map contains the header name and the name of the secret with the content of the header. This secret + should contain a header path in the data containing a header value. + A special case is usage of the kubernetes token in the header. to specify this use + a string "kubernetes" instead of the secret name type: object sseReadTimeout: default: 10 diff --git a/config/manifests/bases/lightspeed-operator.clusterserviceversion.yaml b/config/manifests/bases/lightspeed-operator.clusterserviceversion.yaml index 6943d27f5..79894e360 100644 --- a/config/manifests/bases/lightspeed-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/lightspeed-operator.clusterserviceversion.yaml @@ -104,7 +104,12 @@ spec: - description: Enable Server Sent Events displayName: Enable Server Sent Events path: mcpServers[0].streamableHTTP.enableSSE - - description: Headers to send to the MCP server + - description: |- + Headers to send to the MCP server + the map contains the header name and the name of the secret with the content of the header. This secret + should contain a header path in the data containing a header value. + A special case is usage of the kubernetes token in the header. to specify this use + a string "kubernetes" instead of the secret name displayName: Headers path: mcpServers[0].streamableHTTP.headers - description: SSE Read Timeout, default is 10 seconds diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 970679b8b..9bb1ba1de 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -277,4 +277,12 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt' OpenShiftMCPServerTimeout = 60 // MCP server SSE read timeout, sec OpenShiftMCPServerHTTPReadTimeout = 30 + // Authorization header for OpenShift MCP server + K8S_AUTH_HEADER = "kubernetes-authorization" + // Constant, defining usage of kubernetes token + KUBERNETES_PLACEHOLDER = "kubernetes" + // MCPHeadersMountRoot is the directory hosting MCP headers in the container + MCPHeadersMountRoot = "/etc/mcp/headers" + // Header Secret Data Path + MCPSECRETDATAPATH = "header" ) diff --git a/internal/controller/ols_app_server_assets.go b/internal/controller/ols_app_server_assets.go index d41361dff..88a6b6c3f 100644 --- a/internal/controller/ols_app_server_assets.go +++ b/internal/controller/ols_app_server_assets.go @@ -344,13 +344,17 @@ func (r *OLSConfigReconciler) generateOLSConfigMap(ctx context.Context, cr *olsv URL: fmt.Sprintf(OpenShiftMCPServerURL, OpenShiftMCPServerPort), Timeout: OpenShiftMCPServerTimeout, SSEReadTimeout: OpenShiftMCPServerHTTPReadTimeout, + Headers: map[string]string{K8S_AUTH_HEADER: KUBERNETES_PLACEHOLDER}, }, }, } } if cr.Spec.FeatureGates != nil && slices.Contains(cr.Spec.FeatureGates, FeatureGateMCPServer) { - mcpServers := generateMCPServerConfigs(cr) + mcpServers, err := r.generateMCPServerConfigs(ctx, cr) + if err != nil { + return nil, err + } if appSrvConfigFile.MCPServers == nil { appSrvConfigFile.MCPServers = mcpServers } else { @@ -730,47 +734,86 @@ const ( StreamableHTTPField ) -func generateMCPServerConfigs(cr *olsv1alpha1.OLSConfig) []MCPServerConfig { +func (r *OLSConfigReconciler) generateMCPServerConfigs(ctx context.Context, cr *olsv1alpha1.OLSConfig) ([]MCPServerConfig, error) { if cr.Spec.MCPServers == nil { - return nil + return nil, nil } servers := []MCPServerConfig{} + var overall_error error + overall_error = nil for _, server := range cr.Spec.MCPServers { + // check all the secrets + sse, err := r.generateMCPStreamableHTTPTransportConfig(ctx, &server, SSEField) + if err != nil { + overall_error = err + continue + } + streamableHTTP, err := r.generateMCPStreamableHTTPTransportConfig(ctx, &server, StreamableHTTPField) + if err != nil { + overall_error = err + continue + } servers = append(servers, MCPServerConfig{ Name: server.Name, Transport: getMCPTransport(&server), - SSE: generateMCPStreamableHTTPTransportConfig(&server, SSEField), - StreamableHTTP: generateMCPStreamableHTTPTransportConfig(&server, StreamableHTTPField), + SSE: sse, + StreamableHTTP: streamableHTTP, }) } - return servers + return servers, overall_error } -func generateMCPStreamableHTTPTransportConfig(server *olsv1alpha1.MCPServer, field int) *StreamableHTTPTransportConfig { +func (r *OLSConfigReconciler) generateMCPStreamableHTTPTransportConfig(ctx context.Context, server *olsv1alpha1.MCPServer, field int) (*StreamableHTTPTransportConfig, error) { if server == nil || server.StreamableHTTP == nil { - return nil + return nil, nil } switch field { case SSEField: if !server.StreamableHTTP.EnableSSE { - return nil + return nil, nil } case StreamableHTTPField: if server.StreamableHTTP.EnableSSE { - return nil + return nil, nil } default: - return nil + return nil, nil + } + + // convert headers to paths + headers := make(map[string]string, len(server.StreamableHTTP.Headers)) + for k, v := range server.StreamableHTTP.Headers { + if v == KUBERNETES_PLACEHOLDER { + headers[k] = v + } else { + secret := &corev1.Secret{} + err := r.Get(ctx, client.ObjectKey{Name: v, Namespace: r.Options.Namespace}, secret) + if err != nil { + if apierrors.IsNotFound(err) { + r.logger.Error(err, fmt.Sprint("Header secret ", v, " for MCP server ", server.Name, " is not found")) + return nil, fmt.Errorf("MCP %s header secret %s is not found", server.Name, v) + } + r.logger.Error(err, fmt.Sprint("Failed to get header", v, " for MCP server ", server.Name)) + return nil, fmt.Errorf("failed to get secret %s for MCP provider %s: %w", v, server.Name, err) + } + // make sure the secret has header path + if _, ok := secret.Data[MCPSECRETDATAPATH]; !ok { + r.logger.Error(err, fmt.Sprint("Header", v, " for MCP server ", server.Name, " does not contain 'header' path")) + return nil, fmt.Errorf("header %s for MCP server %s is missing key 'header'", v, server.Name) + } + // update header + headers[k] = path.Join(MCPHeadersMountRoot, v, MCPSECRETDATAPATH) + } } return &StreamableHTTPTransportConfig{ URL: server.StreamableHTTP.URL, Timeout: server.StreamableHTTP.Timeout, SSEReadTimeout: server.StreamableHTTP.SSEReadTimeout, - Headers: server.StreamableHTTP.Headers, - } + Headers: headers, + }, nil } func getMCPTransport(server *olsv1alpha1.MCPServer) MCPTransport { diff --git a/internal/controller/ols_app_server_assets_test.go b/internal/controller/ols_app_server_assets_test.go index ea7482082..ef06c1a41 100644 --- a/internal/controller/ols_app_server_assets_test.go +++ b/internal/controller/ols_app_server_assets_test.go @@ -101,7 +101,7 @@ var _ = Describe("App server assets", func() { }) It("should generate the olsconfig config map", func() { - createTelemetryPullSecret() + createTelemetryPullSecret(true) major, minor, err := GetOpenshiftVersion(k8sClient, ctx) Expect(err).NotTo(HaveOccurred()) @@ -304,12 +304,53 @@ var _ = Describe("App server assets", func() { "URL": Equal(fmt.Sprintf(OpenShiftMCPServerURL, OpenShiftMCPServerPort)), "Timeout": Equal(OpenShiftMCPServerTimeout), "SSEReadTimeout": Equal(OpenShiftMCPServerHTTPReadTimeout), + "Headers": Equal(map[string]string{"kubernetes-authorization": "kubernetes"}), })), }))) }) + It("should fail to generate configmap with additional MCP server if the headers are not configured correctly", func() { + cr.Spec.FeatureGates = []olsv1alpha1.FeatureGate{FeatureGateMCPServer} + createHeaderSecret("garbage", false) + cr.Spec.MCPServers = []olsv1alpha1.MCPServer{ + { + Name: "testMCP", + StreamableHTTP: &olsv1alpha1.MCPServerStreamableHTTPTransport{ + URL: "https://testMCP.com", + Timeout: 10, + SSEReadTimeout: 10, + Headers: map[string]string{ + "header1": "value3", + }, + }, + }, + } + _, err := r.generateOLSConfigMap(context.TODO(), cr) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MCP testMCP header secret value3 is not found")) + + cr.Spec.MCPServers = []olsv1alpha1.MCPServer{ + { + Name: "testMCP", + StreamableHTTP: &olsv1alpha1.MCPServerStreamableHTTPTransport{ + URL: "https://testMCP.com", + Timeout: 10, + SSEReadTimeout: 10, + Headers: map[string]string{ + "header1": "garbage", + }, + }, + }, + } + _, err = r.generateOLSConfigMap(context.TODO(), cr) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("header garbage for MCP server testMCP is missing key 'header'")) + }) + It("should generate configmap with additional MCP server if feature gate is enabled", func() { cr.Spec.FeatureGates = []olsv1alpha1.FeatureGate{FeatureGateMCPServer} + createHeaderSecret("value1", true) + createHeaderSecret("value2", true) cr.Spec.MCPServers = []olsv1alpha1.MCPServer{ { Name: "testMCP", @@ -348,7 +389,7 @@ var _ = Describe("App server assets", func() { Timeout: 10, SSEReadTimeout: 10, Headers: map[string]string{ - "header1": "value1", + "header1": MCPHeadersMountRoot + "/value1/" + MCPSECRETDATAPATH, }, })) Expect(appSrvConfigFile.MCPServers[0].SSE).To(BeNil()) @@ -360,7 +401,7 @@ var _ = Describe("App server assets", func() { Timeout: 10, SSEReadTimeout: 10, Headers: map[string]string{ - "header2": "value2", + "header2": MCPHeadersMountRoot + "/value2/" + MCPSECRETDATAPATH, }, })) Expect(appSrvConfigFile.MCPServers[1].StreamableHTTP).To(BeNil()) @@ -368,6 +409,7 @@ var _ = Describe("App server assets", func() { It("should not generate configmap with additional MCP server if feature gate is missing", func() { Expect(cr.Spec.FeatureGates).To(BeNil()) + createHeaderSecret("value1", true) cr.Spec.MCPServers = []olsv1alpha1.MCPServer{ { Name: "testMCP", @@ -427,10 +469,19 @@ var _ = Describe("App server assets", func() { "URL": Equal("https://testMCP.com"), "Timeout": BeNumerically("==", 10), "SSEReadTimeout": BeNumerically("==", 10), - "Headers": Equal(map[string]string{"header1": "value1"}), + "Headers": Equal(map[string]string{"header1": MCPHeadersMountRoot + "/value1/" + MCPSECRETDATAPATH}), + })), + }))) + Expect(appSrvConfigFile.MCPServers).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("openshift"), + "Transport": Equal(StreamableHTTP), + "StreamableHTTP": PointTo(MatchFields(IgnoreExtras, Fields{ + "URL": Equal(fmt.Sprintf(OpenShiftMCPServerURL, OpenShiftMCPServerPort)), + "Timeout": Equal(OpenShiftMCPServerTimeout), + "SSEReadTimeout": Equal(OpenShiftMCPServerHTTPReadTimeout), + "Headers": Equal(map[string]string{"kubernetes-authorization": "kubernetes"}), })), }))) - }) It("should place APIVersion in ProviderConfig for Azure OpenAI provider", func() { // Configure CR with Azure OpenAI provider including APIVersion @@ -454,7 +505,7 @@ var _ = Describe("App server assets", func() { It("should generate the OLS deployment", func() { By("generate full deployment when telemetry pull secret exists") - createTelemetryPullSecret() + createTelemetryPullSecret(true) dep, err := r.generateOLSDeployment(cr) Expect(err).NotTo(HaveOccurred()) @@ -531,7 +582,7 @@ var _ = Describe("App server assets", func() { Expect(dep.Spec.Template.Spec.Volumes).To(ConsistOf(get8RequiredVolumes())) By("generate deployment without data collector when telemetry pull secret does not contain telemetry token") - createTelemetryPullSecretWithoutTelemetryToken() + createTelemetryPullSecret(false) dep, err = r.generateOLSDeployment(cr) Expect(err).NotTo(HaveOccurred()) @@ -661,7 +712,7 @@ var _ = Describe("App server assets", func() { }) It("should switch data collection on and off as CR defines in .spec.ols_config.user_data_collection", func() { - createTelemetryPullSecret() + createTelemetryPullSecret(true) defer deleteTelemetryPullSecret() By("Switching data collection off") cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{ @@ -896,7 +947,7 @@ var _ = Describe("App server assets", func() { }) It("should generate deployment with MCP server sidecar when introspectionEnabled is true", func() { - createTelemetryPullSecret() + createTelemetryPullSecret(true) defer deleteTelemetryPullSecret() By("Enabling introspection") @@ -943,7 +994,7 @@ var _ = Describe("App server assets", func() { It("should deploy MCP container independently of data collection settings", func() { By("Test case 1: introspection enabled, data collection enabled - should have both MCP and data collector containers") - createTelemetryPullSecret() + createTelemetryPullSecret(true) cr.Spec.OLSConfig.IntrospectionEnabled = true cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{ FeedbackDisabled: false, @@ -1068,7 +1119,7 @@ var _ = Describe("App server assets", func() { It("should generate the olsconfig config map", func() { // todo: this test is not complete // generateOLSConfigMap should return an error if the CR is missing required fields - createTelemetryPullSecret() + createTelemetryPullSecret(true) major, minor, err := GetOpenshiftVersion(k8sClient, ctx) Expect(err).NotTo(HaveOccurred()) cm, err := r.generateOLSConfigMap(context.TODO(), cr) @@ -1128,7 +1179,7 @@ user_data_collector_config: It("should generate the olsconfig config map without user_data_collector_config", func() { // pull-secret without telemetry token should disable data collection // and user_data_collector_config should not be present in the config - createTelemetryPullSecretWithoutTelemetryToken() + createTelemetryPullSecret(false) major, minor, err := GetOpenshiftVersion(k8sClient, ctx) Expect(err).NotTo(HaveOccurred()) cm, err := r.generateOLSConfigMap(context.TODO(), cr) @@ -1203,7 +1254,7 @@ user_data_collector_config: {} It("should generate the OLS deployment", func() { // todo: update this test after updating the test for generateOLSConfigMap - createTelemetryPullSecret() + createTelemetryPullSecret(true) defer deleteTelemetryPullSecret() dep, err := r.generateOLSDeployment(cr) Expect(err).NotTo(HaveOccurred()) @@ -1466,7 +1517,6 @@ user_data_collector_config: {} _, err = r.generateOLSConfigMap(ctx, cr) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to validate additional CA certificate")) - }) }) @@ -1812,7 +1862,8 @@ func addRHELAIProvider(cr *olsv1alpha1.OLSConfig) *olsv1alpha1.OLSConfig { return cr } -func createTelemetryPullSecret() { +func createTelemetryPullSecret(token bool) { + const telemetryToken = ` { "auths": { @@ -1823,25 +1874,8 @@ func createTelemetryPullSecret() { } } ` - pullSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pull-secret", - Namespace: "openshift-config", - }, - Data: map[string][]byte{ - ".dockerconfigjson": []byte(telemetryToken), - }, - } - err := k8sClient.Create(ctx, pullSecret) - // Ignore "already exists" errors since the secret may have been created by another test - if err != nil && !apierrors.IsAlreadyExists(err) { - Expect(err).NotTo(HaveOccurred()) - } -} - -func createTelemetryPullSecretWithoutTelemetryToken() { - const telemetryToken = ` + const telemetryNoToken = ` { "auths": { "other.token": { @@ -1851,14 +1885,22 @@ func createTelemetryPullSecretWithoutTelemetryToken() { } } ` + pullSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pull-secret", Namespace: "openshift-config", }, - Data: map[string][]byte{ + } + + if token { + pullSecret.Data = map[string][]byte{ ".dockerconfigjson": []byte(telemetryToken), - }, + } + } else { + pullSecret.Data = map[string][]byte{ + ".dockerconfigjson": []byte(telemetryNoToken), + } } err := k8sClient.Create(ctx, pullSecret) @@ -1894,6 +1936,32 @@ func createPostgresCacheConfig() PostgresCacheConfig { } } +func createHeaderSecret(name string, header bool) { + + headerSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: OLSNamespaceDefault, + }, + } + + if header { + headerSecret.Data = map[string][]byte{ + MCPSECRETDATAPATH: []byte(name), + } + } else { + headerSecret.Data = map[string][]byte{ + "garbage": []byte(name), + } + } + + err := k8sClient.Create(ctx, headerSecret) + // Ignore "already exists" errors since the secret may have been created by another test + if err != nil && !apierrors.IsAlreadyExists(err) { + Expect(err).NotTo(HaveOccurred()) + } +} + func get7RequiredVolumeMounts() []corev1.VolumeMount { return []corev1.VolumeMount{ { diff --git a/internal/controller/ols_app_server_deployment.go b/internal/controller/ols_app_server_deployment.go index 46933f46f..194fc2ea1 100644 --- a/internal/controller/ols_app_server_deployment.go +++ b/internal/controller/ols_app_server_deployment.go @@ -291,6 +291,29 @@ func (r *OLSConfigReconciler) generateOLSDeployment(cr *olsv1alpha1.OLSConfig) ( }, ) + // mount the volumes and add Volume mounts for the MCP server headers + for _, server := range cr.Spec.MCPServers { + for _, v := range server.StreamableHTTP.Headers { + if v == KUBERNETES_PLACEHOLDER { + continue + } + volumes = append(volumes, corev1.Volume{ + Name: "header-" + v, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: v, + DefaultMode: &volumeDefaultMode, + }, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "header-" + v, + MountPath: path.Join(MCPHeadersMountRoot, v), + ReadOnly: true, + }) + } + } + initContainers := []corev1.Container{} if len(cr.Spec.OLSConfig.RAG) > 0 { ragInitContainers := r.generateRAGInitContainers(cr) diff --git a/internal/controller/ols_app_server_reconciliator_test.go b/internal/controller/ols_app_server_reconciliator_test.go index 3518d5eed..e4ed5a9d7 100644 --- a/internal/controller/ols_app_server_reconciliator_test.go +++ b/internal/controller/ols_app_server_reconciliator_test.go @@ -1135,4 +1135,74 @@ var _ = Describe("App server reconciliator", Ordered, func() { }) + Context("MCP Headers", Ordered, func() { + var volumeDefaultMode = int32(420) + BeforeEach(func() { + By("Set OLSConfig CR to default") + err := k8sClient.Get(ctx, crNamespacedName, cr) + Expect(err).NotTo(HaveOccurred()) + crDefault := getDefaultOLSConfigCR() + cr.Spec = crDefault.Spec + }) + + It("should create additional volumes and volume mounts when MCP headers are defined", func() { + cr.Spec.FeatureGates = []olsv1alpha1.FeatureGate{FeatureGateMCPServer} + cr.Spec.MCPServers = []olsv1alpha1.MCPServer{ + { + Name: "testMCP", + StreamableHTTP: &olsv1alpha1.MCPServerStreamableHTTPTransport{ + URL: "https://testMCP.com", + Timeout: 10, + SSEReadTimeout: 10, + Headers: map[string]string{ + "header1": "value1", + }, + }, + }, + { + Name: "testMCP2", + StreamableHTTP: &olsv1alpha1.MCPServerStreamableHTTPTransport{ + URL: "https://testMCP2.com", + Timeout: 10, + SSEReadTimeout: 10, + Headers: map[string]string{ + "header2": "value2", + }, + EnableSSE: true, + }, + }, + } + deployment, err := reconciler.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(deployment.Spec.Template.Spec.Volumes).To(ContainElement(corev1.Volume{ + Name: "header-value1", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "value1", + DefaultMode: &volumeDefaultMode, + }, + }, + })) + Expect(deployment.Spec.Template.Spec.Volumes).To(ContainElement(corev1.Volume{ + Name: "header-value2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "value2", + DefaultMode: &volumeDefaultMode, + }, + }, + })) + Expect(deployment.Spec.Template.Spec.Containers[0].VolumeMounts).To(ContainElement(corev1.VolumeMount{ + Name: "header-value1", + MountPath: "/etc/mcp/headers/value1", + ReadOnly: true, + })) + Expect(deployment.Spec.Template.Spec.Containers[0].VolumeMounts).To(ContainElement(corev1.VolumeMount{ + Name: "header-value2", + MountPath: "/etc/mcp/headers/value2", + ReadOnly: true, + })) + }) + }) + })