Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ func main() {
LightspeedServiceImage: imagesMap["lightspeed-service"],
LightspeedServicePostgresImage: imagesMap["postgres-image"],
OpenShiftMCPServerImage: imagesMap["openshift-mcp-server-image"],
DataverseExporterImage: imagesMap["dataverse-exporter-image"],
Namespace: namespace,
ReconcileInterval: time.Duration(reconcilerIntervalMinutes) * time.Minute, // #nosec G115
},
Expand Down
16 changes: 14 additions & 2 deletions internal/controller/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,6 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt'
MetricsReaderServiceAccountName = "lightspeed-operator-metrics-reader"
// MCP server image
OpenShiftMCPServerImageDefault = "quay.io/redhat-user-workloads/crt-nshift-lightspeed-tenant/openshift-mcp-server@sha256:3a035744b772104c6c592acf8a813daced19362667ed6dab73a00d17eb9c3a43"
// Dataverse exporter image
DataverseExporterImageDefault = "quay.io/redhat-user-workloads/crt-nshift-lightspeed-tenant/lightspeed-to-dataverse-exporter@sha256:ccb6705a5e7ff0c4d371dc72dc8cf319574a2d64bcc0a89ccc7130f626656722"
// MCP server URL
OpenShiftMCPServerURL = "http://localhost:%d/mcp"
// MCP server port
Expand All @@ -287,4 +285,18 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt'
MCPSECRETDATAPATH = "header"
// OCP RAG image
OcpRagImageDefault = "quay.io/redhat-user-workloads/crt-nshift-lightspeed-tenant/lightspeed-rag-content-lsc@sha256:edf031376f6ad3a06d3ad1b2e3b06ed6139a03f5c32f01ffee012240e9169639"

/*** Data Exporter Constants ***/
// Dataverse exporter image
DataverseExporterImageDefault = "quay.io/redhat-user-workloads/crt-nshift-lightspeed-tenant/lightspeed-to-dataverse-exporter@sha256:ccb6705a5e7ff0c4d371dc72dc8cf319574a2d64bcc0a89ccc7130f626656722"
// ExporterConfigCmName is the name of the exporter configmap
ExporterConfigCmName = "lightspeed-exporter-config"
// ExporterConfigVolumeName is the name of the volume for exporter configuration
ExporterConfigVolumeName = "exporter-config"
// ExporterConfigMountPath is the path where exporter config is mounted
ExporterConfigMountPath = "/etc/config"
// ExporterConfigFilename is the name of the exporter configuration file
ExporterConfigFilename = "config.yaml"
// OLSUserDataMountPath is the path where user data is mounted in the app server container
OLSUserDataMountPath = "/app-root/ols-user-data"
)
30 changes: 30 additions & 0 deletions internal/controller/ols_app_server_assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,36 @@ func (r *OLSConfigReconciler) generateOLSConfigMap(ctx context.Context, cr *olsv
return &cm, nil
}

func (r *OLSConfigReconciler) generateExporterConfigMap(cr *olsv1alpha1.OLSConfig) (*corev1.ConfigMap, error) {
// Collection interval is set to 300 seconds in production (5 minutes)
exporterConfigContent := `service_id: "ols"
ingress_server_url: "https://console.redhat.com/api/ingress/v1/upload"
allowed_subdirs:
- feedback
- transcripts
# Collection settings
collection_interval: 300
cleanup_after_send: true
ingress_connection_timeout: 30`

cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: ExporterConfigCmName,
Namespace: r.Options.Namespace,
Labels: generateAppServerSelectorLabels(),
},
Data: map[string]string{
ExporterConfigFilename: exporterConfigContent,
},
}

if err := controllerutil.SetControllerReference(cr, &cm, r.Scheme); err != nil {
return nil, err
}

return &cm, nil
}

