Skip to content

Commit d1328e1

Browse files
Merge pull request #73 from omkarjoshi0304/OSPRH-23918
feat(controller): add OCP version auto-detection for RAG configuration
2 parents 1d430c1 + ac2a209 commit d1328e1

19 files changed

Lines changed: 827 additions & 18 deletions

api/v1beta1/conditions.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2025.
3+
34
Licensed under the Apache License, Version 2.0 (the "License");
45
you may not use this file except in compliance with the License.
56
You may obtain a copy of the License at
@@ -24,8 +25,11 @@ const (
2425
OpenStackLightspeedReadyCondition condition.Type = "OpenStackLightspeedReady"
2526

2627
// OpenShift Lightspeed Operator Status=True condition which indicates if OpenShift Lightspeed is installed and
27-
// operational and it can be used by OpenStack Lihgtspeed operator.
28+
// operational and it can be used by OpenStack Lightspeed operator.
2829
OpenShiftLightspeedOperatorReadyCondition condition.Type = "OpenShiftLightspeedOperatorReady"
30+
31+
// OCPRAGCondition Status=True condition which indicates the OCP RAG version resolution status
32+
OCPRAGCondition condition.Type = "OCPRAGReady"
2933
)
3034

3135
// Common Messages used by API objects.
@@ -42,6 +46,21 @@ const (
4246
// OpenShiftLightspeedOperatorWaiting
4347
OpenShiftLightspeedOperatorWaiting = "Waiting for the OpenShift Lightspeed operator to deploy."
4448

45-
// OpenShiftLigthspeedOperatorReady
49+
// OpenShiftLightspeedOperatorReady
4650
OpenShiftLightspeedOperatorReady = "OpenShift Lightspeed operator is ready."
51+
52+
// OCPRAGDisabledMessage
53+
OCPRAGDisabledMessage = "OCP RAG is disabled"
54+
55+
// OCPRAGVersionResolvedMessage
56+
OCPRAGVersionResolvedMessage = "OCP RAG version resolved: %s"
57+
58+
// OCPRAGVersionFallbackMessage
59+
OCPRAGVersionFallbackMessage = "Cluster version %s is not explicitly supported. Using 'latest' OCP documentation. Supported versions: %v"
60+
61+
// OCPRAGDetectionFailedMessage
62+
OCPRAGDetectionFailedMessage = "Failed to detect OCP cluster version"
63+
64+
// OCPRAGOverrideInvalidMessage
65+
OCPRAGOverrideInvalidMessage = "Invalid OCP RAG version override"
4766
)

api/v1beta1/openstacklightspeed_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ type OpenStackLightspeedSpec struct {
3737
// +kubebuilder:validation:Optional
3838
// ContainerImage for the OpenStack Lightspeed RAG container (will be set to environmental default if empty)
3939
RAGImage string `json:"ragImage"`
40+
41+
// +kubebuilder:validation:Optional
42+
// +kubebuilder:default=false
43+
// Enables automatic OCP documentation based on cluster version
44+
EnableOCPRAG bool `json:"enableOCPRAG,omitempty"`
45+
46+
// +kubebuilder:validation:Optional
47+
// Allows forcing a specific OCP version instead of auto-detection.
48+
// Format should be like "4.15", "4.16", etc.
49+
OCPRAGVersionOverride string `json:"ocpVersionOverride,omitempty"`
4050
}
4151

4252
// OpenStackLightspeedCore defines the desired state of OpenStackLightspeed
@@ -106,6 +116,11 @@ type OpenStackLightspeedStatus struct {
106116

107117
// ObservedGeneration - the most recent generation observed for this object.
108118
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
119+
120+
// +optional
121+
// ActiveOCPRAGVersion contains the OCP version being used for RAG configuration
122+
// Will be one of: "4.16", "4.18", "latest", or empty if OCP RAG is disabled
123+
ActiveOCPRAGVersion string `json:"activeOCPRAGVersion,omitempty"`
109124
}
110125

111126
// +kubebuilder:object:root=true

bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ spec:
5858
description: Namespace where the CatalogSource containing the OLS
5959
operator is located
6060
type: string
61+
enableOCPRAG:
62+
default: false
63+
description: Enables automatic OCP documentation based on cluster
64+
version
65+
type: boolean
6166
feedbackDisabled:
6267
description: Disable feedback collection
6368
type: boolean
@@ -99,6 +104,11 @@ spec:
99104
description: Name of the model to use at the API endpoint provided
100105
in LLMEndpoint
101106
type: string
107+
ocpVersionOverride:
108+
description: |-
109+
Allows forcing a specific OCP version instead of auto-detection.
110+
Format should be like "4.15", "4.16", etc.
111+
type: string
102112
ragImage:
103113
description: ContainerImage for the OpenStack Lightspeed RAG container
104114
(will be set to environmental default if empty)
@@ -118,6 +128,11 @@ spec:
118128
status:
119129
description: OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed
120130
properties:
131+
activeOCPRAGVersion:
132+
description: |-
133+
ActiveOCPRAGVersion contains the OCP version being used for RAG configuration
134+
Will be one of: "4.16", "4.18", "latest", or empty if OCP RAG is disabled
135+
type: string
121136
conditions:
122137
description: Conditions
123138
items:

bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ spec:
5959
spec:
6060
clusterPermissions:
6161
- rules:
62+
- apiGroups:
63+
- config.openshift.io
64+
resources:
65+
- clusterversions
66+
verbs:
67+
- get
68+
- list
69+
- watch
6270
- apiGroups:
6371
- lightspeed.openstack.org
6472
resources:

config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ spec:
5858
description: Namespace where the CatalogSource containing the OLS
5959
operator is located
6060
type: string
61+
enableOCPRAG:
62+
default: false
63+
description: Enables automatic OCP documentation based on cluster
64+
version
65+
type: boolean
6166
feedbackDisabled:
6267
description: Disable feedback collection
6368
type: boolean
@@ -99,6 +104,11 @@ spec:
99104
description: Name of the model to use at the API endpoint provided
100105
in LLMEndpoint
101106
type: string
107+
ocpVersionOverride:
108+
description: |-
109+
Allows forcing a specific OCP version instead of auto-detection.
110+
Format should be like "4.15", "4.16", etc.
111+
type: string
102112
ragImage:
103113
description: ContainerImage for the OpenStack Lightspeed RAG container
104114
(will be set to environmental default if empty)
@@ -118,6 +128,11 @@ spec:
118128
status:
119129
description: OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed
120130
properties:
131+
activeOCPRAGVersion:
132+
description: |-
133+
ActiveOCPRAGVersion contains the OCP version being used for RAG configuration
134+
Will be one of: "4.16", "4.18", "latest", or empty if OCP RAG is disabled
135+
type: string
121136
conditions:
122137
description: Conditions
123138
items:

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: manager-role
66
rules:
7+
- apiGroups:
8+
- config.openshift.io
9+
resources:
10+
- clusterversions
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- lightspeed.openstack.org
917
resources:

internal/controller/funcs.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,30 @@ func GetOLSConfig(ctx context.Context, helper *common_helper.Helper) (uns.Unstru
143143
"OLSConfig")
144144
}
145145

