Skip to content

Commit 967e602

Browse files
authored
Merge pull request #9 from umago/okp-version-detection
OKP: Filter by product version
2 parents 171cb0a + 5ef9c77 commit 967e602

10 files changed

Lines changed: 243 additions & 29 deletions

File tree

api/v1beta1/openstacklightspeed_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const (
5555
//
5656
// Supported fields:
5757
// - featureFlags: list of experimental feature flags to enable (e.g. ["okp"])
58-
// - okpChunkFilterQuery: Solr filter query for OKP searches (default: "product:(*openstack* OR *openshift*)")
58+
// - okpChunkFilterQuery: Solr filter query for OKP searches (default: version-aware query combining detected OpenStack and OCP versions)
5959
// - okpRagOnly: when true, only OKP is used as a RAG source (default: false)
6060
type DevSpec struct {
6161
FeatureFlags []string `json:"featureFlags,omitempty"`

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/openstack-k8s-operators/lightspeed-operator
33
go 1.24.6
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.4.0
67
github.com/go-logr/logr v1.4.3
78
github.com/onsi/ginkgo/v2 v2.27.5
89
github.com/onsi/gomega v1.39.0
@@ -22,7 +23,6 @@ replace github.com/openshift/api => github.com/openshift/api v0.0.0-202507112000
2223

2324
require (
2425
cel.dev/expr v0.24.0 // indirect
25-
github.com/Masterminds/semver/v3 v3.4.0 // indirect
2626
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
2727
github.com/beorn7/perks v1.0.1 // indirect
2828
github.com/blang/semver/v4 v4.0.0 // indirect

internal/controller/common.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,24 @@ func isOKPEnabled(instance *apiv1beta1.OpenStackLightspeed) bool {
148148
return slices.Contains(config.FeatureFlags, "okp")
149149
}
150150

151-
// getOKPChunkFilterQuery returns the chunk filter query from the dev config, or the default.
152-
func getOKPChunkFilterQuery(instance *apiv1beta1.OpenStackLightspeed) string {
151+
// getOKPChunkFilterQuery returns the chunk filter query from the dev config, or a version-aware default.
152+
func getOKPChunkFilterQuery(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) string {
153153
config, _ := parseDevConfig(instance)
154154
if config.OKPChunkFilterQuery != "" {
155155
return config.OKPChunkFilterQuery
156156
}
157-
return OKPDefaultChunkFilterQuery
157+
158+
logger := h.GetLogger()
159+
160+
ocpVersion, err := DetectOCPVersion(ctx, h)
161+
if err != nil {
162+
logger.Error(err, "Failed to detect OCP version, using default", "default", OKPDefaultOCPVersion)
163+
ocpVersion = OKPDefaultOCPVersion
164+
}
165+
166+
osVersion := detectRHOSOVersion(ocpVersion, logger)
167+
168+
return fmt.Sprintf(OKPChunkFilterQueryFmt, osVersion, ocpVersion)
158169
}
159170

160171
// getDeployment retrieves deployment from the cluster

internal/controller/constants.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,16 @@ const (
120120
ServiceIDRHOSO = "rhos-lightspeed"
121121

122122
// OKP (Offline Knowledge Portal)
123-
OKPContainerName = "okp"
124-
OKPContainerPort = int32(8080)
125-
OKPDeploymentName = "lightspeed-okp-server"
126-
OKPServiceName = "lightspeed-okp-server"
127-
OKPServicePort = int32(8080)
128-
OKPAccessKeySecretKey = "access_key"
129-
OKPDefaultChunkFilterQuery = "product:(*openstack* OR *openshift*)"
130-
ExternalProvidersDir = "/app-root/providers.d"
123+
OKPContainerName = "okp"
124+
OKPContainerPort = int32(8080)
125+
OKPDeploymentName = "lightspeed-okp-server"
126+
OKPServiceName = "lightspeed-okp-server"
127+
OKPServicePort = int32(8080)
128+
OKPAccessKeySecretKey = "access_key"
129+
OKPDefaultOCPVersion = "4.21"
130+
OKPDefaultRHOSOVersion = "18.0"
131+
OKPChunkFilterQueryFmt = "((product:*openstack* AND product_version:%s) OR (product:*openshift* AND product_version:%s))"
132+
ExternalProvidersDir = "/app-root/providers.d"
131133

132134
// Console Plugin
133135
ConsoleUIConfigMapName = "lightspeed-console-plugin"

internal/controller/lcore_config.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package controller
1818

1919
import (
20+
"context"
2021
_ "embed"
2122
"fmt"
2223

@@ -203,25 +204,26 @@ ingress_connection_timeout: 30
203204
}
204205
}
205206

206-
func buildOKPConfig(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} {
207+
func buildOKPConfig(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} {
207208
offline := true
208209
if instance.Spec.OKP != nil && instance.Spec.OKP.Offline != nil {
209210
offline = *instance.Spec.OKP.Offline
210211
}
211212

212-
okpConfig := map[string]interface{}{
213-
"rhokp_url": "${env.RH_SERVER_OKP}",
214-
"offline": offline,
213+
return map[string]interface{}{
214+
"rhokp_url": "${env.RH_SERVER_OKP}",
215+
"offline": offline,
216+
"chunk_filter_query": getOKPChunkFilterQuery(ctx, h, instance),
215217
}
216-
okpConfig["chunk_filter_query"] = getOKPChunkFilterQuery(instance)
217-
return okpConfig
218218
}
219219

220220
// buildLCoreConfigYAML assembles the complete Lightspeed Core Service configuration and converts to YAML.
221221
// NOTE: MCP servers, quota handlers, and tools approval features are disabled for OpenStack Lightspeed.
222-
func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) {
222+
func buildLCoreConfigYAML(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) {
223+
okpEnabled := isOKPEnabled(instance)
224+
223225
ragInline := []interface{}{}
224-
if isOKPEnabled(instance) {
226+
if okpEnabled {
225227
ragInline = append(ragInline, "okp")
226228
}
227229
ragConfig := map[string]interface{}{
@@ -243,8 +245,8 @@ func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStac
243245
"rag": ragConfig,
244246
}
245247

246-
if isOKPEnabled(instance) {
247-
config["okp"] = buildOKPConfig(instance)
248+
if okpEnabled {
249+
config["okp"] = buildOKPConfig(ctx, h, instance)
248250
}
249251

250252
// Convert to YAML

internal/controller/lcore_reconciler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func reconcileLcoreConfigMap(h *common_helper.Helper, ctx context.Context, insta
217217
logger := h.GetLogger()
218218

219219
// Build the YAML data
220-
yamlData, err := buildLCoreConfigYAML(h, instance)
220+
yamlData, err := buildLCoreConfigYAML(ctx, h, instance)
221221
if err != nil {
222222
return fmt.Errorf("%w: %v", ErrGenerateAPIConfigmap, err)
223223
}

internal/controller/llama_stack_config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,16 @@ func buildLlamaStackVectorDB(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLig
254254
}
255255
}
256256

257-
func buildLlamaStackVectorIO(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) []interface{} {
257+
func buildLlamaStackVectorIO(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed, chunkFilterQuery string) []interface{} {
258258
providers := buildLlamaStackVectorDB(h, instance)
259259
if isOKPEnabled(instance) {
260-
providers = append(providers, buildOKPVectorIOProvider(instance))
260+
providers = append(providers, buildOKPVectorIOProvider(chunkFilterQuery))
261261
}
262262
return providers
263263
}
264264

265-
func buildOKPVectorIOProvider(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} {
266-
chunkFilterQuery := "is_chunk:true AND " + getOKPChunkFilterQuery(instance)
265+
func buildOKPVectorIOProvider(chunkFilterQuery string) map[string]interface{} {
266+
chunkFilterQuery = "is_chunk:true AND " + chunkFilterQuery
267267

268268
return map[string]interface{}{
269269
"provider_id": "okp_solr",
@@ -426,8 +426,10 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance
426426
return "", fmt.Errorf("failed to build inference providers: %w", err)
427427
}
428428

429+
okpChunkFilterQuery := ""
429430
if isOKPEnabled(instance) {
430431
config["external_providers_dir"] = ExternalProvidersDir
432+
okpChunkFilterQuery = getOKPChunkFilterQuery(ctx, h, instance)
431433
}
432434

433435
// Build providers map - only include providers for enabled APIs
@@ -437,7 +439,7 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance
437439
"inference": inferenceProviders,
438440
"safety": buildLlamaStackSafety(h, instance),
439441
"tool_runtime": buildLlamaStackToolRuntime(h, instance),
440-
"vector_io": buildLlamaStackVectorIO(h, instance),
442+
"vector_io": buildLlamaStackVectorIO(h, instance, okpChunkFilterQuery),
441443
}
442444

443445
// Add top-level fields
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"github.com/Masterminds/semver/v3"
21+
"github.com/go-logr/logr"
22+
)
23+
24+
// ocpVersionBound maps an OCP version upper bound to a RHOSO version.
25+
type ocpVersionBound struct {
26+
maxOCPVersion string
27+
rhosoVersion string
28+
}
29+
30+
// ocpToRHOSOVersionMap maps OCP version upper bounds to RHOSO versions.
31+
// Entries must be in ascending order of maxOCPVersion.
32+
// Versions above the highest bound fall back to OKPDefaultRHOSOVersion.
33+
// Add a new entry here when a new RHOSO version's content becomes available in the knowledge base.
34+
var ocpToRHOSOVersionMap = []ocpVersionBound{
35+
{"4.21", "18.0"},
36+
// When RHOSO 19.0 content is available, add: {"4.XX", "19.0"}
37+
}
38+
39+
// detectRHOSOVersion returns the RHOSO version corresponding to the given OCP version.
40+
// Falls back to OKPDefaultRHOSOVersion if the version cannot be parsed or is above all defined bounds.
41+
func detectRHOSOVersion(ocpVersion string, logger logr.Logger) string {
42+
detected, err := semver.NewVersion(ocpVersion)
43+
if err != nil {
44+
logger.Info("Failed to parse OCP version, using default RHOSO version",
45+
"ocpVersion", ocpVersion, "default", OKPDefaultRHOSOVersion)
46+
return OKPDefaultRHOSOVersion
47+
}
48+
49+
for _, entry := range ocpToRHOSOVersionMap {
50+
bound, err := semver.NewVersion(entry.maxOCPVersion)
51+
if err != nil {
52+
logger.Info("Invalid bound in RHOSO version map, using default",
53+
"bound", entry.maxOCPVersion, "default", OKPDefaultRHOSOVersion)
54+
return OKPDefaultRHOSOVersion
55+
}
56+
if detected.Compare(bound) <= 0 {
57+
return entry.rhosoVersion
58+
}
59+
}
60+
61+
logger.Info("OCP version above all known bounds, using default RHOSO version",
62+
"ocpVersion", ocpVersion, "default", OKPDefaultRHOSOVersion)
63+
return OKPDefaultRHOSOVersion
64+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"testing"
21+
22+
"github.com/go-logr/logr"
23+
)
24+
25+
func TestDetectRHOSOVersion(t *testing.T) {
26+
logger := logr.Discard()
27+
28+
tests := []struct {
29+
name string
30+
ocpVersion string
31+
expected string
32+
}{
33+
{
34+
name: "Version below bound returns mapped RHOSO version",
35+
ocpVersion: "4.16",
36+
expected: "18.0",
37+
},
38+
{
39+
name: "Version at bound returns mapped RHOSO version",
40+
ocpVersion: "4.21",
41+
expected: "18.0",
42+
},
43+
{
44+
name: "Version with patch at bound returns mapped RHOSO version",
45+
ocpVersion: "4.21.3",
46+
expected: "18.0",
47+
},
48+
{
49+
name: "Version above all known bounds falls back to default",
50+
ocpVersion: "4.22",
51+
expected: OKPDefaultRHOSOVersion,
52+
},
53+
{
54+
name: "Far future version falls back to default",
55+
ocpVersion: "5.0",
56+
expected: OKPDefaultRHOSOVersion,
57+
},
58+
{
59+
name: "Invalid version string falls back to default",
60+
ocpVersion: "not-a-version",
61+
expected: OKPDefaultRHOSOVersion,
62+
},
63+
{
64+
name: "Empty version string falls back to default",
65+
ocpVersion: "",
66+
expected: OKPDefaultRHOSOVersion,
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
result := detectRHOSOVersion(tt.ocpVersion, logger)
73+
if result != tt.expected {
74+
t.Errorf("detectRHOSOVersion(%q) = %q, want %q", tt.ocpVersion, result, tt.expected)
75+
}
76+
})
77+
}
78+
}
79+
80+
func TestDetectRHOSOVersionMapOrdering(t *testing.T) {
81+
logger := logr.Discard()
82+
83+
// Save and restore the global map so this test is self-contained.
84+
original := ocpToRHOSOVersionMap
85+
t.Cleanup(func() { ocpToRHOSOVersionMap = original })
86+
87+
ocpToRHOSOVersionMap = []ocpVersionBound{
88+
{"4.21", "18.0"},
89+
{"5.99", "19.0"},
90+
}
91+
92+
tests := []struct {
93+
name string
94+
ocpVersion string
95+
expected string
96+
}{
97+
{
98+
name: "Version matched by first entry",
99+
ocpVersion: "4.16",
100+
expected: "18.0",
101+
},
102+
{
103+
name: "Version at boundary of first entry",
104+
ocpVersion: "4.21",
105+
expected: "18.0",
106+
},
107+
{
108+
name: "Version matched by second entry",
109+
ocpVersion: "5.0",
110+
expected: "19.0",
111+
},
112+
{
113+
name: "Version at boundary of second entry",
114+
ocpVersion: "5.99",
115+
expected: "19.0",
116+
},
117+
{
118+
name: "Version above all bounds falls back to default",
119+
ocpVersion: "6.0",
120+
expected: OKPDefaultRHOSOVersion,
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
result := detectRHOSOVersion(tt.ocpVersion, logger)
127+
if result != tt.expected {
128+
t.Errorf("detectRHOSOVersion(%q) = %q, want %q", tt.ocpVersion, result, tt.expected)
129+
}
130+
})
131+
}
132+
}

test/kuttl/tests/okp-configuration/02-create-okp-resources.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ spec:
2121
dev:
2222
featureFlags:
2323
- okp
24+
okpChunkFilterQuery: "product:(*openstack* OR *openshift*)"

0 commit comments

Comments
 (0)