Skip to content

Commit f58834c

Browse files
committed
Support changing all the images
Currently we can only change the image for the RAG using the `ragImage` field in our CR, but we have many other images subject to change. In this patch we add functionality to change the images used by the operator for the following: - lighspeed-core - exporter - postgres - console image - OKP image - RHOS MCP server To provide a consistent interface we can now set all the images under the `images` field and we remove the top level `ragImage` field (now available under `images`) for consistency. Removing the field should not be a problem since we don't have a stable CRD yet.
1 parent 8f895e5 commit f58834c

26 files changed

Lines changed: 457 additions & 120 deletions

api/v1beta1/openstacklightspeed_types.go

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"reflect"
21+
2022
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2123
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
2224
"k8s.io/apimachinery/pkg/api/resource"
@@ -99,8 +101,9 @@ type OpenStackLightspeedSpec struct {
99101
OpenStackLightspeedCore `json:",inline"`
100102

101103
// +kubebuilder:validation:Optional
102-
// ContainerImage for the OpenStack Lightspeed RAG container (will be set to environmental default if empty)
103-
RAGImage string `json:"ragImage"`
104+
// Images configures container images used by the operator.
105+
// When omitted, each image defaults to its environment variable or hardcoded fallback.
106+
Images OpenStackLightspeedImages `json:"images,omitempty"`
104107

105108
// +kubebuilder:validation:Optional
106109
// +kubebuilder:default=false
@@ -289,42 +292,74 @@ func (instance OpenStackLightspeed) IsReady() bool {
289292
return instance.Status.Conditions.IsTrue(OpenStackLightspeedReadyCondition)
290293
}
291294

295+
// OpenStackLightspeedImages groups container image URLs used by the operator.
296+
type OpenStackLightspeedImages struct {
297+
RAGImageURL string `json:"ragImage,omitempty"`
298+
LCoreImageURL string `json:"lcoreImage,omitempty"`
299+
ExporterImageURL string `json:"exporterImage,omitempty"`
300+
PostgresImageURL string `json:"postgresImage,omitempty"`
301+
ConsoleImageURL string `json:"consoleImage,omitempty"`
302+
ConsoleImagePF5URL string `json:"consoleImagePF5,omitempty"`
303+
OKPImageURL string `json:"okpImage,omitempty"`
304+
MCPServerImageURL string `json:"mcpServerImage,omitempty"`
305+
}
306+
292307
type OpenStackLightspeedDefaults struct {
293-
RAGImageURL string
294-
LCoreImageURL string
295-
ExporterImageURL string
296-
PostgresImageURL string
297-
ConsoleImageURL string
298-
ConsoleImagePF5URL string
299-
OKPImageURL string
300-
MCPServerImageURL string
301-
MaxTokensForResponse int
308+
OpenStackLightspeedImages `json:",inline"`
309+
MaxTokensForResponse int `json:"maxTokensForResponse,omitempty"`
302310
}
303311

304312
var OpenStackLightspeedDefaultValues OpenStackLightspeedDefaults
305313

306-
// SetupDefaults - initializes OpenStackLightspeedDefaultValues with default values from env vars
314+
// envVarDefaults holds the pristine env-var defaults set once by SetupDefaults.
315+
// MergeDefaults copies from this so that removing dev overrides correctly
316+
// reverts to the original values (the exported global gets overwritten each reconcile).
317+
var envVarDefaults OpenStackLightspeedDefaults
318+
319+
// mergeImages applies non-zero fields from src onto dst.
320+
func mergeImages(dst, src *OpenStackLightspeedImages) {
321+
dstVal := reflect.ValueOf(dst).Elem()
322+
srcVal := reflect.ValueOf(src).Elem()
323+
for i := 0; i < srcVal.NumField(); i++ {
324+
if !srcVal.Field(i).IsZero() {
325+
dstVal.Field(i).Set(srcVal.Field(i))
326+
}
327+
}
328+
}
329+
330+
// SetupDefaults initializes OpenStackLightspeedDefaultValues from env vars.
331+
// Call once at startup; the values never change inside a container.
307332
func SetupDefaults() {
308-
// Acquire environmental defaults and initialize OpenStackLightspeed defaults with them
309-
openStackLightspeedDefaults := OpenStackLightspeedDefaults{
310-
RAGImageURL: util.GetEnvVar(
311-
"RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT", OpenStackLightspeedContainerImage),
312-
LCoreImageURL: util.GetEnvVar(
313-
"RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT", LCoreContainerImage),
314-
ExporterImageURL: util.GetEnvVar(
315-
"RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT", ExporterContainerImage),
316-
PostgresImageURL: util.GetEnvVar(
317-
"RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT", PostgresContainerImage),
318-
ConsoleImageURL: util.GetEnvVar(
319-
"RELATED_IMAGE_CONSOLE_IMAGE_URL_DEFAULT", ConsoleContainerImage),
320-
ConsoleImagePF5URL: util.GetEnvVar(
321-
"RELATED_IMAGE_CONSOLE_PF5_IMAGE_URL_DEFAULT", ConsoleContainerImagePF5),
322-
OKPImageURL: util.GetEnvVar(
323-
"RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT", OKPContainerImage),
324-
MCPServerImageURL: util.GetEnvVar(
325-
"RELATED_IMAGE_MCP_SERVER_IMAGE_URL_DEFAULT", MCPServerContainerImage),
333+
envVarDefaults = OpenStackLightspeedDefaults{
334+
OpenStackLightspeedImages: OpenStackLightspeedImages{
335+
RAGImageURL: util.GetEnvVar(
336+
"RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT", OpenStackLightspeedContainerImage),
337+
LCoreImageURL: util.GetEnvVar(
338+
"RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT", LCoreContainerImage),
339+
ExporterImageURL: util.GetEnvVar(
340+
"RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT", ExporterContainerImage),
341+
PostgresImageURL: util.GetEnvVar(
342+
"RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT", PostgresContainerImage),
343+
ConsoleImageURL: util.GetEnvVar(
344+
"RELATED_IMAGE_CONSOLE_IMAGE_URL_DEFAULT", ConsoleContainerImage),
345+
ConsoleImagePF5URL: util.GetEnvVar(
346+
"RELATED_IMAGE_CONSOLE_PF5_IMAGE_URL_DEFAULT", ConsoleContainerImagePF5),
347+
OKPImageURL: util.GetEnvVar(
348+
"RELATED_IMAGE_OKP_IMAGE_URL_DEFAULT", OKPContainerImage),
349+
MCPServerImageURL: util.GetEnvVar(
350+
"RELATED_IMAGE_MCP_SERVER_IMAGE_URL_DEFAULT", MCPServerContainerImage),
351+
},
326352
MaxTokensForResponse: MaxTokensForResponseDefault,
327353
}
354+
OpenStackLightspeedDefaultValues = envVarDefaults
355+
}
328356

329-
OpenStackLightspeedDefaultValues = openStackLightspeedDefaults
357+
// MergeDefaults returns a copy of the env-var defaults with the spec image
358+
// overrides (if any) applied on top.
359+
func MergeDefaults(specImages *OpenStackLightspeedImages) OpenStackLightspeedDefaults {
360+
merged := envVarDefaults
361+
if specImages != nil {
362+
mergeImages(&merged.OpenStackLightspeedImages, specImages)
363+
}
364+
return merged
330365
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 v1beta1
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"testing"
23+
)
24+
25+
// TestOpenStackLightspeedImagesFieldTypes guards the mergeImages
26+
// reflection-based implementation against future struct changes.
27+
// mergeImages uses reflect and IsZero to copy non-zero fields; this
28+
// only works correctly for simple types (string, int) where the zero
29+
// value reliably means "not set". Adding an unexported field, or a
30+
// complex type (slice, map, pointer, struct), would cause silent
31+
// misbehavior or a panic.
32+
func TestOpenStackLightspeedImagesFieldTypes(t *testing.T) {
33+
allowedKinds := map[reflect.Kind]bool{
34+
reflect.String: true,
35+
}
36+
37+
typ := reflect.TypeOf(OpenStackLightspeedImages{})
38+
for i := 0; i < typ.NumField(); i++ {
39+
field := typ.Field(i)
40+
41+
if !field.IsExported() {
42+
t.Errorf("field %q is unexported; mergeImages uses reflect to set fields "+
43+
"and cannot write unexported fields (will panic)", field.Name)
44+
continue
45+
}
46+
47+
if !allowedKinds[field.Type.Kind()] {
48+
t.Errorf("field %q has type %s (kind %s); mergeImages relies on IsZero "+
49+
"to detect unset values, which is only reliable for string — "+
50+
"add handling in mergeImages before using this type",
51+
field.Name, field.Type, field.Type.Kind())
52+
}
53+
}
54+
}
55+
56+
func TestMergeImages(t *testing.T) {
57+
dst := OpenStackLightspeedImages{
58+
RAGImageURL: "original-rag",
59+
LCoreImageURL: "original-lcore",
60+
}
61+
src := OpenStackLightspeedImages{
62+
RAGImageURL: "override-rag",
63+
ExporterImageURL: "override-exporter",
64+
MCPServerImageURL: "override-mcp",
65+
}
66+
67+
mergeImages(&dst, &src)
68+
69+
checks := []struct {
70+
name string
71+
got string
72+
want string
73+
}{
74+
{"RAGImageURL (overridden)", dst.RAGImageURL, "override-rag"},
75+
{"LCoreImageURL (kept)", dst.LCoreImageURL, "original-lcore"},
76+
{"ExporterImageURL (set from zero)", dst.ExporterImageURL, "override-exporter"},
77+
{"MCPServerImageURL (set from zero)", dst.MCPServerImageURL, "override-mcp"},
78+
{"PostgresImageURL (both zero)", dst.PostgresImageURL, ""},
79+
}
80+
for _, tc := range checks {
81+
if tc.got != tc.want {
82+
t.Errorf("%s: got %q, want %q", tc.name, tc.got, tc.want)
83+
}
84+
}
85+
}
86+
87+
func TestMergeImages_EmptySrc(t *testing.T) {
88+
dst := OpenStackLightspeedImages{
89+
RAGImageURL: "keep-this",
90+
}
91+
src := OpenStackLightspeedImages{}
92+
93+
mergeImages(&dst, &src)
94+
95+
if dst.RAGImageURL != "keep-this" {
96+
t.Errorf("RAGImageURL changed unexpectedly to %q", dst.RAGImageURL)
97+
}
98+
}
99+
100+
func TestMergeDefaults_GlobalWriteBackDoesNotCorruptBase(t *testing.T) {
101+
SetupDefaults()
102+
original := OpenStackLightspeedDefaultValues
103+
104+
specImages := &OpenStackLightspeedImages{RAGImageURL: "custom-rag"}
105+
merged := MergeDefaults(specImages)
106+
OpenStackLightspeedDefaultValues = merged
107+
108+
reverted := MergeDefaults(nil)
109+
if reverted.RAGImageURL != original.RAGImageURL {
110+
t.Errorf("RAGImageURL not reverted: got %q, want %q",
111+
reverted.RAGImageURL, original.RAGImageURL)
112+
}
113+
}
114+
115+
func TestMergeImages_AllFields(t *testing.T) {
116+
// Verify mergeImages touches every field by setting all src fields
117+
// to non-zero and confirming they all arrive in dst.
118+
typ := reflect.TypeOf(OpenStackLightspeedImages{})
119+
src := OpenStackLightspeedImages{}
120+
srcVal := reflect.ValueOf(&src).Elem()
121+
for i := 0; i < typ.NumField(); i++ {
122+
f := srcVal.Field(i)
123+
f.SetString(fmt.Sprintf("val-%d", i))
124+
}
125+
126+
dst := OpenStackLightspeedImages{}
127+
mergeImages(&dst, &src)
128+
129+
if !reflect.DeepEqual(dst, src) {
130+
t.Errorf("after merging fully-populated src into zero dst, values differ:\n dst: %+v\n src: %+v", dst, src)
131+
}
132+
}

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@ spec:
8383
feedbackDisabled:
8484
description: Disable feedback collection
8585
type: boolean
86+
images:
87+
description: |-
88+
Images configures container images used by the operator.
89+
When omitted, each image defaults to its environment variable or hardcoded fallback.
90+
properties:
91+
consoleImage:
92+
type: string
93+
consoleImagePF5:
94+
type: string
95+
exporterImage:
96+
type: string
97+
lcoreImage:
98+
type: string
99+
mcpServerImage:
100+
type: string
101+
okpImage:
102+
type: string
103+
postgresImage:
104+
type: string
105+
ragImage:
106+
type: string
107+
type: object
86108
llmAPIVersion:
87109
description: LLM API Version for LLM providers that require it (e.g.,
88110
Microsoft Azure OpenAI)
@@ -180,10 +202,6 @@ spec:
180202
When false, uses reference_url (online).
181203
type: boolean
182204
type: object
183-
ragImage:
184-
description: ContainerImage for the OpenStack Lightspeed RAG container
185-
(will be set to environmental default if empty)
186-
type: string
187205
tlsCACertBundle:
188206
description: Configmap name containing a CA Certificates bundle
189207
type: string

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@ spec:
8383
feedbackDisabled:
8484
description: Disable feedback collection
8585
type: boolean
86+
images:
87+
description: |-
88+
Images configures container images used by the operator.
89+
When omitted, each image defaults to its environment variable or hardcoded fallback.
90+
properties:
91+
consoleImage:
92+
type: string
93+
consoleImagePF5:
94+
type: string
95+
exporterImage:
96+
type: string
97+
lcoreImage:
98+
type: string
99+
mcpServerImage:
100+
type: string
101+
okpImage:
102+
type: string
103+
postgresImage:
104+
type: string
105+
ragImage:
106+
type: string
107+
type: object
86108
llmAPIVersion:
87109
description: LLM API Version for LLM providers that require it (e.g.,
88110
Microsoft Azure OpenAI)
@@ -180,10 +202,6 @@ spec:
180202
When false, uses reference_url (online).
181203
type: boolean
182204
type: object
183-
ragImage:
184-
description: ContainerImage for the OpenStack Lightspeed RAG container
185-
(will be set to environmental default if empty)
186-
type: string
187205
tlsCACertBundle:
188206
description: Configmap name containing a CA Certificates bundle
189207
type: string

config/samples/api_v1beta1_openstacklightspeed.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,13 @@ spec:
2727
# - okp
2828
# okpChunkFilterQuery: "product:(*openstack* OR *openshift*)"
2929
# okpRagOnly: true
30+
# Uncomment to customize container images:
31+
# images:
32+
# ragImage: "quay.io/openstack-lightspeed/rag-content:custom"
33+
# lcoreImage: "quay.io/lightspeed-core/lightspeed-stack:custom"
34+
# exporterImage: "quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:custom"
35+
# postgresImage: "registry.redhat.io/rhel9/postgresql-16:custom"
36+
# consoleImage: "registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-rhel9:custom"
37+
# consoleImagePF5: "registry.redhat.io/openshift-lightspeed/lightspeed-console-plugin-pf5-rhel9:custom"
38+
# okpImage: "registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:custom"
39+
# mcpServerImage: "quay.io/openstack-lightspeed/rhos-mcps:custom"

0 commit comments

Comments
 (0)