Skip to content

Commit 46d36ca

Browse files
Akrogclaude
andcommitted
Support changing all the images
We used to be able to change the RAG image using the `ragImage` field in our CR, but we removed it and said we would make a consistent interface to modify ALL possible images via the CRD. 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 - RAG Image To provide a consistent interface, similar to what we can do in the other openstack operators, we can now set all the images under the `images` field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ab6e100 commit 46d36ca

18 files changed

Lines changed: 378 additions & 29 deletions

api/v1beta1/openstacklightspeed_types.go

Lines changed: 68 additions & 29 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"
@@ -98,6 +100,11 @@ type DatabaseSpec struct {
98100
type OpenStackLightspeedSpec struct {
99101
OpenStackLightspeedCore `json:",inline"`
100102

103+
// +kubebuilder:validation:Optional
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"`
107+
101108
// +kubebuilder:validation:Optional
102109
// Database configures persistent storage for PostgreSQL data.
103110
// When omitted, an emptyDir volume is used (data is lost on pod reschedule).
@@ -274,42 +281,74 @@ func (instance OpenStackLightspeed) IsReady() bool {
274281
return instance.Status.Conditions.IsTrue(OpenStackLightspeedReadyCondition)
275282
}
276283

284+
// OpenStackLightspeedImages groups container image URLs used by the operator.
285+
type OpenStackLightspeedImages struct {
286+
RAGImageURL string `json:"ragImage,omitempty"`
287+
LCoreImageURL string `json:"lcoreImage,omitempty"`
288+
ExporterImageURL string `json:"exporterImage,omitempty"`
289+
PostgresImageURL string `json:"postgresImage,omitempty"`
290+
ConsoleImageURL string `json:"consoleImage,omitempty"`
291+
ConsoleImagePF5URL string `json:"consoleImagePF5,omitempty"`
292+
OKPImageURL string `json:"okpImage,omitempty"`
293+
MCPServerImageURL string `json:"mcpServerImage,omitempty"`
294+
}
295+
277296
type OpenStackLightspeedDefaults struct {
278-
RAGImageURL string
279-
LCoreImageURL string
280-
ExporterImageURL string
281-
PostgresImageURL string
282-
ConsoleImageURL string
283-
ConsoleImagePF5URL string
284-
OKPImageURL string
285-
MCPServerImageURL string
286-
MaxTokensForResponse int
297+
OpenStackLightspeedImages `json:",inline"`
298+
MaxTokensForResponse int `json:"maxTokensForResponse,omitempty"`
287299
}
288300

289301
var OpenStackLightspeedDefaultValues OpenStackLightspeedDefaults
290302

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

314-
OpenStackLightspeedDefaultValues = openStackLightspeedDefaults
346+
// MergeDefaults returns a copy of the env-var defaults with the spec image
347+
// overrides (if any) applied on top.
348+
func MergeDefaults(specImages *OpenStackLightspeedImages) OpenStackLightspeedDefaults {
349+
merged := envVarDefaults
350+
if specImages != nil {
351+
mergeImages(&merged.OpenStackLightspeedImages, specImages)
352+
}
353+
return merged
315354
}
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ spec:
7979
default: true
8080
description: Enable feedback collection
8181
type: boolean
82+
images:
83+
description: |-
84+
Images configures container images used by the operator.
85+
When omitted, each image defaults to its environment variable or hardcoded fallback.
86+
properties:
87+
consoleImage:
88+
type: string
89+
consoleImagePF5:
90+
type: string
91+
exporterImage:
92+
type: string
93+
lcoreImage:
94+
type: string
95+
mcpServerImage:
96+
type: string
97+
okpImage:
98+
type: string
99+
postgresImage:
100+
type: string
101+
ragImage:
102+
type: string
103+
type: object
82104
llmAPIVersion:
83105
description: LLM API Version for LLM providers that require it (e.g.,
84106
Microsoft Azure OpenAI)

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ spec:
7979
default: true
8080
description: Enable feedback collection
8181
type: boolean
82+
images:
83+
description: |-
84+
Images configures container images used by the operator.
85+
When omitted, each image defaults to its environment variable or hardcoded fallback.
86+
properties:
87+
consoleImage:
88+
type: string
89+
consoleImagePF5:
90+
type: string
91+
exporterImage:
92+
type: string
93+
lcoreImage:
94+
type: string
95+
mcpServerImage:
96+
type: string
97+
okpImage:
98+
type: string
99+
postgresImage:
100+
type: string
101+
ragImage:
102+
type: string
103+
type: object
82104
llmAPIVersion:
83105
description: LLM API Version for LLM providers that require it (e.g.,
84106
Microsoft Azure OpenAI)

config/samples/api_v1beta1_openstacklightspeed.yaml

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

internal/controller/openstacklightspeed_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl.
218218
return ctrl.Result{}, nil
219219
}
220220

221+
apiv1beta1.OpenStackLightspeedDefaultValues = apiv1beta1.MergeDefaults(&instance.Spec.Images)
222+
221223
if instance.Spec.MaxTokensForResponse == 0 {
222224
instance.Spec.MaxTokensForResponse = apiv1beta1.OpenStackLightspeedDefaultValues.MaxTokensForResponse
223225
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../common/mock-objects/mock-resources.yaml
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../common/mock-objects/assert-mock-objects-created.yaml

0 commit comments

Comments
 (0)