func (r *OLSConfigReconciler) addAdditionalCAFileNames(ctx context.Context, cr *corev1.LocalObjectReference, certDirectory string) ([]string, error) {
// get data from the referenced configmap
cm := &corev1.ConfigMap{}
Expand Down
112 changes: 79 additions & 33 deletions internal/controller/ols_app_server_assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var _ = Describe("App server assets", func() {
OpenshiftMinor: "456",
LightspeedServiceImage: "lightspeed-service:latest",
OpenShiftMCPServerImage: "openshift-mcp-server:latest",
DataverseExporterImage: "dataverse-exporter:latest",
Namespace: OLSNamespaceDefault,
}
cr = getDefaultOLSConfigCR()
Expand Down Expand Up @@ -528,30 +529,33 @@ var _ = Describe("App server assets", func() {
Value: path.Join("/etc/ols", OLSConfigFilename),
},
}))
Expect(dep.Spec.Template.Spec.Containers[0].VolumeMounts).To(ConsistOf(get9RequiredVolumeMounts()))
Expect(dep.Spec.Template.Spec.Containers[0].VolumeMounts).To(ConsistOf(get10RequiredVolumeMounts()))
Expect(dep.Spec.Template.Spec.Containers[0].Resources).To(Equal(corev1.ResourceRequirements{
Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("4Gi")},
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("1Gi")},
Claims: []corev1.ResourceClaim{},
}))
// telemetry container
Expect(dep.Spec.Template.Spec.Containers[1].Image).To(Equal(rOptions.LightspeedServiceImage))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-service-user-data-collector"))
// dataverse exporter container
Expect(dep.Spec.Template.Spec.Containers[1].Image).To(Equal(rOptions.DataverseExporterImage))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter"))
Expect(dep.Spec.Template.Spec.Containers[1].Resources).ToNot(BeNil())
Expect(dep.Spec.Template.Spec.Containers[1].Command).To(Equal([]string{"python3.11", "/app-root/ols/user_data_collection/data_collector.py"}))
Expect(dep.Spec.Template.Spec.Containers[1].Env).To(Equal([]corev1.EnvVar{
{
Name: "OLS_CONFIG_FILE",
Value: path.Join("/etc/ols", OLSConfigFilename),
},
Expect(dep.Spec.Template.Spec.Containers[1].Args).To(Equal([]string{
"--mode",
"openshift",
"--config",
path.Join(ExporterConfigMountPath, ExporterConfigFilename),
"--log-level",
"INFO",
"--data-dir",
OLSUserDataMountPath,
}))
Expect(dep.Spec.Template.Spec.Containers[1].VolumeMounts).To(ConsistOf(get9RequiredVolumeMounts()))
Expect(dep.Spec.Template.Spec.Containers[1].VolumeMounts).To(ConsistOf(get10RequiredVolumeMounts()))
Expect(dep.Spec.Template.Spec.Containers[1].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{},
}))
Expect(dep.Spec.Template.Spec.Volumes).To(ConsistOf(get9RequiredVolumes()))
Expect(dep.Spec.Template.Spec.Volumes).To(ConsistOf(get10RequiredVolumes()))
Expect(dep.Spec.Selector.MatchLabels).To(Equal(generateAppServerSelectorLabels()))

By("generate deployment without data collector when telemetry pull secret does not exist")
Expand Down Expand Up @@ -754,17 +758,20 @@ var _ = Describe("App server assets", func() {
deployment, err = r.generateOLSDeployment(cr)
Expect(err).NotTo(HaveOccurred())
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(2))
Expect(deployment.Spec.Template.Spec.Containers[1].Image).To(Equal(rOptions.LightspeedServiceImage))
Expect(deployment.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-service-user-data-collector"))
Expect(deployment.Spec.Template.Spec.Containers[1].Image).To(Equal(rOptions.DataverseExporterImage))
Expect(deployment.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter"))
Expect(deployment.Spec.Template.Spec.Containers[1].Resources).ToNot(BeNil())
Expect(deployment.Spec.Template.Spec.Containers[1].Command).To(Equal([]string{"python3.11", "/app-root/ols/user_data_collection/data_collector.py"}))
Expect(deployment.Spec.Template.Spec.Containers[1].Env).To(Equal([]corev1.EnvVar{
{
Name: "OLS_CONFIG_FILE",
Value: path.Join("/etc/ols", OLSConfigFilename),
},
Expect(deployment.Spec.Template.Spec.Containers[1].Args).To(Equal([]string{
"--mode",
"openshift",
"--config",
path.Join(ExporterConfigMountPath, ExporterConfigFilename),
"--log-level",
"INFO",
"--data-dir",
OLSUserDataMountPath,
}))
Expect(deployment.Spec.Template.Spec.Containers[1].VolumeMounts).To(ConsistOf(get9RequiredVolumeMounts()))
Expect(deployment.Spec.Template.Spec.Containers[1].VolumeMounts).To(ConsistOf(get10RequiredVolumeMounts()))
Expect(deployment.Spec.Template.Spec.Volumes).To(ContainElement(
corev1.Volume{
Name: "ols-user-data",
Expand Down Expand Up @@ -978,22 +985,22 @@ var _ = Describe("App server assets", func() {
}))

// Verify MCP server has the same volume mounts as other containers
Expect(openshiftMCPServerContainer.VolumeMounts).To(ConsistOf(get9RequiredVolumeMounts()))
Expect(openshiftMCPServerContainer.VolumeMounts).To(ConsistOf(get10RequiredVolumeMounts()))

By("Disabling introspection")
cr.Spec.OLSConfig.IntrospectionEnabled = false

dep, err = r.generateOLSDeployment(cr)
Expect(err).NotTo(HaveOccurred())

// Should have only 2 containers: main app and telemetry (no MCP server)
// Should have only 2 containers: main app and dataverse exporter (no MCP server)
Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2))
Expect(dep.Spec.Template.Spec.Containers[0].Name).To(Equal("lightspeed-service-api"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-service-user-data-collector"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter"))
})

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")
By("Test case 1: introspection enabled, data collection enabled - should have both MCP and dataverse exporter containers")
createTelemetryPullSecret(true)
cr.Spec.OLSConfig.IntrospectionEnabled = true
cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{
Expand All @@ -1005,10 +1012,10 @@ var _ = Describe("App server assets", func() {
Expect(err).NotTo(HaveOccurred())
Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(3))
Expect(dep.Spec.Template.Spec.Containers[0].Name).To(Equal("lightspeed-service-api"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-service-user-data-collector"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter"))
Expect(dep.Spec.Template.Spec.Containers[2].Name).To(Equal("openshift-mcp-server"))

By("Test case 2: introspection enabled, data collection disabled - should have only MCP container (no data collector)")
By("Test case 2: introspection enabled, data collection disabled - should have only MCP container (no dataverse exporter)")
cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{
FeedbackDisabled: true,
TranscriptsDisabled: true,
Expand All @@ -1020,7 +1027,7 @@ var _ = Describe("App server assets", func() {
Expect(dep.Spec.Template.Spec.Containers[0].Name).To(Equal("lightspeed-service-api"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("openshift-mcp-server"))

By("Test case 3: introspection disabled, data collection enabled - should have only data collector container (no MCP)")
By("Test case 3: introspection disabled, data collection enabled - should have only dataverse exporter container (no MCP)")
cr.Spec.OLSConfig.IntrospectionEnabled = false
cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{
FeedbackDisabled: false,
Expand All @@ -1031,7 +1038,7 @@ var _ = Describe("App server assets", func() {
Expect(err).NotTo(HaveOccurred())
Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2))
Expect(dep.Spec.Template.Spec.Containers[0].Name).To(Equal("lightspeed-service-api"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-service-user-data-collector"))
Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter"))

By("Test case 4: introspection disabled, data collection disabled - should have only main container")
cr.Spec.OLSConfig.IntrospectionEnabled = false
Expand Down Expand Up @@ -1079,10 +1086,12 @@ var _ = Describe("App server assets", func() {
BeforeEach(func() {
cr = getEmptyOLSConfigCR()
rOptions = &OLSConfigReconcilerOptions{
OpenShiftMajor: "123",
OpenshiftMinor: "456",
LightspeedServiceImage: "lightspeed-service:latest",
Namespace: OLSNamespaceDefault,
OpenShiftMajor: "123",
OpenshiftMinor: "456",
LightspeedServiceImage: "lightspeed-service:latest",
OpenShiftMCPServerImage: "openshift-mcp-server:latest",
DataverseExporterImage: "dataverse-exporter:latest",
Namespace: OLSNamespaceDefault,
}
r = &OLSConfigReconciler{
Options: *rOptions,
Expand Down Expand Up @@ -1281,6 +1290,11 @@ user_data_collector_config: {}
Name: "ols-user-data",
ReadOnly: false,
MountPath: "/app-root/ols-user-data",
},
corev1.VolumeMount{
Name: ExporterConfigVolumeName,
ReadOnly: true,
MountPath: ExporterConfigMountPath,
}),
))
Expect(dep.Spec.Template.Spec.Volumes).To(ConsistOf(
Expand All @@ -1290,6 +1304,15 @@ user_data_collector_config: {}
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
corev1.Volume{
Name: ExporterConfigVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: ExporterConfigCmName},
DefaultMode: &defaultVolumeMode,
},
},
}),
))
Expect(dep.Spec.Selector.MatchLabels).To(Equal(generateAppServerSelectorLabels()))
Expand Down Expand Up @@ -2019,6 +2042,15 @@ func get9RequiredVolumeMounts() []corev1.VolumeMount {
})
}

func get10RequiredVolumeMounts() []corev1.VolumeMount {
return append(get9RequiredVolumeMounts(),
corev1.VolumeMount{
Name: ExporterConfigVolumeName,
ReadOnly: true,
MountPath: ExporterConfigMountPath,
})
}

func get7RequiredVolumes() []corev1.Volume {

return []corev1.Volume{
Expand Down Expand Up @@ -2103,3 +2135,17 @@ func get9RequiredVolumes() []corev1.Volume {
},
})
}

func get10RequiredVolumes() []corev1.Volume {
defaultVolumeMode := int32(420)
return append(get9RequiredVolumes(),
corev1.Volume{
Name: ExporterConfigVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: ExporterConfigCmName},
DefaultMode: &defaultVolumeMode,
},
},
})
}
51 changes: 38 additions & 13 deletions internal/controller/ols_app_server_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ func (r *OLSConfigReconciler) generateOLSDeployment(cr *olsv1alpha1.OLSConfig) (
const OLSConfigMountPath = "/etc/ols"
const OLSConfigVolumeName = "cm-olsconfig"
const OLSUserDataVolumeName = "ols-user-data"
const OLSUserDataMountPath = "/app-root/ols-user-data"

revisionHistoryLimit := int32(1)
volumeDefaultMode := int32(420)
Expand Down Expand Up @@ -152,6 +151,20 @@ func (r *OLSConfigReconciler) generateOLSDeployment(cr *olsv1alpha1.OLSConfig) (
},
}
volumes = append(volumes, olsUserDataVolume)

// Add exporter config volume
exporterConfigVolume := corev1.Volume{
Name: ExporterConfigVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: ExporterConfigCmName,
},
DefaultMode: &volumeDefaultMode,
},
},
}
volumes = append(volumes, exporterConfigVolume)
}

// Mount "kube-root-ca.crt" configmap
Expand Down Expand Up @@ -243,8 +256,14 @@ func (r *OLSConfigReconciler) generateOLSDeployment(cr *olsv1alpha1.OLSConfig) (
Name: OLSUserDataVolumeName,
MountPath: OLSUserDataMountPath,
}
exporterConfigVolumeMount := corev1.VolumeMount{
Name: ExporterConfigVolumeName,
MountPath: ExporterConfigMountPath,
ReadOnly: true,
}

if dataCollectorEnabled {
volumeMounts = append(volumeMounts, olsUserDataVolumeMount)
volumeMounts = append(volumeMounts, olsUserDataVolumeMount, exporterConfigVolumeMount)
}

// Volumemount OpenShift certificates configmap
Expand Down Expand Up @@ -410,26 +429,32 @@ func (r *OLSConfigReconciler) generateOLSDeployment(cr *olsv1alpha1.OLSConfig) (
// 2. MCP server container (if enabled)

if dataCollectorEnabled {
// Add telemetry container
telemetryContainer := corev1.Container{
Name: "lightspeed-service-user-data-collector",
Image: r.Options.LightspeedServiceImage,
// Add data exporter container
exporterContainer := corev1.Container{
Name: "lightspeed-to-dataverse-exporter",
Image: r.Options.DataverseExporterImage,
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &[]bool{false}[0],
ReadOnlyRootFilesystem: &[]bool{true}[0],
},
VolumeMounts: volumeMounts,
Env: []corev1.EnvVar{
{
Name: "OLS_CONFIG_FILE",
Value: path.Join(OLSConfigMountPath, OLSConfigFilename),
},
// running in openshift mode ensures that cluster_id is set
// as identity_id
// make logging configurable via config: OLS-2191
Args: []string{
"--mode",
"openshift",
"--config",
path.Join(ExporterConfigMountPath, ExporterConfigFilename),
"--log-level",
"INFO",
"--data-dir",
OLSUserDataMountPath,
},
Command: []string{"python3.11", "/app-root/ols/user_data_collection/data_collector.py"},
Resources: *data_collector_resources,
}
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, telemetryContainer)
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, exporterContainer)
}

// Add OpenShift MCP server sidecar container if introspection is enabled
Expand Down
Loading