From 4c72b193c185412769a586035dbe8d2e57b99e55 Mon Sep 17 00:00:00 2001 From: blublinsky Date: Thu, 29 Jan 2026 12:39:56 +0000 Subject: [PATCH] adding support for library mode lcore deployment --- ...tspeed-operator.clusterserviceversion.yaml | 5 +- cmd/main.go | 10 + config/manager/manager.yaml | 1 + internal/controller/lcore/config.go | 23 +- internal/controller/lcore/deployment.go | 703 ++++++++++++------ internal/controller/lcore/deployment_test.go | 188 ++++- internal/controller/lcore/suite_test.go | 1 + internal/controller/olsconfig_helpers.go | 4 + internal/controller/reconciler/interface.go | 3 + internal/controller/utils/constants.go | 6 +- internal/controller/utils/testing.go | 9 + internal/controller/utils/types.go | 1 + 12 files changed, 727 insertions(+), 227 deletions(-) diff --git a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml index 6f480fe1e..47f8d5212 100644 --- a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml +++ b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml @@ -36,9 +36,9 @@ metadata: } } ] - capabilities: Basic Install + capabilities: Seamless Upgrades console.openshift.io/operator-monitoring-default: "true" - createdAt: "2025-12-18T10:30:55Z" + createdAt: "2026-01-28T20:15:15Z" features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" features.operators.openshift.io/csi: "false" @@ -606,6 +606,7 @@ spec: - --cert-dir=/etc/tls/private - --lcore-image=quay.io/lightspeed-core/lightspeed-stack:dev-latest - --use-lcore=false + - --lcore-server=true - --service-image=quay.io/openshift-lightspeed/lightspeed-service-api:latest - --console-image=quay.io/openshift-lightspeed/lightspeed-console-plugin:latest - --console-image-pf5=quay.io/openshift-lightspeed/lightspeed-console-plugin-pf5:latest diff --git a/cmd/main.go b/cmd/main.go index d8c20d6f5..7edd563b2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -177,6 +177,7 @@ func main() { var dataverseExporterImage string var ocpRagImage string var useLCore bool + var lcoreServerMode bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -197,6 +198,7 @@ func main() { flag.StringVar(&dataverseExporterImage, "dataverse-exporter-image", utils.DataverseExporterImageDefault, "The image of the dataverse exporter container.") flag.StringVar(&ocpRagImage, "ocp-rag-image", utils.OcpRagImageDefault, "The image with the OCP RAG databases.") flag.BoolVar(&useLCore, "use-lcore", false, "Use LCore instead of AppServer for the application server deployment.") + flag.BoolVar(&lcoreServerMode, "lcore-server", true, "Use LCore in a server mode.") opts := zap.Options{ Development: true, } @@ -219,6 +221,13 @@ func main() { } setupLog.Info("========================================") setupLog.Info(">>> BACKEND CONFIGURATION <<<", "backendType", backendType) + if useLCore { + deploymentMode := "server" + if !lcoreServerMode { + deploymentMode = "library" + } + setupLog.Info(">>> LCORE DEPLOYMENT MODE <<<", "mode", deploymentMode) + } setupLog.Info("========================================") setupLog.Info("Starting the operator", "metricsAddr", metricsAddr, "probeAddr", probeAddr, "certDir", certDir, "certName", certName, "keyName", keyName, "namespace", namespace) @@ -430,6 +439,7 @@ func main() { DataverseExporterImage: imagesMap["dataverse-exporter-image"], LightspeedCoreImage: imagesMap["lightspeed-core"], UseLCore: useLCore, + LCoreServerMode: lcoreServerMode, Namespace: namespace, PrometheusAvailable: prometheusAvailable, }, diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 3bee5526d..313c073c5 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -73,6 +73,7 @@ spec: - "--cert-dir=/etc/tls/private" - "--lcore-image=quay.io/lightspeed-core/lightspeed-stack:dev-latest" - "--use-lcore=false" + - "--lcore-server=true" image: quay.io/openshift-lightspeed/lightspeed-operator:latest imagePullPolicy: Always name: manager diff --git a/internal/controller/lcore/config.go b/internal/controller/lcore/config.go index 7f1086087..c4ac6e0eb 100644 --- a/internal/controller/lcore/config.go +++ b/internal/controller/lcore/config.go @@ -681,7 +681,7 @@ func buildLlamaStackYAML(r reconciler.Reconciler, ctx context.Context, cr *olsv1 // LCore Config component builder functions (return maps for maintainability) // ============================================================================ -func buildLCoreServiceConfig(_ reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) map[string]interface{} { +func buildLCoreServiceConfig(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) map[string]interface{} { // Map LogLevel from OLSConfig // Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL // Default to info if not specified @@ -693,7 +693,7 @@ func buildLCoreServiceConfig(_ reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) // color_log: enable colored logs for DEBUG, disable for production (INFO+) colorLog := logLevel == olsv1alpha1.LogLevelDebug - return map[string]interface{}{ + serviceConfig := map[string]interface{}{ "host": "0.0.0.0", "port": utils.OLSAppServerContainerPort, "auth_enabled": false, @@ -707,14 +707,27 @@ func buildLCoreServiceConfig(_ reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) "tls_key_path": "/etc/certs/lightspeed-tls/tls.key", }, } + + return serviceConfig } -func buildLCoreLlamaStackConfig(_ reconciler.Reconciler, _ *olsv1alpha1.OLSConfig) map[string]interface{} { - return map[string]interface{}{ - "use_as_library_client": false, +func buildLCoreLlamaStackConfig(r reconciler.Reconciler, _ *olsv1alpha1.OLSConfig) map[string]interface{} { + // Server mode: llama-stack runs as a separate service (container) + // Library mode: llama-stack runs as an embedded library + isLibraryMode := r != nil && !r.GetLCoreServerMode() + + llamaStackConfig := map[string]interface{}{ + "use_as_library_client": isLibraryMode, "url": "http://localhost:8321", "api_key": "xyzzy", } + + // In library mode, add path to llama-stack config file + if isLibraryMode { + llamaStackConfig["library_client_config_path"] = utils.LlamaStackConfigMountPath + } + + return llamaStackConfig } func buildLCoreUserDataCollectionConfig(_ reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) map[string]interface{} { diff --git a/internal/controller/lcore/deployment.go b/internal/controller/lcore/deployment.go index cebe34b71..09a860658 100644 --- a/internal/controller/lcore/deployment.go +++ b/internal/controller/lcore/deployment.go @@ -80,6 +80,35 @@ func getOLSMCPServerResources(cr *olsv1alpha1.OLSConfig) *corev1.ResourceRequire ) } +// addOpenShiftMCPServerSidecar adds the OpenShift MCP server sidecar container to the deployment +// if introspection is enabled in the CR. This modifies the deployment in place. +func addOpenShiftMCPServerSidecar(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig, deployment *appsv1.Deployment) { + if !cr.Spec.OLSConfig.IntrospectionEnabled { + return + } + + openshiftMCPServerContainer := corev1.Container{ + Name: utils.OpenShiftMCPServerContainerName, + Image: r.GetOpenShiftMCPServerImage(), + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{false}[0], + ReadOnlyRootFilesystem: &[]bool{true}[0], + }, + Command: []string{ + "/openshift-mcp-server", + "--read-only", + "--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort), + }, + Resources: *getOLSMCPServerResources(cr), + } + + deployment.Spec.Template.Spec.Containers = append( + deployment.Spec.Template.Spec.Containers, + openshiftMCPServerContainer, + ) +} + // buildLlamaStackEnvVars builds environment variables for all LLM providers // For Azure providers, it reads the secret to support both API key and client credentials func buildLlamaStackEnvVars(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) ([]corev1.EnvVar, error) { @@ -262,148 +291,384 @@ func validateMCPHeaderSecret(r reconciler.Reconciler, ctx context.Context, secre return nil } -// GenerateLCoreDeployment generates the Deployment for LCore (llama-stack + lightspeed-stack) -func GenerateLCoreDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*appsv1.Deployment, error) { - ctx := context.Background() - revisionHistoryLimit := int32(1) - volumeDefaultMode := utils.VolumeDefaultMode - - llamaStackResources := getLlamaStackResources(cr) - lightspeedStackResources := getLightspeedStackResources(cr) +// ============================================================================ +// Helper functions for building common deployment components +// ============================================================================ - // Get ResourceVersions for tracking - these resources should already exist - // If they don't exist, we'll get empty strings which is fine for initial creation - lcoreConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LCoreConfigCmName) - llamaStackConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LlamaStackConfigCmName) - - // Labels for the deployment - labels := map[string]string{ +// buildCommonLabels returns the standard labels for LCore deployments +func buildCommonLabels() map[string]string { + return map[string]string{ "app": "lightspeed-stack", "app.kubernetes.io/component": "application-server", "app.kubernetes.io/managed-by": "lightspeed-operator", "app.kubernetes.io/name": "lightspeed-service-api", "app.kubernetes.io/part-of": "openshift-lightspeed", } +} - // Define volumes - volumes := []corev1.Volume{ - { - Name: "llama-stack-config", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: utils.LlamaStackConfigCmName, - }, - DefaultMode: &volumeDefaultMode, +// buildConfigVolumes creates the base config volumes for LCore (both server and library modes need both configs) +// buildLCoreConfigVolumeAndMount creates both the volume and volume mount for lightspeed-stack config +func buildLCoreConfigVolumeAndMount(volumeDefaultMode *int32) (corev1.Volume, corev1.VolumeMount) { + volume := corev1.Volume{ + Name: "lightspeed-stack-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.LCoreConfigCmName, }, + DefaultMode: volumeDefaultMode, }, }, - { - Name: "lightspeed-stack-config", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: utils.LCoreConfigCmName, - }, - DefaultMode: &volumeDefaultMode, + } + + volumeMount := corev1.VolumeMount{ + Name: "lightspeed-stack-config", + MountPath: utils.LCoreConfigMountPath, + SubPath: utils.LCoreConfigFilename, + ReadOnly: true, + } + + return volume, volumeMount +} + +// buildLlamaStackConfigVolumeAndMount creates both the volume and volume mount for llama-stack config +func buildLlamaStackConfigVolumeAndMount(volumeDefaultMode *int32) (corev1.Volume, corev1.VolumeMount) { + volume := corev1.Volume{ + Name: "llama-stack-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.LlamaStackConfigCmName, }, + DefaultMode: volumeDefaultMode, }, }, - { - Name: "llama-cache", + } + + volumeMount := corev1.VolumeMount{ + Name: "llama-stack-config", + MountPath: utils.LlamaStackConfigMountPath, + SubPath: utils.LlamaStackConfigFilename, + ReadOnly: true, + } + + return volume, volumeMount +} + +// addTLSVolumesAndMounts adds TLS certificate volumes and mounts if not using custom TLS +func addTLSVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, cr *olsv1alpha1.OLSConfig, volumeDefaultMode *int32) { + usesCustomTLS := cr.Spec.OLSConfig.TLSSecurityProfile != nil && string(cr.Spec.OLSConfig.TLSSecurityProfile.Type) == "Custom" + if !usesCustomTLS { + *volumes = append(*volumes, corev1.Volume{ + Name: "secret-lightspeed-tls", VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, + Secret: &corev1.SecretVolumeSource{ + SecretName: utils.OLSCertsSecretName, + DefaultMode: volumeDefaultMode, + }, + }, + }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: "secret-lightspeed-tls", + MountPath: path.Join(utils.OLSAppCertsMountRoot, "lightspeed-tls"), + ReadOnly: true, + }) + } +} + +// addOpenShiftCAVolumesAndMounts adds OpenShift service CA bundle volumes and mounts +func addOpenShiftCAVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, volumeDefaultMode *int32) { + *volumes = append(*volumes, corev1.Volume{ + Name: "openshift-service-ca", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: utils.OLSCAConfigMap, + }, + DefaultMode: volumeDefaultMode, }, }, - { - Name: utils.OpenShiftCAVolumeName, + }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: "openshift-service-ca", + MountPath: "/etc/certs/service-ca", + ReadOnly: true, + }) +} + +// addOpenShiftRootCAVolumesAndMounts adds OpenShift root CA (kube-root-ca.crt) volumes and mounts +func addOpenShiftRootCAVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, volumeDefaultMode *int32) { + *volumes = append(*volumes, corev1.Volume{ + Name: utils.OpenShiftCAVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "kube-root-ca.crt", + }, + DefaultMode: volumeDefaultMode, + }, + }, + }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: utils.OpenShiftCAVolumeName, + MountPath: "/etc/pki/ca-trust/extracted/pem", + ReadOnly: true, + }) +} + +// addLlamaCacheVolumesAndMounts adds llama-cache EmptyDir volume and mount for Llama Stack workspace +func addLlamaCacheVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount) { + *volumes = append(*volumes, corev1.Volume{ + Name: "llama-cache", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: "llama-cache", + MountPath: "/app-root/.llama", + ReadOnly: false, + }) +} + +// addPostgresCAVolumesAndMounts adds PostgreSQL CA ConfigMap volume and mount for TLS verification +func addPostgresCAVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, mountPath string) { + *volumes = append(*volumes, utils.GetPostgresCAConfigVolume()) + *volumeMounts = append(*volumeMounts, utils.GetPostgresCAVolumeMount(mountPath)) +} + +// addUserCAVolumesAndMounts adds user-provided CA certificate volumes and mounts +// Mounts at /etc/pki/ca-trust/source/anchors (system trust store path) +func addUserCAVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, cr *olsv1alpha1.OLSConfig, volumeDefaultMode *int32) { + _ = utils.ForEachExternalConfigMap(cr, func(name, source string) error { + if source != "additional-ca" { + return nil + } + + *volumes = append(*volumes, corev1.Volume{ + Name: utils.AdditionalCAVolumeName, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "kube-root-ca.crt", + Name: name, }, - DefaultMode: &volumeDefaultMode, + DefaultMode: volumeDefaultMode, }, }, - }, - } - - // PostgreSQL CA ConfigMap volume (for TLS certificate verification) - volumes = append(volumes, utils.GetPostgresCAConfigVolume()) + }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: utils.AdditionalCAVolumeName, + MountPath: "/etc/pki/ca-trust/source/anchors", + ReadOnly: true, + }) + return nil + }) +} - // Add external TLS secret if provided by user - var tlsVolumeMounts []corev1.VolumeMount +// addCustomTLSVolumesAndMounts adds user-provided custom TLS certificate volumes and mounts if specified +func addCustomTLSVolumesAndMounts(volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, cr *olsv1alpha1.OLSConfig, volumeDefaultMode *int32) { if cr.Spec.OLSConfig.TLSConfig != nil && cr.Spec.OLSConfig.TLSConfig.KeyCertSecretRef.Name != "" { - // User provided custom TLS secret - volumes = append(volumes, corev1.Volume{ + *volumes = append(*volumes, corev1.Volume{ Name: "secret-lightspeed-tls", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: cr.Spec.OLSConfig.TLSConfig.KeyCertSecretRef.Name, - DefaultMode: &volumeDefaultMode, + DefaultMode: volumeDefaultMode, }, }, }) + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: "secret-lightspeed-tls", + MountPath: path.Join(utils.OLSAppCertsMountRoot, "lightspeed-tls"), + ReadOnly: true, + }) + } +} - tlsVolumeMounts = []corev1.VolumeMount{ - { - Name: "secret-lightspeed-tls", - MountPath: path.Join(utils.OLSAppCertsMountRoot, "lightspeed-tls"), - ReadOnly: true, - }, +// addMCPHeaderSecretVolumesAndMounts adds MCP header secret volumes and mounts for HTTP MCP servers +// This validates and mounts header secrets at /etc/mcp/headers/ +func addMCPHeaderSecretVolumesAndMounts(r reconciler.Reconciler, ctx context.Context, volumes *[]corev1.Volume, volumeMounts *[]corev1.VolumeMount, cr *olsv1alpha1.OLSConfig, volumeDefaultMode *int32) error { + // Only add MCP header secrets if feature gate is enabled + if cr.Spec.FeatureGates == nil || !slices.Contains(cr.Spec.FeatureGates, utils.FeatureGateMCPServer) { + return nil + } + + // Filter to HTTP-only servers (no logging needed here, already logged in config) + filteredServers := FilterHTTPMCPServers(r, cr, cr.Spec.MCPServers) + + for _, server := range filteredServers { + if server.StreamableHTTP != nil && server.StreamableHTTP.Headers != nil { + for headerName, secretRef := range server.StreamableHTTP.Headers { + // Skip special placeholders + if secretRef == utils.KUBERNETES_PLACEHOLDER || secretRef == "" { + continue + } + + // Validate secret exists and has correct structure + // This provides fail-fast validation consistent with AppServer + if err := validateMCPHeaderSecret(r, ctx, secretRef, server.Name, headerName); err != nil { + return err + } + + *volumes = append(*volumes, corev1.Volume{ + Name: "header-" + secretRef, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretRef, + DefaultMode: volumeDefaultMode, + }, + }, + }) + + *volumeMounts = append(*volumeMounts, corev1.VolumeMount{ + Name: "header-" + secretRef, + MountPath: path.Join(utils.MCPHeadersMountRoot, secretRef), + ReadOnly: true, + }) + } } } - // llama-stack container volume mounts - llamaStackVolumeMounts := []corev1.VolumeMount{ - { - Name: "llama-stack-config", - MountPath: "/app-root/run.yaml", - SubPath: "run.yaml", - ReadOnly: true, - }, - { - Name: "llama-cache", - MountPath: "/app-root/.llama", - ReadOnly: false, + return nil +} + +// buildLightspeedStackLivenessProbe creates the liveness probe for lightspeed-stack container +func buildLightspeedStackLivenessProbe() *corev1.Probe { + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "sh", + "-c", + "curl -k --fail -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" https://localhost:8443/liveness", + }, + }, }, - { - Name: utils.OpenShiftCAVolumeName, - MountPath: "/etc/pki/ca-trust/extracted/pem", - ReadOnly: true, + InitialDelaySeconds: 20, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 3, + } +} + +// buildLightspeedStackReadinessProbe creates the readiness probe for lightspeed-stack container +func buildLightspeedStackReadinessProbe() *corev1.Probe { + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "sh", + "-c", + "curl -k --fail -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" https://localhost:8443/readiness", + }, + }, }, - // PostgreSQL CA ConfigMap (service-ca.crt for TLS verification) - utils.GetPostgresCAVolumeMount("/etc/certs/postgres-ca"), + InitialDelaySeconds: 20, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 3, } +} - // User provided CA certificates - create both volumes and volume mounts in single pass - _ = utils.ForEachExternalConfigMap(cr, func(name, source string) error { - var volumeName, llamaStackMountPath string - switch source { - case "additional-ca": - volumeName = utils.AdditionalCAVolumeName - llamaStackMountPath = "/etc/pki/ca-trust/source/anchors" - default: - return nil - } +// buildLightspeedStackContainer creates the base lightspeed-stack container +func buildLightspeedStackContainer(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig, volumeMounts []corev1.VolumeMount, envVars []corev1.EnvVar) corev1.Container { + lightspeedStackResources := getLightspeedStackResources(cr) - volumes = append(volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - DefaultMode: &volumeDefaultMode, - }, + return corev1.Container{ + Name: "lightspeed-service-api", + Image: r.GetLCoreImage(), + ImagePullPolicy: corev1.PullAlways, + Ports: []corev1.ContainerPort{ + { + ContainerPort: utils.OLSAppServerContainerPort, + Name: "https", + Protocol: corev1.ProtocolTCP, }, - }) + }, + Env: envVars, + VolumeMounts: volumeMounts, + Resources: *lightspeedStackResources, + LivenessProbe: buildLightspeedStackLivenessProbe(), + ReadinessProbe: buildLightspeedStackReadinessProbe(), + } +} - llamaStackVolumeMounts = append(llamaStackVolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: llamaStackMountPath, - ReadOnly: true, - }) - return nil - }) +// ============================================================================ +// Deployment generation functions +// ============================================================================ + +// GenerateLCoreDeployment generates the Deployment for LCore based on the server mode +func GenerateLCoreDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*appsv1.Deployment, error) { + if r.GetLCoreServerMode() { + return generateLCoreServerDeployment(r, cr) + } + return generateLCoreLibraryDeployment(r, cr) +} + +// generateLCoreServerDeployment generates the Deployment for LCore in server mode (llama-stack + lightspeed-stack) +func generateLCoreServerDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*appsv1.Deployment, error) { + ctx := context.Background() + revisionHistoryLimit := int32(1) + volumeDefaultMode := utils.VolumeDefaultMode + + llamaStackResources := getLlamaStackResources(cr) + lightspeedStackResources := getLightspeedStackResources(cr) + + // Get ResourceVersions for tracking - these resources should already exist + // If they don't exist, we'll get empty strings which is fine for initial creation + lcoreConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LCoreConfigCmName) + llamaStackConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LlamaStackConfigCmName) + + // Use helper functions to build common components + labels := buildCommonLabels() + + // Build config volumes and mounts using helper functions + llamaStackVolume, llamaStackConfigMount := buildLlamaStackConfigVolumeAndMount(&volumeDefaultMode) + lcoreVolume, lcoreConfigMount := buildLCoreConfigVolumeAndMount(&volumeDefaultMode) + + // Define volumes + volumes := []corev1.Volume{ + llamaStackVolume, + lcoreVolume, + } + + // Add PostgreSQL CA ConfigMap volume and mount (for TLS certificate verification) + var postgresCAMounts []corev1.VolumeMount + addPostgresCAVolumesAndMounts(&volumes, &postgresCAMounts, "/etc/certs/postgres-ca") + + // Add llama-cache EmptyDir for Llama Stack workspace + var llamaCacheMounts []corev1.VolumeMount + addLlamaCacheVolumesAndMounts(&volumes, &llamaCacheMounts) + + // Add TLS volumes and mounts (custom if provided, default otherwise) + var tlsVolumeMounts []corev1.VolumeMount + addCustomTLSVolumesAndMounts(&volumes, &tlsVolumeMounts, cr, &volumeDefaultMode) + if len(tlsVolumeMounts) == 0 { + // No custom TLS, add default service-ca TLS + addTLSVolumesAndMounts(&volumes, &tlsVolumeMounts, cr, &volumeDefaultMode) + } + + // Add OpenShift CA bundles (both service-ca and root CA) + var openShiftCAMounts []corev1.VolumeMount + addOpenShiftCAVolumesAndMounts(&volumes, &openShiftCAMounts, &volumeDefaultMode) + addOpenShiftRootCAVolumesAndMounts(&volumes, &openShiftCAMounts, &volumeDefaultMode) + + // llama-stack container volume mounts + llamaStackVolumeMounts := []corev1.VolumeMount{ + llamaStackConfigMount, + } + + // Add PostgreSQL CA mount to llama-stack container + llamaStackVolumeMounts = append(llamaStackVolumeMounts, postgresCAMounts...) + + // Add llama-cache mount to llama-stack container + llamaStackVolumeMounts = append(llamaStackVolumeMounts, llamaCacheMounts...) + + // Add OpenShift CA mounts to llama-stack container + llamaStackVolumeMounts = append(llamaStackVolumeMounts, openShiftCAMounts...) + + // Add user-provided CA certificates to llama-stack container + addUserCAVolumesAndMounts(&volumes, &llamaStackVolumeMounts, cr, &volumeDefaultMode) // Build environment variables for LLM providers llamaStackEnvVars, err := buildLlamaStackEnvVars(r, ctx, cr) @@ -519,113 +784,28 @@ func GenerateLCoreDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) // Build lightspeed-stack volume mounts lightspeedStackVolumeMounts := []corev1.VolumeMount{ - { - Name: "lightspeed-stack-config", - MountPath: "/app-root/lightspeed-stack.yaml", - SubPath: "lightspeed-stack.yaml", - }, + lcoreConfigMount, } // Add TLS volume mounts from external secrets lightspeedStackVolumeMounts = append(lightspeedStackVolumeMounts, tlsVolumeMounts...) - // TLS certificate volume and mount - only if using service-ca (not custom TLS) - // If user provides TLSConfig.KeyCertSecretRef, it's already mounted above via ForEachExternalSecret - if cr.Spec.OLSConfig.TLSConfig == nil || cr.Spec.OLSConfig.TLSConfig.KeyCertSecretRef.Name == "" { - volumes = append(volumes, corev1.Volume{ - Name: "secret-lightspeed-tls", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: utils.OLSCertsSecretName, - DefaultMode: &volumeDefaultMode, - }, - }, - }) - lightspeedStackVolumeMounts = append(lightspeedStackVolumeMounts, corev1.VolumeMount{ - Name: "secret-lightspeed-tls", - MountPath: path.Join(utils.OLSAppCertsMountRoot, "lightspeed-tls"), - ReadOnly: true, - }) - } - // PostgreSQL CA ConfigMap (service-ca.crt for OpenShift CA) lightspeedStackVolumeMounts = append(lightspeedStackVolumeMounts, utils.GetPostgresCAVolumeMount(path.Join(utils.OLSAppCertsMountRoot, "postgres-ca"))) // Mount MCP server header secrets - only for HTTP-compatible servers - if cr.Spec.FeatureGates != nil && slices.Contains(cr.Spec.FeatureGates, utils.FeatureGateMCPServer) { - // Filter to HTTP-only servers (no logging needed here, already logged in config) - filteredServers := FilterHTTPMCPServers(r, cr, cr.Spec.MCPServers) - - for _, server := range filteredServers { - if server.StreamableHTTP != nil && server.StreamableHTTP.Headers != nil { - for headerName, secretRef := range server.StreamableHTTP.Headers { - // Skip special placeholders - if secretRef == utils.KUBERNETES_PLACEHOLDER || secretRef == "" { - continue - } - - // Validate secret exists and has correct structure - // This provides fail-fast validation consistent with AppServer - if err := validateMCPHeaderSecret(r, ctx, secretRef, server.Name, headerName); err != nil { - return nil, err - } - - volumes = append(volumes, corev1.Volume{ - Name: "header-" + secretRef, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretRef, - DefaultMode: &volumeDefaultMode, - }, - }, - }) - - lightspeedStackVolumeMounts = append(lightspeedStackVolumeMounts, corev1.VolumeMount{ - Name: "header-" + secretRef, - MountPath: path.Join(utils.MCPHeadersMountRoot, secretRef), - ReadOnly: true, - }) - } - } - } + if err := addMCPHeaderSecretVolumesAndMounts(r, ctx, &volumes, &lightspeedStackVolumeMounts, cr, &volumeDefaultMode); err != nil { + return nil, err } lightspeedStackContainer.VolumeMounts = lightspeedStackVolumeMounts - lightspeedStackContainer.LivenessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "-c", - "curl -k --fail -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" https://localhost:8443/liveness", - }, - }, - }, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 3, - } - lightspeedStackContainer.ReadinessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "sh", - "-c", - "curl -k --fail -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" https://localhost:8443/readiness", - }, - }, - }, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 3, - } + lightspeedStackContainer.LivenessProbe = buildLightspeedStackLivenessProbe() + lightspeedStackContainer.ReadinessProbe = buildLightspeedStackReadinessProbe() lightspeedStackContainer.Resources = *lightspeedStackResources deployment := appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: "lightspeed-stack-deployment", + Name: utils.LCoreDeploymentName, Namespace: r.GetNamespace(), Labels: labels, Annotations: map[string]string{ @@ -670,27 +850,7 @@ func GenerateLCoreDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) } // Add OpenShift MCP server sidecar container if introspection is enabled - if cr.Spec.OLSConfig.IntrospectionEnabled { - openshiftMCPServerContainer := corev1.Container{ - Name: utils.OpenShiftMCPServerContainerName, - Image: r.GetOpenShiftMCPServerImage(), - ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: &[]bool{false}[0], - ReadOnlyRootFilesystem: &[]bool{true}[0], - }, - Command: []string{ - "/openshift-mcp-server", - "--read-only", - "--port", fmt.Sprintf("%d", utils.OpenShiftMCPServerPort), - }, - Resources: *getOLSMCPServerResources(cr), - } - deployment.Spec.Template.Spec.Containers = append( - deployment.Spec.Template.Spec.Containers, - openshiftMCPServerContainer, - ) - } + addOpenShiftMCPServerSidecar(r, cr, &deployment) return &deployment, nil } @@ -782,3 +942,124 @@ func updateLCoreDeployment(r reconciler.Reconciler, ctx context.Context, existin return nil } + +// generateLCoreLibraryDeployment generates the Deployment for LCore in library mode (single container) +func generateLCoreLibraryDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (*appsv1.Deployment, error) { + ctx := context.Background() + revisionHistoryLimit := int32(1) + volumeDefaultMode := utils.VolumeDefaultMode + + // Get ResourceVersions for tracking + lcoreConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LCoreConfigCmName) + llamaStackConfigMapResourceVersion, _ := utils.GetConfigMapResourceVersion(r, ctx, utils.LlamaStackConfigCmName) + + // Use helper functions to build common components + labels := buildCommonLabels() + + // Build config volumes and mounts using helper functions + llamaStackVolume, llamaStackConfigMount := buildLlamaStackConfigVolumeAndMount(&volumeDefaultMode) + lcoreVolume, lcoreConfigMount := buildLCoreConfigVolumeAndMount(&volumeDefaultMode) + + // Library mode needs both volumes + volumes := []corev1.Volume{ + llamaStackVolume, + lcoreVolume, + } + + // Library mode container needs both config mounts + volumeMounts := []corev1.VolumeMount{ + llamaStackConfigMount, + lcoreConfigMount, + } + + // Add llama-cache EmptyDir for Llama Stack workspace + addLlamaCacheVolumesAndMounts(&volumes, &volumeMounts) + + // Add TLS volumes and mounts (custom if provided, default otherwise) + var tlsVolumeMounts []corev1.VolumeMount + addCustomTLSVolumesAndMounts(&volumes, &tlsVolumeMounts, cr, &volumeDefaultMode) + if len(tlsVolumeMounts) == 0 { + // No custom TLS, add default service-ca TLS + addTLSVolumesAndMounts(&volumes, &tlsVolumeMounts, cr, &volumeDefaultMode) + } + volumeMounts = append(volumeMounts, tlsVolumeMounts...) + + // Add OpenShift CA bundles (both service-ca and root CA) + addOpenShiftCAVolumesAndMounts(&volumes, &volumeMounts, &volumeDefaultMode) + addOpenShiftRootCAVolumesAndMounts(&volumes, &volumeMounts, &volumeDefaultMode) + + // Add PostgreSQL CA ConfigMap (for database TLS verification) + addPostgresCAVolumesAndMounts(&volumes, &volumeMounts, "/etc/certs/postgres-ca") + + // Add user CA certificates + addUserCAVolumesAndMounts(&volumes, &volumeMounts, cr, &volumeDefaultMode) + + // Add MCP header secrets for HTTP MCP servers + if err := addMCPHeaderSecretVolumesAndMounts(r, ctx, &volumes, &volumeMounts, cr, &volumeDefaultMode); err != nil { + return nil, err + } + + // Build environment variables for library mode + // Library mode needs env vars from both llama-stack and lightspeed-stack + llamaStackEnvVars, err := buildLlamaStackEnvVars(r, ctx, cr) + if err != nil { + return nil, fmt.Errorf("failed to build llama-stack env vars: %w", err) + } + lightspeedStackEnvVars := buildLightspeedStackEnvVars(r, cr) + + // Combine env vars (llama-stack + lightspeed-stack) + combinedEnvVars := append(llamaStackEnvVars, lightspeedStackEnvVars...) + + // Create the lightspeed-stack container using helper + lightspeedStackContainer := buildLightspeedStackContainer(r, cr, volumeMounts, combinedEnvVars) + + deployment := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.LCoreDeploymentName, + Namespace: r.GetNamespace(), + Labels: labels, + Annotations: map[string]string{ + utils.LCoreConfigMapResourceVersionAnnotation: lcoreConfigMapResourceVersion, + utils.LlamaStackConfigMapResourceVersionAnnotation: llamaStackConfigMapResourceVersion, + }, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "lightspeed-stack", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: utils.OLSAppServerServiceAccountName, + Containers: []corev1.Container{ + lightspeedStackContainer, + }, + Volumes: volumes, + }, + }, + RevisionHistoryLimit: &revisionHistoryLimit, + }, + } + + // Apply pod-level scheduling constraints + utils.ApplyPodDeploymentConfig(&deployment, cr.Spec.OLSConfig.DeploymentConfig.APIContainer, true) + + if len(cr.Spec.OLSConfig.RAG) > 0 { + if cr.Spec.OLSConfig.ImagePullSecrets != nil { + deployment.Spec.Template.Spec.ImagePullSecrets = cr.Spec.OLSConfig.ImagePullSecrets + } + } + + if err := controllerutil.SetControllerReference(cr, &deployment, r.GetScheme()); err != nil { + return nil, err + } + + // Add OpenShift MCP server sidecar container if introspection is enabled + addOpenShiftMCPServerSidecar(r, cr, &deployment) + + return &deployment, nil +} diff --git a/internal/controller/lcore/deployment_test.go b/internal/controller/lcore/deployment_test.go index eaa730d60..10d127131 100644 --- a/internal/controller/lcore/deployment_test.go +++ b/internal/controller/lcore/deployment_test.go @@ -23,9 +23,10 @@ import ( // mockReconciler is a minimal mock for testing deployment generation type mockReconciler struct { reconciler.Reconciler - namespace string - scheme *runtime.Scheme - image string + namespace string + scheme *runtime.Scheme + image string + lcoreServerMode bool } func (m *mockReconciler) GetNamespace() string { @@ -64,6 +65,10 @@ func (m *mockReconciler) GetOpenShiftMCPServerImage() string { return utils.OpenShiftMCPServerImageDefault } +func (m *mockReconciler) GetLCoreServerMode() bool { + return m.lcoreServerMode +} + func (m *mockReconciler) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { // Return NotFound error for all Get calls in tests // This simulates the ConfigMaps not existing yet during deployment generation @@ -91,7 +96,9 @@ func TestGenerateLCoreDeployment(t *testing.T) { } // Create a mock reconciler - r := &mockReconciler{} + r := &mockReconciler{ + lcoreServerMode: true, // Test server mode (2 containers) + } // Generate the deployment deployment, err := GenerateLCoreDeployment(r, cr) @@ -309,7 +316,9 @@ func TestGenerateLCoreDeploymentWithAdditionalCA(t *testing.T) { } // Create a mock reconciler - r := &mockReconciler{} + r := &mockReconciler{ + lcoreServerMode: true, // Test server mode (2 containers) + } // Generate the deployment deployment, err := GenerateLCoreDeployment(r, cr) @@ -429,7 +438,9 @@ func TestGenerateLCoreDeploymentWithIntrospection(t *testing.T) { } // Create a mock reconciler with OpenShift MCP server image - r := &mockReconciler{} + r := &mockReconciler{ + lcoreServerMode: true, // Test server mode (2 containers + MCP sidecar) + } // Generate the deployment deployment, err := GenerateLCoreDeployment(r, cr) @@ -550,7 +561,9 @@ func TestGenerateLCoreDeploymentWithMCPHeaderSecrets(t *testing.T) { } // Create a mock reconciler - r := &mockReconciler{} + r := &mockReconciler{ + lcoreServerMode: true, // Test server mode (2 containers) + } // Generate the deployment deployment, err := GenerateLCoreDeployment(r, cr) @@ -627,7 +640,9 @@ func TestGenerateLCoreDeploymentWithoutIntrospection(t *testing.T) { } // Create a mock reconciler - r := &mockReconciler{} + r := &mockReconciler{ + lcoreServerMode: true, // Test server mode (2 containers) + } // Generate the deployment deployment, err := GenerateLCoreDeployment(r, cr) @@ -708,6 +723,163 @@ func TestGetOLSMCPServerResources(t *testing.T) { } } +func TestGenerateLCoreDeploymentLibraryMode(t *testing.T) { + // Create a minimal OLSConfig CR + cr := &olsv1alpha1.OLSConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: olsv1alpha1.OLSConfigSpec{ + LLMConfig: olsv1alpha1.LLMSpec{ + Providers: []olsv1alpha1.ProviderSpec{ + { + Name: "test-provider", + CredentialsSecretRef: corev1.LocalObjectReference{ + Name: "test-secret", + }, + }, + }, + }, + }, + } + + // Create a mock reconciler in library mode + r := &mockReconciler{ + lcoreServerMode: false, // Test library mode (1 container) + } + + // Generate the deployment + deployment, err := GenerateLCoreDeployment(r, cr) + if err != nil { + t.Fatalf("GenerateLCoreDeployment returned error: %v", err) + } + + // Verify deployment is not nil + if deployment == nil { + t.Fatal("GenerateLCoreDeployment returned nil deployment") + } + + // Verify basic metadata + if deployment.Name != "lightspeed-stack-deployment" { + t.Errorf("Expected deployment name 'lightspeed-stack-deployment', got '%s'", deployment.Name) + } + if deployment.Namespace != utils.OLSNamespaceDefault { + t.Errorf("Expected namespace '%s', got '%s'", utils.OLSNamespaceDefault, deployment.Namespace) + } + + // Verify labels + expectedLabels := map[string]string{ + "app": "lightspeed-stack", + "app.kubernetes.io/component": "application-server", + "app.kubernetes.io/managed-by": "lightspeed-operator", + "app.kubernetes.io/name": "lightspeed-service-api", + "app.kubernetes.io/part-of": "openshift-lightspeed", + } + for key, expectedValue := range expectedLabels { + if actualValue, ok := deployment.Labels[key]; !ok { + t.Errorf("Missing label '%s'", key) + } else if actualValue != expectedValue { + t.Errorf("Label '%s': expected '%s', got '%s'", key, expectedValue, actualValue) + } + } + + // Verify service account + if deployment.Spec.Template.Spec.ServiceAccountName != utils.OLSAppServerServiceAccountName { + t.Errorf("Expected ServiceAccountName '%s', got '%s'", + utils.OLSAppServerServiceAccountName, + deployment.Spec.Template.Spec.ServiceAccountName) + } + + // Verify containers - should have ONLY 1 (lightspeed-stack with embedded llama-stack) + containers := deployment.Spec.Template.Spec.Containers + if len(containers) != 1 { + t.Fatalf("Expected 1 container in library mode (lightspeed-stack), got %d", len(containers)) + } + + // Verify lightspeed-stack container + lightspeedStackContainer := containers[0] + if lightspeedStackContainer.Name != "lightspeed-service-api" { + t.Errorf("Expected container name 'lightspeed-service-api', got '%s'", lightspeedStackContainer.Name) + } + if len(lightspeedStackContainer.Ports) != 1 || lightspeedStackContainer.Ports[0].ContainerPort != utils.OLSAppServerContainerPort { + t.Errorf("Expected container port %d, got %v", + utils.OLSAppServerContainerPort, lightspeedStackContainer.Ports) + } + if lightspeedStackContainer.LivenessProbe == nil { + t.Error("lightspeed-stack container missing liveness probe") + } + if lightspeedStackContainer.ReadinessProbe == nil { + t.Error("lightspeed-stack container missing readiness probe") + } + + // Verify volumes - library mode needs BOTH config volumes (LCore + Llama Stack) + volumes := deployment.Spec.Template.Spec.Volumes + volumeNames := make(map[string]bool) + for _, vol := range volumes { + volumeNames[vol.Name] = true + } + + // Both configs must be present + if !volumeNames[utils.LCoreConfigCmName] { + t.Error("Missing LCore config volume in library mode") + } + if !volumeNames[utils.LlamaStackConfigCmName] { + t.Error("Missing Llama Stack config volume in library mode") + } + // Library mode also needs llama-cache for model downloads + if !volumeNames[utils.LlamaCacheVolumeName] { + t.Error("Missing llama-cache volume in library mode") + } + // Should have TLS + if !volumeNames["secret-lightspeed-tls"] { + t.Error("Missing TLS volume in library mode") + } + // Should have OpenShift root CA + if !volumeNames[utils.OpenShiftCAVolumeName] { + t.Error("Missing OpenShift root CA volume in library mode") + } + // Should have Postgres CA + if !volumeNames[utils.PostgresCAVolume] { + t.Error("Missing Postgres CA volume in library mode") + } + + // Verify volume mounts in lightspeed-stack container + volumeMounts := lightspeedStackContainer.VolumeMounts + volumeMountNames := make(map[string]bool) + for _, mount := range volumeMounts { + volumeMountNames[mount.Name] = true + } + + // Verify both configs are mounted + if !volumeMountNames[utils.LCoreConfigCmName] { + t.Error("Missing LCore config mount in library mode") + } + if !volumeMountNames[utils.LlamaStackConfigCmName] { + t.Error("Missing Llama Stack config mount in library mode") + } + if !volumeMountNames[utils.LlamaCacheVolumeName] { + t.Error("Missing llama-cache mount in library mode") + } + if !volumeMountNames["secret-lightspeed-tls"] { + t.Error("Missing TLS mount in library mode") + } + + // Verify that deployment can be marshaled to YAML (valid k8s object) + yamlBytes, err := yaml.Marshal(deployment) + if err != nil { + t.Fatalf("Failed to marshal deployment to YAML: %v", err) + } + + // Verify we can unmarshal it back + var unmarshaledDeployment appsv1.Deployment + err = yaml.Unmarshal(yamlBytes, &unmarshaledDeployment) + if err != nil { + t.Fatalf("Failed to unmarshal deployment YAML: %v", err) + } + + t.Logf("Successfully validated LCore Deployment in Library Mode (%d bytes of YAML)", len(yamlBytes)) +} + func TestGenerateLCoreDeploymentWithRAG(t *testing.T) { imagePullSecrets := []corev1.LocalObjectReference{ { diff --git a/internal/controller/lcore/suite_test.go b/internal/controller/lcore/suite_test.go index 202e58bfb..535959bfc 100644 --- a/internal/controller/lcore/suite_test.go +++ b/internal/controller/lcore/suite_test.go @@ -145,6 +145,7 @@ var _ = BeforeSuite(func() { // Set default flags for test reconciler (can be overridden in specific tests) if tr, ok := testReconcilerInstance.(*utils.TestReconciler); ok { tr.PrometheusAvailable = true + tr.SetLCoreServerMode(true) // Default to server mode (2 containers) } cr = &olsv1alpha1.OLSConfig{} diff --git a/internal/controller/olsconfig_helpers.go b/internal/controller/olsconfig_helpers.go index 7f381f66f..20c4bfaf5 100644 --- a/internal/controller/olsconfig_helpers.go +++ b/internal/controller/olsconfig_helpers.go @@ -85,6 +85,10 @@ func (r *OLSConfigReconciler) UseLCore() bool { return r.Options.UseLCore } +func (r *OLSConfigReconciler) GetLCoreServerMode() bool { + return r.Options.LCoreServerMode +} + // Status management // UpdateStatusCondition updates the complete status of the OLSConfig Custom Resource instance. diff --git a/internal/controller/reconciler/interface.go b/internal/controller/reconciler/interface.go index 4c492e997..55db2b822 100644 --- a/internal/controller/reconciler/interface.go +++ b/internal/controller/reconciler/interface.go @@ -71,4 +71,7 @@ type Reconciler interface { // UseLCore returns whether LCore backend is enabled instead of AppServer UseLCore() bool + + // GetLCoreServerMode returns whether LCore should run in server mode (true) or library mode (false) + GetLCoreServerMode() bool } diff --git a/internal/controller/utils/constants.go b/internal/controller/utils/constants.go index 48e71d470..588b88c95 100644 --- a/internal/controller/utils/constants.go +++ b/internal/controller/utils/constants.go @@ -349,7 +349,7 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt' // LCoreConfigCmName name for the LCore config map LCoreConfigCmName = "lightspeed-stack-config" // LlamaStackImageDefault default image for Llama Stack - LlamaStackImageDefault = "quay.io/lightspeed-core/lightspeed-stack:dev-latest" + LlamaStackImageDefault = "quay.io/lightspeed-core/lightspeed-stack:dev-20260125-5f817cd" // LlamaStackConfigHashKey is the key of the hash value of the Llama Stack configmap LlamaStackConfigHashKey = "hash/llamastackconfig" // LCoreDeploymentName is the name of the LCore deployment (used for testing) @@ -368,6 +368,10 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt' LlamaStackConfigFilename = "run.yaml" // LCoreConfigFilename is the filename for LCore config (used for testing) LCoreConfigFilename = "lightspeed-stack.yaml" + // LlamaStackConfigMountPath is the mount path for Llama Stack config file + LlamaStackConfigMountPath = "/app-root/run.yaml" + // LCoreConfigMountPath is the mount path for LCore config file + LCoreConfigMountPath = "/app-root/lightspeed-stack.yaml" // KubeRootCAMountPath is the mount path for kube-root-ca.crt (used for testing) KubeRootCAMountPath = "/etc/pki/ca-trust/extracted/pem" // AdditionalCAMountPath is the mount path for additional CA certificates (used for testing) diff --git a/internal/controller/utils/testing.go b/internal/controller/utils/testing.go index 7e4377f6c..648a3d41e 100644 --- a/internal/controller/utils/testing.go +++ b/internal/controller/utils/testing.go @@ -27,6 +27,7 @@ type TestReconciler struct { PrometheusAvailable bool watcherConfig interface{} useLCore bool + lcoreServerMode bool } func (r *TestReconciler) GetScheme() *runtime.Scheme { @@ -85,6 +86,14 @@ func (r *TestReconciler) UseLCore() bool { return r.useLCore } +func (r *TestReconciler) GetLCoreServerMode() bool { + return r.lcoreServerMode +} + +func (r *TestReconciler) SetLCoreServerMode(lcoreServerMode bool) { + r.lcoreServerMode = lcoreServerMode +} + func (r *TestReconciler) SetWatcherConfig(config interface{}) { r.watcherConfig = config } diff --git a/internal/controller/utils/types.go b/internal/controller/utils/types.go index d22da0dbb..08823dd10 100644 --- a/internal/controller/utils/types.go +++ b/internal/controller/utils/types.go @@ -25,6 +25,7 @@ type OLSConfigReconcilerOptions struct { OpenShiftMCPServerImage string LightspeedCoreImage string UseLCore bool + LCoreServerMode bool Namespace string PrometheusAvailable bool }