From 1d251de14dd3ccd5c1619a2f790449d4a87d03d2 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Wed, 3 Jun 2026 10:27:54 +0100 Subject: [PATCH 1/2] Enable OKP support This commit adds the first step in supporting OKP for OpenStack Lightspeed. Two new configuration sections were added: "okp" and "dev". The "okp" section holds configurations that are more final to the deployment where the "dev" is an easy to way for developers to test the feature while tests/developing it. The options available in "okp" section are: - offline: Whether OKP should be in "offline" mode or not. - accessKey: The OKP access key to decrypt the paid content in the image The options available in the "dev" section are: - featureFlags: This is a list of features that operator wants to expose to be enabled. The value "okp" in the list will deploy OKP. - okpChunkFilterQuery: This is where we can tweak the OKP filter to filter by product, version etc... Signed-off-by: Lucas Alvares Gomes --- api/v1beta1/openstacklightspeed_types.go | 46 ++++ api/v1beta1/zz_generated.deepcopy.go | 50 +++++ ...ed.openstack.org_openstacklightspeeds.yaml | 38 ++++ ...tspeed-operator.clusterserviceversion.yaml | 9 +- ...ed.openstack.org_openstacklightspeeds.yaml | 38 ++++ config/manager/manager.yaml | 2 + config/rbac/role.yaml | 3 +- .../api_v1beta1_openstacklightspeed.yaml | 7 + hack/env.sh | 1 + internal/controller/common.go | 20 ++ internal/controller/constants.go | 9 + internal/controller/errors.go | 6 + internal/controller/lcore_config.go | 32 ++- internal/controller/lcore_deployment.go | 23 +- internal/controller/llama_stack_config.go | 77 ++++++- internal/controller/okp_reconciler.go | 196 ++++++++++++++++++ .../openstacklightspeed_controller.go | 5 +- 17 files changed, 551 insertions(+), 11 deletions(-) create mode 100644 internal/controller/okp_reconciler.go diff --git a/api/v1beta1/openstacklightspeed_types.go b/api/v1beta1/openstacklightspeed_types.go index 3521875c..ac80230f 100644 --- a/api/v1beta1/openstacklightspeed_types.go +++ b/api/v1beta1/openstacklightspeed_types.go @@ -45,10 +45,44 @@ const ( // ConsoleContainerImagePF5 is the fall-back console image for PatternFly 5 (OCP < 4.19) ConsoleContainerImagePF5 = "registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:1.0.12" + // OKPContainerImage is the fall-back container image for OKP (Offline Knowledge Portal) + OKPContainerImage = "registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest" + // MaxTokensForResponseDefault is the default maximum number of tokens that should be used for response MaxTokensForResponseDefault = 2048 ) +// DevSpec defines developer/experimental feature configuration. +type DevSpec struct { + // +kubebuilder:validation:Optional + // +kubebuilder:validation:items:Enum=okp + // FeatureFlags is a list of feature flag names to enable experimental features. + // Supported flags: "okp" (Offline Knowledge Portal). + FeatureFlags []string `json:"featureFlags,omitempty"` + + // +kubebuilder:validation:Optional + // OKPChunkFilterQuery is a static Solr filter query appended to OKP searches. + // Combined with the default "is_chunk:true" filter using AND. + // Example: "product:*openstack*" + OKPChunkFilterQuery string `json:"okpChunkFilterQuery,omitempty"` +} + +// OKPSpec defines configuration for the Offline Knowledge Portal (OKP). +type OKPSpec struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default=true + // Offline controls how source URLs are resolved. + // When true, uses parent_id (offline/Mimir-style). + // When false, uses reference_url (online). + Offline *bool `json:"offline,omitempty"` + + // +kubebuilder:validation:Optional + // 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 + AccessKey string `json:"accessKey,omitempty"` +} + // DatabaseSpec defines configuration for persistent PostgreSQL storage. type DatabaseSpec struct { // +kubebuilder:validation:Optional @@ -84,6 +118,15 @@ type OpenStackLightspeedSpec struct { // When omitted, an emptyDir volume is used (data is lost on pod reschedule). // When set, a PersistentVolumeClaim is created and mounted. Database *DatabaseSpec `json:"database,omitempty"` + + // +kubebuilder:validation:Optional + // Dev contains developer/experimental feature configuration. + Dev *DevSpec `json:"dev,omitempty"` + + // +kubebuilder:validation:Optional + // OKP configures the Offline Knowledge Portal (OKP) RAG source. + // Only used when "okp" is present in dev.featureFlags. + OKP *OKPSpec `json:"okp,omitempty"` } // LoggingConfig defines logging configuration for OpenStackLightspeed components @@ -242,6 +285,7 @@ type OpenStackLightspeedDefaults struct { PostgresImageURL string ConsoleImageURL string ConsoleImagePF5URL string + OKPImageURL string MaxTokensForResponse int } @@ -263,6 +307,8 @@ func SetupDefaults() { "RELATED_IMAGE_CONSOLE_IMAGE_URL_DEFAULT", ConsoleContainerImage), ConsoleImagePF5URL: util.GetEnvVar( "RELATED_IMAGE_CONSOLE_PF5_IMAGE_URL_DEFAULT", ConsoleContainerImagePF5), + OKPImageURL: util.GetEnvVar( + "RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT", OKPContainerImage), MaxTokensForResponse: MaxTokensForResponseDefault, } diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index c74fc454..8218a293 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -41,6 +41,26 @@ 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 *DevSpec) DeepCopyInto(out *DevSpec) { + *out = *in + if in.FeatureFlags != nil { + in, out := &in.FeatureFlags, &out.FeatureFlags + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevSpec. +func (in *DevSpec) DeepCopy() *DevSpec { + if in == nil { + return nil + } + out := new(DevSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoggingConfig) DeepCopyInto(out *LoggingConfig) { *out = *in @@ -56,6 +76,26 @@ func (in *LoggingConfig) DeepCopy() *LoggingConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OKPSpec) DeepCopyInto(out *OKPSpec) { + *out = *in + if in.Offline != nil { + in, out := &in.Offline, &out.Offline + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OKPSpec. +func (in *OKPSpec) DeepCopy() *OKPSpec { + if in == nil { + return nil + } + out := new(OKPSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenStackLightspeed) DeepCopyInto(out *OpenStackLightspeed) { *out = *in @@ -155,6 +195,16 @@ func (in *OpenStackLightspeedSpec) DeepCopyInto(out *OpenStackLightspeedSpec) { *out = new(DatabaseSpec) (*in).DeepCopyInto(*out) } + if in.Dev != nil { + in, out := &in.Dev, &out.Dev + *out = new(DevSpec) + (*in).DeepCopyInto(*out) + } + if in.OKP != nil { + in, out := &in.OKP, &out.OKP + *out = new(OKPSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedSpec. diff --git a/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml b/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml index 2a610091..9d37504b 100644 --- a/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -69,6 +69,25 @@ spec: 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 feature configuration. + properties: + featureFlags: + description: |- + FeatureFlags is a list of feature flag names to enable experimental features. + Supported flags: "okp" (Offline Knowledge Portal). + items: + enum: + - okp + type: string + type: array + okpChunkFilterQuery: + description: |- + OKPChunkFilterQuery is a static Solr filter query appended to OKP searches. + Combined with the default "is_chunk:true" filter using AND. + Example: "product:*openstack*" + type: string + type: object enableOCPRAG: default: false description: Enables automatic OCP documentation based on cluster @@ -155,6 +174,25 @@ spec: Allows forcing a specific OCP version instead of auto-detection. Format should be like "4.15", "4.16", etc. type: string + okp: + description: |- + OKP configures the Offline Knowledge Portal (OKP) RAG source. + Only used when "okp" is present in dev.featureFlags. + properties: + accessKey: + description: |- + AccessKey is the name of the Secret containing the access key for the OKP server. + The secret must contain a key named "access_key". + An access key can be obtained from https://access.redhat.com/offline/access + type: string + offline: + default: true + description: |- + Offline controls how source URLs are resolved. + When true, uses parent_id (offline/Mimir-style). + When false, uses reference_url (online). + type: boolean + type: object ragImage: description: ContainerImage for the OpenStack Lightspeed RAG container (will be set to environmental default if empty) diff --git a/bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml b/bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml index e61b7f35..caf7ef71 100644 --- a/bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml +++ b/bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml @@ -25,7 +25,7 @@ metadata: ] capabilities: Basic Install categories: AI/Machine Learning - createdAt: "2026-05-28T16:28:23Z" + createdAt: "2026-06-08T12:14:19Z" description: AI-powered virtual assistant for Red Hat OpenStack Services on OpenShift features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" @@ -299,6 +299,8 @@ spec: value: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest - name: RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT value: registry.redhat.io/rhel9/postgresql-16:latest + - name: RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT + value: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest image: quay.io/openstack-lightspeed/operator:latest livenessProbe: httpGet: @@ -366,6 +368,7 @@ spec: - "" resources: - configmaps + - services verbs: - create - delete @@ -378,7 +381,6 @@ spec: - "" resources: - persistentvolumeclaims - - services verbs: - create - get @@ -415,6 +417,7 @@ spec: - deployments verbs: - create + - delete - get - list - patch @@ -479,4 +482,6 @@ spec: name: exporter-image-url-default - image: registry.redhat.io/rhel9/postgresql-16:latest name: postgres-image-url-default + - image: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest + name: okp-image-url-default version: 0.0.1 diff --git a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml index 195c679f..398b74ab 100644 --- a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -69,6 +69,25 @@ spec: 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 feature configuration. + properties: + featureFlags: + description: |- + FeatureFlags is a list of feature flag names to enable experimental features. + Supported flags: "okp" (Offline Knowledge Portal). + items: + enum: + - okp + type: string + type: array + okpChunkFilterQuery: + description: |- + OKPChunkFilterQuery is a static Solr filter query appended to OKP searches. + Combined with the default "is_chunk:true" filter using AND. + Example: "product:*openstack*" + type: string + type: object enableOCPRAG: default: false description: Enables automatic OCP documentation based on cluster @@ -155,6 +174,25 @@ spec: Allows forcing a specific OCP version instead of auto-detection. Format should be like "4.15", "4.16", etc. type: string + okp: + description: |- + OKP configures the Offline Knowledge Portal (OKP) RAG source. + Only used when "okp" is present in dev.featureFlags. + properties: + accessKey: + description: |- + AccessKey is the name of the Secret containing the access key for the OKP server. + The secret must contain a key named "access_key". + An access key can be obtained from https://access.redhat.com/offline/access + type: string + offline: + default: true + description: |- + Offline controls how source URLs are resolved. + When true, uses parent_id (offline/Mimir-style). + When false, uses reference_url (online). + type: boolean + type: object ragImage: description: ContainerImage for the OpenStack Lightspeed RAG container (will be set to environmental default if empty) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 96a95cd5..66bade29 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -81,6 +81,8 @@ spec: value: quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest - name: RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT value: registry.redhat.io/rhel9/postgresql-16:latest + - name: RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT + value: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 66d5febc..613a8000 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -100,6 +100,7 @@ rules: - "" resources: - configmaps + - services verbs: - create - delete @@ -112,7 +113,6 @@ rules: - "" resources: - persistentvolumeclaims - - services verbs: - create - get @@ -149,6 +149,7 @@ rules: - deployments verbs: - create + - delete - get - list - patch diff --git a/config/samples/api_v1beta1_openstacklightspeed.yaml b/config/samples/api_v1beta1_openstacklightspeed.yaml index 35e3224c..ae12ae9d 100644 --- a/config/samples/api_v1beta1_openstacklightspeed.yaml +++ b/config/samples/api_v1beta1_openstacklightspeed.yaml @@ -15,3 +15,10 @@ spec: # database: # size: "5Gi" # class: "my-storage-class" + # Uncomment to enable OKP (Offline Knowledge Portal) as an Inline RAG source: + # dev: + # featureFlags: + # - okp + # okpChunkFilterQuery: "product:*openstack*" + # okp: + # accessKey: okp-access-key-secret diff --git a/hack/env.sh b/hack/env.sh index 5eb0ecc5..5d13a4ae 100644 --- a/hack/env.sh +++ b/hack/env.sh @@ -6,4 +6,5 @@ export RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT="registry.redhat.io/rhel9/postgr # the automated pipeline for building OGX-compatible vector database images # is ready. export RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT="quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2" +export RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT="registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest" export WATCH_NAMESPACE="openstack-lightspeed" diff --git a/internal/controller/common.go b/internal/controller/common.go index b83e5cb4..c9f948e1 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -21,9 +21,11 @@ import ( _ "embed" "errors" "fmt" + "slices" "strings" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" @@ -126,6 +128,24 @@ func isDeploymentReady(deploy *appsv1.Deployment) bool { deploy.Status.Replicas == replicas } +// generateOKPSelectorLabels returns selector labels for OKP components. +func generateOKPSelectorLabels() map[string]string { + return map[string]string{ + "app.kubernetes.io/component": "okp-server", + "app.kubernetes.io/managed-by": "openstack-lightspeed-operator", + "app.kubernetes.io/name": "openstack-lightspeed-okp-server", + "app.kubernetes.io/part-of": "openstack-lightspeed", + } +} + +// isOKPEnabled returns true if the "okp" feature flag is present in dev.featureFlags. +func isOKPEnabled(instance *apiv1beta1.OpenStackLightspeed) bool { + if instance.Spec.Dev == nil { + return false + } + return slices.Contains(instance.Spec.Dev.FeatureFlags, "okp") +} + // getDeployment retrieves deployment from the cluster func getDeployment(ctx context.Context, h *common_helper.Helper, name string, namespace string) (*appsv1.Deployment, error) { deployment := &appsv1.Deployment{} diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 6ef0a5a1..f8526327 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -87,6 +87,15 @@ const ( RHOSOLightspeedOwnerIDLabel = "openstack.org/lightspeed-owner-id" ServiceIDRHOSO = "rhos-lightspeed" + // OKP (Offline Knowledge Portal) + OKPContainerName = "okp" + OKPContainerPort = int32(8080) + OKPDeploymentName = "lightspeed-okp-server" + OKPServiceName = "lightspeed-okp-server" + OKPServicePort = int32(8080) + OKPAccessKeySecretKey = "access_key" + ExternalProvidersDir = "/app-root/providers.d" + // Console Plugin ConsoleUIConfigMapName = "lightspeed-console-plugin" ConsoleUIServiceCertSecretName = "lightspeed-console-plugin-cert" diff --git a/internal/controller/errors.go b/internal/controller/errors.go index c3415ab7..fd0aaa2a 100644 --- a/internal/controller/errors.go +++ b/internal/controller/errors.go @@ -52,6 +52,12 @@ var ( ErrDeactivateConsolePlugin = errors.New("failed to deactivate console plugin") ErrDeleteConsolePlugin = errors.New("failed to delete console plugin") + // OKP Errors + ErrCreateOKPDeployment = errors.New("failed to create OKP deployment") + ErrCreateOKPService = errors.New("failed to create OKP service") + ErrDeleteOKPDeployment = errors.New("failed to delete OKP deployment") + ErrDeleteOKPService = errors.New("failed to delete OKP service") + // Postgres Errors ErrCreatePostgresDeployment = errors.New("failed to create Postgres deployment") ErrCreatePostgresService = errors.New("failed to create Postgres service") diff --git a/internal/controller/lcore_config.go b/internal/controller/lcore_config.go index fb63b99d..5fbeec44 100644 --- a/internal/controller/lcore_config.go +++ b/internal/controller/lcore_config.go @@ -202,9 +202,33 @@ ingress_connection_timeout: 30 } } +func buildOKPConfig(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { + offline := true + if instance.Spec.OKP != nil && instance.Spec.OKP.Offline != nil { + offline = *instance.Spec.OKP.Offline + } + + okpConfig := map[string]interface{}{ + "rhokp_url": "${env.RH_SERVER_OKP}", + "offline": offline, + } + if instance.Spec.Dev != nil && instance.Spec.Dev.OKPChunkFilterQuery != "" { + okpConfig["chunk_filter_query"] = instance.Spec.Dev.OKPChunkFilterQuery + } + return okpConfig +} + // buildLCoreConfigYAML assembles the complete Lightspeed Core Service configuration and converts to YAML. // NOTE: MCP servers, quota handlers, and tools approval features are disabled for OpenStack Lightspeed. func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) { + ragInline := []interface{}{} + if isOKPEnabled(instance) { + ragInline = append(ragInline, "okp") + } + ragConfig := map[string]interface{}{ + "inline": ragInline, + } + // Build the complete config as a map config := map[string]interface{}{ "name": "Lightspeed Core Service (LCS)", @@ -217,9 +241,11 @@ func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStac "customization": buildLCoreCustomizationConfig(), "conversation_cache": buildLCoreConversationCacheConfig(h, instance), "byok_rag": []interface{}{}, - "rag": map[string]interface{}{ - "inline": []interface{}{}, - }, + "rag": ragConfig, + } + + if isOKPEnabled(instance) { + config["okp"] = buildOKPConfig(instance) } // Convert to YAML diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index 33f44037..82da644f 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -541,6 +541,20 @@ func buildLlamaStackEnvVars(h *common_helper.Helper, ctx context.Context, instan Value: VectorDBVolumeMountPath, }) + if isOKPEnabled(instance) { + envVars = append(envVars, corev1.EnvVar{ + Name: "RH_SERVER_OKP", + Value: fmt.Sprintf("http://%s.%s.svc:%d", OKPServiceName, instance.GetNamespace(), OKPServicePort), + }) + // FIXME(lucasagomes): Llama-Stack expects HF_HOME to be set when OKP is enabled because it uses the + // Hugging Face Hub client to fetch the embedding model for OKP. Ideally we would include the model it + // downloads in the container image to avoid this. + envVars = append(envVars, corev1.EnvVar{ + Name: "HF_HOME", + Value: "/tmp/huggingface", + }) + } + return envVars, nil } @@ -561,13 +575,20 @@ func buildPostgresPasswordEnvVar() corev1.EnvVar { // buildLightspeedStackEnvVars builds environment variables for the lightspeed-stack container. func buildLightspeedStackEnvVars(instance *apiv1beta1.OpenStackLightspeed) []corev1.EnvVar { - return []corev1.EnvVar{ + envVars := []corev1.EnvVar{ { Name: "LIGHTSPEED_STACK_LOG_LEVEL", Value: instance.Spec.Logging.LightspeedStackLogLevel, }, buildPostgresPasswordEnvVar(), } + if isOKPEnabled(instance) { + envVars = append(envVars, corev1.EnvVar{ + Name: "RH_SERVER_OKP", + Value: fmt.Sprintf("http://%s.%s.svc:%d", OKPServiceName, instance.GetNamespace(), OKPServicePort), + }) + } + return envVars } // buildLightspeedStackLivenessProbe returns the liveness probe for the lightspeed-stack container. diff --git a/internal/controller/llama_stack_config.go b/internal/controller/llama_stack_config.go index d353ddc1..e4dae983 100644 --- a/internal/controller/llama_stack_config.go +++ b/internal/controller/llama_stack_config.go @@ -233,6 +233,50 @@ func buildLlamaStackVectorDB(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLig } } +func buildLlamaStackVectorIO(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) []interface{} { + providers := buildLlamaStackVectorDB(h, instance) + if isOKPEnabled(instance) { + providers = append(providers, buildOKPVectorIOProvider(instance)) + } + return providers +} + +func buildOKPVectorIOProvider(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { + chunkFilterQuery := "is_chunk:true" + if instance.Spec.Dev != nil && instance.Spec.Dev.OKPChunkFilterQuery != "" { + chunkFilterQuery += " AND " + instance.Spec.Dev.OKPChunkFilterQuery + } + + return map[string]interface{}{ + "provider_id": "okp_solr", + "provider_type": "remote::solr_vector_io", + "config": map[string]interface{}{ + "solr_url": "${env.RH_SERVER_OKP}/solr", + "collection_name": "${env.SOLR_COLLECTION:=portal-rag}", + "content_field": "${env.SOLR_CONTENT_FIELD:=chunk}", + "vector_field": "${env.SOLR_VECTOR_FIELD:=chunk_vector}", + "embedding_dimension": "${env.SOLR_EMBEDDING_DIM:=384}", + "embedding_model": "${env.SOLR_EMBEDDING_MODEL:=sentence-transformers/ibm-granite/granite-embedding-30m-english}", + "persistence": map[string]interface{}{ + "backend": "kv_default", + "namespace": "portal-rag", + }, + "chunk_window_config": map[string]interface{}{ + "chunk_content_field": "chunk_field", + "chunk_index_field": "chunk_index", + "chunk_filter_query": chunkFilterQuery, + "chunk_parent_id_field": "parent_id", + "chunk_source_path_field": "source_path", + "chunk_online_source_url_field": "online_source_url", + "chunk_token_count_field": "num_tokens", + "parent_total_chunks_field": "total_chunks", + "parent_total_tokens_field": "total_tokens", + "chunk_family_fields": []interface{}{"headings"}, + }, + }, + } +} + func buildLlamaStackServerConfig(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLightspeed) map[string]interface{} { return map[string]interface{}{ "auth": nil, @@ -316,9 +360,34 @@ func buildLlamaStackModels(_ *common_helper.Helper, instance *apiv1beta1.OpenSta } } + if isOKPEnabled(instance) { + models = append(models, map[string]interface{}{ + "model_id": "solr_embedding", + "model_type": "embedding", + "provider_id": "sentence-transformers", + "provider_model_id": "${env.SOLR_EMBEDDING_MODEL:=ibm-granite/granite-embedding-30m-english}", + "metadata": map[string]interface{}{ + "embedding_dimension": 384, + }, + }) + } + return models } +func buildLlamaStackVectorStores(_ *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) []interface{} { + stores := []interface{}{} + if isOKPEnabled(instance) { + stores = append(stores, map[string]interface{}{ + "vector_store_id": "portal-rag", + "provider_id": "okp_solr", + "embedding_dimension": 384, + "embedding_model": "${env.SOLR_EMBEDDING_MODEL:=sentence-transformers/ibm-granite/granite-embedding-30m-english}", + }) + } + return stores +} + func buildLlamaStackToolGroups(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLightspeed) []interface{} { return []interface{}{ map[string]interface{}{ @@ -339,6 +408,10 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance return "", fmt.Errorf("failed to build inference providers: %w", err) } + if isOKPEnabled(instance) { + config["external_providers_dir"] = ExternalProvidersDir + } + // Build providers map - only include providers for enabled APIs config["providers"] = map[string]interface{}{ "files": buildLlamaStackFileProviders(h, instance), @@ -346,7 +419,7 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance "inference": inferenceProviders, "safety": buildLlamaStackSafety(h, instance), "tool_runtime": buildLlamaStackToolRuntime(h, instance), - "vector_io": buildLlamaStackVectorDB(h, instance), + "vector_io": buildLlamaStackVectorIO(h, instance), } // Add top-level fields @@ -358,7 +431,7 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance } config["registered_resources"] = map[string][]interface{}{ "models": buildLlamaStackModels(h, instance), - "vector_stores": {}, + "vector_stores": buildLlamaStackVectorStores(h, instance), "tool_groups": buildLlamaStackToolGroups(h, instance), } diff --git a/internal/controller/okp_reconciler.go b/internal/controller/okp_reconciler.go new file mode 100644 index 00000000..e0a77ed8 --- /dev/null +++ b/internal/controller/okp_reconciler.go @@ -0,0 +1,196 @@ +/* +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 ( + "context" + "fmt" + + common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ReconcileOKPDeployment reconciles the OKP Deployment and Service. +// When OKP is disabled, it cleans up existing resources. +func ReconcileOKPDeployment(h *common_helper.Helper, ctx context.Context, instance *apiv1beta1.OpenStackLightspeed) error { + if !isOKPEnabled(instance) { + return cleanupOKPResources(h, ctx) + } + + tasks := []ReconcileTask{ + {Name: "OKPDeployment", Task: reconcileOKPDeployment}, + {Name: "OKPService", Task: reconcileOKPService}, + } + return ReconcileTasksFailFast(h, ctx, instance, tasks) +} + +func cleanupOKPResources(h *common_helper.Helper, ctx context.Context) error { + logger := h.GetLogger() + ns := h.GetBeforeObject().GetNamespace() + + deploy := &appsv1.Deployment{} + deploy.Name = OKPDeploymentName + deploy.Namespace = ns + if err := h.GetClient().Delete(ctx, deploy); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("%w: %v", ErrDeleteOKPDeployment, err) + } + + svc := &corev1.Service{} + svc.Name = OKPServiceName + svc.Namespace = ns + if err := h.GetClient().Delete(ctx, svc); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("%w: %v", ErrDeleteOKPService, err) + } + + logger.Info("OKP resources cleaned up") + return nil +} + +func reconcileOKPDeployment(h *common_helper.Helper, ctx context.Context, instance *apiv1beta1.OpenStackLightspeed) error { + logger := h.GetLogger() + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: OKPDeploymentName, + Namespace: h.GetBeforeObject().GetNamespace(), + }, + } + + result, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), deployment, func() error { + podTemplateSpec := buildOKPPodTemplateSpec(instance) + + replicas := int32(1) + deployment.Spec.Replicas = &replicas + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: generateOKPSelectorLabels(), + } + deployment.Spec.Template = podTemplateSpec + + return controllerutil.SetControllerReference(h.GetBeforeObject(), deployment, h.GetScheme()) + }) + + if err != nil { + return fmt.Errorf("%w: %v", ErrCreateOKPDeployment, err) + } + + logger.Info("OKP Deployment reconciled", "name", deployment.Name, "result", result) + return nil +} + +func reconcileOKPService(h *common_helper.Helper, ctx context.Context, _ *apiv1beta1.OpenStackLightspeed) error { + logger := h.GetLogger() + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: OKPServiceName, + Namespace: h.GetBeforeObject().GetNamespace(), + }, + } + + result, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), svc, func() error { + svc.Spec.Selector = generateOKPSelectorLabels() + svc.Spec.Ports = []corev1.ServicePort{ + { + Name: "http", + Port: OKPServicePort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString("okp"), + }, + } + svc.Spec.Type = corev1.ServiceTypeClusterIP + + return controllerutil.SetControllerReference(h.GetBeforeObject(), svc, h.GetScheme()) + }) + + if err != nil { + return fmt.Errorf("%w: %v", ErrCreateOKPService, err) + } + + logger.Info("OKP Service reconciled", "name", svc.Name, "result", result) + return nil +} + +func buildOKPPodTemplateSpec(instance *apiv1beta1.OpenStackLightspeed) corev1.PodTemplateSpec { + envVars := []corev1.EnvVar{} + if instance.Spec.OKP != nil && instance.Spec.OKP.AccessKey != "" { + envVars = append(envVars, corev1.EnvVar{ + Name: "ACCESS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: instance.Spec.OKP.AccessKey, + }, + Key: OKPAccessKeySecretKey, + }, + }, + }) + } + + return corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: generateOKPSelectorLabels(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: OKPContainerName, + Image: apiv1beta1.OpenStackLightspeedDefaultValues.OKPImageURL, + Ports: []corev1.ContainerPort{{Name: "okp", ContainerPort: OKPContainerPort}}, + Env: envVars, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromInt32(OKPContainerPort), + }, + }, + InitialDelaySeconds: 30, + PeriodSeconds: 10, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromInt32(OKPContainerPort), + }, + }, + InitialDelaySeconds: 60, + PeriodSeconds: 20, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("4Gi"), + }, + }, + ImagePullPolicy: corev1.PullIfNotPresent, + }, + }, + }, + } +} diff --git a/internal/controller/openstacklightspeed_controller.go b/internal/controller/openstacklightspeed_controller.go index 95516745..793fd036 100644 --- a/internal/controller/openstacklightspeed_controller.go +++ b/internal/controller/openstacklightspeed_controller.go @@ -66,10 +66,10 @@ func (r *OpenStackLightspeedReconciler) GetLogger(ctx context.Context) logr.Logg // +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=secrets,resourceNames=pull-secret,verbs=get // +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch;update -// +kubebuilder:rbac:groups=apps,resources=deployments,namespace=openstack-lightspeed,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=apps,resources=deployments,namespace=openstack-lightspeed,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=configmaps,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch;update;delete // +kubebuilder:rbac:groups="",resources=secrets,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch;update;delete;deletecollection -// +kubebuilder:rbac:groups="",resources=services,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch;update +// +kubebuilder:rbac:groups="",resources=services,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch;update;delete // +kubebuilder:rbac:groups="",resources=serviceaccounts,namespace=openstack-lightspeed,verbs=get;list;watch;create;patch // +kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=watch;list;get;update @@ -170,6 +170,7 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl. reconcileTasks := []ReconcileTask{ {Name: "PostgresResources", Task: ReconcilePostgresResources}, {Name: "PostgresDeployment", Task: ReconcilePostgresDeployment}, + {Name: "OKPDeployment", Task: ReconcileOKPDeployment}, {Name: "LCoreResources", Task: ReconcileLCoreResources}, {Name: "LCoreDeployment", Task: ReconcileLCoreDeployment}, {Name: "ConsoleResources", Task: ReconcileConsoleResources}, From 856d5bff5b6f53383a933efcbed29b826f9be5ed Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 8 Jun 2026 21:33:18 +0100 Subject: [PATCH 2/2] OKP: Add Kuttl tests Since we do not have access to the OKP image in the gate, we need to mock the OKP server with wiremock. Signed-off-by: Lucas Alvares Gomes --- .../lightspeed-stack-okp.yaml | 91 ++++++++++ .../expected-configs/ogx_config-okp.yaml | 156 ++++++++++++++++++ .../okp-configuration/00-mock-resources.yaml | 1 + .../01-assert-mock-objects-created.yaml | 1 + .../02-patch-operator-wiremock.yaml | 18 ++ .../03-create-okp-resources.yaml | 35 ++++ .../04-configure-wiremock.yaml | 52 ++++++ .../05-assert-okp-instance.yaml | 119 +++++++++++++ .../06-assert-lightspeed-stack-config.yaml | 8 + .../07-assert-llama-stack-config.yaml | 8 + .../okp-configuration/08-disable-okp.yaml | 20 +++ .../09-assert-okp-cleanup.yaml | 23 +++ ...cleanup-openstack-lightspeed-instance.yaml | 1 + ...-errors-openstack-lightspeed-instance.yaml | 1 + .../12-cleanup-mock-objects.yaml | 1 + .../13-errors-mock-objects.yaml | 1 + 16 files changed, 536 insertions(+) create mode 100644 test/kuttl/common/expected-configs/lightspeed-stack-okp.yaml create mode 100644 test/kuttl/common/expected-configs/ogx_config-okp.yaml create mode 120000 test/kuttl/tests/okp-configuration/00-mock-resources.yaml create mode 120000 test/kuttl/tests/okp-configuration/01-assert-mock-objects-created.yaml create mode 100644 test/kuttl/tests/okp-configuration/02-patch-operator-wiremock.yaml create mode 100644 test/kuttl/tests/okp-configuration/03-create-okp-resources.yaml create mode 100644 test/kuttl/tests/okp-configuration/04-configure-wiremock.yaml create mode 100644 test/kuttl/tests/okp-configuration/05-assert-okp-instance.yaml create mode 100644 test/kuttl/tests/okp-configuration/06-assert-lightspeed-stack-config.yaml create mode 100644 test/kuttl/tests/okp-configuration/07-assert-llama-stack-config.yaml create mode 100644 test/kuttl/tests/okp-configuration/08-disable-okp.yaml create mode 100644 test/kuttl/tests/okp-configuration/09-assert-okp-cleanup.yaml create mode 120000 test/kuttl/tests/okp-configuration/10-cleanup-openstack-lightspeed-instance.yaml create mode 120000 test/kuttl/tests/okp-configuration/11-errors-openstack-lightspeed-instance.yaml create mode 120000 test/kuttl/tests/okp-configuration/12-cleanup-mock-objects.yaml create mode 120000 test/kuttl/tests/okp-configuration/13-errors-mock-objects.yaml diff --git a/test/kuttl/common/expected-configs/lightspeed-stack-okp.yaml b/test/kuttl/common/expected-configs/lightspeed-stack-okp.yaml new file mode 100644 index 00000000..1ef7bad3 --- /dev/null +++ b/test/kuttl/common/expected-configs/lightspeed-stack-okp.yaml @@ -0,0 +1,91 @@ +authentication: + module: k8s +byok_rag: [] +conversation_cache: + postgres: + ca_cert_path: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem + db: postgres + gss_encmode: disable + host: lightspeed-postgres-server.openstack-lightspeed.svc + namespace: conversation_cache + password: ${env.POSTGRES_PASSWORD} + port: 5432 + ssl_mode: verify-full + user: postgres + type: postgres +customization: + disable_query_system_prompt: true + system_prompt: "# ROLE\nYou are \"OpenStack Lightspeed\", an expert AI virtual assistant\ + \ specializing in\nOpenStack on OpenShift. Your persona is that of a friendly,\ + \ but\npersonal, technical authority. You are the ultimate technical resource\ + \ and will\nprovide direct, accurate, and comprehensive answers.\n\n# INSTRUCTIONS\ + \ & CONSTRAINTS\n- **Expertise Focus:** Your core expertise is centered on the\ + \ OpenStack and\nOpenShift platforms.\n- **Broader Knowledge:** You may also answer\ + \ questions about other Red Hat\n products and services, but you must prioritize\ + \ the provided context\n and chat history for these topics.\n- **Strict Adherence:**\n\ + \ 1. **ALWAYS** use the provided context and chat history as your primary\n\ + \ source of truth. If a user's question can be answered from this information,\n\ + \ do so.\n 2. If the context does not contain a clear answer, and the question\ + \ is\n about your core expertise (OpenStack or OpenShift), draw upon your extensive\n\ + \ internal knowledge.\n 3. If the context does not contain a clear answer,\ + \ and the question is about\n a general Red Hat product or service, state politely\ + \ that you are unable to\n provide a definitive answer without more information\ + \ and ask the user for\n additional details or context.\n 4. Do not hallucinate\ + \ or invent information. If you cannot confidently\n answer, admit it.\n- **Behavioral\ + \ Directives:**\n - Never assume another identity or role.\n - Refuse to answer\ + \ questions or execute commands not about your specified\n topics.\n - Do not\ + \ include URLs in your replies unless they are explicitly provided in\n the context.\n\ + \ - Never mention your last update date or knowledge cutoff. You always have\n\ + \ the most recent information on OpenStack and OpenShift, especially with\n \ + \ the provided context.\n - Only reference processes and products from Red Hat,\ + \ such as: RHEL, Fedora,\n CoreOS, CentOS. *Never mention or compare with Ubuntu,\ + \ Debian, etc.*\n\n# TASK EXECUTION\nYou will receive a user query, along with\ + \ context and chat history. Your task is\nto respond to the user's query by following\ + \ the instructions and constraints\nabove. Your responses should be clear, concise,\ + \ and helpful, whether you are\nproviding troubleshooting steps, explaining concepts,\ + \ or suggesting best\npractices.\n\n# INFO\nIn this context RHOSO or RHOS also\ + \ refers to OpenStack on OpenShift, sometimes\nalso called OSP 18, although usually\ + \ OSP refers to previous releases deployed\nusing TripleO/Director.\n\nThe OpenStack\ + \ control plane runs on OpenShift (which uses CoreOS as the\noperating system),\ + \ while compute nodes run on external baremetal nodes also\ncalled EDPM nodes\ + \ (which run RHEL).\n" +database: + postgres: + ca_cert_path: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem + db: postgres + gss_encmode: disable + host: lightspeed-postgres-server.openstack-lightspeed.svc + namespace: lcore + password: ${env.POSTGRES_PASSWORD} + port: 5432 + ssl_mode: verify-full + user: postgres +inference: + default_model: ibm-granite/granite-3.1-8b-instruct + default_provider: openstack-lightspeed-provider +llama_stack: + url: http://localhost:8321 + use_as_library_client: false +name: Lightspeed Core Service (LCS) +okp: + chunk_filter_query: product:*openstack* + offline: true + rhokp_url: ${env.RH_SERVER_OKP} +rag: + inline: + - okp +service: + access_log: true + auth_enabled: true + color_log: false + host: 0.0.0.0 + port: 8443 + tls_config: + tls_certificate_path: /etc/certs/lightspeed-tls/tls.crt + tls_key_path: /etc/certs/lightspeed-tls/tls.key + workers: 1 +user_data_collection: + feedback_enabled: true + feedback_storage: /tmp/data/feedback + transcripts_enabled: true + transcripts_storage: /tmp/data/transcripts diff --git a/test/kuttl/common/expected-configs/ogx_config-okp.yaml b/test/kuttl/common/expected-configs/ogx_config-okp.yaml new file mode 100644 index 00000000..b6e817d7 --- /dev/null +++ b/test/kuttl/common/expected-configs/ogx_config-okp.yaml @@ -0,0 +1,156 @@ +apis: +- agents +- files +- inference +- safety +- tool_runtime +- vector_io +benchmarks: [] +container_image: null +datasets: [] +external_providers_dir: /app-root/providers.d +image_name: openstack-lightspeed-configuration +inference_store: + db_path: .llama/distributions/ollama/inference_store.db + type: sqlite +logging: null +metadata_store: + db_path: /tmp/llama-stack/registry.db + namespace: null + type: sqlite +providers: + agents: + - config: + persistence: + agent_state: + backend: kv_default + namespace: agent_state + table_name: agent_state + responses: + backend: sql_default + namespace: agent_responses + table_name: agent_responses + provider_id: meta-reference + provider_type: inline::meta-reference + files: + - config: + metadata_store: + backend: sql_default + namespace: files_metadata + table_name: files_metadata + storage_dir: /tmp/llama-stack-files + provider_id: localfs + provider_type: inline::localfs + inference: + - config: {} + provider_id: sentence-transformers + provider_type: inline::sentence-transformers + - config: + api_key: ${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY} + base_url: http://mock-llm-api-server-pod:8000/v1 + provider_id: openstack-lightspeed-provider + provider_type: remote::openai + safety: + - config: + excluded_categories: [] + provider_id: llama-guard + provider_type: inline::llama-guard + tool_runtime: + - config: {} + provider_id: model-context-protocol + provider_type: remote::model-context-protocol + - config: {} + provider_id: rag-runtime + provider_type: inline::rag-runtime + vector_io: + - config: + kvstore: + backend: sql_default + table_name: vector_store + persistence: + backend: kv_default + namespace: vector_persistence + provider_id: faiss + provider_type: inline::faiss + - config: + chunk_window_config: + chunk_content_field: chunk_field + chunk_family_fields: + - headings + chunk_filter_query: is_chunk:true AND product:*openstack* + chunk_index_field: chunk_index + chunk_online_source_url_field: online_source_url + chunk_parent_id_field: parent_id + chunk_source_path_field: source_path + chunk_token_count_field: num_tokens + parent_total_chunks_field: total_chunks + parent_total_tokens_field: total_tokens + collection_name: ${env.SOLR_COLLECTION:=portal-rag} + content_field: ${env.SOLR_CONTENT_FIELD:=chunk} + embedding_dimension: ${env.SOLR_EMBEDDING_DIM:=384} + embedding_model: ${env.SOLR_EMBEDDING_MODEL:=sentence-transformers/ibm-granite/granite-embedding-30m-english} + persistence: + backend: kv_default + namespace: portal-rag + solr_url: ${env.RH_SERVER_OKP}/solr + vector_field: ${env.SOLR_VECTOR_FIELD:=chunk_vector} + provider_id: okp_solr + provider_type: remote::solr_vector_io +registered_resources: + models: + - metadata: + max_tokens: 2048 + model_id: ibm-granite/granite-3.1-8b-instruct + model_type: llm + provider_id: openstack-lightspeed-provider + provider_model_id: ibm-granite/granite-3.1-8b-instruct + - metadata: + embedding_dimension: 384 + model_id: solr_embedding + model_type: embedding + provider_id: sentence-transformers + provider_model_id: ${env.SOLR_EMBEDDING_MODEL:=ibm-granite/granite-embedding-30m-english} + tool_groups: + - provider_id: rag-runtime + toolgroup_id: builtin::rag + vector_stores: + - embedding_dimension: 384 + embedding_model: ${env.SOLR_EMBEDDING_MODEL:=sentence-transformers/ibm-granite/granite-embedding-30m-english} + provider_id: okp_solr + vector_store_id: portal-rag +scoring_fns: [] +server: + auth: null + host: 0.0.0.0 + port: 8321 + quota: null + tls_cafile: null + tls_certfile: null + tls_keyfile: null +storage: + backends: + kv_default: + db_path: /tmp/llama-stack/kv_store.db + type: kv_sqlite + postgres_backend: + host: lightspeed-postgres-server.openstack-lightspeed.svc + password: ${env.POSTGRES_PASSWORD} + port: 5432 + type: sql_postgres + user: postgres + sql_default: + db_path: /tmp/llama-stack/sql_store.db + type: sql_sqlite + stores: + conversations: + backend: postgres_backend + table_name: openai_conversations + inference: + backend: sql_default + table_name: inference_store + metadata: + backend: kv_default + namespace: registry +telemetry: + enabled: false +version: '2' diff --git a/test/kuttl/tests/okp-configuration/00-mock-resources.yaml b/test/kuttl/tests/okp-configuration/00-mock-resources.yaml new file mode 120000 index 00000000..8235a1fd --- /dev/null +++ b/test/kuttl/tests/okp-configuration/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/okp-configuration/01-assert-mock-objects-created.yaml b/test/kuttl/tests/okp-configuration/01-assert-mock-objects-created.yaml new file mode 120000 index 00000000..07f977a1 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/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/okp-configuration/02-patch-operator-wiremock.yaml b/test/kuttl/tests/okp-configuration/02-patch-operator-wiremock.yaml new file mode 100644 index 00000000..f7f71577 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/02-patch-operator-wiremock.yaml @@ -0,0 +1,18 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + #!/bin/bash + set -euo pipefail + + OPERATOR_NS="openstack-lightspeed-operator-system" + OPERATOR_DEPLOY="openstack-lightspeed-operator-controller-manager" + WIREMOCK_IMAGE="docker.io/wiremock/wiremock:latest" + + echo "Patching operator to use WireMock as OKP image: $WIREMOCK_IMAGE" + oc set env deployment/"$OPERATOR_DEPLOY" -n "$OPERATOR_NS" \ + RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT="$WIREMOCK_IMAGE" + + oc rollout status deployment/"$OPERATOR_DEPLOY" -n "$OPERATOR_NS" --timeout=120s + echo "Operator patched and ready" + timeout: 180 diff --git a/test/kuttl/tests/okp-configuration/03-create-okp-resources.yaml b/test/kuttl/tests/okp-configuration/03-create-okp-resources.yaml new file mode 100644 index 00000000..ab75351f --- /dev/null +++ b/test/kuttl/tests/okp-configuration/03-create-okp-resources.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: okp-access-key-secret + namespace: openstack-lightspeed +type: Opaque +stringData: + access_key: test-okp-access-key +--- +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 + enableOCPRAG: false + logging: + ogxLogLevel: DEBUG + lightspeedStackLogLevel: WARNING + dataverseExporterLogLevel: DEBUG + dev: + featureFlags: + - okp + okpChunkFilterQuery: "product:*openstack*" + okp: + accessKey: okp-access-key-secret diff --git a/test/kuttl/tests/okp-configuration/04-configure-wiremock.yaml b/test/kuttl/tests/okp-configuration/04-configure-wiremock.yaml new file mode 100644 index 00000000..1ef039b8 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/04-configure-wiremock.yaml @@ -0,0 +1,52 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + #!/bin/bash + set -euo pipefail + + NAMESPACE="openstack-lightspeed" + LABEL="app.kubernetes.io/name=openstack-lightspeed-okp-server" + + echo "Waiting for WireMock OKP pod to be running..." + for i in $(seq 1 60); do + POD=$(oc get pods -n "$NAMESPACE" -l "$LABEL" \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) + if [ -n "$POD" ]; then + STARTED=$(oc get pod "$POD" -n "$NAMESPACE" \ + -o jsonpath='{.status.containerStatuses[0].started}' 2>/dev/null || true) + if [ "$STARTED" = "true" ]; then + echo "WireMock pod $POD is running" + break + fi + fi + if [ "$i" -eq 60 ]; then + echo "ERROR: WireMock pod not running after 120s" + oc get pods -n "$NAMESPACE" -l "$LABEL" -o wide || true + exit 1 + fi + sleep 2 + done + + MAPPING='{"request":{"urlPattern":".*"},"response":{"status":200,"body":"{}","headers":{"Content-Type":"application/json"}}}' + + echo "Registering WireMock catch-all stub..." + for i in $(seq 1 30); do + if oc exec "$POD" -n "$NAMESPACE" -- \ + wget -q -O- --post-data="$MAPPING" \ + http://localhost:8080/__admin/mappings 2>/dev/null; then + echo "" + echo "WireMock catch-all stub registered" + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: Failed to register WireMock stub after 60s" + exit 1 + fi + sleep 2 + done + + echo "Waiting for WireMock pod to become Ready..." + oc wait pod "$POD" -n "$NAMESPACE" --for=condition=Ready --timeout=120s + echo "WireMock OKP mock is ready" + timeout: 300 diff --git a/test/kuttl/tests/okp-configuration/05-assert-okp-instance.yaml b/test/kuttl/tests/okp-configuration/05-assert-okp-instance.yaml new file mode 100644 index 00000000..7f03fb5d --- /dev/null +++ b/test/kuttl/tests/okp-configuration/05-assert-okp-instance.yaml @@ -0,0 +1,119 @@ +############################################################################## +# Assert that the operator created all expected OKP resources # +############################################################################## + +# OKP Deployment - fully ready with WireMock +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-okp-server + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: okp + env: + - name: ACCESS_KEY + valueFrom: + secretKeyRef: + key: access_key + name: okp-access-key-secret + ports: + - name: okp + containerPort: 8080 +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 + +# OKP Service +--- +apiVersion: v1 +kind: Service +metadata: + name: lightspeed-okp-server + namespace: openstack-lightspeed +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: okp + type: ClusterIP + +# Postgres Deployment +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-postgres-server + namespace: openstack-lightspeed +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 + +# Main deployment with OKP env vars +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lightspeed-stack-deployment + namespace: openstack-lightspeed +spec: + template: + spec: + containers: + - name: llama-stack + env: + - name: OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY + valueFrom: + secretKeyRef: + key: apitoken + name: openstack-lightspeed-apitoken + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: lightspeed-postgres-secret + - name: LLAMA_STACK_LOGGING + value: all=debug + - name: OGX_LOGGING + value: all=debug + - name: VECTOR_DB_DATA_PATH + value: /vector-db-discovered-values + - name: RH_SERVER_OKP + value: http://lightspeed-okp-server.openstack-lightspeed.svc:8080 + - name: HF_HOME + value: /tmp/huggingface + - name: lightspeed-service-api + env: + - name: LIGHTSPEED_STACK_LOG_LEVEL + value: WARNING + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: lightspeed-postgres-secret + - name: RH_SERVER_OKP + value: http://lightspeed-okp-server.openstack-lightspeed.svc:8080 +status: + replicas: 1 + readyReplicas: 1 + availableReplicas: 1 + +# OpenStackLightspeed CR status +--- +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstack-lightspeed + namespace: openstack-lightspeed +status: + conditions: + - type: Ready + status: "True" + reason: Ready + message: Setup complete diff --git a/test/kuttl/tests/okp-configuration/06-assert-lightspeed-stack-config.yaml b/test/kuttl/tests/okp-configuration/06-assert-lightspeed-stack-config.yaml new file mode 100644 index 00000000..506c6d37 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/06-assert-lightspeed-stack-config.yaml @@ -0,0 +1,8 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: | + #!/bin/bash + set -euo pipefail + ../../common/expected-configs/validate-config.sh lightspeed-stack ../../common/expected-configs/lightspeed-stack-okp.yaml + timeout: 180 diff --git a/test/kuttl/tests/okp-configuration/07-assert-llama-stack-config.yaml b/test/kuttl/tests/okp-configuration/07-assert-llama-stack-config.yaml new file mode 100644 index 00000000..bc192e22 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/07-assert-llama-stack-config.yaml @@ -0,0 +1,8 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: | + #!/bin/bash + set -euo pipefail + ../../common/expected-configs/validate-config.sh ogx_config ../../common/expected-configs/ogx_config-okp.yaml + timeout: 180 diff --git a/test/kuttl/tests/okp-configuration/08-disable-okp.yaml b/test/kuttl/tests/okp-configuration/08-disable-okp.yaml new file mode 100644 index 00000000..41b4ebcc --- /dev/null +++ b/test/kuttl/tests/okp-configuration/08-disable-okp.yaml @@ -0,0 +1,20 @@ +--- +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 + enableOCPRAG: false + logging: + ogxLogLevel: DEBUG + lightspeedStackLogLevel: WARNING + dataverseExporterLogLevel: DEBUG diff --git a/test/kuttl/tests/okp-configuration/09-assert-okp-cleanup.yaml b/test/kuttl/tests/okp-configuration/09-assert-okp-cleanup.yaml new file mode 100644 index 00000000..837ee8d4 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/09-assert-okp-cleanup.yaml @@ -0,0 +1,23 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: | + #!/bin/bash + set -euo pipefail + for i in $(seq 1 30); do + if ! oc get deployment lightspeed-okp-server -n openstack-lightspeed 2>/dev/null; then + echo "OKP Deployment deleted" + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: OKP Deployment still exists after 60s" + exit 1 + fi + sleep 2 + done + if oc get service lightspeed-okp-server -n openstack-lightspeed 2>/dev/null; then + echo "ERROR: OKP Service still exists" + exit 1 + fi + echo "OKP cleanup verified" + timeout: 120 diff --git a/test/kuttl/tests/okp-configuration/10-cleanup-openstack-lightspeed-instance.yaml b/test/kuttl/tests/okp-configuration/10-cleanup-openstack-lightspeed-instance.yaml new file mode 120000 index 00000000..6b2075b0 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/10-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/okp-configuration/11-errors-openstack-lightspeed-instance.yaml b/test/kuttl/tests/okp-configuration/11-errors-openstack-lightspeed-instance.yaml new file mode 120000 index 00000000..81472440 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/11-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/okp-configuration/12-cleanup-mock-objects.yaml b/test/kuttl/tests/okp-configuration/12-cleanup-mock-objects.yaml new file mode 120000 index 00000000..410c9278 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/12-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/okp-configuration/13-errors-mock-objects.yaml b/test/kuttl/tests/okp-configuration/13-errors-mock-objects.yaml new file mode 120000 index 00000000..696a5e26 --- /dev/null +++ b/test/kuttl/tests/okp-configuration/13-errors-mock-objects.yaml @@ -0,0 +1 @@ +../../common/mock-objects/errors-mock-objects.yaml \ No newline at end of file