diff --git a/api/v1beta1/openstacklightspeed_types.go b/api/v1beta1/openstacklightspeed_types.go index 33844ee..f5d22bc 100644 --- a/api/v1beta1/openstacklightspeed_types.go +++ b/api/v1beta1/openstacklightspeed_types.go @@ -55,7 +55,7 @@ const ( // // Supported fields: // - featureFlags: list of experimental feature flags to enable (e.g. ["okp"]) -// - okpChunkFilterQuery: Solr filter query for OKP searches (default: "product:(*openstack* OR *openshift*)") +// - 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: false) type DevSpec struct { FeatureFlags []string `json:"featureFlags,omitempty"` diff --git a/go.mod b/go.mod index c832f9a..509bba4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openstack-k8s-operators/lightspeed-operator go 1.24.6 require ( + github.com/Masterminds/semver/v3 v3.4.0 github.com/go-logr/logr v1.4.3 github.com/onsi/ginkgo/v2 v2.27.5 github.com/onsi/gomega v1.39.0 @@ -22,7 +23,6 @@ replace github.com/openshift/api => github.com/openshift/api v0.0.0-202507112000 require ( cel.dev/expr v0.24.0 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect diff --git a/internal/controller/common.go b/internal/controller/common.go index 2755a38..da9b623 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -148,13 +148,24 @@ func isOKPEnabled(instance *apiv1beta1.OpenStackLightspeed) bool { return slices.Contains(config.FeatureFlags, "okp") } -// getOKPChunkFilterQuery returns the chunk filter query from the dev config, or the default. -func getOKPChunkFilterQuery(instance *apiv1beta1.OpenStackLightspeed) string { +// 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 { config, _ := parseDevConfig(instance) if config.OKPChunkFilterQuery != "" { return config.OKPChunkFilterQuery } - return OKPDefaultChunkFilterQuery + + logger := h.GetLogger() + + ocpVersion, err := DetectOCPVersion(ctx, h) + if err != nil { + logger.Error(err, "Failed to detect OCP version, using default", "default", OKPDefaultOCPVersion) + ocpVersion = OKPDefaultOCPVersion + } + + osVersion := detectRHOSOVersion(ocpVersion, logger) + + return fmt.Sprintf(OKPChunkFilterQueryFmt, osVersion, ocpVersion) } // getDeployment retrieves deployment from the cluster diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 7e42faf..088d7ff 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -120,14 +120,16 @@ const ( 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" - OKPDefaultChunkFilterQuery = "product:(*openstack* OR *openshift*)" - ExternalProvidersDir = "/app-root/providers.d" + OKPContainerName = "okp" + OKPContainerPort = int32(8080) + OKPDeploymentName = "lightspeed-okp-server" + OKPServiceName = "lightspeed-okp-server" + OKPServicePort = int32(8080) + OKPAccessKeySecretKey = "access_key" + OKPDefaultOCPVersion = "4.21" + OKPDefaultRHOSOVersion = "18.0" + OKPChunkFilterQueryFmt = "((product:*openstack* AND product_version:%s) OR (product:*openshift* AND product_version:%s))" + ExternalProvidersDir = "/app-root/providers.d" // Console Plugin ConsoleUIConfigMapName = "lightspeed-console-plugin" diff --git a/internal/controller/lcore_config.go b/internal/controller/lcore_config.go index 5729570..f1981a0 100644 --- a/internal/controller/lcore_config.go +++ b/internal/controller/lcore_config.go @@ -17,6 +17,7 @@ limitations under the License. package controller import ( + "context" _ "embed" "fmt" @@ -203,25 +204,26 @@ ingress_connection_timeout: 30 } } -func buildOKPConfig(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { +func buildOKPConfig(ctx context.Context, h *common_helper.Helper, 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, + return map[string]interface{}{ + "rhokp_url": "${env.RH_SERVER_OKP}", + "offline": offline, + "chunk_filter_query": getOKPChunkFilterQuery(ctx, h, instance), } - okpConfig["chunk_filter_query"] = getOKPChunkFilterQuery(instance) - 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) { +func buildLCoreConfigYAML(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) { + okpEnabled := isOKPEnabled(instance) + ragInline := []interface{}{} - if isOKPEnabled(instance) { + if okpEnabled { ragInline = append(ragInline, "okp") } ragConfig := map[string]interface{}{ @@ -243,8 +245,8 @@ func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStac "rag": ragConfig, } - if isOKPEnabled(instance) { - config["okp"] = buildOKPConfig(instance) + if okpEnabled { + config["okp"] = buildOKPConfig(ctx, h, instance) } // Convert to YAML diff --git a/internal/controller/lcore_reconciler.go b/internal/controller/lcore_reconciler.go index 6f7228e..74dfbea 100644 --- a/internal/controller/lcore_reconciler.go +++ b/internal/controller/lcore_reconciler.go @@ -217,7 +217,7 @@ func reconcileLcoreConfigMap(h *common_helper.Helper, ctx context.Context, insta logger := h.GetLogger() // Build the YAML data - yamlData, err := buildLCoreConfigYAML(h, instance) + yamlData, err := buildLCoreConfigYAML(ctx, h, instance) if err != nil { return fmt.Errorf("%w: %v", ErrGenerateAPIConfigmap, err) } diff --git a/internal/controller/llama_stack_config.go b/internal/controller/llama_stack_config.go index f0d054a..b8941cd 100644 --- a/internal/controller/llama_stack_config.go +++ b/internal/controller/llama_stack_config.go @@ -254,16 +254,16 @@ func buildLlamaStackVectorDB(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLig } } -func buildLlamaStackVectorIO(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) []interface{} { +func buildLlamaStackVectorIO(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed, chunkFilterQuery string) []interface{} { providers := buildLlamaStackVectorDB(h, instance) if isOKPEnabled(instance) { - providers = append(providers, buildOKPVectorIOProvider(instance)) + providers = append(providers, buildOKPVectorIOProvider(chunkFilterQuery)) } return providers } -func buildOKPVectorIOProvider(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { - chunkFilterQuery := "is_chunk:true AND " + getOKPChunkFilterQuery(instance) +func buildOKPVectorIOProvider(chunkFilterQuery string) map[string]interface{} { + chunkFilterQuery = "is_chunk:true AND " + chunkFilterQuery return map[string]interface{}{ "provider_id": "okp_solr", @@ -426,8 +426,10 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance return "", fmt.Errorf("failed to build inference providers: %w", err) } + okpChunkFilterQuery := "" if isOKPEnabled(instance) { config["external_providers_dir"] = ExternalProvidersDir + okpChunkFilterQuery = getOKPChunkFilterQuery(ctx, h, instance) } // Build providers map - only include providers for enabled APIs @@ -437,7 +439,7 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance "inference": inferenceProviders, "safety": buildLlamaStackSafety(h, instance), "tool_runtime": buildLlamaStackToolRuntime(h, instance), - "vector_io": buildLlamaStackVectorIO(h, instance), + "vector_io": buildLlamaStackVectorIO(h, instance, okpChunkFilterQuery), } // Add top-level fields diff --git a/internal/controller/openstack_version.go b/internal/controller/openstack_version.go new file mode 100644 index 0000000..2dc9542 --- /dev/null +++ b/internal/controller/openstack_version.go @@ -0,0 +1,64 @@ +/* +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 ( + "github.com/Masterminds/semver/v3" + "github.com/go-logr/logr" +) + +// ocpVersionBound maps an OCP version upper bound to a RHOSO version. +type ocpVersionBound struct { + maxOCPVersion string + rhosoVersion string +} + +// ocpToRHOSOVersionMap maps OCP version upper bounds to RHOSO versions. +// Entries must be in ascending order of maxOCPVersion. +// Versions above the highest bound fall back to OKPDefaultRHOSOVersion. +// Add a new entry here when a new RHOSO version's content becomes available in the knowledge base. +var ocpToRHOSOVersionMap = []ocpVersionBound{ + {"4.21", "18.0"}, + // When RHOSO 19.0 content is available, add: {"4.XX", "19.0"} +} + +// detectRHOSOVersion returns the RHOSO version corresponding to the given OCP version. +// Falls back to OKPDefaultRHOSOVersion if the version cannot be parsed or is above all defined bounds. +func detectRHOSOVersion(ocpVersion string, logger logr.Logger) string { + detected, err := semver.NewVersion(ocpVersion) + if err != nil { + logger.Info("Failed to parse OCP version, using default RHOSO version", + "ocpVersion", ocpVersion, "default", OKPDefaultRHOSOVersion) + return OKPDefaultRHOSOVersion + } + + for _, entry := range ocpToRHOSOVersionMap { + bound, err := semver.NewVersion(entry.maxOCPVersion) + if err != nil { + logger.Info("Invalid bound in RHOSO version map, using default", + "bound", entry.maxOCPVersion, "default", OKPDefaultRHOSOVersion) + return OKPDefaultRHOSOVersion + } + if detected.Compare(bound) <= 0 { + return entry.rhosoVersion + } + } + + logger.Info("OCP version above all known bounds, using default RHOSO version", + "ocpVersion", ocpVersion, "default", OKPDefaultRHOSOVersion) + return OKPDefaultRHOSOVersion +} diff --git a/internal/controller/openstack_version_test.go b/internal/controller/openstack_version_test.go new file mode 100644 index 0000000..c60eec6 --- /dev/null +++ b/internal/controller/openstack_version_test.go @@ -0,0 +1,132 @@ +/* +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" + + "github.com/go-logr/logr" +) + +func TestDetectRHOSOVersion(t *testing.T) { + logger := logr.Discard() + + tests := []struct { + name string + ocpVersion string + expected string + }{ + { + name: "Version below bound returns mapped RHOSO version", + ocpVersion: "4.16", + expected: "18.0", + }, + { + name: "Version at bound returns mapped RHOSO version", + ocpVersion: "4.21", + expected: "18.0", + }, + { + name: "Version with patch at bound returns mapped RHOSO version", + ocpVersion: "4.21.3", + expected: "18.0", + }, + { + name: "Version above all known bounds falls back to default", + ocpVersion: "4.22", + expected: OKPDefaultRHOSOVersion, + }, + { + name: "Far future version falls back to default", + ocpVersion: "5.0", + expected: OKPDefaultRHOSOVersion, + }, + { + name: "Invalid version string falls back to default", + ocpVersion: "not-a-version", + expected: OKPDefaultRHOSOVersion, + }, + { + name: "Empty version string falls back to default", + ocpVersion: "", + expected: OKPDefaultRHOSOVersion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detectRHOSOVersion(tt.ocpVersion, logger) + if result != tt.expected { + t.Errorf("detectRHOSOVersion(%q) = %q, want %q", tt.ocpVersion, result, tt.expected) + } + }) + } +} + +func TestDetectRHOSOVersionMapOrdering(t *testing.T) { + logger := logr.Discard() + + // Save and restore the global map so this test is self-contained. + original := ocpToRHOSOVersionMap + t.Cleanup(func() { ocpToRHOSOVersionMap = original }) + + ocpToRHOSOVersionMap = []ocpVersionBound{ + {"4.21", "18.0"}, + {"5.99", "19.0"}, + } + + tests := []struct { + name string + ocpVersion string + expected string + }{ + { + name: "Version matched by first entry", + ocpVersion: "4.16", + expected: "18.0", + }, + { + name: "Version at boundary of first entry", + ocpVersion: "4.21", + expected: "18.0", + }, + { + name: "Version matched by second entry", + ocpVersion: "5.0", + expected: "19.0", + }, + { + name: "Version at boundary of second entry", + ocpVersion: "5.99", + expected: "19.0", + }, + { + name: "Version above all bounds falls back to default", + ocpVersion: "6.0", + expected: OKPDefaultRHOSOVersion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detectRHOSOVersion(tt.ocpVersion, logger) + if result != tt.expected { + t.Errorf("detectRHOSOVersion(%q) = %q, want %q", tt.ocpVersion, result, tt.expected) + } + }) + } +} diff --git a/test/kuttl/tests/okp-configuration/02-create-okp-resources.yaml b/test/kuttl/tests/okp-configuration/02-create-okp-resources.yaml index 4df3cfd..49e9134 100644 --- a/test/kuttl/tests/okp-configuration/02-create-okp-resources.yaml +++ b/test/kuttl/tests/okp-configuration/02-create-okp-resources.yaml @@ -21,3 +21,4 @@ spec: dev: featureFlags: - okp + okpChunkFilterQuery: "product:(*openstack* OR *openshift*)"