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 cb91144b..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,12 +64,26 @@ 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 +// - 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"` 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"` + + // 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). @@ -83,6 +100,35 @@ 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"` + + // +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. +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"` + + // +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. @@ -95,46 +141,62 @@ type DatabaseSpec struct { // StorageClass name for the PersistentVolumeClaim. If omitted, the cluster's // default StorageClass is used. Class string `json:"class,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default:={requests: {cpu: "30m", memory: "300Mi"}, limits: {cpu: "500m", memory: "2Gi"}} + // Rarources sets compute resources for the PostgreSQL container. + Resources corev1.ResourceRequirements `json:"resources,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). + 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"` } -// 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 { +// ConsoleSpec defines configuration for the lightspeed console plugin. +type ConsoleSpec 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:default:={requests: {cpu: "50m", memory: "64Mi"}, limits: {cpu: "200m", memory: "256Mi"}} + // Resources sets compute resources for the lightspeed-console-plugin + // container and its init container. + Resources corev1.ResourceRequirements `json:"resources,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"` + // 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. +type LightspeedSpec struct { // +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"` + // +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: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;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"` // +kubebuilder:validation:Optional - // +kubebuilder:default:={requests: {cpu: "50m", memory: "64Mi"}, limits: {cpu: "200m", memory: "256Mi"}} - // ConsolePlugin sets compute resources for the lightspeed-console-plugin - // container and its init container. - ConsolePlugin corev1.ResourceRequirements `json:"consolePlugin,omitempty"` + // 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 - // +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"` + // 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. @@ -208,6 +270,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 +278,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 +287,53 @@ 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"` + + // +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 @@ -317,18 +384,23 @@ 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:={} + // Lightspeed configures the lightspeed-service-api container. + Lightspeed *LightspeedSpec `json:"lightspeed,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:default={} - // Logging configuration for OpenStackLightspeed components - Logging LoggingConfig `json:"logging"` + // RAG configures the RAG vector database init container. + RAG *RAG `json:"rag,omitempty"` } // OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed @@ -409,6 +481,7 @@ func (instance OpenStackLightspeed) IsReady() bool { type OpenStackLightspeedDefaults struct { RAGImageURL string LCoreImageURL string + OGXImageURL string ExporterImageURL string PostgresImageURL string ConsoleImageURL string @@ -431,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 8b2724b2..339666eb 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 LoggingConfig. -func (in *LoggingConfig) DeepCopy() *LoggingConfig { +// 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(LoggingConfig) + 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 OGXSpec. +func (in *OGXSpec) DeepCopy() *OGXSpec { + if in == nil { + return nil + } + 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,26 @@ 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) + 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) + } + if in.RAG != nil { + in, out := &in.RAG, &out.RAG + *out = new(RAG) **out = **in } - out.Logging = in.Logging } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedCore. @@ -236,8 +329,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 +423,34 @@ 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 *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 + 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..09e457d4 100644 --- a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -49,226 +49,15 @@ 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: + console: 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. + description: Console configures the lightspeed console plugin. 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 + containerImage: + description: ContainerImage overrides the console plugin container + image. When unset, the operator default is used. 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: - 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. - properties: - consolePlugin: + resources: default: limits: cpu: 200m @@ -277,7 +66,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 +125,43 @@ 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 + 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 + 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 +219,89 @@ 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: + 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: + 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: + 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. + 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 +359,72 @@ 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: + 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. + 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 +482,30 @@ 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 + containerImage: + description: ContainerImage overrides the OKP container image. + When unset, the operator default is used. + 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 +513,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 +572,96 @@ 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 + 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 - transcriptsEnabled: - description: Enable conversation transcripts collection - type: boolean required: - llmCredentials - llmEndpoint 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 6d9e89e5..c6832168 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 @@ -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 cae6fbb6..eea9c917 100644 --- a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml +++ b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml @@ -11,38 +11,52 @@ 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" # 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" + # containerImage: quay.io/sclorg/postgresql-16-c10s:latest + # 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" + # containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + # 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" + # containerImage: quay.io/lightspeed-core/lightspeed-stack:latest + # Uncomment to customize dataverse exporter configuration: + # dataverseExporter: + # logLevel: "INFO" + # feedback: + # 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: @@ -51,11 +65,37 @@ 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" + # containerImage: quay.io/openstack-lightspeed/lightspeed-mcps:latest # okp: # accessKey: okp-access-key-secret + # offline: true + # resources: + # requests: + # cpu: "500m" + # memory: "2Gi" + # limits: + # cpu: "2" + # memory: "4Gi" + # containerImage: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + # console: + # resources: + # requests: + # cpu: "50m" + # memory: "256Mi" + # 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 37896554..7c9b323b 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,65 @@ default entirely: .. code-block:: yaml spec: - resources: - llamaStack: + dev: + featureFlags: + - rhoso_mcps + rhosMCP: + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + memory: "200Mi" + 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"} + +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: @@ -249,10 +300,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 +316,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/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/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..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" @@ -35,6 +34,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" @@ -154,21 +154,33 @@ 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 - } +// 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, _ := instance.ParseDevConfig() + if devConfig.RhosMCP != nil && + (len(devConfig.RhosMCP.Resources.Requests) > 0 || len(devConfig.RhosMCP.Resources.Limits) > 0) { + return devConfig.RhosMCP.Resources } - return devConfig, nil + 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) + devConfig, err := instance.ParseDevConfig() if err != nil { return false, err } @@ -177,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/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/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/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/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_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..a175ccee 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -69,9 +69,14 @@ 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, + Image: instance.OGXContainerImage(), Command: []string{"python3", VectorDBScriptsMountPath + "/" + LlamaStartupWrapperKey, "stack", "run", VectorDBVolumeOGXConfigPath}, Ports: []corev1.ContainerPort{{Name: "llama-stack", ContainerPort: LlamaStackContainerPort}}, VolumeMounts: llamaStackMounts, @@ -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,9 +140,14 @@ 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, + Image: instance.LightspeedContainerImage(), Args: []string{"-c", VectorDBVolumeLightspeedStackConfigPath}, Ports: []corev1.ContainerPort{{Name: "https", ContainerPort: OpenStackLightspeedAppServerContainerPort}}, VolumeMounts: lightspeedStackMounts, @@ -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} @@ -154,12 +164,12 @@ 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", "--config", path.Join(ExporterConfigMountPath, ExporterConfigFilename), - "--log-level", instance.Spec.Logging.DataverseExporterLogLevel, + "--log-level", dataverseExporterLogLevel(instance), "--data-dir", LCoreUserDataMountPath, }, VolumeMounts: []corev1.VolumeMount{ @@ -203,9 +213,20 @@ 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: instance.Spec.Resources.MCP, + 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{ @@ -272,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, @@ -299,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, @@ -702,7 +723,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 +798,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..25cfa26e 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) @@ -151,8 +151,12 @@ func BuildMCPServerConfigMap( instance *apiv1beta1.OpenStackLightspeed, openStackReady bool, ) (corev1.ConfigMap, error) { - devConfig, _ := parseDevConfig(instance) - configData, err := buildMCPServerConfigData(openStackReady, devConfig.RhosMCPConfig) + devConfig, _ := instance.ParseDevConfig() + 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..b968276a 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(), @@ -127,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{ @@ -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/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 c2c86953..9b81ee2b 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(), @@ -158,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{ { @@ -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/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/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/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 33ef4d27..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 @@ -13,18 +13,20 @@ 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: + containerImage: quay.io/openstack-lightspeed/lightspeed-mcps:latest + config: | + 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..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 @@ -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 @@ -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 82% 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 index bca96d83..81ffe88d 100644 --- a/test/kuttl/tests/rhoso-mcps-configuration/06-disable-rhoso-mcps.yaml +++ b/test/kuttl/tests/rhoso-mcps-configuration/09-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/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 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