146+
// BuildRAGConfigs builds the RAG configuration array.
147+
// OpenStack RAG is always included first.
148+
// OCP RAG is added if ocpVersion is provided.
149+
func BuildRAGConfigs(instance *apiv1beta1.OpenStackLightspeed, ocpVersion string) []interface{} {
150+
rags := []interface{}{
151+
// OpenStack RAG
152+
map[string]interface{}{
153+
"image": instance.Spec.RAGImage,
154+
"indexPath": OpenStackLightspeedVectorDBPath,
155+
},
156+
}
157+
158+
// Add OCP RAG if enabled
159+
if ocpVersion != "" {
160+
rags = append(rags, map[string]interface{}{
161+
"image": instance.Spec.RAGImage,
162+
"indexPath": GetOCPVectorDBPath(ocpVersion),
163+
"indexID": GetOCPIndexName(ocpVersion),
164+
})
165+
}
166+
167+
return rags
168+
}
169+
146170
// PatchOLSConfig patches OLSConfig with information from OpenStackLightspeed instance.
147171
func PatchOLSConfig(
148172
helper *common_helper.Helper,
@@ -193,17 +217,10 @@ func PatchOLSConfig(
193217
}
194218

195219
// Patch the RAG section
196-
// NOTE(lucasagomes): We don't need indexID here because the tag on our RAG images
197-
// already matches the indexID that the Vector DB used when it was built. OLS leverages
198-
// that to set the right index.
199-
openstackRAG := []interface{}{
200-
map[string]interface{}{
201-
"image": instance.Spec.RAGImage,
202-
"indexPath": OpenStackLightspeedVectorDBPath,
203-
},
204-
}
220+
// Build RAG array with priorities using BuildRAGConfigs
221+
ragConfigs := BuildRAGConfigs(instance, instance.Status.ActiveOCPRAGVersion)
205222

206-
if err := uns.SetNestedSlice(olsConfig.Object, openstackRAG, "spec", "ols", "rag"); err != nil {
223+
if err := uns.SetNestedSlice(olsConfig.Object, ragConfigs, "spec", "ols", "rag"); err != nil {
207224
return err
208225
}
209226

internal/controller/ocp_version.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2025.
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+
"context"
21+
"fmt"
22+
"regexp"
23+
"slices"
24+
"strings"
25+
26+
common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
27+
uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
)
31+
32+
const (
33+
// OpenStackLightspeedOCPVectorDBPath - base path for OCP vector databases
34+
OpenStackLightspeedOCPVectorDBPath = "/rag/ocp_vector_db/ocp"
35+
36+
// OpenStackLightspeedOCPIndexPrefix - prefix for OCP index names
37+
OpenStackLightspeedOCPIndexPrefix = "ocp-product-docs"
38+
39+
// Supported OCP versions in the RAG database
40+
OCPVersion416 = "4.16"
41+
OCPVersion418 = "4.18"
42+
OCPVersionLatest = "latest"
43+
)
44+
45+
// SupportedOCPVersions lists the OCP versions available in the RAG database
46+
var SupportedOCPVersions = []string{OCPVersion416, OCPVersion418, OCPVersionLatest}
47+
48+
// DetectOCPVersion detects the OpenShift cluster version
49+
func DetectOCPVersion(ctx context.Context, helper *common_helper.Helper) (string, error) {
50+
// Use raw client to access cluster-scoped resources
51+
rawClient, err := GetRawClient(helper)
52+
if err != nil {
53+
return "", fmt.Errorf("failed to get raw client: %w", err)
54+
}
55+
56+
// Get ClusterVersion object
57+
clusterVersion := &uns.Unstructured{}
58+
clusterVersion.SetGroupVersionKind(schema.GroupVersionKind{
59+
Group: "config.openshift.io",
60+
Version: "v1",
61+
Kind: "ClusterVersion",
62+
})
63+
64+
err = rawClient.Get(ctx, client.ObjectKey{Name: "version"}, clusterVersion)
65+
if err != nil {
66+
return "", fmt.Errorf("failed to get ClusterVersion: %w", err)
67+
}
68+
69+
// Extract version from status.desired.version
70+
// NOTE: We intentionally use desired.version rather than history[0].version because:
71+
// - During OCP upgrades, desired.version reflects the target version
72+
// - Users troubleshooting upgrade issues need docs for the NEW version
73+
// - This provides proactive access to relevant documentation
74+
version, found, err := uns.NestedString(clusterVersion.Object, "status", "desired", "version")
75+
if err != nil {
76+
return "", fmt.Errorf("failed to extract version from ClusterVersion: %w", err)
77+
}
78+
if !found {
79+
return "", fmt.Errorf("version field not found in ClusterVersion status.desired.version")
80+
}
81+
82+
// Parse version to get major.minor (e.g., "4.15.0" -> "4.15")
83+
majorMinor, err := ParseMajorMinorVersion(version)
84+
if err != nil {
85+
return "", fmt.Errorf("failed to parse version %s: %w", version, err)
86+
}
87+
88+
return majorMinor, nil
89+
}
90+
91+
// ParseMajorMinorVersion extracts major.minor version from full version string
92+
// Example: "4.15.0-0.nightly-2024-01-15-123456" -> "4.15"
93+
func ParseMajorMinorVersion(fullVersion string) (string, error) {
94+
// Match major.minor pattern at the start
95+
re := regexp.MustCompile(`^(\d+\.\d+)`)
96+
matches := re.FindStringSubmatch(fullVersion)
97+
98+
if len(matches) < 2 {
99+
return "", fmt.Errorf("invalid version format: %s", fullVersion)
100+
}
101+
102+
return matches[1], nil
103+
}
104+
105+
// GetOCPIndexName converts version to index name format
106+
// Example: "4.16" -> "ocp-product-docs-4_16"
107+
//
108+
// "latest" -> "ocp-product-docs-latest"
109+
func GetOCPIndexName(version string) string {
110+
// Replace dots with underscores (no-op for "latest")
111+
versionFormatted := strings.ReplaceAll(version, ".", "_")
112+
return fmt.Sprintf("%s-%s", OpenStackLightspeedOCPIndexPrefix, versionFormatted)
113+
}
114+
115+
// GetOCPVectorDBPath returns the full path to OCP vector DB for given version
116+
// Example: "4.16" -> "/rag/ocp_vector_db/ocp-4.16"
117+
//
118+
// "latest" -> "/rag/ocp_vector_db/ocp-latest"
119+
func GetOCPVectorDBPath(version string) string {
120+
return fmt.Sprintf("%s-%s", OpenStackLightspeedOCPVectorDBPath, version)
121+
}
122+
123+
// IsSupportedOCPVersion checks if the version is explicitly supported in RAG DB
124+
func IsSupportedOCPVersion(version string) bool {
125+
return slices.Contains(SupportedOCPVersions, version)
126+
}
127+
128+
// ResolveOCPVersion determines the OCP version to use for RAG configuration
129+
// Returns (version, isFallback, error)
130+
// - version: The version to use (might be "latest" as fallback)
131+
// - isFallback: true if falling back to "latest" for unsupported version
132+
// - error: any error during version resolution
133+
func ResolveOCPVersion(detectedVersion, overrideVersion string, enableOCPRAG bool) (string, bool, error) {
134+
if !enableOCPRAG {
135+
return "", false, nil
136+
}
137+
138+
// Use override if provided
139+
if overrideVersion != "" {
140+
return overrideVersion, false, nil
141+
}
142+
143+
if detectedVersion == "" {
144+
return "", false, fmt.Errorf("no OCP version detected")
145+
}
146+
147+
// Check if detected version is supported
148+
if IsSupportedOCPVersion(detectedVersion) {
149+
return detectedVersion, false, nil
150+
}
151+
152+
// Fallback to latest for unsupported versions
153+
return OCPVersionLatest, true, nil
154+
}

0 commit comments

Comments
 (0)