diff --git a/cmd/main.go b/cmd/main.go index 657f9788e..f2941c5b5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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 }, diff --git a/internal/controller/constants.go b/internal/controller/constants.go index e5f59059f..c1e03f4d2 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -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 @@ -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" ) diff --git a/internal/controller/ols_app_server_assets.go b/internal/controller/ols_app_server_assets.go index 88a6b6c3f..abc63dc87 100644 --- a/internal/controller/ols_app_server_assets.go +++ b/internal/controller/ols_app_server_assets.go @@ -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{} diff --git a/internal/controller/ols_app_server_assets_test.go b/internal/controller/ols_app_server_assets_test.go index ef06c1a41..b34384a6f 100644 --- a/internal/controller/ols_app_server_assets_test.go +++ b/internal/controller/ols_app_server_assets_test.go @@ -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() @@ -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") @@ -611,6 +615,70 @@ var _ = Describe("App server assets", func() { deleteTelemetryPullSecret() }) + It("should use configured log level for data collector container", func() { + createTelemetryPullSecret(true) + + By("using default INFO log level when not specified") + cr.Spec.OLSDataCollectorConfig = olsv1alpha1.OLSDataCollectorSpec{} + dep, err := r.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2)) + // data collector container should be the second container + Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter")) + Expect(dep.Spec.Template.Spec.Containers[1].Args).To(ContainElement("INFO")) + + By("using DEBUG log level when configured") + cr.Spec.OLSDataCollectorConfig = olsv1alpha1.OLSDataCollectorSpec{ + LogLevel: "DEBUG", + } + dep, err = r.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2)) + Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter")) + Expect(dep.Spec.Template.Spec.Containers[1].Args).To(Equal([]string{ + "--mode", + "openshift", + "--config", + path.Join(ExporterConfigMountPath, ExporterConfigFilename), + "--log-level", + "DEBUG", + "--data-dir", + OLSUserDataMountPath, + })) + + By("using WARNING log level when configured") + cr.Spec.OLSDataCollectorConfig = olsv1alpha1.OLSDataCollectorSpec{ + LogLevel: "WARNING", + } + dep, err = r.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2)) + Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter")) + Expect(dep.Spec.Template.Spec.Containers[1].Args).To(ContainElement("WARNING")) + + By("using ERROR log level when configured") + cr.Spec.OLSDataCollectorConfig = olsv1alpha1.OLSDataCollectorSpec{ + LogLevel: "ERROR", + } + dep, err = r.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2)) + Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter")) + Expect(dep.Spec.Template.Spec.Containers[1].Args).To(ContainElement("ERROR")) + + By("using CRITICAL log level when configured") + cr.Spec.OLSDataCollectorConfig = olsv1alpha1.OLSDataCollectorSpec{ + LogLevel: "CRITICAL", + } + dep, err = r.generateOLSDeployment(cr) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(2)) + Expect(dep.Spec.Template.Spec.Containers[1].Name).To(Equal("lightspeed-to-dataverse-exporter")) + Expect(dep.Spec.Template.Spec.Containers[1].Args).To(ContainElement("CRITICAL")) + + deleteTelemetryPullSecret() + }) + It("should generate the OLS service", func() { service, err := r.generateService(cr) Expect(err).NotTo(HaveOccurred()) @@ -754,17 +822,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", @@ -978,7 +1049,7 @@ 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 @@ -986,14 +1057,14 @@ var _ = Describe("App server assets", func() { 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{ @@ -1005,10 +1076,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, @@ -1020,7 +1091,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, @@ -1031,7 +1102,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 @@ -1079,10 +1150,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, @@ -1281,6 +1354,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( @@ -1290,6 +1368,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())) @@ -2019,6 +2106,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{ @@ -2103,3 +2199,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, + }, + }, + }) +} diff --git a/internal/controller/ols_app_server_deployment.go b/internal/controller/ols_app_server_deployment.go index 194fc2ea1..8790ba39b 100644 --- a/internal/controller/ols_app_server_deployment.go +++ b/internal/controller/ols_app_server_deployment.go @@ -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) @@ -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 @@ -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 @@ -410,26 +429,35 @@ 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 + logLevel := cr.Spec.OLSDataCollectorConfig.LogLevel + if logLevel == "" { + logLevel = "INFO" + } + 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 + Args: []string{ + "--mode", + "openshift", + "--config", + path.Join(ExporterConfigMountPath, ExporterConfigFilename), + "--log-level", + logLevel, + "--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 diff --git a/internal/controller/ols_app_server_reconciliator.go b/internal/controller/ols_app_server_reconciliator.go index 77e0795ff..b2d3d9ceb 100644 --- a/internal/controller/ols_app_server_reconciliator.go +++ b/internal/controller/ols_app_server_reconciliator.go @@ -38,6 +38,10 @@ func (r *OLSConfigReconciler) reconcileAppServer(ctx context.Context, olsconfig Name: "reconcile OLSConfigMap", Task: r.reconcileOLSConfigMap, }, + { + Name: "reconcile ExporterConfigMap", + Task: r.reconcileExporterConfigMap, + }, { Name: "reconcile Additional CA ConfigMap", Task: r.reconcileOLSAdditionalCAConfigMap, @@ -138,6 +142,60 @@ func (r *OLSConfigReconciler) reconcileOLSConfigMap(ctx context.Context, cr *ols return nil } +func (r *OLSConfigReconciler) reconcileExporterConfigMap(ctx context.Context, cr *olsv1alpha1.OLSConfig) error { + // Only create exporter configmap if data collector is enabled + dataCollectorEnabled, err := r.dataCollectorEnabled(cr) + if err != nil { + return err + } + + if !dataCollectorEnabled { + // Attempt to delete exporter configmap if it exists + foundCm := &corev1.ConfigMap{} + err := r.Client.Get(ctx, client.ObjectKey{Name: ExporterConfigCmName, Namespace: r.Options.Namespace}, foundCm) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to get exporter configmap: %w", err) + } + if err == nil { + if delErr := r.Delete(ctx, foundCm); delErr != nil && !errors.IsNotFound(delErr) { + return fmt.Errorf("failed to delete exporter configmap: %w", delErr) + } + r.logger.Info("Data collector not enabled, exporter configmap deleted", "configmap", foundCm.Name) + } else { + r.logger.Info("Data collector not enabled, exporter configmap does not exist") + } + return nil + } + + cm, err := r.generateExporterConfigMap(cr) + if err != nil { + return fmt.Errorf("failed to generate exporter configmap: %w", err) + } + + foundCm := &corev1.ConfigMap{} + err = r.Client.Get(ctx, client.ObjectKey{Name: ExporterConfigCmName, Namespace: r.Options.Namespace}, foundCm) + if err != nil && errors.IsNotFound(err) { + r.logger.Info("creating a new exporter configmap", "configmap", cm.Name) + err = r.Create(ctx, cm) + if err != nil { + return fmt.Errorf("failed to create exporter configmap: %w", err) + } + return nil + } else if err != nil { + return fmt.Errorf("failed to get exporter configmap: %w", err) + } + + // Update existing configmap + foundCm.Data = cm.Data + err = r.Update(ctx, foundCm) + if err != nil { + return fmt.Errorf("failed to update exporter configmap: %w", err) + } + + r.logger.Info("Exporter configmap reconciled", "configmap", cm.Name) + return nil +} + func (r *OLSConfigReconciler) reconcileOLSAdditionalCAConfigMap(ctx context.Context, cr *olsv1alpha1.OLSConfig) error { if cr.Spec.OLSConfig.AdditionalCAConfigMapRef == nil { // no additional CA certs, skip diff --git a/internal/controller/ols_app_server_reconciliator_test.go b/internal/controller/ols_app_server_reconciliator_test.go index e4ed5a9d7..becba5799 100644 --- a/internal/controller/ols_app_server_reconciliator_test.go +++ b/internal/controller/ols_app_server_reconciliator_test.go @@ -451,6 +451,46 @@ var _ = Describe("App server reconciliator", Ordered, func() { Expect(err).NotTo(HaveOccurred()) }) + It("should create exporter configmap when data collector is enabled", func() { + By("Enable telemetry via pull secret and reconcile") + // Ensure exporter container has a valid image when enabled + reconciler.Options.DataverseExporterImage = DataverseExporterImageDefault + reconciler.Options.OpenShiftMCPServerImage = OpenShiftMCPServerImageDefault + createTelemetryPullSecret(true) + defer deleteTelemetryPullSecret() + err := reconciler.reconcileAppServer(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Verify exporter configmap exists") + cm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: ExporterConfigCmName, Namespace: OLSNamespaceDefault}, cm) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should delete exporter configmap when data collector is disabled", func() { + By("Ensure exporter configmap exists by enabling telemetry and reconciling") + // Ensure exporter container has a valid image when enabled + reconciler.Options.DataverseExporterImage = DataverseExporterImageDefault + reconciler.Options.OpenShiftMCPServerImage = OpenShiftMCPServerImageDefault + createTelemetryPullSecret(true) + err := reconciler.reconcileAppServer(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Verify exporter configmap exists") + cm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: ExporterConfigCmName, Namespace: OLSNamespaceDefault}, cm) + Expect(err).NotTo(HaveOccurred()) + + By("Disable telemetry and reconcile to trigger deletion") + deleteTelemetryPullSecret() + err = reconciler.reconcileAppServer(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Verify exporter configmap has been deleted") + err = k8sClient.Get(ctx, types.NamespacedName{Name: ExporterConfigCmName, Namespace: OLSNamespaceDefault}, &corev1.ConfigMap{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + It("should return error when the LLM provider token secret does not have required keys", func() { By("General provider: the token secret miss 'apitoken' key") secret, _ := generateRandomSecret() diff --git a/internal/controller/olsconfig_controller.go b/internal/controller/olsconfig_controller.go index 764ec4566..237a99b05 100644 --- a/internal/controller/olsconfig_controller.go +++ b/internal/controller/olsconfig_controller.go @@ -67,6 +67,7 @@ type OLSConfigReconcilerOptions struct { LightspeedServicePostgresImage string ConsoleUIImage string OpenShiftMCPServerImage string + DataverseExporterImage string Namespace string ReconcileInterval time.Duration }