From 6e26f317396cfe971efbafd2a865244a2ae1671b Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Tue, 8 Sep 2026 14:33:02 +0200 Subject: [PATCH 1/3] CRD: Create per component sections Instead of having a section for the logs and a section for the resource limits we start having sections for different compoments like other OpenStack Operators do. The new groupings are: - lightspeed - ogx - database - okp - console - dataverseExporter Within the `dev` section we change the `rhosMCP` field so we can hold both the YAML configuration override and the resource limit. Co-Authored-By: Claude Opus 4.6 --- api/v1beta1/openstacklightspeed_types.go | 161 +++-- api/v1beta1/zz_generated.deepcopy.go | 148 ++++- ...ed.openstack.org_openstacklightspeeds.yaml | 570 ++++++++---------- ...tspeed-operator.clusterserviceversion.yaml | 32 +- ...ightspeed_v1beta1_openstacklightspeed.yaml | 68 ++- docs/configuration.rst | 81 ++- docs/usage.rst | 6 +- internal/controller/assets/postgres.conf.tmpl | 2 +- internal/controller/common.go | 25 + internal/controller/common_test.go | 106 ++++ internal/controller/console_deployment.go | 9 +- internal/controller/lcore_config.go | 27 +- internal/controller/lcore_deployment.go | 34 +- internal/controller/mcp_server.go | 16 +- internal/controller/okp_reconciler.go | 7 +- internal/controller/postgres_deployment.go | 7 +- internal/controller/postgres_reconciler.go | 9 +- .../assert-openstack-lightspeed-instance.yaml | 49 +- .../create-openstack-lightspeed-instance.yaml | 10 +- ...-create-openstack-lightspeed-instance.yaml | 10 +- ...-create-openstack-lightspeed-instance.yaml | 10 +- ...-assert-openstack-lightspeed-instance.yaml | 8 + .../02-create-rhoso-mcps-resources.yaml | 10 +- .../04-update-rhos-mcp-config.yaml | 27 +- .../05-assert-rhos-mcp-config-merged.yaml | 2 +- .../06-disable-rhoso-mcps.yaml | 10 +- ...-update-openstack-lightspeed-instance.yaml | 42 +- .../08-assert-openstacklightspeed-update.yaml | 41 +- 28 files changed, 969 insertions(+), 558 deletions(-) diff --git a/api/v1beta1/openstacklightspeed_types.go b/api/v1beta1/openstacklightspeed_types.go index cb91144b..38bd5b22 100644 --- a/api/v1beta1/openstacklightspeed_types.go +++ b/api/v1beta1/openstacklightspeed_types.go @@ -61,12 +61,23 @@ const ( // - featureFlags: list of experimental feature flags to enable. Configuration options for experimental features must also live within the `DevSpec`. // - okpChunkFilterQuery: Solr filter query for OKP searches (default: version-aware query combining detected OpenStack and OCP versions) // - okpRagOnly: when true, only OKP is used as a RAG source (default: true) -// - rhosMCPConfig: custom YAML configuration for the rhos-mcps service; deep-merged on top of the operator defaults, openstack.enabled and openshift.enabled are always overridden by the operator +// - rhosMCPC: configuration for the rhos-mcps sidecar (resources and custom YAML config); config is deep-merged on top of the operator defaults, openstack.enabled and openshift.enabled are always overridden by the operator type DevSpec struct { FeatureFlags []string `json:"featureFlags,omitempty"` OKPChunkFilterQuery string `json:"okpChunkFilterQuery,omitempty"` OKPRagOnly *bool `json:"okpRagOnly,omitempty"` - RhosMCPConfig string `json:"rhosMCPConfig,omitempty"` + // rhosMCP configures the rhos-mcps sidecar container (only used when the rhoso_mcps feature flag is enabled). + RhosMCP *RhosMCPSpec `json:"rhosMCP,omitempty"` +} + +// RhosMCPSpec defines configuration for the rhos-mcps sidecar container. +type RhosMCPSpec struct { + // +kubebuilder:default:={requests: {cpu: "50m", memory: "64Mi"}, limits: {memory: "200Mi"}} + // Resources sets compute resources for the rhos-mcps sidecar container. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Config is a YAML string that overrides the default configuration (file internal/controller/assets/mcp_server_config.yaml.tmpl) for the rhos-mcps service. + Config string `json:"config,omitempty"` } // OKPSpec defines configuration for the Offline Knowledge Portal (OKP). @@ -83,6 +94,27 @@ type OKPSpec struct { // The secret must contain a key named "access_key". // An access key can be obtained from https://access.redhat.com/offline/access AccessKey string `json:"accessKey,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default:={requests: {cpu: "500m", memory: "2Gi"}, limits: {cpu: "2", memory: "4Gi"}} + // Resources sets compute resources for the Offline Knowledge Portal container. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// OGXSpec defines configuration for the OGX/llama-stack container. +type OGXSpec struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default:={requests: {cpu: "500m", memory: "2Gi"}, limits: {cpu: "2", memory: "8Gi"}} + // Resources sets compute resources for the llama-stack (OGX) container + // in the lightspeed-stack deployment. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default="all=info" + // +kubebuilder:validation:Pattern=`^\w+(?:=\w+)?(?:,\w+(?:=\w+)?)*$` + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OGX Log Level" + // Log level configuration for the OGX/llama-stack container. Supports standard levels (INFO, DEBUG) or fine-grained control using format "component=level,component=level" (e.g., "core=debug,providers=info"). + LogLevel string `json:"logLevel,omitempty"` } // DatabaseSpec defines configuration for persistent PostgreSQL storage. @@ -95,46 +127,43 @@ type DatabaseSpec struct { // StorageClass name for the PersistentVolumeClaim. If omitted, the cluster's // default StorageClass is used. Class string `json:"class,omitempty"` -} - -// ContainerResourcesSpec defines resource requirements for each container -// managed by the operator. Defaults are applied by the API server via -// kubebuilder markers. Users may override any container's resources in -// the CR; the provided value replaces the default entirely. -type ContainerResourcesSpec struct { - // +kubebuilder:validation:Optional - // +kubebuilder:default:={requests: {cpu: "500m", memory: "2Gi"}, limits: {cpu: "2", memory: "8Gi"}} - // LlamaStack sets compute resources for the llama-stack (OGX) container - // in the lightspeed-stack deployment. - LlamaStack corev1.ResourceRequirements `json:"llamaStack,omitempty"` - - // +kubebuilder:validation:Optional - // +kubebuilder:default:={requests: {cpu: "250m", memory: "512Mi"}, limits: {cpu: "1", memory: "2Gi"}} - // LightspeedService sets compute resources for the lightspeed-service-api - // container in the lightspeed-stack deployment. - LightspeedService corev1.ResourceRequirements `json:"lightspeedService,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:default:={requests: {cpu: "30m", memory: "300Mi"}, limits: {cpu: "500m", memory: "2Gi"}} - // Postgres sets compute resources for the PostgreSQL container. - Postgres corev1.ResourceRequirements `json:"postgres,omitempty"` + // Rarources sets compute resources for the PostgreSQL container. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:default:={requests: {cpu: "500m", memory: "2Gi"}, limits: {cpu: "2", memory: "4Gi"}} - // OKP sets compute resources for the Offline Knowledge Portal container. - OKP corev1.ResourceRequirements `json:"okp,omitempty"` + // +kubebuilder:validation:Enum=DEBUG;INFO + // +kubebuilder:default="INFO" + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="PostgreSQL Log Level" + // Log level for the PostgreSQL container. When set to DEBUG, enables logging of all SQL statements (log_statement = all). + LogLevel string `json:"logLevel,omitempty"` +} +// ConsoleSpec defines configuration for the lightspeed console plugin. +type ConsoleSpec struct { // +kubebuilder:validation:Optional // +kubebuilder:default:={requests: {cpu: "50m", memory: "64Mi"}, limits: {cpu: "200m", memory: "256Mi"}} - // ConsolePlugin sets compute resources for the lightspeed-console-plugin + // Resources sets compute resources for the lightspeed-console-plugin // container and its init container. - ConsolePlugin corev1.ResourceRequirements `json:"consolePlugin,omitempty"` + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} +// LightspeedSpec defines configuration for the lightspeed-service-api container. +type LightspeedSpec struct { // +kubebuilder:validation:Optional - // +kubebuilder:default:={requests: {cpu: "50m", memory: "64Mi"}, limits: {memory: "200Mi"}} - // MCP sets compute resources for the RHOSO MCP server sidecar container - // (only created when the rhoso_mcps feature flag is enabled). - MCP corev1.ResourceRequirements `json:"mcp,omitempty"` + // +kubebuilder:default:={requests: {cpu: "250m", memory: "512Mi"}, limits: {cpu: "1", memory: "2Gi"}} + // Resources sets compute resources for the lightspeed-service-api + // container in the lightspeed-stack deployment. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=DEBUG;INFO;WARNING;ERROR;CRITICAL + // +kubebuilder:default="INFO" + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Lightspeed Stack Log Level" + // Log level for the lightspeed-service-api container. Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. + LogLevel string `json:"logLevel,omitempty"` } // QuotaLimiterSpec defines a single quota limiter enforced by lightspeed-stack. @@ -208,6 +237,7 @@ type OpenStackLightspeedSpec struct { OpenStackLightspeedCore `json:",inline"` // +kubebuilder:validation:Optional + // +kubebuilder:default:={} // Database configures persistent storage for PostgreSQL data. // A PersistentVolumeClaim is always created and mounted; when Database // is omitted, the default size is used and the cluster's default @@ -215,6 +245,7 @@ type OpenStackLightspeedSpec struct { Database *DatabaseSpec `json:"database,omitempty"` // +kubebuilder:validation:Optional + // +kubebuilder:default:={} // OKP configures the Offline Knowledge Portal (OKP) RAG source. OKP *OKPSpec `json:"okp,omitempty"` @@ -223,50 +254,49 @@ type OpenStackLightspeedSpec struct { // When omitted or Limiters is empty, quota enforcement is disabled. Quotas *QuotaSpec `json:"quotas,omitempty"` - // +kubebuilder:validation:Optional - // +kubebuilder:default:={} - // Resources configures compute resource requirements for individual - // containers managed by the operator. Each field has sensible defaults - // applied by the API server. Override any container's resources to - // replace its defaults entirely. - Resources ContainerResourcesSpec `json:"resources,omitempty"` - // +kubebuilder:validation:Optional // +kubebuilder:pruning:PreserveUnknownFields // Dev contains developer/experimental configuration. // This section is not part of the stable API and may change at any time without backward compatibility. Dev runtime.RawExtension `json:"dev,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default:={} + // Console configures the lightspeed console plugin. + Console *ConsoleSpec `json:"console,omitempty"` } -// LoggingConfig defines logging configuration for OpenStackLightspeed components -type LoggingConfig struct { +// DataverseExporterFeedback defines feedback collection configuration for the dataverse exporter. +type DataverseExporterFeedback struct { // +kubebuilder:validation:Optional - // +kubebuilder:default="all=info" - // +kubebuilder:validation:Pattern=`^\w+(?:=\w+)?(?:,\w+(?:=\w+)?)*$` - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OGX Log Level" - // Log level configuration for the OGX/llama-stack container. Supports standard levels (INFO, DEBUG) or fine-grained control using format "component=level,component=level" (e.g., "core=debug,providers=info"). - OGXLogLevel string `json:"ogxLogLevel,omitempty"` + // +kubebuilder:default=true + // Enable feedback collection. + Enabled *bool `json:"enabled,omitempty"` +} +// DataverseExporterTranscripts defines conversation transcript collection configuration for the dataverse exporter. +type DataverseExporterTranscripts struct { // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=DEBUG;INFO;WARNING;ERROR;CRITICAL - // +kubebuilder:default="INFO" - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Lightspeed Stack Log Level" - // Log level for the lightspeed-service-api container. Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. - LightspeedStackLogLevel string `json:"lightspeedStackLogLevel,omitempty"` + // Enable conversation transcripts collection. + Enabled bool `json:"enabled,omitempty"` +} +// DataverseExporter defines configuration for the dataverse exporter sidecar. +type DataverseExporter struct { // +kubebuilder:validation:Optional // +kubebuilder:validation:Enum=DEBUG;INFO;WARNING;ERROR;CRITICAL // +kubebuilder:default="INFO" // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Dataverse Exporter Log Level" // Log level for the dataverse exporter sidecar container. Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. - DataverseExporterLogLevel string `json:"dataverseExporterLogLevel,omitempty"` + LogLevel string `json:"logLevel,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=DEBUG;INFO - // +kubebuilder:default="INFO" - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="PostgreSQL Log Level" - // Log level for the PostgreSQL container. When set to DEBUG, enables logging of all SQL statements (log_statement = all). - PostgresLogLevel string `json:"postgresLogLevel,omitempty"` + // Feedback configures user feedback collection. + Feedback *DataverseExporterFeedback `json:"feedback,omitempty"` + + // +kubebuilder:validation:Optional + // Transcripts configures conversation transcript collection. + Transcripts *DataverseExporterTranscripts `json:"transcripts,omitempty"` } // OpenStackLightspeedCore defines the desired state of OpenStackLightspeed @@ -317,18 +347,19 @@ type OpenStackLightspeedCore struct { LLMAPIVersion string `json:"llmAPIVersion,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:default=true - // Enable feedback collection - FeedbackEnabled *bool `json:"feedbackEnabled,omitempty"` + // +kubebuilder:default:={} + // DataverseExporter configures the dataverse exporter sidecar (feedback, transcripts, logging). + DataverseExporter *DataverseExporter `json:"dataverseExporter,omitempty"` // +kubebuilder:validation:Optional - // Enable conversation transcripts collection - TranscriptsEnabled bool `json:"transcriptsEnabled,omitempty"` + // +kubebuilder:default:={} + // OGX configures the OGX/llama-stack container. + OGX *OGXSpec `json:"ogx,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:default={} - // Logging configuration for OpenStackLightspeed components - Logging LoggingConfig `json:"logging"` + // +kubebuilder:default:={} + // Lightspeed configures the lightspeed-service-api container. + Lightspeed *LightspeedSpec `json:"lightspeed,omitempty"` } // OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8b2724b2..0e0f5ba0 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -26,22 +26,17 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ContainerResourcesSpec) DeepCopyInto(out *ContainerResourcesSpec) { +func (in *ConsoleSpec) DeepCopyInto(out *ConsoleSpec) { *out = *in - in.LlamaStack.DeepCopyInto(&out.LlamaStack) - in.LightspeedService.DeepCopyInto(&out.LightspeedService) - in.Postgres.DeepCopyInto(&out.Postgres) - in.OKP.DeepCopyInto(&out.OKP) - in.ConsolePlugin.DeepCopyInto(&out.ConsolePlugin) - in.MCP.DeepCopyInto(&out.MCP) + in.Resources.DeepCopyInto(&out.Resources) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerResourcesSpec. -func (in *ContainerResourcesSpec) DeepCopy() *ContainerResourcesSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsoleSpec. +func (in *ConsoleSpec) DeepCopy() *ConsoleSpec { if in == nil { return nil } - out := new(ContainerResourcesSpec) + out := new(ConsoleSpec) in.DeepCopyInto(out) return out } @@ -50,6 +45,7 @@ func (in *ContainerResourcesSpec) DeepCopy() *ContainerResourcesSpec { func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) { *out = *in out.Size = in.Size.DeepCopy() + in.Resources.DeepCopyInto(&out.Resources) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseSpec. @@ -62,6 +58,66 @@ func (in *DatabaseSpec) DeepCopy() *DatabaseSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataverseExporter) DeepCopyInto(out *DataverseExporter) { + *out = *in + if in.Feedback != nil { + in, out := &in.Feedback, &out.Feedback + *out = new(DataverseExporterFeedback) + (*in).DeepCopyInto(*out) + } + if in.Transcripts != nil { + in, out := &in.Transcripts, &out.Transcripts + *out = new(DataverseExporterTranscripts) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataverseExporter. +func (in *DataverseExporter) DeepCopy() *DataverseExporter { + if in == nil { + return nil + } + out := new(DataverseExporter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataverseExporterFeedback) DeepCopyInto(out *DataverseExporterFeedback) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataverseExporterFeedback. +func (in *DataverseExporterFeedback) DeepCopy() *DataverseExporterFeedback { + if in == nil { + return nil + } + out := new(DataverseExporterFeedback) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataverseExporterTranscripts) DeepCopyInto(out *DataverseExporterTranscripts) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataverseExporterTranscripts. +func (in *DataverseExporterTranscripts) DeepCopy() *DataverseExporterTranscripts { + if in == nil { + return nil + } + out := new(DataverseExporterTranscripts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DevSpec) DeepCopyInto(out *DevSpec) { *out = *in @@ -75,6 +131,11 @@ func (in *DevSpec) DeepCopyInto(out *DevSpec) { *out = new(bool) **out = **in } + if in.RhosMCP != nil { + in, out := &in.RhosMCP, &out.RhosMCP + *out = new(RhosMCPSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevSpec. @@ -88,16 +149,33 @@ func (in *DevSpec) DeepCopy() *DevSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LoggingConfig) DeepCopyInto(out *LoggingConfig) { +func (in *LightspeedSpec) DeepCopyInto(out *LightspeedSpec) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LightspeedSpec. +func (in *LightspeedSpec) DeepCopy() *LightspeedSpec { + if in == nil { + return nil + } + out := new(LightspeedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OGXSpec) DeepCopyInto(out *OGXSpec) { *out = *in + in.Resources.DeepCopyInto(&out.Resources) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoggingConfig. -func (in *LoggingConfig) DeepCopy() *LoggingConfig { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OGXSpec. +func (in *OGXSpec) DeepCopy() *OGXSpec { if in == nil { return nil } - out := new(LoggingConfig) + out := new(OGXSpec) in.DeepCopyInto(out) return out } @@ -110,6 +188,7 @@ func (in *OKPSpec) DeepCopyInto(out *OKPSpec) { *out = new(bool) **out = **in } + in.Resources.DeepCopyInto(&out.Resources) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OKPSpec. @@ -152,12 +231,21 @@ func (in *OpenStackLightspeed) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenStackLightspeedCore) DeepCopyInto(out *OpenStackLightspeedCore) { *out = *in - if in.FeedbackEnabled != nil { - in, out := &in.FeedbackEnabled, &out.FeedbackEnabled - *out = new(bool) - **out = **in + if in.DataverseExporter != nil { + in, out := &in.DataverseExporter, &out.DataverseExporter + *out = new(DataverseExporter) + (*in).DeepCopyInto(*out) + } + if in.OGX != nil { + in, out := &in.OGX, &out.OGX + *out = new(OGXSpec) + (*in).DeepCopyInto(*out) + } + if in.Lightspeed != nil { + in, out := &in.Lightspeed, &out.Lightspeed + *out = new(LightspeedSpec) + (*in).DeepCopyInto(*out) } - out.Logging = in.Logging } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedCore. @@ -236,8 +324,12 @@ func (in *OpenStackLightspeedSpec) DeepCopyInto(out *OpenStackLightspeedSpec) { *out = new(QuotaSpec) (*in).DeepCopyInto(*out) } - in.Resources.DeepCopyInto(&out.Resources) in.Dev.DeepCopyInto(&out.Dev) + if in.Console != nil { + in, out := &in.Console, &out.Console + *out = new(ConsoleSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedSpec. @@ -326,3 +418,19 @@ func (in *QuotaSpec) DeepCopy() *QuotaSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RhosMCPSpec) DeepCopyInto(out *RhosMCPSpec) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RhosMCPSpec. +func (in *RhosMCPSpec) DeepCopy() *RhosMCPSpec { + if in == nil { + return nil + } + out := new(RhosMCPSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml index 60587552..56c5c720 100644 --- a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -49,226 +49,11 @@ spec: spec: description: OpenStackLightspeedSpec defines the desired state of OpenStackLightspeed properties: - database: - description: |- - Database configures persistent storage for PostgreSQL data. - A PersistentVolumeClaim is always created and mounted; when Database - is omitted, the default size is used and the cluster's default - StorageClass applies. - properties: - class: - description: |- - StorageClass name for the PersistentVolumeClaim. If omitted, the cluster's - default StorageClass is used. - type: string - size: - anyOf: - - type: integer - - type: string - description: Size of the PersistentVolumeClaim for PostgreSQL - data. Defaults to 1Gi. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - dev: - description: |- - Dev contains developer/experimental configuration. - This section is not part of the stable API and may change at any time without backward compatibility. - type: object - x-kubernetes-preserve-unknown-fields: true - feedbackEnabled: - default: true - description: Enable feedback collection - type: boolean - llmAPIVersion: - description: LLM API Version for LLM providers that require it (e.g., - Microsoft Azure OpenAI) - type: string - llmCredentials: - description: |- - Secret name containing API token for the LLMEndpoint. The secret must contain - a field named "apitoken" which holds the token value. - type: string - llmDeploymentName: - description: Deployment name for LLM providers that require it (e.g., - Microsoft Azure OpenAI) - type: string - llmEndpoint: - description: URL pointing to the LLM - pattern: ^https?://.+ - type: string - llmEndpointType: - description: Type of the provider serving the LLM - enum: - - azure_openai - - openai - - watsonx - - rhoai_vllm - - rhelai_vllm - - gemini - type: string - llmProjectID: - description: Project ID for LLM providers that require it (e.g., WatsonX) - type: string - logging: - default: {} - description: Logging configuration for OpenStackLightspeed components - properties: - dataverseExporterLogLevel: - default: INFO - description: 'Log level for the dataverse exporter sidecar container. - Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, - CRITICAL.' - enum: - - DEBUG - - INFO - - WARNING - - ERROR - - CRITICAL - type: string - lightspeedStackLogLevel: - default: INFO - description: 'Log level for the lightspeed-service-api container. - Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, - CRITICAL.' - enum: - - DEBUG - - INFO - - WARNING - - ERROR - - CRITICAL - type: string - ogxLogLevel: - default: all=info - description: Log level configuration for the OGX/llama-stack container. - Supports standard levels (INFO, DEBUG) or fine-grained control - using format "component=level,component=level" (e.g., "core=debug,providers=info"). - pattern: ^\w+(?:=\w+)?(?:,\w+(?:=\w+)?)*$ - type: string - postgresLogLevel: - default: INFO - description: Log level for the PostgreSQL container. When set - to DEBUG, enables logging of all SQL statements (log_statement - = all). - enum: - - DEBUG - - INFO - type: string - type: object - maxTokensForResponse: - description: MaxTokensForResponse defines the maximum number of tokens - to be used for the response generation - minimum: 1 - type: integer - modelName: - description: Name of the model to use at the API endpoint provided - in LLMEndpoint - type: string - okp: - description: OKP configures the Offline Knowledge Portal (OKP) RAG - source. - properties: - accessKey: - description: |- - AccessKey is the name of the Secret containing the access key for the OKP server. - The secret must contain a key named "access_key". - An access key can be obtained from https://access.redhat.com/offline/access - type: string - offline: - default: true - description: |- - Offline controls how source URLs are resolved. - When true, uses parent_id (offline/Mimir-style). - When false, uses reference_url (online). - type: boolean - type: object - quotas: - description: |- - Quotas configures quota enforcement (limiters, scheduler, token history) for lightspeed-stack. - When omitted or Limiters is empty, quota enforcement is disabled. - properties: - enableTokenHistory: - default: false - description: EnableTokenHistory enables the token_usage table - for per-user/model/provider accounting. - type: boolean - limiters: - description: |- - Limiters configures the quota limiters enforced by lightspeed-stack. - When empty, quota enforcement is disabled. - items: - description: QuotaLimiterSpec defines a single quota limiter - enforced by lightspeed-stack. - properties: - initialQuota: - description: InitialQuota is the number of tokens granted - when the limiter resets. - minimum: 0 - type: integer - name: - description: Name is a human-readable identifier for the - limiter. - type: string - period: - description: Period is the quota reset interval, expressed - as an interval literal (e.g. "1 hour", "30 seconds", "1 - day", "1 hour 30 minutes"). - pattern: ^[1-9]\d*\s+(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months)(\s+[1-9]\d*\s+(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months))*$ - type: string - quotaIncrease: - description: QuotaIncrease is the number of tokens added - by the scheduler for this limiter. - minimum: 0 - type: integer - type: - description: 'Type of the limiter: userLimiter enforces - quota per user, clusterLimiter enforces quota across the - whole cluster.' - enum: - - userLimiter - - clusterLimiter - type: string - required: - - initialQuota - - name - - period - - quotaIncrease - - type - type: object - type: array - scheduler: - description: Scheduler configures the background quota reset/increase - scheduler. - properties: - databaseReconnectionCount: - default: 10 - description: DatabaseReconnectionCount is the number of times - the scheduler retries connecting to the database. - minimum: 1 - type: integer - databaseReconnectionDelay: - default: 1 - description: DatabaseReconnectionDelay is the delay, in seconds, - between database reconnection attempts. - minimum: 1 - type: integer - period: - default: 5 - description: Period is the interval, in seconds, at which - the scheduler checks limiters for reset/increase. - minimum: 1 - type: integer - type: object - type: object - resources: + console: default: {} - description: |- - Resources configures compute resource requirements for individual - containers managed by the operator. Each field has sensible defaults - applied by the API server. Override any container's resources to - replace its defaults entirely. + description: Console configures the lightspeed console plugin. properties: - consolePlugin: + resources: default: limits: cpu: 200m @@ -277,7 +62,7 @@ spec: cpu: 50m memory: 64Mi description: |- - ConsolePlugin sets compute resources for the lightspeed-console-plugin + Resources sets compute resources for the lightspeed-console-plugin container and its init container. properties: claims: @@ -336,17 +121,39 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object - lightspeedService: + type: object + database: + default: {} + description: |- + Database configures persistent storage for PostgreSQL data. + A PersistentVolumeClaim is always created and mounted; when Database + is omitted, the default size is used and the cluster's default + StorageClass applies. + properties: + class: + description: |- + StorageClass name for the PersistentVolumeClaim. If omitted, the cluster's + default StorageClass is used. + type: string + logLevel: + default: INFO + description: Log level for the PostgreSQL container. When set + to DEBUG, enables logging of all SQL statements (log_statement + = all). + enum: + - DEBUG + - INFO + type: string + resources: default: limits: - cpu: "1" + cpu: 500m memory: 2Gi requests: - cpu: 250m - memory: 512Mi - description: |- - LightspeedService sets compute resources for the lightspeed-service-api - container in the lightspeed-stack deployment. + cpu: 30m + memory: 300Mi + description: Rarources sets compute resources for the PostgreSQL + container. properties: claims: description: |- @@ -404,17 +211,81 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object - llamaStack: + size: + anyOf: + - type: integer + - type: string + description: Size of the PersistentVolumeClaim for PostgreSQL + data. Defaults to 1Gi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + dataverseExporter: + default: {} + description: DataverseExporter configures the dataverse exporter sidecar + (feedback, transcripts, logging). + properties: + feedback: + description: Feedback configures user feedback collection. + properties: + enabled: + default: true + description: Enable feedback collection. + type: boolean + type: object + logLevel: + default: INFO + description: 'Log level for the dataverse exporter sidecar container. + Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, + CRITICAL.' + enum: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + type: string + transcripts: + description: Transcripts configures conversation transcript collection. + properties: + enabled: + description: Enable conversation transcripts collection. + type: boolean + type: object + type: object + dev: + description: |- + Dev contains developer/experimental configuration. + This section is not part of the stable API and may change at any time without backward compatibility. + type: object + x-kubernetes-preserve-unknown-fields: true + lightspeed: + default: {} + description: Lightspeed configures the lightspeed-service-api container. + properties: + logLevel: + default: INFO + description: 'Log level for the lightspeed-service-api container. + Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, + CRITICAL.' + enum: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + type: string + resources: default: limits: - cpu: "2" - memory: 8Gi - requests: - cpu: 500m + cpu: "1" memory: 2Gi + requests: + cpu: 250m + memory: 512Mi description: |- - LlamaStack sets compute resources for the llama-stack (OGX) container - in the lightspeed-stack deployment. + Resources sets compute resources for the lightspeed-service-api + container in the lightspeed-stack deployment. properties: claims: description: |- @@ -472,16 +343,68 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object - mcp: + type: object + llmAPIVersion: + description: LLM API Version for LLM providers that require it (e.g., + Microsoft Azure OpenAI) + type: string + llmCredentials: + description: |- + Secret name containing API token for the LLMEndpoint. The secret must contain + a field named "apitoken" which holds the token value. + type: string + llmDeploymentName: + description: Deployment name for LLM providers that require it (e.g., + Microsoft Azure OpenAI) + type: string + llmEndpoint: + description: URL pointing to the LLM + pattern: ^https?://.+ + type: string + llmEndpointType: + description: Type of the provider serving the LLM + enum: + - azure_openai + - openai + - watsonx + - rhoai_vllm + - rhelai_vllm + - gemini + type: string + llmProjectID: + description: Project ID for LLM providers that require it (e.g., WatsonX) + type: string + maxTokensForResponse: + description: MaxTokensForResponse defines the maximum number of tokens + to be used for the response generation + minimum: 1 + type: integer + modelName: + description: Name of the model to use at the API endpoint provided + in LLMEndpoint + type: string + ogx: + default: {} + description: OGX configures the OGX/llama-stack container. + properties: + logLevel: + default: all=info + description: Log level configuration for the OGX/llama-stack container. + Supports standard levels (INFO, DEBUG) or fine-grained control + using format "component=level,component=level" (e.g., "core=debug,providers=info"). + pattern: ^\w+(?:=\w+)?(?:,\w+(?:=\w+)?)*$ + type: string + resources: default: limits: - memory: 200Mi + cpu: "2" + memory: 8Gi requests: - cpu: 50m - memory: 64Mi + cpu: 500m + memory: 2Gi description: |- - MCP sets compute resources for the RHOSO MCP server sidecar container - (only created when the rhoso_mcps feature flag is enabled). + Resources sets compute resources for the llama-stack (OGX) container + in the lightspeed-stack deployment. properties: claims: description: |- @@ -539,7 +462,26 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object - okp: + type: object + okp: + default: {} + description: OKP configures the Offline Knowledge Portal (OKP) RAG + source. + properties: + accessKey: + description: |- + AccessKey is the name of the Secret containing the access key for the OKP server. + The secret must contain a key named "access_key". + An access key can be obtained from https://access.redhat.com/offline/access + type: string + offline: + default: true + description: |- + Offline controls how source URLs are resolved. + When true, uses parent_id (offline/Mimir-style). + When false, uses reference_url (online). + type: boolean + resources: default: limits: cpu: "2" @@ -547,8 +489,8 @@ spec: requests: cpu: 500m memory: 2Gi - description: OKP sets compute resources for the Offline Knowledge - Portal container. + description: Resources sets compute resources for the Offline + Knowledge Portal container. properties: claims: description: |- @@ -606,80 +548,88 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object - postgres: - default: - limits: - cpu: 500m - memory: 2Gi - requests: - cpu: 30m - memory: 300Mi - description: Postgres sets compute resources for the PostgreSQL - container. + type: object + quotas: + description: |- + Quotas configures quota enforcement (limiters, scheduler, token history) for lightspeed-stack. + When omitted or Limiters is empty, quota enforcement is disabled. + properties: + enableTokenHistory: + default: false + description: EnableTokenHistory enables the token_usage table + for per-user/model/provider accounting. + type: boolean + limiters: + description: |- + Limiters configures the quota limiters enforced by lightspeed-stack. + When empty, quota enforcement is disabled. + items: + description: QuotaLimiterSpec defines a single quota limiter + enforced by lightspeed-stack. + properties: + initialQuota: + description: InitialQuota is the number of tokens granted + when the limiter resets. + minimum: 0 + type: integer + name: + description: Name is a human-readable identifier for the + limiter. + type: string + period: + description: Period is the quota reset interval, expressed + as an interval literal (e.g. "1 hour", "30 seconds", "1 + day", "1 hour 30 minutes"). + pattern: ^[1-9]\d*\s+(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months)(\s+[1-9]\d*\s+(second|seconds|minute|minutes|hour|hours|day|days|week|weeks|month|months))*$ + type: string + quotaIncrease: + description: QuotaIncrease is the number of tokens added + by the scheduler for this limiter. + minimum: 0 + type: integer + type: + description: 'Type of the limiter: userLimiter enforces + quota per user, clusterLimiter enforces quota across the + whole cluster.' + enum: + - userLimiter + - clusterLimiter + type: string + required: + - initialQuota + - name + - period + - quotaIncrease + - type + type: object + type: array + scheduler: + description: Scheduler configures the background quota reset/increase + scheduler. properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This is an alpha field and requires enabling the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object + databaseReconnectionCount: + default: 10 + description: DatabaseReconnectionCount is the number of times + the scheduler retries connecting to the database. + minimum: 1 + type: integer + databaseReconnectionDelay: + default: 1 + description: DatabaseReconnectionDelay is the delay, in seconds, + between database reconnection attempts. + minimum: 1 + type: integer + period: + default: 5 + description: Period is the interval, in seconds, at which + the scheduler checks limiters for reset/increase. + minimum: 1 + type: integer type: object type: object tlsCACertBundle: description: Configmap name containing a CA Certificates bundle type: string - transcriptsEnabled: - description: Enable conversation transcripts collection - type: boolean required: - llmCredentials - llmEndpoint diff --git a/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml b/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml index 6d9e89e5..f307d56b 100644 --- a/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml @@ -97,6 +97,18 @@ spec: name: openstack-lightspeed-database version: v1 specDescriptors: + - description: Log level for the PostgreSQL container. When set to DEBUG, enables + logging of all SQL statements (log_statement = all). + displayName: PostgreSQL Log Level + path: database.logLevel + - description: 'Log level for the dataverse exporter sidecar container. Supports + standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL.' + displayName: Dataverse Exporter Log Level + path: dataverseExporter.logLevel + - description: 'Log level for the lightspeed-service-api container. Supports + standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL.' + displayName: Lightspeed Stack Log Level + path: lightspeed.logLevel - description: |- Secret name containing API token for the LLMEndpoint. The secret must contain a field named "apitoken" which holds the token value. @@ -108,26 +120,14 @@ spec: - description: Type of the provider serving the LLM displayName: Provider Type path: llmEndpointType - - description: 'Log level for the dataverse exporter sidecar container. Supports - standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL.' - displayName: Dataverse Exporter Log Level - path: logging.dataverseExporterLogLevel - - description: 'Log level for the lightspeed-service-api container. Supports - standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL.' - displayName: Lightspeed Stack Log Level - path: logging.lightspeedStackLogLevel + - description: Name of the model to use at the API endpoint provided in LLMEndpoint + displayName: Model Name + path: modelName - description: Log level configuration for the OGX/llama-stack container. Supports standard levels (INFO, DEBUG) or fine-grained control using format "component=level,component=level" (e.g., "core=debug,providers=info"). displayName: OGX Log Level - path: logging.ogxLogLevel - - description: Log level for the PostgreSQL container. When set to DEBUG, enables - logging of all SQL statements (log_statement = all). - displayName: PostgreSQL Log Level - path: logging.postgresLogLevel - - description: Name of the model to use at the API endpoint provided in LLMEndpoint - displayName: Model Name - path: modelName + path: ogx.logLevel - description: Configmap name containing a CA Certificates bundle displayName: TLS CA Certificate Bundle path: tlsCACertBundle diff --git a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml index cae6fbb6..b352e91f 100644 --- a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml +++ b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml @@ -15,34 +15,41 @@ spec: # database: # size: "5Gi" # class: "my-storage-class" - # Uncomment to customize container resource requests/limits: - # resources: - # llamaStack: + # resources: + # requests: + # cpu: "30m" + # memory: "300Mi" + # limits: + # cpu: "500m" + # memory: "2Gi" + # logLevel: "INFO" + # Uncomment to customize OGX/llama-stack container resources and log level: + # ogx: + # resources: # requests: # cpu: "500m" # memory: "2Gi" # limits: # cpu: "2" # memory: "8Gi" - # lightspeedService: + # logLevel: "all=info" + # Uncomment to customize lightspeed-service-api container resources and log level: + # lightspeed: + # resources: # requests: # cpu: "250m" # memory: "512Mi" # limits: # cpu: "1" # memory: "2Gi" - # postgres: - # requests: - # cpu: "30m" - # memory: "300Mi" - # limits: - # cpu: "500m" - # memory: "2Gi" - # Uncomment to customize log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL): - # logging: - # ogxLogLevel: "all=info" - # lightspeedStackLogLevel: "INFO" - # dataverseExporterLogLevel: "INFO" + # logLevel: "INFO" + # Uncomment to customize dataverse exporter configuration: + # dataverseExporter: + # logLevel: "INFO" + # feedback: + # enabled: true + # transcripts: + # enabled: false # Uncomment to enable RHOSO MCPs (MCP server sidecar with OpenStack/OpenShift tools) # dev: # featureFlags: @@ -51,11 +58,34 @@ spec: # okpRagOnly: true # # Custom MCP server config (deep-merged with operator defaults; # # openstack.enabled and openshift.enabled are always overridden): - # rhosMCPConfig: | - # debug: true - # workers: 4 + # rhosMCP: + # config: | + # debug: true + # workers: 4 + # resources: + # requests: + # cpu: "50m" + # memory: "64Mi" + # limits: + # memory: "200Mi" # okp: # accessKey: okp-access-key-secret + # offline: true + # resources: + # requests: + # cpu: "500m" + # memory: "2Gi" + # limits: + # cpu: "2" + # memory: "4Gi" + # console: + # resources: + # requests: + # cpu: "50m" + # memory: "256Mi" + # limits: + # cpu: "1" + # memory: "2Gi" # Uncomment to enable quota enforcement (opt-in; omit or leave limiters empty to disable): # quotas: # limiters: diff --git a/docs/configuration.rst b/docs/configuration.rst index 37896554..39fa187e 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -44,12 +44,6 @@ Core fields * - ``llmAPIVersion`` - No - Required by some providers (e.g. Azure OpenAI). - * - ``feedbackEnabled`` - - No - - User feedback collection. Defaults to ``true``. - * - ``transcriptsEnabled`` - - No - - Conversation transcript collection. Defaults to ``false``. .. _supported-providers: @@ -69,7 +63,7 @@ Supported LLM providers (``llmEndpointType``) ``oc explain openstacklightspeed.spec.llmEndpointType`` on your cluster for the current, authoritative list. -Logging (``logging``) +Logging ----------------------- .. list-table:: @@ -79,20 +73,37 @@ Logging (``logging``) * - Field - Default - Description - * - ``logging.ogxLogLevel`` + * - ``ogx.logLevel`` - ``all=info`` - llama-stack/OGX container. Standard level, or ``component=level`` pairs (e.g. ``core=debug,providers=info``). - * - ``logging.lightspeedStackLogLevel`` + * - ``lightspeed.logLevel`` - ``INFO`` - lightspeed-service-api container. ``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``. - * - ``logging.dataverseExporterLogLevel`` - - ``INFO`` - - Feedback/transcript exporter sidecar. Same values as above. - * - ``logging.postgresLogLevel`` + * - ``database.logLevel`` - ``INFO`` - PostgreSQL container. ``DEBUG`` also logs every SQL statement. +Dataverse exporter (``dataverseExporter``) +-------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Field + - Default + - Description + * - ``dataverseExporter.logLevel`` + - ``INFO`` + - Feedback/transcript exporter sidecar. ``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``. + * - ``dataverseExporter.feedback.enabled`` + - ``true`` + - User feedback collection (thumbs-up/down on responses). + * - ``dataverseExporter.transcripts.enabled`` + - ``false`` + - Full conversation transcript collection. + Persistent storage (``database``) ------------------------------------ @@ -115,25 +126,26 @@ default entirely: .. code-block:: yaml spec: - resources: - llamaStack: + ogx: + resources: requests: {cpu: "500m", memory: "2Gi"} limits: {cpu: "2", memory: "8Gi"} - lightspeedService: + console: + resources: + requests: {cpu: "50m", memory: "64Mi"} + limits: {cpu: "200m", memory: "256Mi"} + lightspeed: + resources: requests: {cpu: "250m", memory: "512Mi"} limits: {cpu: "1", memory: "2Gi"} - postgres: + database: + resources: requests: {cpu: "30m", memory: "300Mi"} limits: {cpu: "500m", memory: "2Gi"} - okp: + okp: + resources: requests: {cpu: "500m", memory: "2Gi"} limits: {cpu: "2", memory: "4Gi"} - consolePlugin: - requests: {cpu: "50m", memory: "64Mi"} - limits: {cpu: "200m", memory: "256Mi"} - mcp: - requests: {cpu: "50m", memory: "64Mi"} - limits: {memory: "200Mi"} .. _offline-knowledge-portal: @@ -249,10 +261,14 @@ Developer / experimental options (``dev``) featureFlags: - rhoso_mcps # enables the read-only MCP introspection sidecar okpChunkFilterQuery: "product:(*openstack* OR *openshift*)" # example override - okpRagOnly: false # include bundled community docs too, not just OKP - rhosMCPConfig: | - debug: true - workers: 4 + okpRagOnly: true # include bundled community docs too, not just OKP + rhosMCP: + resources: + requests: {cpu: "50m", memory: "64Mi"} + limits: {memory: "200Mi"} + config: | + debug: true + workers: 4 * ``okpChunkFilterQuery`` and ``okpRagOnly`` take effect immediately, with no ``featureFlags`` entry needed — they're independent of @@ -261,7 +277,8 @@ Developer / experimental options (``dev``) * ``rhoso_mcps`` — the one flag that does need to be set. Deploys the MCP introspection sidecar, which is read-only **by default**. See :doc:`usage`. -* ``rhosMCPConfig`` is deep-merged on top of the operator's own defaults - — it can override anything the default config sets, including the - ``allow_write`` flags that keep introspection read-only. Only set this - if you understand exactly what you're overriding. +* ``rhosMCP`` is deep-merged on top of the operator's own defaults + — ``config`` can override anything the default config sets, including the + ``allow_write`` flags that keep introspection read-only. ``resources`` sets + compute resources for the rhos-mcps sidecar (defaults shown above). Only set + ``config`` if you understand exactly what you're overriding. diff --git a/docs/usage.rst b/docs/usage.rst index 8e19d029..6d962f97 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -52,8 +52,10 @@ configuration and an example. Feedback and transcripts ---------------------------- -* ``feedbackEnabled`` (default ``true``) — thumbs-up/down on responses. -* ``transcriptsEnabled`` (default ``false``) — full conversation transcripts. +Configured under ``dataverseExporter`` on the CR (:doc:`configuration`): + +* ``dataverseExporter.feedback.enabled`` (default ``true``) — thumbs-up/down on responses. +* ``dataverseExporter.transcripts.enabled`` (default ``false``) — full conversation transcripts. Both configured on the CR (:doc:`configuration`). Used to improve answer quality — disable either if that doesn't fit your data policy. diff --git a/internal/controller/assets/postgres.conf.tmpl b/internal/controller/assets/postgres.conf.tmpl index 7189ebad..059b32f7 100644 --- a/internal/controller/assets/postgres.conf.tmpl +++ b/internal/controller/assets/postgres.conf.tmpl @@ -11,6 +11,6 @@ logging_collector = off log_connections = on log_disconnections = on log_lock_waits = on -{{- if eq .PostgresLogLevel "DEBUG" }} +{{- if eq .LogLevel "DEBUG" }} log_statement = all {{- end }} diff --git a/internal/controller/common.go b/internal/controller/common.go index 075f840e..6284c469 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -35,6 +35,7 @@ import ( corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -166,6 +167,30 @@ func parseDevConfig(instance *apiv1beta1.OpenStackLightspeed) (apiv1beta1.DevSpe return devConfig, nil } +// defaultRhosMCPResources returns the default resource requirements for the rhos-mcps sidecar. +func defaultRhosMCPResources() corev1.ResourceRequirements { + return corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("200Mi"), + }, + } +} + +// getRhosMCPResources returns compute resources for the rhos-mcps sidecar from dev.rhosMCP.resources, +// falling back to operator defaults when unset. +func getRhosMCPResources(instance *apiv1beta1.OpenStackLightspeed) corev1.ResourceRequirements { + devConfig, _ := parseDevConfig(instance) + if devConfig.RhosMCP != nil && + (len(devConfig.RhosMCP.Resources.Requests) > 0 || len(devConfig.RhosMCP.Resources.Limits) > 0) { + return devConfig.RhosMCP.Resources + } + return defaultRhosMCPResources() +} + // isRHOSOMCPEnabled returns true if the "rhoso_mcps" feature flag is present in the dev config. func isRHOSOMCPEnabled(instance *apiv1beta1.OpenStackLightspeed) (bool, error) { devConfig, err := parseDevConfig(instance) diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index 7ca8799f..f5f640d2 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -17,9 +17,15 @@ limitations under the License. package controller import ( + "encoding/json" "fmt" "strings" "testing" + + apiv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // TestOKPChunkFilterQueryFmtExcludesOpenShiftVirtualization guards against OKP RAG @@ -118,3 +124,103 @@ func TestGenerateRandomStringUniqueness(t *testing.T) { t.Errorf("generateRandomString(%d) returned identical values across two calls: %q", length, a) } } + +func TestGetRhosMCPResources_DefaultsWhenUnset(t *testing.T) { + instance := &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + } + + resources := getRhosMCPResources(instance) + defaults := defaultRhosMCPResources() + + if !resources.Requests.Cpu().Equal(*defaults.Requests.Cpu()) { + t.Errorf("expected default CPU request %v, got %v", defaults.Requests.Cpu(), resources.Requests.Cpu()) + } + if !resources.Requests.Memory().Equal(*defaults.Requests.Memory()) { + t.Errorf("expected default memory request %v, got %v", defaults.Requests.Memory(), resources.Requests.Memory()) + } + if !resources.Limits.Memory().Equal(*defaults.Limits.Memory()) { + t.Errorf("expected default memory limit %v, got %v", defaults.Limits.Memory(), resources.Limits.Memory()) + } +} + +func TestGetRhosMCPResources_CustomFromDevConfig(t *testing.T) { + devRaw, err := json.Marshal(map[string]interface{}{ + "rhosMCP": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]string{ + "cpu": "100m", + "memory": "128Mi", + }, + "limits": map[string]string{ + "memory": "256Mi", + }, + }, + }, + }) + if err != nil { + t.Fatalf("failed to marshal dev config: %v", err) + } + + instance := &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: apiv1beta1.OpenStackLightspeedSpec{ + Dev: runtime.RawExtension{Raw: devRaw}, + }, + } + + resources := getRhosMCPResources(instance) + + expectedCPU := resource.MustParse("100m") + if !resources.Requests.Cpu().Equal(expectedCPU) { + t.Errorf("expected CPU request %v, got %v", expectedCPU, resources.Requests.Cpu()) + } + expectedMemory := resource.MustParse("128Mi") + if !resources.Requests.Memory().Equal(expectedMemory) { + t.Errorf("expected memory request %v, got %v", expectedMemory, resources.Requests.Memory()) + } + expectedLimit := resource.MustParse("256Mi") + if !resources.Limits.Memory().Equal(expectedLimit) { + t.Errorf("expected memory limit %v, got %v", expectedLimit, resources.Limits.Memory()) + } +} + +func TestBuildMCPServerConfigMap_UsesDevRhosMCPConfig(t *testing.T) { + devRaw, err := json.Marshal(map[string]interface{}{ + "rhosMCP": map[string]interface{}{ + "config": "debug: true\nworkers: 2\n", + }, + }) + if err != nil { + t.Fatalf("failed to marshal dev config: %v", err) + } + + instance := &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: apiv1beta1.OpenStackLightspeedSpec{ + Dev: runtime.RawExtension{Raw: devRaw}, + }, + } + + configMap, err := BuildMCPServerConfigMap(instance, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + configData := configMap.Data["config.yaml"] + if configData == "" { + t.Fatal("expected config.yaml data") + } + if !containsAll(configData, "debug: true", "workers: 2") { + t.Errorf("expected merged config to contain user overrides, got:\n%s", configData) + } +} + +func containsAll(s string, subs ...string) bool { + for _, sub := range subs { + if !strings.Contains(s, sub) { + return false + } + } + return true +} diff --git a/internal/controller/console_deployment.go b/internal/controller/console_deployment.go index 514a1fa1..12c0e299 100644 --- a/internal/controller/console_deployment.go +++ b/internal/controller/console_deployment.go @@ -49,7 +49,10 @@ const consoleLocalesPath = "/usr/share/nginx/html/locales/en/" + consoleLocalesF // Includes an init container that rewrites OpenShift references to OpenStack // in the locales JSON file using an emptyDir volume. func buildConsoleDeploymentSpec(consoleImage string, instance *apiv1beta1.OpenStackLightspeed) appsv1.DeploymentSpec { - consoleRes := instance.Spec.Resources.ConsolePlugin + consoleResources := corev1.ResourceRequirements{} + if instance.Spec.Console != nil { + consoleResources = instance.Spec.Console.Resources + } replicas := int32(1) volumeDefaultMode := VolumeDefaultMode @@ -89,7 +92,7 @@ func buildConsoleDeploymentSpec(consoleImage string, instance *apiv1beta1.OpenSt "awk '" + consoleLocalesRewriteAwk + "' " + consoleLocalesPath + " > /locales-rewrite/" + consoleLocalesFilename, }, - Resources: consoleRes, + Resources: consoleResources, VolumeMounts: []corev1.VolumeMount{ { Name: "locales-rewrite", @@ -113,7 +116,7 @@ func buildConsoleDeploymentSpec(consoleImage string, instance *apiv1beta1.OpenSt SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: toPtr(false), }, - Resources: consoleRes, + Resources: consoleResources, VolumeMounts: []corev1.VolumeMount{ { Name: "lightspeed-console-plugin-cert", diff --git a/internal/controller/lcore_config.go b/internal/controller/lcore_config.go index b4abd8c5..16303b3d 100644 --- a/internal/controller/lcore_config.go +++ b/internal/controller/lcore_config.go @@ -105,8 +105,8 @@ func buildLCoreLlamaStackConfig() map[string]interface{} { } func buildLCoreUserDataCollectionConfig(_ *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { - feedbackEnabled := instance.Spec.FeedbackEnabled == nil || *instance.Spec.FeedbackEnabled - transcriptsEnabled := instance.Spec.TranscriptsEnabled + feedbackEnabled := isDataverseExporterFeedbackEnabled(instance) + transcriptsEnabled := isDataverseExporterTranscriptsEnabled(instance) return map[string]interface{}{ "feedback_enabled": feedbackEnabled, @@ -237,7 +237,28 @@ func buildLCoreQuotaHandlersConfig(h *common_helper.Helper, instance *apiv1beta1 // isDataCollectionEnabled returns true if at least one of feedback or transcripts is enabled. func isDataCollectionEnabled(instance *apiv1beta1.OpenStackLightspeed) bool { - return (instance.Spec.FeedbackEnabled == nil || *instance.Spec.FeedbackEnabled) || instance.Spec.TranscriptsEnabled + return isDataverseExporterFeedbackEnabled(instance) || isDataverseExporterTranscriptsEnabled(instance) +} + +func isDataverseExporterFeedbackEnabled(instance *apiv1beta1.OpenStackLightspeed) bool { + if instance.Spec.DataverseExporter == nil || instance.Spec.DataverseExporter.Feedback == nil || instance.Spec.DataverseExporter.Feedback.Enabled == nil { + return true + } + return *instance.Spec.DataverseExporter.Feedback.Enabled +} + +func isDataverseExporterTranscriptsEnabled(instance *apiv1beta1.OpenStackLightspeed) bool { + if instance.Spec.DataverseExporter == nil || instance.Spec.DataverseExporter.Transcripts == nil { + return false + } + return instance.Spec.DataverseExporter.Transcripts.Enabled +} + +func dataverseExporterLogLevel(instance *apiv1beta1.OpenStackLightspeed) string { + if instance.Spec.DataverseExporter == nil { + return "" + } + return instance.Spec.DataverseExporter.LogLevel } // buildExporterConfigMap creates the ConfigMap for the dataverse exporter sidecar. diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index 6a8d9c59..902ef528 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -69,6 +69,11 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins ReadOnly: true, }) + ogxResources := corev1.ResourceRequirements{} + if instance.Spec.OGX != nil { + ogxResources = instance.Spec.OGX.Resources + } + llamaStackContainer := corev1.Container{ Name: "llama-stack", Image: apiv1beta1.OpenStackLightspeedDefaultValues.LCoreImageURL, @@ -109,7 +114,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins TimeoutSeconds: LlamaStackProbeTimeoutSeconds, FailureThreshold: LlamaStackProbeFailureThreshold, }, - Resources: instance.Spec.Resources.LlamaStack, + Resources: ogxResources, ImagePullPolicy: corev1.PullIfNotPresent, } @@ -135,6 +140,11 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins }) } + lightspeedResources := corev1.ResourceRequirements{} + if instance.Spec.Lightspeed != nil { + lightspeedResources = instance.Spec.Lightspeed.Resources + } + lightspeedStackContainer := corev1.Container{ Name: "lightspeed-service-api", Image: apiv1beta1.OpenStackLightspeedDefaultValues.LCoreImageURL, @@ -145,7 +155,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins StartupProbe: buildLightspeedStackStartupProbe(), LivenessProbe: buildLightspeedStackLivenessProbe(), ReadinessProbe: buildLightspeedStackReadinessProbe(), - Resources: instance.Spec.Resources.LightspeedService, + Resources: lightspeedResources, ImagePullPolicy: corev1.PullIfNotPresent, } containers := []corev1.Container{llamaStackContainer, lightspeedStackContainer} @@ -159,7 +169,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins Args: []string{ "--mode", "openshift", "--config", path.Join(ExporterConfigMountPath, ExporterConfigFilename), - "--log-level", instance.Spec.Logging.DataverseExporterLogLevel, + "--log-level", dataverseExporterLogLevel(instance), "--data-dir", LCoreUserDataMountPath, }, VolumeMounts: []corev1.VolumeMount{ @@ -205,7 +215,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins Name: "rhoso-mcps", Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL, VolumeMounts: mcpMounts, - Resources: instance.Spec.Resources.MCP, + Resources: getRhosMCPResources(instance), LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -702,7 +712,7 @@ func buildLightspeedStackEnvVars(instance *apiv1beta1.OpenStackLightspeed) []cor envVars := []corev1.EnvVar{ { Name: "LIGHTSPEED_STACK_LOG_LEVEL", - Value: instance.Spec.Logging.LightspeedStackLogLevel, + Value: getLightspeedLogLevel(instance), }, } envVars = append(envVars, corev1.EnvVar{ @@ -777,12 +787,24 @@ func buildLightspeedStackReadinessProbe() *corev1.Probe { } } +// getLightspeedLogLevel returns the log level for the lightspeed-service-api container. +// Defaults to "INFO" when unset. +func getLightspeedLogLevel(instance *apiv1beta1.OpenStackLightspeed) string { + if instance.Spec.Lightspeed != nil && instance.Spec.Lightspeed.LogLevel != "" { + return instance.Spec.Lightspeed.LogLevel + } + return "INFO" +} + // getOGXLogLevel returns the log level for OGX/llama-stack container. // Supports either standard levels (INFO, DEBUG, WARNING, ERROR, CRITICAL) or fine-grained control. // Examples: "INFO" -> "all=info", "DEBUG" -> "all=debug", "core=debug,providers=info" -> "core=debug,providers=info" // Defaults to "all=info" if not specified. func getOGXLogLevel(instance *apiv1beta1.OpenStackLightspeed) string { - logLevel := instance.Spec.Logging.OGXLogLevel + logLevel := "" + if instance.Spec.OGX != nil { + logLevel = instance.Spec.OGX.LogLevel + } // If it's a simple level (INFO, DEBUG, etc.), convert to "all=" format // Otherwise, pass through for fine-grained control (e.g., "core=debug,providers=info") diff --git a/internal/controller/mcp_server.go b/internal/controller/mcp_server.go index dea0d7a8..a088c0e9 100644 --- a/internal/controller/mcp_server.go +++ b/internal/controller/mcp_server.go @@ -97,9 +97,9 @@ func deepMerge(base, override map[string]interface{}) map[string]interface{} { } // buildMCPServerConfigData renders the MCP server config template and, when -// rhosMCPConfig is provided, deep-merges the user config on top. The +// rhosMCP is provided, deep-merges the user config on top. The // openstack.enabled and openshift.enabled flags are always enforced. -func buildMCPServerConfigData(openStackReady bool, rhosMCPConfig string) (string, error) { +func buildMCPServerConfigData(openStackReady bool, rhosMCP string) (string, error) { var buf bytes.Buffer err := mcpServerConfigTmpl.Execute(&buf, mcpServerConfigParams{ OpenStackEnabled: openStackReady, @@ -109,7 +109,7 @@ func buildMCPServerConfigData(openStackReady bool, rhosMCPConfig string) (string return "", fmt.Errorf("failed to render MCP server config template: %w", err) } - if rhosMCPConfig == "" { + if rhosMCP == "" { return buf.String(), nil } @@ -119,8 +119,8 @@ func buildMCPServerConfigData(openStackReady bool, rhosMCPConfig string) (string } var userConfig map[string]interface{} - if err := yaml.Unmarshal([]byte(rhosMCPConfig), &userConfig); err != nil { - return "", fmt.Errorf("failed to parse rhosMCPConfig: %w", err) + if err := yaml.Unmarshal([]byte(rhosMCP), &userConfig); err != nil { + return "", fmt.Errorf("failed to parse rhosMCP: %w", err) } merged := deepMerge(baseConfig, userConfig) @@ -152,7 +152,11 @@ func BuildMCPServerConfigMap( openStackReady bool, ) (corev1.ConfigMap, error) { devConfig, _ := parseDevConfig(instance) - configData, err := buildMCPServerConfigData(openStackReady, devConfig.RhosMCPConfig) + rhosMCPConfigYAML := "" + if devConfig.RhosMCP != nil { + rhosMCPConfigYAML = devConfig.RhosMCP.Config + } + configData, err := buildMCPServerConfigData(openStackReady, rhosMCPConfigYAML) if err != nil { return corev1.ConfigMap{}, err } diff --git a/internal/controller/okp_reconciler.go b/internal/controller/okp_reconciler.go index a2e453ba..45fc3c2d 100644 --- a/internal/controller/okp_reconciler.go +++ b/internal/controller/okp_reconciler.go @@ -118,6 +118,11 @@ func buildOKPPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) corev1.Po }) } + resources := corev1.ResourceRequirements{} + if instance.Spec.OKP != nil { + resources = instance.Spec.OKP.Resources + } + return corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: generateOKPSelectorLabels(), @@ -150,7 +155,7 @@ func buildOKPPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) corev1.Po InitialDelaySeconds: 60, PeriodSeconds: 20, }, - Resources: instance.Spec.Resources.OKP, + Resources: resources, ImagePullPolicy: corev1.PullIfNotPresent, }, }, diff --git a/internal/controller/postgres_deployment.go b/internal/controller/postgres_deployment.go index c2c86953..5b45503f 100644 --- a/internal/controller/postgres_deployment.go +++ b/internal/controller/postgres_deployment.go @@ -148,6 +148,11 @@ func buildPostgresPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) core } envVars = append(envVars, buildPostgresCredsEnvVars()...) + resources := corev1.ResourceRequirements{} + if instance.Spec.Database != nil { + resources = instance.Spec.Database.Resources + } + return corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: generatePostgresSelectorLabels(), @@ -179,7 +184,7 @@ func buildPostgresPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) core LivenessProbe: buildPostgresProbe(PostgresLivenessProbePeriodSeconds, PostgresLivenessProbeTimeoutSeconds, PostgresLivenessProbeFailureThreshold, 0), ReadinessProbe: buildPostgresProbe(PostgresReadinessProbePeriodSeconds, PostgresReadinessProbeTimeoutSeconds, PostgresReadinessProbeFailureThreshold, 0), VolumeMounts: volumeMounts, - Resources: instance.Spec.Resources.Postgres, + Resources: resources, Env: envVars, }, }, diff --git a/internal/controller/postgres_reconciler.go b/internal/controller/postgres_reconciler.go index 87613ad4..b60318e1 100644 --- a/internal/controller/postgres_reconciler.go +++ b/internal/controller/postgres_reconciler.go @@ -41,11 +41,16 @@ var postgresConfigTmpl = template.Must( ) func buildPostgresConfig(instance *apiv1beta1.OpenStackLightspeed) (string, error) { + logLevel := "" + if instance.Spec.Database != nil { + logLevel = instance.Spec.Database.LogLevel + } + var buf bytes.Buffer err := postgresConfigTmpl.Execute(&buf, struct { - PostgresLogLevel string + LogLevel string }{ - PostgresLogLevel: instance.Spec.Logging.PostgresLogLevel, + LogLevel: logLevel, }) if err != nil { return "", err diff --git a/test/kuttl/common/openstack-lightspeed-instance/assert-openstack-lightspeed-instance.yaml b/test/kuttl/common/openstack-lightspeed-instance/assert-openstack-lightspeed-instance.yaml index 82010fd3..eb72062f 100644 --- a/test/kuttl/common/openstack-lightspeed-instance/assert-openstack-lightspeed-instance.yaml +++ b/test/kuttl/common/openstack-lightspeed-instance/assert-openstack-lightspeed-instance.yaml @@ -461,9 +461,52 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING + console: + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + database: + logLevel: INFO + resources: + limits: + cpu: 500m + memory: 2Gi + requests: + cpu: 30m + memory: 300Mi + dataverseExporter: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 512Mi + ogx: + logLevel: DEBUG + resources: + limits: + cpu: "2" + memory: 8Gi + requests: + cpu: 500m + memory: 2Gi + okp: + offline: true + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: 500m + memory: 2Gi status: conditions: - type: Ready diff --git a/test/kuttl/common/openstack-lightspeed-instance/create-openstack-lightspeed-instance.yaml b/test/kuttl/common/openstack-lightspeed-instance/create-openstack-lightspeed-instance.yaml index 859ab461..5ce8d5b1 100644 --- a/test/kuttl/common/openstack-lightspeed-instance/create-openstack-lightspeed-instance.yaml +++ b/test/kuttl/common/openstack-lightspeed-instance/create-openstack-lightspeed-instance.yaml @@ -13,10 +13,12 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: okpChunkFilterQuery: "product:(*openstack* OR *openshift*)" quotas: diff --git a/test/kuttl/tests/application-credentials/05-create-openstack-lightspeed-instance.yaml b/test/kuttl/tests/application-credentials/05-create-openstack-lightspeed-instance.yaml index dea52c6d..ac34e8d0 100644 --- a/test/kuttl/tests/application-credentials/05-create-openstack-lightspeed-instance.yaml +++ b/test/kuttl/tests/application-credentials/05-create-openstack-lightspeed-instance.yaml @@ -13,10 +13,12 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: featureFlags: - rhoso_mcps diff --git a/test/kuttl/tests/dynamic-crd-watch-recovery/05-create-openstack-lightspeed-instance.yaml b/test/kuttl/tests/dynamic-crd-watch-recovery/05-create-openstack-lightspeed-instance.yaml index dea52c6d..ac34e8d0 100644 --- a/test/kuttl/tests/dynamic-crd-watch-recovery/05-create-openstack-lightspeed-instance.yaml +++ b/test/kuttl/tests/dynamic-crd-watch-recovery/05-create-openstack-lightspeed-instance.yaml @@ -13,10 +13,12 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: featureFlags: - rhoso_mcps diff --git a/test/kuttl/tests/persistent-database/04-assert-openstack-lightspeed-instance.yaml b/test/kuttl/tests/persistent-database/04-assert-openstack-lightspeed-instance.yaml index 9d391c46..4930aaf0 100644 --- a/test/kuttl/tests/persistent-database/04-assert-openstack-lightspeed-instance.yaml +++ b/test/kuttl/tests/persistent-database/04-assert-openstack-lightspeed-instance.yaml @@ -43,6 +43,14 @@ metadata: namespace: openstack-lightspeed spec: database: + logLevel: INFO + resources: + limits: + cpu: 500m + memory: 2Gi + requests: + cpu: 30m + memory: 300Mi size: "2Gi" status: conditions: diff --git a/test/kuttl/tests/rhoso-mcps-configuration/02-create-rhoso-mcps-resources.yaml b/test/kuttl/tests/rhoso-mcps-configuration/02-create-rhoso-mcps-resources.yaml index dea52c6d..ac34e8d0 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/02-create-rhoso-mcps-resources.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/02-create-rhoso-mcps-resources.yaml @@ -13,10 +13,12 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: featureFlags: - rhoso_mcps diff --git a/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml b/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml index 33ef4d27..8c4cbbab 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml @@ -13,18 +13,21 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: featureFlags: - rhoso_mcps - rhosMCPConfig: | - debug: true - workers: 4 - openstack: - enabled: true - allow_write: true - openshift: - enabled: false + rhosMCP: + config: | + debug: true + workers: 4 + openstack: + enabled: true + allow_write: true + openshift: + enabled: false diff --git a/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml b/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml index 9b95675f..71bfe958 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml @@ -1,5 +1,5 @@ ############################################################################## -# Assert that rhosMCPConfig is deep-merged with operator defaults # +# Assert that rhosMCP is deep-merged with operator defaults # ############################################################################## apiVersion: kuttl.dev/v1beta1 kind: TestAssert diff --git a/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml b/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml index bca96d83..81ffe88d 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml @@ -13,9 +13,11 @@ spec: llmProjectID: test-project-id llmDeploymentName: test-deployment-name llmAPIVersion: v1 - logging: - ogxLogLevel: DEBUG - lightspeedStackLogLevel: WARNING - dataverseExporterLogLevel: DEBUG + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG dev: featureFlags: [] diff --git a/test/kuttl/tests/update-openstacklightspeed/07-update-openstack-lightspeed-instance.yaml b/test/kuttl/tests/update-openstacklightspeed/07-update-openstack-lightspeed-instance.yaml index cef67dc3..2ed8f947 100644 --- a/test/kuttl/tests/update-openstacklightspeed/07-update-openstack-lightspeed-instance.yaml +++ b/test/kuttl/tests/update-openstacklightspeed/07-update-openstack-lightspeed-instance.yaml @@ -28,47 +28,53 @@ spec: llmProjectID: test-project-id-UPDATE llmDeploymentName: test-deployment-name-UPDATE llmAPIVersion: v1.1 - feedbackEnabled: false - transcriptsEnabled: false - logging: - ogxLogLevel: "core=debug,providers=info" - lightspeedStackLogLevel: ERROR - dataverseExporterLogLevel: ERROR - resources: - llamaStack: + dataverseExporter: + feedback: + enabled: false + transcripts: + enabled: false + logLevel: ERROR + ogx: + logLevel: "core=debug,providers=info" + resources: requests: cpu: "500m" memory: "2Gi" limits: cpu: "2" memory: "9Gi" - lightspeedService: + lightspeed: + logLevel: ERROR + resources: requests: cpu: "250m" memory: "512Mi" limits: cpu: "1" memory: "3Gi" - postgres: + console: + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "300Mi" + database: + resources: requests: cpu: "30m" memory: "300Mi" limits: cpu: "500m" memory: "3Gi" - okp: + okp: + resources: requests: cpu: "500m" memory: "2Gi" limits: cpu: "2" memory: "5Gi" - consolePlugin: - requests: - cpu: "50m" - memory: "64Mi" - limits: - cpu: "200m" - memory: "300Mi" dev: okpChunkFilterQuery: "product:(*openstack* OR *openshift*)" diff --git a/test/kuttl/tests/update-openstacklightspeed/08-assert-openstacklightspeed-update.yaml b/test/kuttl/tests/update-openstacklightspeed/08-assert-openstacklightspeed-update.yaml index 7af4dd82..2fb0ed39 100644 --- a/test/kuttl/tests/update-openstacklightspeed/08-assert-openstacklightspeed-update.yaml +++ b/test/kuttl/tests/update-openstacklightspeed/08-assert-openstacklightspeed-update.yaml @@ -329,47 +329,54 @@ spec: llmProjectID: test-project-id-UPDATE llmDeploymentName: test-deployment-name-UPDATE llmAPIVersion: v1.1 - feedbackEnabled: false - transcriptsEnabled: false - resources: - llamaStack: + dataverseExporter: + feedback: + enabled: false + transcripts: + enabled: false + logLevel: ERROR + ogx: + logLevel: "core=debug,providers=info" + resources: requests: cpu: 500m memory: 2Gi limits: cpu: "2" memory: 9Gi - lightspeedService: + console: + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 300Mi + lightspeed: + logLevel: ERROR + resources: requests: cpu: 250m memory: 512Mi limits: cpu: "1" memory: 3Gi - postgres: + database: + resources: requests: cpu: 30m memory: 300Mi limits: cpu: 500m memory: 3Gi - okp: + okp: + resources: requests: cpu: 500m memory: 2Gi limits: cpu: "2" memory: 5Gi - consolePlugin: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 200m - memory: 300Mi - logging: - ogxLogLevel: "core=debug,providers=info" - lightspeedStackLogLevel: ERROR status: conditions: - type: Ready From 07e951447f80a546e5d2e8b50a2512058380a6a3 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Fri, 11 Sep 2026 22:39:12 +0200 Subject: [PATCH 2/3] Add startup probe for MCP The MCP container is missing an MCP startup probe like other containers, so we add it in this patch. --- internal/controller/constants.go | 9 +++++---- internal/controller/lcore_deployment.go | 11 +++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 94bbd309..c1e176af 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -151,10 +151,11 @@ const ( // -- Health probe settings for the rhoso-mcps container. -------------------- - MCPServerHealthPath = "/health" - MCPServerProbePeriodSeconds = int32(10) - MCPServerProbeTimeoutSeconds = int32(5) - MCPServerProbeFailureThreshold = int32(3) + MCPServerHealthPath = "/health" + MCPServerProbePeriodSeconds = int32(10) + MCPServerProbeTimeoutSeconds = int32(5) + MCPServerStartupProbeFailureThreshold = int32(30) + MCPServerProbeFailureThreshold = int32(3) // --------------------------------------------------------------------------- diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index 902ef528..ba8b46ad 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -216,6 +216,17 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL, VolumeMounts: mcpMounts, Resources: getRhosMCPResources(instance), + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: MCPServerHealthPath, + Port: intstr.FromInt32(MCPServerPort), + }, + }, + PeriodSeconds: MCPServerProbePeriodSeconds, + TimeoutSeconds: MCPServerProbeTimeoutSeconds, + FailureThreshold: MCPServerStartupProbeFailureThreshold, + }, LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ From 9c634a97ec6a586c18f1082db643fc13c4086bfa Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Thu, 10 Sep 2026 13:14:24 +0200 Subject: [PATCH 3/3] Support changing containter images Add functionality to allow changing the image for the different containers. Image is set in the `imageContainer` field that lives in each of the different sections with a new section named `rag` being added and allowing to set a different image for ogx/llama-stack and the lightspeed service itself. New environmental variable for OGX is also added named RELATED_IMAGE_OGX_IMAGE_URL_DEFAULT. This changes the behavior of the OGX image which previously would *always* be the same as for lightspeed. Now they have the same initial default, but they are not tied together, so changing the lightspeed image (via env vars or CR) will not make OGX use the same image. Jira: OSPRH-34773 Co-Authored-By: Claude Opus 4.6 --- api/v1beta1/openstacklightspeed_dev.go | 31 +++ api/v1beta1/openstacklightspeed_images.go | 98 ++++++++++ .../openstacklightspeed_images_test.go | 182 ++++++++++++++++++ api/v1beta1/openstacklightspeed_types.go | 46 ++++- api/v1beta1/zz_generated.deepcopy.go | 20 ++ ...ed.openstack.org_openstacklightspeeds.yaml | 32 +++ config/manager/manager.yaml | 2 + ...tspeed-operator.clusterserviceversion.yaml | 2 + ...ightspeed_v1beta1_openstacklightspeed.yaml | 10 + docs/configuration.rst | 39 ++++ hack/env.sh | 1 + internal/controller/common.go | 19 +- internal/controller/console_reconciler.go | 14 +- .../controller/console_reconciler_test.go | 15 ++ internal/controller/container_images_test.go | 140 ++++++++++++++ internal/controller/lcore_deployment.go | 14 +- internal/controller/mcp_server.go | 2 +- internal/controller/okp_reconciler.go | 2 +- .../openstacklightspeed_controller.go | 2 +- internal/controller/postgres_deployment.go | 2 +- .../00-mock-resources.yaml | 1 + .../01-assert-mock-objects-created.yaml | 1 + ...tack-lightspeed-with-container-images.yaml | 47 +++++ .../03-assert-container-images.yaml | 149 ++++++++++++++ .../04-update-console-container-image.yaml | 47 +++++ .../05-assert-updated-console-image.yaml | 51 +++++ ...leanup-openstack-lightspeed-instance.yaml} | 0 ...errors-openstack-lightspeed-instance.yaml} | 0 .../08-cleanup-mock-objects.yaml} | 0 .../09-errors-mock-objects.yaml} | 0 .../03-assert-rhoso-mcps-instance.yaml | 1 + .../04-update-rhos-mcp-config.yaml | 3 +- .../05-assert-rhos-mcp-config-merged.yaml | 6 +- .../06-assert-rhos-mcp-container-image.yaml | 26 +++ .../07-update-rhos-mcp-container-image.yaml | 31 +++ ...sert-rhos-mcp-default-container-image.yaml | 26 +++ ...o-mcps.yaml => 09-disable-rhoso-mcps.yaml} | 0 ...yaml => 10-errors-rhoso-mcps-cleanup.yaml} | 0 ...cleanup-openstack-lightspeed-instance.yaml | 1 + ...-errors-openstack-lightspeed-instance.yaml | 1 + .../13-cleanup-mock-objects.yaml | 1 + .../14-errors-mock-objects.yaml | 1 + 42 files changed, 1027 insertions(+), 39 deletions(-) create mode 100644 api/v1beta1/openstacklightspeed_dev.go create mode 100644 api/v1beta1/openstacklightspeed_images.go create mode 100644 api/v1beta1/openstacklightspeed_images_test.go create mode 100644 internal/controller/container_images_test.go create mode 120000 test/kuttl/tests/container-image-overrides/00-mock-resources.yaml create mode 120000 test/kuttl/tests/container-image-overrides/01-assert-mock-objects-created.yaml create mode 100644 test/kuttl/tests/container-image-overrides/02-create-openstack-lightspeed-with-container-images.yaml create mode 100644 test/kuttl/tests/container-image-overrides/03-assert-container-images.yaml create mode 100644 test/kuttl/tests/container-image-overrides/04-update-console-container-image.yaml create mode 100644 test/kuttl/tests/container-image-overrides/05-assert-updated-console-image.yaml rename test/kuttl/tests/{rhoso-mcps-configuration/08-cleanup-openstack-lightspeed-instance.yaml => container-image-overrides/06-cleanup-openstack-lightspeed-instance.yaml} (100%) rename test/kuttl/tests/{rhoso-mcps-configuration/09-errors-openstack-lightspeed-instance.yaml => container-image-overrides/07-errors-openstack-lightspeed-instance.yaml} (100%) rename test/kuttl/tests/{rhoso-mcps-configuration/10-cleanup-mock-objects.yaml => container-image-overrides/08-cleanup-mock-objects.yaml} (100%) rename test/kuttl/tests/{rhoso-mcps-configuration/11-errors-mock-objects.yaml => container-image-overrides/09-errors-mock-objects.yaml} (100%) create mode 100644 test/kuttl/tests/rhoso-mcps-configuration/06-assert-rhos-mcp-container-image.yaml create mode 100644 test/kuttl/tests/rhoso-mcps-configuration/07-update-rhos-mcp-container-image.yaml create mode 100644 test/kuttl/tests/rhoso-mcps-configuration/08-assert-rhos-mcp-default-container-image.yaml rename test/kuttl/tests/rhoso-mcps-configuration/{06-disable-rhoso-mcps.yaml => 09-disable-rhoso-mcps.yaml} (100%) rename test/kuttl/tests/rhoso-mcps-configuration/{07-errors-rhoso-mcps-cleanup.yaml => 10-errors-rhoso-mcps-cleanup.yaml} (100%) create mode 120000 test/kuttl/tests/rhoso-mcps-configuration/11-cleanup-openstack-lightspeed-instance.yaml create mode 120000 test/kuttl/tests/rhoso-mcps-configuration/12-errors-openstack-lightspeed-instance.yaml create mode 120000 test/kuttl/tests/rhoso-mcps-configuration/13-cleanup-mock-objects.yaml create mode 120000 test/kuttl/tests/rhoso-mcps-configuration/14-errors-mock-objects.yaml diff --git a/api/v1beta1/openstacklightspeed_dev.go b/api/v1beta1/openstacklightspeed_dev.go new file mode 100644 index 00000000..128650ab --- /dev/null +++ b/api/v1beta1/openstacklightspeed_dev.go @@ -0,0 +1,31 @@ +/* +Copyright 2026 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import "encoding/json" + +// ParseDevConfig unmarshals the Dev RawExtension into a DevSpec. +// Returns a zero-value DevSpec and an error on malformed input. +func (instance *OpenStackLightspeed) ParseDevConfig() (DevSpec, error) { + var devConfig DevSpec + if len(instance.Spec.Dev.Raw) > 0 { + if err := json.Unmarshal(instance.Spec.Dev.Raw, &devConfig); err != nil { + return devConfig, err + } + } + return devConfig, nil +} diff --git a/api/v1beta1/openstacklightspeed_images.go b/api/v1beta1/openstacklightspeed_images.go new file mode 100644 index 00000000..aa4a478a --- /dev/null +++ b/api/v1beta1/openstacklightspeed_images.go @@ -0,0 +1,98 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +func resolveContainerImage(manifestImage, defaultImage string) string { + if manifestImage != "" { + return manifestImage + } + return defaultImage +} + +// RAGContainerImage returns the RAG init-container image for this instance. +func (instance *OpenStackLightspeed) RAGContainerImage() string { + manifestImage := "" + if instance.Spec.RAG != nil { + manifestImage = instance.Spec.RAG.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.RAGImageURL) +} + +// OGXContainerImage returns the OGX/llama-stack container image for this instance. +func (instance *OpenStackLightspeed) OGXContainerImage() string { + manifestImage := "" + if instance.Spec.OGX != nil { + manifestImage = instance.Spec.OGX.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.OGXImageURL) +} + +// LightspeedContainerImage returns the lightspeed-service-api container image for this instance. +func (instance *OpenStackLightspeed) LightspeedContainerImage() string { + manifestImage := "" + if instance.Spec.Lightspeed != nil { + manifestImage = instance.Spec.Lightspeed.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.LCoreImageURL) +} + +// ExporterContainerImage returns the dataverse exporter sidecar container image for this instance. +func (instance *OpenStackLightspeed) ExporterContainerImage() string { + manifestImage := "" + if instance.Spec.DataverseExporter != nil { + manifestImage = instance.Spec.DataverseExporter.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.ExporterImageURL) +} + +// PostgresContainerImage returns the PostgreSQL container image for this instance. +func (instance *OpenStackLightspeed) PostgresContainerImage() string { + manifestImage := "" + if instance.Spec.Database != nil { + manifestImage = instance.Spec.Database.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.PostgresImageURL) +} + +// OKPContainerImage returns the OKP container image for this instance. +func (instance *OpenStackLightspeed) OKPContainerImage() string { + manifestImage := "" + if instance.Spec.OKP != nil { + manifestImage = instance.Spec.OKP.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.OKPImageURL) +} + +// ConsoleContainerImage returns the console plugin container image for this instance. +// When spec.console.containerImage is unset, ocpDefault is used (typically PF5/PF6 selection). +func (instance *OpenStackLightspeed) ConsoleContainerImage(ocpDefault string) string { + if instance.Spec.Console != nil && instance.Spec.Console.ContainerImage != "" { + return instance.Spec.Console.ContainerImage + } + return ocpDefault +} + +// MCPContainerImage returns the MCP container image for this instance. +func (instance *OpenStackLightspeed) MCPContainerImage() string { + manifestImage := "" + + devConfig, err := instance.ParseDevConfig() + if err == nil && devConfig.RhosMCP != nil && devConfig.RhosMCP.ContainerImage != "" { + manifestImage = devConfig.RhosMCP.ContainerImage + } + return resolveContainerImage(manifestImage, OpenStackLightspeedDefaultValues.MCPServerImageURL) +} diff --git a/api/v1beta1/openstacklightspeed_images_test.go b/api/v1beta1/openstacklightspeed_images_test.go new file mode 100644 index 00000000..257182d7 --- /dev/null +++ b/api/v1beta1/openstacklightspeed_images_test.go @@ -0,0 +1,182 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "testing" +) + +func TestResolveContainerImage(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + manifestImage string + defaultImage string + expectedResult string + }{ + { + name: "manifest override", + manifestImage: "example.com/custom:1.0", + defaultImage: "example.com/default:1.0", + expectedResult: "example.com/custom:1.0", + }, + { + name: "empty manifest uses default", + manifestImage: "", + defaultImage: "example.com/default:1.0", + expectedResult: "example.com/default:1.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := resolveContainerImage(tt.manifestImage, tt.defaultImage); got != tt.expectedResult { + t.Errorf("resolveContainerImage() = %q, want %q", got, tt.expectedResult) + } + }) + } +} + +func TestOpenStackLightspeedContainerImages(t *testing.T) { + OpenStackLightspeedDefaultValues = OpenStackLightspeedDefaults{ + RAGImageURL: "default/rag:1", + LCoreImageURL: "default/lcore:1", + OGXImageURL: "default/ogx:1", + ExporterImageURL: "default/exporter:1", + PostgresImageURL: "default/postgres:1", + OKPImageURL: "default/okp:1", + } + + instance := &OpenStackLightspeed{ + Spec: OpenStackLightspeedSpec{ + OpenStackLightspeedCore: OpenStackLightspeedCore{ + RAG: &RAG{ContainerImage: "custom/rag:2"}, + OGX: &OGXSpec{ContainerImage: "custom/ogx:2"}, + Lightspeed: &LightspeedSpec{ContainerImage: "custom/lcore:2"}, + DataverseExporter: &DataverseExporter{ContainerImage: "custom/exporter:2"}, + }, + Console: &ConsoleSpec{ContainerImage: "custom/console:2"}, + Database: &DatabaseSpec{ContainerImage: "custom/postgres:2"}, + OKP: &OKPSpec{ContainerImage: "custom/okp:2"}, + }, + } + + if got := instance.RAGContainerImage(); got != "custom/rag:2" { + t.Errorf("RAGContainerImage() = %q, want %q", got, "custom/rag:2") + } + if got := instance.OGXContainerImage(); got != "custom/ogx:2" { + t.Errorf("OGXContainerImage() = %q, want %q", got, "custom/ogx:2") + } + if got := instance.LightspeedContainerImage(); got != "custom/lcore:2" { + t.Errorf("LightspeedContainerImage() = %q, want %q", got, "custom/lcore:2") + } + if got := instance.ExporterContainerImage(); got != "custom/exporter:2" { + t.Errorf("ExporterContainerImage() = %q, want %q", got, "custom/exporter:2") + } + if got := instance.PostgresContainerImage(); got != "custom/postgres:2" { + t.Errorf("PostgresContainerImage() = %q, want %q", got, "custom/postgres:2") + } + if got := instance.OKPContainerImage(); got != "custom/okp:2" { + t.Errorf("OKPContainerImage() = %q, want %q", got, "custom/okp:2") + } + if got := instance.ConsoleContainerImage("default/console-pf5:1"); got != "custom/console:2" { + t.Errorf("ConsoleContainerImage() = %q, want %q", got, "custom/console:2") + } +} + +func TestOpenStackLightspeedContainerImagesUseDefaults(t *testing.T) { + OpenStackLightspeedDefaultValues = OpenStackLightspeedDefaults{ + RAGImageURL: "default/rag:1", + LCoreImageURL: "default/lcore:1", + OGXImageURL: "default/ogx:1", + ExporterImageURL: "default/exporter:1", + PostgresImageURL: "default/postgres:1", + OKPImageURL: "default/okp:1", + } + + instance := &OpenStackLightspeed{ + Spec: OpenStackLightspeedSpec{}, + } + + if got := instance.RAGContainerImage(); got != "default/rag:1" { + t.Errorf("RAGContainerImage() = %q, want %q", got, "default/rag:1") + } + if got := instance.OGXContainerImage(); got != "default/ogx:1" { + t.Errorf("OGXContainerImage() = %q, want %q", got, "default/ogx:1") + } + if got := instance.LightspeedContainerImage(); got != "default/lcore:1" { + t.Errorf("LightspeedContainerImage() = %q, want %q", got, "default/lcore:1") + } + if got := instance.ExporterContainerImage(); got != "default/exporter:1" { + t.Errorf("ExporterContainerImage() = %q, want %q", got, "default/exporter:1") + } + if got := instance.PostgresContainerImage(); got != "default/postgres:1" { + t.Errorf("PostgresContainerImage() = %q, want %q", got, "default/postgres:1") + } + if got := instance.OKPContainerImage(); got != "default/okp:1" { + t.Errorf("OKPContainerImage() = %q, want %q", got, "default/okp:1") + } + if got := instance.ConsoleContainerImage("default/console-pf5:1"); got != "default/console-pf5:1" { + t.Errorf("ConsoleContainerImage() = %q, want %q", got, "default/console-pf5:1") + } +} + +func TestSetupDefaults_OGXImageURLFromEnv(t *testing.T) { + t.Setenv("RELATED_IMAGE_OGX_IMAGE_URL_DEFAULT", "env/ogx:1") + t.Setenv("RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT", "env/lcore:1") + t.Setenv("RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT", "env/rag:1") + t.Setenv("RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT", "env/exporter:1") + t.Setenv("RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT", "env/postgres:1") + t.Setenv("RELATED_IMAGE_CONSOLE_IMAGE_URL_DEFAULT", "env/console:1") + t.Setenv("RELATED_IMAGE_CONSOLE_PF5_IMAGE_URL_DEFAULT", "env/console-pf5:1") + t.Setenv("RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT", "env/okp:1") + t.Setenv("RELATED_IMAGE_MCP_SERVER_IMAGE_URL_DEFAULT", "env/mcp:1") + + SetupDefaults() + + if got := OpenStackLightspeedDefaultValues.OGXImageURL; got != "env/ogx:1" { + t.Errorf("OGXImageURL = %q, want %q", got, "env/ogx:1") + } + if got := OpenStackLightspeedDefaultValues.LCoreImageURL; got != "env/lcore:1" { + t.Errorf("LCoreImageURL = %q, want %q", got, "env/lcore:1") + } +} + +func TestOpenStackLightspeedContainerImages_OGXAndLightspeedIndependent(t *testing.T) { + OpenStackLightspeedDefaultValues = OpenStackLightspeedDefaults{ + LCoreImageURL: "default/lcore:1", + OGXImageURL: "default/ogx:1", + } + + instance := &OpenStackLightspeed{ + Spec: OpenStackLightspeedSpec{ + OpenStackLightspeedCore: OpenStackLightspeedCore{ + OGX: &OGXSpec{ContainerImage: "custom/ogx:2"}, + Lightspeed: &LightspeedSpec{ContainerImage: "custom/lcore:2"}, + }, + }, + } + + if got := instance.OGXContainerImage(); got != "custom/ogx:2" { + t.Errorf("OGXContainerImage() = %q, want %q", got, "custom/ogx:2") + } + if got := instance.LightspeedContainerImage(); got != "custom/lcore:2" { + t.Errorf("LightspeedContainerImage() = %q, want %q", got, "custom/lcore:2") + } +} diff --git a/api/v1beta1/openstacklightspeed_types.go b/api/v1beta1/openstacklightspeed_types.go index 38bd5b22..a75f2836 100644 --- a/api/v1beta1/openstacklightspeed_types.go +++ b/api/v1beta1/openstacklightspeed_types.go @@ -32,6 +32,9 @@ const ( // LCoreContainerImage is the fall-back container image for LCore LCoreContainerImage = "quay.io/lightspeed-core/lightspeed-stack:latest" + // OGXContainerImage is the fall-back container image for OGX/llama-stack + OGXContainerImage = LCoreContainerImage + // ExporterContainerImage is the fall-back container image for the Dataverse Exporter ExporterContainerImage = "quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest" @@ -61,7 +64,7 @@ const ( // - featureFlags: list of experimental feature flags to enable. Configuration options for experimental features must also live within the `DevSpec`. // - okpChunkFilterQuery: Solr filter query for OKP searches (default: version-aware query combining detected OpenStack and OCP versions) // - okpRagOnly: when true, only OKP is used as a RAG source (default: true) -// - rhosMCPC: configuration for the rhos-mcps sidecar (resources and custom YAML config); config is deep-merged on top of the operator defaults, openstack.enabled and openshift.enabled are always overridden by the operator +// - rhosMCP: configuration for the rhos-mcps sidecar (resources, container image override, and custom YAML config); config is deep-merged on top of the operator defaults, openstack.enabled and openshift.enabled are always overridden by the operator type DevSpec struct { FeatureFlags []string `json:"featureFlags,omitempty"` OKPChunkFilterQuery string `json:"okpChunkFilterQuery,omitempty"` @@ -78,6 +81,9 @@ type RhosMCPSpec struct { // Config is a YAML string that overrides the default configuration (file internal/controller/assets/mcp_server_config.yaml.tmpl) for the rhos-mcps service. Config string `json:"config,omitempty"` + + // ContainerImage overrides the rhos-mcps container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // OKPSpec defines configuration for the Offline Knowledge Portal (OKP). @@ -99,6 +105,10 @@ type OKPSpec struct { // +kubebuilder:default:={requests: {cpu: "500m", memory: "2Gi"}, limits: {cpu: "2", memory: "4Gi"}} // Resources sets compute resources for the Offline Knowledge Portal container. Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the OKP container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // OGXSpec defines configuration for the OGX/llama-stack container. @@ -115,6 +125,10 @@ type OGXSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OGX Log Level" // Log level configuration for the OGX/llama-stack container. Supports standard levels (INFO, DEBUG) or fine-grained control using format "component=level,component=level" (e.g., "core=debug,providers=info"). LogLevel string `json:"logLevel,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the OGX/llama-stack container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // DatabaseSpec defines configuration for persistent PostgreSQL storage. @@ -139,6 +153,10 @@ type DatabaseSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="PostgreSQL Log Level" // Log level for the PostgreSQL container. When set to DEBUG, enables logging of all SQL statements (log_statement = all). LogLevel string `json:"logLevel,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the PostgreSQL container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // ConsoleSpec defines configuration for the lightspeed console plugin. @@ -148,6 +166,10 @@ type ConsoleSpec struct { // Resources sets compute resources for the lightspeed-console-plugin // container and its init container. Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the console plugin container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // LightspeedSpec defines configuration for the lightspeed-service-api container. @@ -164,6 +186,17 @@ type LightspeedSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Lightspeed Stack Log Level" // Log level for the lightspeed-service-api container. Supports standard Python log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. LogLevel string `json:"logLevel,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the lightspeed-service-api container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` +} + +// RAG defines configuration for the RAG vector database init container. +type RAG struct { + // +kubebuilder:validation:Optional + // ContainerImage overrides the RAG init-container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // QuotaLimiterSpec defines a single quota limiter enforced by lightspeed-stack. @@ -297,6 +330,10 @@ type DataverseExporter struct { // +kubebuilder:validation:Optional // Transcripts configures conversation transcript collection. Transcripts *DataverseExporterTranscripts `json:"transcripts,omitempty"` + + // +kubebuilder:validation:Optional + // ContainerImage overrides the dataverse exporter sidecar container image. When unset, the operator default is used. + ContainerImage string `json:"containerImage,omitempty"` } // OpenStackLightspeedCore defines the desired state of OpenStackLightspeed @@ -360,6 +397,10 @@ type OpenStackLightspeedCore struct { // +kubebuilder:default:={} // Lightspeed configures the lightspeed-service-api container. Lightspeed *LightspeedSpec `json:"lightspeed,omitempty"` + + // +kubebuilder:validation:Optional + // RAG configures the RAG vector database init container. + RAG *RAG `json:"rag,omitempty"` } // OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed @@ -440,6 +481,7 @@ func (instance OpenStackLightspeed) IsReady() bool { type OpenStackLightspeedDefaults struct { RAGImageURL string LCoreImageURL string + OGXImageURL string ExporterImageURL string PostgresImageURL string ConsoleImageURL string @@ -462,6 +504,8 @@ func SetupDefaults() { "RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT", OpenStackLightspeedContainerImage), LCoreImageURL: util.GetEnvVar( "RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT", LCoreContainerImage), + OGXImageURL: util.GetEnvVar( + "RELATED_IMAGE_OGX_IMAGE_URL_DEFAULT", OGXContainerImage), ExporterImageURL: util.GetEnvVar( "RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT", ExporterContainerImage), PostgresImageURL: util.GetEnvVar( diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 0e0f5ba0..339666eb 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -246,6 +246,11 @@ func (in *OpenStackLightspeedCore) DeepCopyInto(out *OpenStackLightspeedCore) { *out = new(LightspeedSpec) (*in).DeepCopyInto(*out) } + if in.RAG != nil { + in, out := &in.RAG, &out.RAG + *out = new(RAG) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedCore. @@ -419,6 +424,21 @@ func (in *QuotaSpec) DeepCopy() *QuotaSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RAG) DeepCopyInto(out *RAG) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RAG. +func (in *RAG) DeepCopy() *RAG { + if in == nil { + return nil + } + out := new(RAG) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RhosMCPSpec) DeepCopyInto(out *RhosMCPSpec) { *out = *in diff --git a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml index 56c5c720..09e457d4 100644 --- a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -53,6 +53,10 @@ spec: default: {} description: Console configures the lightspeed console plugin. properties: + containerImage: + description: ContainerImage overrides the console plugin container + image. When unset, the operator default is used. + type: string resources: default: limits: @@ -135,6 +139,10 @@ spec: StorageClass name for the PersistentVolumeClaim. If omitted, the cluster's default StorageClass is used. type: string + containerImage: + description: ContainerImage overrides the PostgreSQL container + image. When unset, the operator default is used. + type: string logLevel: default: INFO description: Log level for the PostgreSQL container. When set @@ -225,6 +233,10 @@ spec: description: DataverseExporter configures the dataverse exporter sidecar (feedback, transcripts, logging). properties: + containerImage: + description: ContainerImage overrides the dataverse exporter sidecar + container image. When unset, the operator default is used. + type: string feedback: description: Feedback configures user feedback collection. properties: @@ -263,6 +275,10 @@ spec: default: {} description: Lightspeed configures the lightspeed-service-api container. properties: + containerImage: + description: ContainerImage overrides the lightspeed-service-api + container image. When unset, the operator default is used. + type: string logLevel: default: INFO description: 'Log level for the lightspeed-service-api container. @@ -387,6 +403,10 @@ spec: default: {} description: OGX configures the OGX/llama-stack container. properties: + containerImage: + description: ContainerImage overrides the OGX/llama-stack container + image. When unset, the operator default is used. + type: string logLevel: default: all=info description: Log level configuration for the OGX/llama-stack container. @@ -474,6 +494,10 @@ spec: The secret must contain a key named "access_key". An access key can be obtained from https://access.redhat.com/offline/access type: string + containerImage: + description: ContainerImage overrides the OKP container image. + When unset, the operator default is used. + type: string offline: default: true description: |- @@ -627,6 +651,14 @@ spec: type: integer type: object type: object + rag: + description: RAG configures the RAG vector database init container. + properties: + containerImage: + description: ContainerImage overrides the RAG init-container image. + When unset, the operator default is used. + type: string + type: object tlsCACertBundle: description: Configmap name containing a CA Certificates bundle type: string diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 26ff7c3c..94abc413 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -78,6 +78,8 @@ spec: value: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx - name: RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT value: quay.io/lightspeed-core/lightspeed-stack:latest + - name: RELATED_IMAGE_OGX_IMAGE_URL_DEFAULT + value: quay.io/lightspeed-core/lightspeed-stack:latest - name: RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT value: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest - name: RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT diff --git a/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml b/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml index f307d56b..c6832168 100644 --- a/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/openstack-lightspeed-operator.clusterserviceversion.yaml @@ -180,4 +180,6 @@ spec: name: operator - image: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx name: rag-content + - image: quay.io/lightspeed-core/lightspeed-stack:latest + name: ogx version: 0.0.0 diff --git a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml index b352e91f..eea9c917 100644 --- a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml +++ b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml @@ -11,6 +11,9 @@ spec: llmCredentials: openstack-lightspeed-apitoken modelName: llama3.1:8b tlsCACertBundle: openstack-lightspeed-certs + # Uncomment to override container images (falls back to operator defaults when unset): + # rag: + # containerImage: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx # Uncomment to customize persistent PostgreSQL storage: # database: # size: "5Gi" @@ -23,6 +26,7 @@ spec: # cpu: "500m" # memory: "2Gi" # logLevel: "INFO" + # containerImage: quay.io/sclorg/postgresql-16-c10s:latest # Uncomment to customize OGX/llama-stack container resources and log level: # ogx: # resources: @@ -33,6 +37,7 @@ spec: # cpu: "2" # memory: "8Gi" # logLevel: "all=info" + # containerImage: quay.io/lightspeed-core/lightspeed-stack:latest # Uncomment to customize lightspeed-service-api container resources and log level: # lightspeed: # resources: @@ -43,6 +48,7 @@ spec: # cpu: "1" # memory: "2Gi" # logLevel: "INFO" + # containerImage: quay.io/lightspeed-core/lightspeed-stack:latest # Uncomment to customize dataverse exporter configuration: # dataverseExporter: # logLevel: "INFO" @@ -50,6 +56,7 @@ spec: # enabled: true # transcripts: # enabled: false + # containerImage: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest # Uncomment to enable RHOSO MCPs (MCP server sidecar with OpenStack/OpenShift tools) # dev: # featureFlags: @@ -68,6 +75,7 @@ spec: # memory: "64Mi" # limits: # memory: "200Mi" + # containerImage: quay.io/openstack-lightspeed/lightspeed-mcps:latest # okp: # accessKey: okp-access-key-secret # offline: true @@ -78,6 +86,7 @@ spec: # limits: # cpu: "2" # memory: "4Gi" + # containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest # console: # resources: # requests: @@ -86,6 +95,7 @@ spec: # limits: # cpu: "1" # memory: "2Gi" + # containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 # Uncomment to enable quota enforcement (opt-in; omit or leave limiters empty to disable): # quotas: # limiters: diff --git a/docs/configuration.rst b/docs/configuration.rst index 39fa187e..7c9b323b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -126,6 +126,16 @@ default entirely: .. code-block:: yaml spec: + dev: + featureFlags: + - rhoso_mcps + rhosMCP: + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + memory: "200Mi" ogx: resources: requests: {cpu: "500m", memory: "2Gi"} @@ -147,6 +157,35 @@ default entirely: requests: {cpu: "500m", memory: "2Gi"} limits: {cpu: "2", memory: "4Gi"} +Container images (``containerImage``) +-------------------------------------- + +Every container has a default container image. Setting one replaces its +default entirely: + +.. code-block:: yaml + + spec: + dev: + featureFlags: + - rhoso_mcps + rhosMCP: + containerImage: quay.io/openstack-lightspeed/lightspeed-mcps:latest + rag: + containerImage: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx + ogx: + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + console: + containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 + lightspeed: + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + database: + containerImage: quay.io/sclorg/postgresql-16-c10s:latest + dataverseExporter; + containerImage: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest + okp: + containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + .. _offline-knowledge-portal: Offline Knowledge Portal (``okp``) diff --git a/hack/env.sh b/hack/env.sh index 52005613..7f20c7c7 100644 --- a/hack/env.sh +++ b/hack/env.sh @@ -1,5 +1,6 @@ #!/bin/bash export RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT="quay.io/lightspeed-core/lightspeed-stack:latest" +export RELATED_IMAGE_OGX_IMAGE_URL_DEFAULT="quay.io/lightspeed-core/lightspeed-stack:latest" export RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT="quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest" export RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT="quay.io/sclorg/postgresql-16-c10s:latest" # TODO(lpiwowar): Replace this with a stable (non-alpha) image version once diff --git a/internal/controller/common.go b/internal/controller/common.go index 6284c469..4dd30164 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -20,7 +20,6 @@ import ( "context" "crypto/rand" _ "embed" // Required for go:embed directives in this package - "encoding/json" "errors" "fmt" "math/big" @@ -155,18 +154,6 @@ func generateOKPSelectorLabels() map[string]string { } } -// parseDevConfig unmarshals the Dev RawExtension into a DevSpec. -// Returns a zero-value DevSpec and an error on malformed input. -func parseDevConfig(instance *apiv1beta1.OpenStackLightspeed) (apiv1beta1.DevSpec, error) { - var devConfig apiv1beta1.DevSpec - if len(instance.Spec.Dev.Raw) > 0 { - if err := json.Unmarshal(instance.Spec.Dev.Raw, &devConfig); err != nil { - return devConfig, err - } - } - return devConfig, nil -} - // defaultRhosMCPResources returns the default resource requirements for the rhos-mcps sidecar. func defaultRhosMCPResources() corev1.ResourceRequirements { return corev1.ResourceRequirements{ @@ -183,7 +170,7 @@ func defaultRhosMCPResources() corev1.ResourceRequirements { // getRhosMCPResources returns compute resources for the rhos-mcps sidecar from dev.rhosMCP.resources, // falling back to operator defaults when unset. func getRhosMCPResources(instance *apiv1beta1.OpenStackLightspeed) corev1.ResourceRequirements { - devConfig, _ := parseDevConfig(instance) + devConfig, _ := instance.ParseDevConfig() if devConfig.RhosMCP != nil && (len(devConfig.RhosMCP.Resources.Requests) > 0 || len(devConfig.RhosMCP.Resources.Limits) > 0) { return devConfig.RhosMCP.Resources @@ -193,7 +180,7 @@ func getRhosMCPResources(instance *apiv1beta1.OpenStackLightspeed) corev1.Resour // isRHOSOMCPEnabled returns true if the "rhoso_mcps" feature flag is present in the dev config. func isRHOSOMCPEnabled(instance *apiv1beta1.OpenStackLightspeed) (bool, error) { - devConfig, err := parseDevConfig(instance) + devConfig, err := instance.ParseDevConfig() if err != nil { return false, err } @@ -202,7 +189,7 @@ func isRHOSOMCPEnabled(instance *apiv1beta1.OpenStackLightspeed) (bool, error) { // getOKPChunkFilterQuery returns the chunk filter query from the dev config, or a version-aware default. func getOKPChunkFilterQuery(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) string { - devConfig, _ := parseDevConfig(instance) + devConfig, _ := instance.ParseDevConfig() if devConfig.OKPChunkFilterQuery != "" { return devConfig.OKPChunkFilterQuery } diff --git a/internal/controller/console_reconciler.go b/internal/controller/console_reconciler.go index 14d2e429..ceadb74c 100644 --- a/internal/controller/console_reconciler.go +++ b/internal/controller/console_reconciler.go @@ -160,8 +160,9 @@ func consoleImageForVersion(version string) string { return apiv1beta1.OpenStackLightspeedDefaultValues.ConsoleImagePF5URL } -// resolveConsoleImage selects the console plugin image based on OCP cluster version. -func resolveConsoleImage(ctx context.Context, h *common_helper.Helper) string { +// resolveConsoleImage selects the console plugin image based on OCP cluster version +// and optional spec.console.containerImage override. +func resolveConsoleImage(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) string { logger := h.GetLogger() version, err := DetectOCPVersion(ctx, h) @@ -169,9 +170,12 @@ func resolveConsoleImage(ctx context.Context, h *common_helper.Helper) string { logger.Info("Failed to detect OCP version for console image, using default", "error", err) } - image := consoleImageForVersion(version) + ocpDefault := consoleImageForVersion(version) + image := instance.ConsoleContainerImage(ocpDefault) - if image == apiv1beta1.OpenStackLightspeedDefaultValues.ConsoleImageURL { + if instance.Spec.Console != nil && instance.Spec.Console.ContainerImage != "" { + logger.Info("Using console image from spec override", "image", image) + } else if image == apiv1beta1.OpenStackLightspeedDefaultValues.ConsoleImageURL { logger.Info("OCP >= 4.19, using PatternFly 6 console image", "version", version) } else { logger.Info("Using PatternFly 5 console image", "version", version) @@ -184,7 +188,7 @@ func resolveConsoleImage(ctx context.Context, h *common_helper.Helper) string { func reconcileConsoleDeploymentResource(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) error { logger := h.GetLogger() - consoleImage := resolveConsoleImage(ctx, h) + consoleImage := resolveConsoleImage(ctx, h, instance) deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/console_reconciler_test.go b/internal/controller/console_reconciler_test.go index de0b54ff..4db5be9f 100644 --- a/internal/controller/console_reconciler_test.go +++ b/internal/controller/console_reconciler_test.go @@ -297,4 +297,19 @@ var _ = ginkgo.Describe("Console Plugin", func() { gomega.Expect(result).To(gomega.Equal(apiv1beta1.OpenStackLightspeedDefaultValues.ConsoleImagePF5URL)) }) }) + + ginkgo.Describe("ConsoleContainerImage override", func() { + ginkgo.It("should override OCP version-based console image selection", func() { + instance := &apiv1beta1.OpenStackLightspeed{ + Spec: apiv1beta1.OpenStackLightspeedSpec{ + Console: &apiv1beta1.ConsoleSpec{ + ContainerImage: "custom/console:override", + }, + }, + } + + result := instance.ConsoleContainerImage(apiv1beta1.OpenStackLightspeedDefaultValues.ConsoleImageURL) + gomega.Expect(result).To(gomega.Equal("custom/console:override")) + }) + }) }) diff --git a/internal/controller/container_images_test.go b/internal/controller/container_images_test.go new file mode 100644 index 00000000..49ade10f --- /dev/null +++ b/internal/controller/container_images_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "testing" + + apiv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testRAGImage = "example.com/rag:override" + testOGXImage = "example.com/ogx:override" + testLightspeedImage = "example.com/lightspeed:override" + testExporterImage = "example.com/exporter:override" + testPostgresImage = "example.com/postgres:override" + testOKPImage = "example.com/okp:override" + testConsoleImage = "example.com/console:override" +) + +func setContainerImageTestDefaults(t *testing.T) { + t.Helper() + apiv1beta1.OpenStackLightspeedDefaultValues = apiv1beta1.OpenStackLightspeedDefaults{ + RAGImageURL: "default/rag:1", + LCoreImageURL: "default/lcore:1", + OGXImageURL: "default/ogx:1", + ExporterImageURL: "default/exporter:1", + PostgresImageURL: "default/postgres:1", + OKPImageURL: "default/okp:1", + ConsoleImageURL: "default/console-pf6:1", + ConsoleImagePF5URL: "default/console-pf5:1", + } +} + +func makeContainerImageTestInstance() *apiv1beta1.OpenStackLightspeed { + feedbackDisabled := false + return &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: apiv1beta1.OpenStackLightspeedSpec{ + OpenStackLightspeedCore: apiv1beta1.OpenStackLightspeedCore{ + LLMEndpoint: "http://mock-llm:8000/v1", + LLMEndpointType: "openai", + ModelName: "test-model", + LLMCredentials: "llm-secret", + RAG: &apiv1beta1.RAG{ContainerImage: testRAGImage}, + OGX: &apiv1beta1.OGXSpec{ContainerImage: testOGXImage}, + Lightspeed: &apiv1beta1.LightspeedSpec{ContainerImage: testLightspeedImage}, + DataverseExporter: &apiv1beta1.DataverseExporter{ + ContainerImage: testExporterImage, + Feedback: &apiv1beta1.DataverseExporterFeedback{Enabled: &feedbackDisabled}, + }, + }, + Console: &apiv1beta1.ConsoleSpec{ContainerImage: testConsoleImage}, + Database: &apiv1beta1.DatabaseSpec{ContainerImage: testPostgresImage}, + OKP: &apiv1beta1.OKPSpec{ContainerImage: testOKPImage}, + }, + } +} + +func TestBuildInitContainers_UsesContainerImageOverrides(t *testing.T) { + setContainerImageTestDefaults(t) + instance := makeContainerImageTestInstance() + + initContainers := buildInitContainers(instance, corev1.ResourceRequirements{}) + if len(initContainers) != 2 { + t.Fatalf("expected 2 init containers, got %d", len(initContainers)) + } + + if got := initContainers[0].Image; got != testRAGImage { + t.Errorf("vector-database-collect image = %q, want %q", got, testRAGImage) + } + if got := initContainers[1].Image; got != testLightspeedImage { + t.Errorf("vector-database-config-build image = %q, want %q", got, testLightspeedImage) + } +} + +func TestBuildPostgresPodTemplateSpec_UsesContainerImageOverride(t *testing.T) { + setContainerImageTestDefaults(t) + instance := makeContainerImageTestInstance() + + podTemplate := buildPostgresPodTemplateSpec(instance) + if len(podTemplate.Spec.Containers) != 1 { + t.Fatalf("expected 1 postgres container, got %d", len(podTemplate.Spec.Containers)) + } + if got := podTemplate.Spec.Containers[0].Image; got != testPostgresImage { + t.Errorf("postgres image = %q, want %q", got, testPostgresImage) + } +} + +func TestBuildOKPPodTemplateSpec_UsesContainerImageOverride(t *testing.T) { + setContainerImageTestDefaults(t) + instance := makeContainerImageTestInstance() + + podTemplate := buildOKPPodTemplateSpec(instance) + if len(podTemplate.Spec.Containers) != 1 { + t.Fatalf("expected 1 okp container, got %d", len(podTemplate.Spec.Containers)) + } + if got := podTemplate.Spec.Containers[0].Image; got != testOKPImage { + t.Errorf("okp image = %q, want %q", got, testOKPImage) + } +} + +func TestBuildConsoleDeploymentSpec_UsesContainerImageOverride(t *testing.T) { + setContainerImageTestDefaults(t) + instance := makeContainerImageTestInstance() + + spec := buildConsoleDeploymentSpec(testConsoleImage, instance) + if len(spec.Template.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(spec.Template.Spec.InitContainers)) + } + if len(spec.Template.Spec.Containers) != 1 { + t.Fatalf("expected 1 container, got %d", len(spec.Template.Spec.Containers)) + } + + if got := spec.Template.Spec.InitContainers[0].Image; got != testConsoleImage { + t.Errorf("console init container image = %q, want %q", got, testConsoleImage) + } + if got := spec.Template.Spec.Containers[0].Image; got != testConsoleImage { + t.Errorf("console container image = %q, want %q", got, testConsoleImage) + } +} diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index ba8b46ad..a175ccee 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -76,7 +76,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins llamaStackContainer := corev1.Container{ Name: "llama-stack", - Image: apiv1beta1.OpenStackLightspeedDefaultValues.LCoreImageURL, + Image: instance.OGXContainerImage(), Command: []string{"python3", VectorDBScriptsMountPath + "/" + LlamaStartupWrapperKey, "stack", "run", VectorDBVolumeOGXConfigPath}, Ports: []corev1.ContainerPort{{Name: "llama-stack", ContainerPort: LlamaStackContainerPort}}, VolumeMounts: llamaStackMounts, @@ -147,7 +147,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins lightspeedStackContainer := corev1.Container{ Name: "lightspeed-service-api", - Image: apiv1beta1.OpenStackLightspeedDefaultValues.LCoreImageURL, + Image: instance.LightspeedContainerImage(), Args: []string{"-c", VectorDBVolumeLightspeedStackConfigPath}, Ports: []corev1.ContainerPort{{Name: "https", ContainerPort: OpenStackLightspeedAppServerContainerPort}}, VolumeMounts: lightspeedStackMounts, @@ -164,7 +164,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins if dataCollectionEnabled { exporterContainer := corev1.Container{ Name: DataverseExporterContainerName, - Image: apiv1beta1.OpenStackLightspeedDefaultValues.ExporterImageURL, + Image: instance.ExporterContainerImage(), ImagePullPolicy: corev1.PullAlways, Args: []string{ "--mode", "openshift", @@ -213,7 +213,7 @@ func buildLCorePodTemplateSpec(ctx context.Context, h *common_helper.Helper, ins mcpContainer := corev1.Container{ Name: "rhoso-mcps", - Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL, + Image: instance.MCPContainerImage(), VolumeMounts: mcpMounts, Resources: getRhosMCPResources(instance), StartupProbe: &corev1.Probe{ @@ -293,7 +293,7 @@ func buildInitContainers(instance *apiv1beta1.OpenStackLightspeed, initResources var containers []corev1.Container containers = append(containers, corev1.Container{ Name: "vector-database-collect", - Image: apiv1beta1.OpenStackLightspeedDefaultValues.RAGImageURL, + Image: instance.RAGContainerImage(), Command: []string{ "sh", VectorDBScriptsMountPath + "/" + VectorDBCollectScriptKey, "--vector-db-path", VectorDBVolumeMountPath, @@ -320,14 +320,14 @@ func buildInitContainers(instance *apiv1beta1.OpenStackLightspeed, initResources "--ogx-config-path", OGXConfigInitContainerMountPath, "--lightspeed-stack-path", LightspeedStackInitContainerMountPath, } - devConfig, _ := parseDevConfig(instance) + devConfig, _ := instance.ParseDevConfig() if devConfig.OKPRagOnly == nil || *devConfig.OKPRagOnly { configBuildCmd = append(configBuildCmd, "--disable-rag-entries") } containers = append(containers, corev1.Container{ Name: "vector-database-config-build", - Image: apiv1beta1.OpenStackLightspeedDefaultValues.LCoreImageURL, + Image: instance.LightspeedContainerImage(), Command: configBuildCmd, SecurityContext: securityContext, Resources: initResources, diff --git a/internal/controller/mcp_server.go b/internal/controller/mcp_server.go index a088c0e9..25cfa26e 100644 --- a/internal/controller/mcp_server.go +++ b/internal/controller/mcp_server.go @@ -151,7 +151,7 @@ func BuildMCPServerConfigMap( instance *apiv1beta1.OpenStackLightspeed, openStackReady bool, ) (corev1.ConfigMap, error) { - devConfig, _ := parseDevConfig(instance) + devConfig, _ := instance.ParseDevConfig() rhosMCPConfigYAML := "" if devConfig.RhosMCP != nil { rhosMCPConfigYAML = devConfig.RhosMCP.Config diff --git a/internal/controller/okp_reconciler.go b/internal/controller/okp_reconciler.go index 45fc3c2d..b968276a 100644 --- a/internal/controller/okp_reconciler.go +++ b/internal/controller/okp_reconciler.go @@ -132,7 +132,7 @@ func buildOKPPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) corev1.Po Containers: []corev1.Container{ { Name: OKPContainerName, - Image: apiv1beta1.OpenStackLightspeedDefaultValues.OKPImageURL, + Image: instance.OKPContainerImage(), Ports: []corev1.ContainerPort{{Name: "okp", ContainerPort: OKPContainerPort}}, Env: envVars, ReadinessProbe: &corev1.Probe{ diff --git a/internal/controller/openstacklightspeed_controller.go b/internal/controller/openstacklightspeed_controller.go index 4a25328f..ddb2d94a 100644 --- a/internal/controller/openstacklightspeed_controller.go +++ b/internal/controller/openstacklightspeed_controller.go @@ -222,7 +222,7 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl. } // Log dev config parse errors so misconfigurations don't silently disable features. - if _, err := parseDevConfig(instance); err != nil { + if _, err := instance.ParseDevConfig(); err != nil { Log.Error(err, "failed to parse dev config, ignoring") } diff --git a/internal/controller/postgres_deployment.go b/internal/controller/postgres_deployment.go index 5b45503f..9b81ee2b 100644 --- a/internal/controller/postgres_deployment.go +++ b/internal/controller/postgres_deployment.go @@ -163,7 +163,7 @@ func buildPostgresPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) core Containers: []corev1.Container{ { Name: PostgresDeploymentName, - Image: apiv1beta1.OpenStackLightspeedDefaultValues.PostgresImageURL, + Image: instance.PostgresContainerImage(), ImagePullPolicy: corev1.PullAlways, Ports: []corev1.ContainerPort{ { diff --git a/test/kuttl/tests/container-image-overrides/00-mock-resources.yaml b/test/kuttl/tests/container-image-overrides/00-mock-resources.yaml new file mode 120000 index 00000000..8235a1fd --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/00-mock-resources.yaml @@ -0,0 +1 @@ +../../common/mock-objects/mock-resources.yaml \ No newline at end of file diff --git a/test/kuttl/tests/container-image-overrides/01-assert-mock-objects-created.yaml b/test/kuttl/tests/container-image-overrides/01-assert-mock-objects-created.yaml new file mode 120000 index 00000000..07f977a1 --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/01-assert-mock-objects-created.yaml @@ -0,0 +1 @@ +../../common/mock-objects/assert-mock-objects-created.yaml \ No newline at end of file diff --git a/test/kuttl/tests/container-image-overrides/02-create-openstack-lightspeed-with-container-images.yaml b/test/kuttl/tests/container-image-overrides/02-create-openstack-lightspeed-with-container-images.yaml new file mode 100644 index 00000000..c134393a --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/02-create-openstack-lightspeed-with-container-images.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +spec: + llmEndpoint: http://mock-llm-api-server-pod:8000/v1 + llmEndpointType: openai + llmCredentials: openstack-lightspeed-apitoken + modelName: ibm-granite/granite-3.1-8b-instruct + tlsCACertBundle: openstack-lightspeed-cert + llmProjectID: test-project-id + llmDeploymentName: test-deployment-name + llmAPIVersion: v1 + rag: + containerImage: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx + ogx: + logLevel: DEBUG + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + lightspeed: + logLevel: WARNING + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + dataverseExporter: + logLevel: DEBUG + containerImage: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest + console: + containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:1.0.12 + database: + containerImage: quay.io/sclorg/postgresql-16-c10s:latest + okp: + containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + dev: + okpChunkFilterQuery: "product:(*openstack* OR *openshift*)" + quotas: + limiters: + - name: per-user-hourly + type: userLimiter + initialQuota: 1000 + quotaIncrease: 1000 + period: "1 hour" + - name: cluster-daily + type: clusterLimiter + initialQuota: 100000 + quotaIncrease: 100000 + period: "1 day" + enableTokenHistory: true diff --git a/test/kuttl/tests/container-image-overrides/03-assert-container-images.yaml b/test/kuttl/tests/container-image-overrides/03-assert-container-images.yaml new file mode 100644 index 00000000..b9a9150d --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/03-assert-container-images.yaml @@ -0,0 +1,149 @@ +# Assert deployments use containerImage values from the OpenStackLightspeed spec. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - type: command + command: ../../common/collectors/collect-workload-diagnostics.sh +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-postgres-server + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: lightspeed-postgres-server + image: quay.io/sclorg/postgresql-16-c10s:latest +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-okp-server + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: okp + image: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-stack-deployment + namespace: openstack-lightspeed +spec: + template: + spec: + initContainers: + - name: vector-database-collect + image: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx + - name: vector-database-config-build + image: quay.io/lightspeed-core/lightspeed-stack:latest + containers: + - name: llama-stack + image: quay.io/lightspeed-core/lightspeed-stack:latest + - name: lightspeed-service-api + image: quay.io/lightspeed-core/lightspeed-stack:latest + - name: lightspeed-to-dataverse-exporter + image: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-console-plugin + namespace: openstack-lightspeed +spec: + template: + spec: + initContainers: + - name: rewrite-locales + image: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:1.0.12 + containers: + - name: lightspeed-console-plugin + image: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:1.0.12 +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +spec: + console: + containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:1.0.12 + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + database: + containerImage: quay.io/sclorg/postgresql-16-c10s:latest + resources: + limits: + cpu: 500m + memory: 2Gi + requests: + cpu: 30m + memory: 300Mi + dataverseExporter: + containerImage: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest + lightspeed: + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: 250m + memory: 512Mi + ogx: + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + resources: + limits: + cpu: "2" + memory: 8Gi + requests: + cpu: 500m + memory: 2Gi + okp: + containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + offline: true + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: 500m + memory: 2Gi + rag: + containerImage: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx +status: + conditions: + - type: Ready + status: "True" + reason: Ready + message: Setup complete + - type: OpenStackLightspeedMCPServerReady + status: "True" + message: "RHOSO MCP server is disabled (rhoso_mcps feature flag not set)" + - type: OpenStackLightspeedReady + status: "True" + reason: Ready + message: OpenStack Lightspeed created diff --git a/test/kuttl/tests/container-image-overrides/04-update-console-container-image.yaml b/test/kuttl/tests/container-image-overrides/04-update-console-container-image.yaml new file mode 100644 index 00000000..232a8369 --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/04-update-console-container-image.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +spec: + llmEndpoint: http://mock-llm-api-server-pod:8000/v1 + llmEndpointType: openai + llmCredentials: openstack-lightspeed-apitoken + modelName: ibm-granite/granite-3.1-8b-instruct + tlsCACertBundle: openstack-lightspeed-cert + llmProjectID: test-project-id + llmDeploymentName: test-deployment-name + llmAPIVersion: v1 + rag: + containerImage: quay.io/openstack-lightspeed/rag-content:os-docs-2026.1-ogx + ogx: + logLevel: DEBUG + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + lightspeed: + logLevel: WARNING + containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + dataverseExporter: + logLevel: DEBUG + containerImage: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest + console: + containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 + database: + containerImage: quay.io/sclorg/postgresql-16-c10s:latest + okp: + containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + dev: + okpChunkFilterQuery: "product:(*openstack* OR *openshift*)" + quotas: + limiters: + - name: per-user-hourly + type: userLimiter + initialQuota: 1000 + quotaIncrease: 1000 + period: "1 hour" + - name: cluster-daily + type: clusterLimiter + initialQuota: 100000 + quotaIncrease: 100000 + period: "1 day" + enableTokenHistory: true diff --git a/test/kuttl/tests/container-image-overrides/05-assert-updated-console-image.yaml b/test/kuttl/tests/container-image-overrides/05-assert-updated-console-image.yaml new file mode 100644 index 00000000..5d51701a --- /dev/null +++ b/test/kuttl/tests/container-image-overrides/05-assert-updated-console-image.yaml @@ -0,0 +1,51 @@ +# Assert console containerImage update is applied to the deployment. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - type: command + command: ../../common/collectors/collect-workload-diagnostics.sh +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-console-plugin + namespace: openstack-lightspeed +spec: + template: + spec: + initContainers: + - name: rewrite-locales + image: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 + containers: + - name: lightspeed-console-plugin + image: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +spec: + console: + containerImage: registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:1.0.12 + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi +status: + conditions: + - type: Ready + status: "True" + reason: Ready + message: Setup complete + - type: OpenStackLightspeedMCPServerReady + status: "True" + message: "RHOSO MCP server is disabled (rhoso_mcps feature flag not set)" + - type: OpenStackLightspeedReady + status: "True" + reason: Ready + message: OpenStack Lightspeed created diff --git a/test/kuttl/tests/rhoso-mcps-configuration/08-cleanup-openstack-lightspeed-instance.yaml b/test/kuttl/tests/container-image-overrides/06-cleanup-openstack-lightspeed-instance.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/08-cleanup-openstack-lightspeed-instance.yaml rename to test/kuttl/tests/container-image-overrides/06-cleanup-openstack-lightspeed-instance.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/09-errors-openstack-lightspeed-instance.yaml b/test/kuttl/tests/container-image-overrides/07-errors-openstack-lightspeed-instance.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/09-errors-openstack-lightspeed-instance.yaml rename to test/kuttl/tests/container-image-overrides/07-errors-openstack-lightspeed-instance.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/10-cleanup-mock-objects.yaml b/test/kuttl/tests/container-image-overrides/08-cleanup-mock-objects.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/10-cleanup-mock-objects.yaml rename to test/kuttl/tests/container-image-overrides/08-cleanup-mock-objects.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/11-errors-mock-objects.yaml b/test/kuttl/tests/container-image-overrides/09-errors-mock-objects.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/11-errors-mock-objects.yaml rename to test/kuttl/tests/container-image-overrides/09-errors-mock-objects.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/03-assert-rhoso-mcps-instance.yaml b/test/kuttl/tests/rhoso-mcps-configuration/03-assert-rhoso-mcps-instance.yaml index 2c60d84b..22d43449 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/03-assert-rhoso-mcps-instance.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/03-assert-rhoso-mcps-instance.yaml @@ -17,6 +17,7 @@ spec: - name: lightspeed-service-api - name: lightspeed-to-dataverse-exporter - name: rhoso-mcps + image: quay.io/openstack-lightspeed/lightspeed-mcps:latest resources: requests: cpu: 50m diff --git a/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml b/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml index 8c4cbbab..0bd74660 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/04-update-rhos-mcp-config.yaml @@ -23,9 +23,8 @@ spec: featureFlags: - rhoso_mcps rhosMCP: + containerImage: quay.io/openstack-lightspeed/lightspeed-mcps:latest config: | - debug: true - workers: 4 openstack: enabled: true allow_write: true diff --git a/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml b/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml index 71bfe958..01e3f485 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/05-assert-rhos-mcp-config-merged.yaml @@ -34,11 +34,9 @@ commands: fi } - # User values should be merged - assert_yaml_field "debug" "True" - assert_yaml_field "workers" "4" - # Template defaults should be preserved + assert_yaml_field "debug" "False" + assert_yaml_field "workers" "1" assert_yaml_field "port" "8080" assert_yaml_field "ip" "0.0.0.0" assert_yaml_field "openstack.ca_cert" "./tls-ca-bundle.pem" diff --git a/test/kuttl/tests/rhoso-mcps-configuration/06-assert-rhos-mcp-container-image.yaml b/test/kuttl/tests/rhoso-mcps-configuration/06-assert-rhos-mcp-container-image.yaml new file mode 100644 index 00000000..33792b05 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/06-assert-rhos-mcp-container-image.yaml @@ -0,0 +1,26 @@ +# Assert dev.rhosMCP.containerImage override is applied to the rhoso-mcps sidecar. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - type: command + command: ../../common/collectors/collect-workload-diagnostics.sh +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-stack-deployment + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: llama-stack + - name: lightspeed-service-api + - name: lightspeed-to-dataverse-exporter + - name: rhoso-mcps + image: quay.io/openstack-lightspeed/lightspeed-mcps:latest +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 diff --git a/test/kuttl/tests/rhoso-mcps-configuration/07-update-rhos-mcp-container-image.yaml b/test/kuttl/tests/rhoso-mcps-configuration/07-update-rhos-mcp-container-image.yaml new file mode 100644 index 00000000..edd63276 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/07-update-rhos-mcp-container-image.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +spec: + llmEndpoint: http://mock-llm-api-server-pod:8000/v1 + llmEndpointType: openai + llmCredentials: openstack-lightspeed-apitoken + modelName: ibm-granite/granite-3.1-8b-instruct + tlsCACertBundle: openstack-lightspeed-cert + llmProjectID: test-project-id + llmDeploymentName: test-deployment-name + llmAPIVersion: v1 + ogx: + logLevel: DEBUG + lightspeed: + logLevel: WARNING + dataverseExporter: + logLevel: DEBUG + dev: + featureFlags: + - rhoso_mcps + rhosMCP: + config: | + openstack: + enabled: true + allow_write: true + openshift: + enabled: false diff --git a/test/kuttl/tests/rhoso-mcps-configuration/08-assert-rhos-mcp-default-container-image.yaml b/test/kuttl/tests/rhoso-mcps-configuration/08-assert-rhos-mcp-default-container-image.yaml new file mode 100644 index 00000000..26ce2ac6 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/08-assert-rhos-mcp-default-container-image.yaml @@ -0,0 +1,26 @@ +# Assert removing dev.rhosMCP.containerImage reverts to the operator default image. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - type: command + command: ../../common/collectors/collect-workload-diagnostics.sh +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-stack-deployment + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: llama-stack + - name: lightspeed-service-api + - name: lightspeed-to-dataverse-exporter + - name: rhoso-mcps + image: quay.io/openstack-lightspeed/lightspeed-mcps:latest +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 diff --git a/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml b/test/kuttl/tests/rhoso-mcps-configuration/09-disable-rhoso-mcps.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml rename to test/kuttl/tests/rhoso-mcps-configuration/09-disable-rhoso-mcps.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/07-errors-rhoso-mcps-cleanup.yaml b/test/kuttl/tests/rhoso-mcps-configuration/10-errors-rhoso-mcps-cleanup.yaml similarity index 100% rename from test/kuttl/tests/rhoso-mcps-configuration/07-errors-rhoso-mcps-cleanup.yaml rename to test/kuttl/tests/rhoso-mcps-configuration/10-errors-rhoso-mcps-cleanup.yaml diff --git a/test/kuttl/tests/rhoso-mcps-configuration/11-cleanup-openstack-lightspeed-instance.yaml b/test/kuttl/tests/rhoso-mcps-configuration/11-cleanup-openstack-lightspeed-instance.yaml new file mode 120000 index 00000000..6b2075b0 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/11-cleanup-openstack-lightspeed-instance.yaml @@ -0,0 +1 @@ +../../common/openstack-lightspeed-instance/cleanup-openstack-lightspeed-instance.yaml \ No newline at end of file diff --git a/test/kuttl/tests/rhoso-mcps-configuration/12-errors-openstack-lightspeed-instance.yaml b/test/kuttl/tests/rhoso-mcps-configuration/12-errors-openstack-lightspeed-instance.yaml new file mode 120000 index 00000000..81472440 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/12-errors-openstack-lightspeed-instance.yaml @@ -0,0 +1 @@ +../../common/openstack-lightspeed-instance/errors-openstack-lightspeed-instance.yaml \ No newline at end of file diff --git a/test/kuttl/tests/rhoso-mcps-configuration/13-cleanup-mock-objects.yaml b/test/kuttl/tests/rhoso-mcps-configuration/13-cleanup-mock-objects.yaml new file mode 120000 index 00000000..410c9278 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/13-cleanup-mock-objects.yaml @@ -0,0 +1 @@ +../../common/mock-objects/cleanup-mock-objects.yaml \ No newline at end of file diff --git a/test/kuttl/tests/rhoso-mcps-configuration/14-errors-mock-objects.yaml b/test/kuttl/tests/rhoso-mcps-configuration/14-errors-mock-objects.yaml new file mode 120000 index 00000000..696a5e26 --- /dev/null +++ b/test/kuttl/tests/rhoso-mcps-configuration/14-errors-mock-objects.yaml @@ -0,0 +1 @@ +../../common/mock-objects/errors-mock-objects.yaml \ No newline at end of file