Skip to content

Commit f563a1d

Browse files
Merge pull request #203 from tmshort/oprun-4599
OPRUN-4599: dynamically resolve catalog image tag from OCP release version
2 parents 5d9f062 + 7a85fa8 commit f563a1d

8 files changed

Lines changed: 205 additions & 15 deletions

File tree

cmd/cluster-olm-operator/main.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
230230

231231
log := klog.FromContext(ctx)
232232

233+
operatorImageVersion := status.VersionForOperatorFromEnv()
234+
currentOCPMinorVersion, err := versionutils.GetCurrentOCPMinorVersion(operatorImageVersion)
235+
if err != nil {
236+
return err
237+
}
238+
catalogImageTag := versionutils.GetCatalogImageTag(currentOCPMinorVersion)
239+
233240
fg, err := cl.ConfigClient.ConfigV1().FeatureGates().Get(ctx, "cluster", metav1.GetOptions{})
234241
if err != nil {
235242
return fmt.Errorf("unable to retrieve featureSet: %w", err)
@@ -252,8 +259,9 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
252259
Scope: meta.RESTScopeRoot,
253260
},
254261
},
255-
FeatureGate: *fg,
256-
Infrastructure: infra,
262+
FeatureGate: *fg,
263+
Infrastructure: infra,
264+
ClusterCatalogImageTag: catalogImageTag,
257265
}
258266

259267
staticResourceControllers, deploymentControllers, clusterCatalogControllers, relatedObjects, err := cb.BuildControllers("catalogd", "operator-controller")
@@ -288,12 +296,6 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
288296
clusterCatalogControllerList = append(clusterCatalogControllerList, controller)
289297
}
290298

291-
operatorImageVersion := status.VersionForOperatorFromEnv()
292-
currentOCPMinorVersion, err := versionutils.GetCurrentOCPMinorVersion(operatorImageVersion)
293-
if err != nil {
294-
return err
295-
}
296-
297299
upgradeableConditionController := controller.NewStaticUpgradeableConditionController(
298300
"OLMStaticUpgradeableConditionController",
299301
cl.OperatorClient,
@@ -338,7 +340,7 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
338340
)
339341

340342
versionGetter := status.NewVersionGetter()
341-
versionGetter.SetVersion("operator", status.VersionForOperatorFromEnv())
343+
versionGetter.SetVersion("operator", operatorImageVersion)
342344

343345
// Add all resources to relatedObjects to ensure that must-gather picks them up.
344346
// Note: These resources are also hard-coded in the ClusterOperator manifest. This way,

internal/versionutils/version_utils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ func ToAllowedSemver(data []byte) (*semver.Version, error) {
6161
return &version, nil
6262
}
6363

64+
// GetCatalogImageTag converts an OCP version to the catalog image tag format used by default catalogs.
65+
// For example, version 4.22.0 returns "v4.22", and version 5.0.0 returns "v5.0".
66+
func GetCatalogImageTag(version *semver.Version) string {
67+
return fmt.Sprintf("v%d.%d", version.Major, version.Minor)
68+
}
69+
6470
// ocpVersion500 is the semver representation of OCP 5.0, which is co-released with 4.23 as an
6571
// equivalent release. Neither upgrades to the other; both upgrade exclusively to 5.1.
6672
var ocpVersion500 = semver.Version{Major: 5, Minor: 0, Patch: 0}

internal/versionutils/version_utils_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ func TestToAllowedSemver(t *testing.T) {
9595
}
9696
}
9797

98+
func TestGetCatalogImageTag(t *testing.T) {
99+
tests := []struct {
100+
name string
101+
version semver.Version
102+
want string
103+
}{
104+
{
105+
name: "4.22",
106+
version: semver.Version{Major: 4, Minor: 22, Patch: 0},
107+
want: "v4.22",
108+
},
109+
{
110+
name: "5.0",
111+
version: semver.Version{Major: 5, Minor: 0, Patch: 0},
112+
want: "v5.0",
113+
},
114+
{
115+
name: "patch ignored",
116+
version: semver.Version{Major: 4, Minor: 22, Patch: 5},
117+
want: "v4.22",
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
got := GetCatalogImageTag(&tt.version)
124+
assert.Equal(t, tt.want, got, "unexpected catalog image tag")
125+
})
126+
}
127+
}
128+
98129
func TestIsOperatorMaxOCPVersionCompatibleWithCluster(t *testing.T) {
99130
tests := []struct {
100131
name string

pkg/controller/builder.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ import (
4040
)
4141

4242
type Builder struct {
43-
Assets string
44-
Clients *clients.Clients
45-
ControllerContext *controllercmd.ControllerContext
46-
KnownRESTMappings map[schema.GroupVersionKind]*meta.RESTMapping
47-
FeatureGate configv1.FeatureGate
48-
Infrastructure *configv1.Infrastructure
43+
Assets string
44+
Clients *clients.Clients
45+
ControllerContext *controllercmd.ControllerContext
46+
KnownRESTMappings map[schema.GroupVersionKind]*meta.RESTMapping
47+
FeatureGate configv1.FeatureGate
48+
Infrastructure *configv1.Infrastructure
49+
ClusterCatalogImageTag string
4950
}
5051

5152
func (b *Builder) BuildControllers(subDirectories ...string) (map[string]factory.Controller, map[string]factory.Controller, map[string]factory.Controller, []configv1.ObjectReference, error) {

pkg/controller/helm.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020
"k8s.io/klog/v2"
2121
)
2222

23+
// catalogVersionSentinel is a placeholder value that openshift.yaml sets for
24+
// options.openshift.catalogs.version to indicate the catalog tag should be
25+
// resolved dynamically from the running cluster's OCP major.minor version.
26+
const catalogVersionSentinel = "ocp-release"
27+
2328
// Expected path structure:
2429
// ${assets}/helm/${subDir}/olmv1/ = chart
2530
// ${assets}/helm/${subDir}/openshift.yaml = primary values file
@@ -80,6 +85,11 @@ func (b *Builder) renderHelmTemplate(helmPath, manifestDir string) error {
8085
if err := values.SetStringValue("options.operatorController.deployment.image", os.Getenv("OPERATOR_CONTROLLER_IMAGE")); err != nil {
8186
return fmt.Errorf("error setting OPERATOR_CONTROLLER_IMAGE: %w", err)
8287
}
88+
// When openshift.yaml sets options.openshift.catalogs.version to catalogVersionSentinel,
89+
// replace it with the tag derived from the running cluster's OCP major.minor version.
90+
if err := applyCatalogImageTagOverride(values, b.ClusterCatalogImageTag); err != nil {
91+
return fmt.Errorf("error setting catalog image tag: %w", err)
92+
}
8393

8494
// On HighlyAvailable topologies scale to 2 replicas and enable the PDB so that rolling
8595
// updates never leave zero running pods. On SingleReplica (SNO) / External topologies
@@ -222,6 +232,21 @@ type DocumentInfo struct {
222232
Order int
223233
}
224234

235+
// applyCatalogImageTagOverride replaces options.openshift.catalogs.version in the
236+
// Helm values when it equals catalogVersionSentinel, substituting the tag derived
237+
// from the running cluster's OCP major.minor version. It is a no-op when clusterTag
238+
// is empty or when the current value is not catalogVersionSentinel.
239+
func applyCatalogImageTagOverride(values *helmvalues.HelmValues, clusterTag string) error {
240+
if clusterTag == "" {
241+
return nil
242+
}
243+
currentTag, found := values.GetStringValue("options.openshift.catalogs.version")
244+
if !found || currentTag != catalogVersionSentinel {
245+
return nil
246+
}
247+
return values.SetStringValue("options.openshift.catalogs.version", clusterTag)
248+
}
249+
225250
func splitYAMLDocuments(content string) []string {
226251
// Split by document separators but preserve the original text
227252
lines := strings.Split(content, "\n")

pkg/controller/helm_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
internalfeatures "github.com/openshift/cluster-olm-operator/internal/featuregates"
1616
"github.com/openshift/cluster-olm-operator/pkg/clients"
17+
"github.com/openshift/cluster-olm-operator/pkg/helmvalues"
1718
)
1819

1920
func TestRenderHelmTemplate(t *testing.T) {
@@ -75,6 +76,56 @@ func TestRenderHelmTemplate(t *testing.T) {
7576
require.Equal(t, compareData, testData)
7677
}
7778

79+
func TestApplyCatalogImageTagOverride(t *testing.T) {
80+
const versionKey = "options.openshift.catalogs.version"
81+
82+
tests := []struct {
83+
name string
84+
initialTag string // set at versionKey before the call; empty means key is absent
85+
clusterTag string
86+
expectedTag string // expected value at versionKey after the call; empty means key absent
87+
}{
88+
{
89+
name: "clusterTag empty - no override regardless of Helm value",
90+
initialTag: catalogVersionSentinel,
91+
clusterTag: "",
92+
expectedTag: catalogVersionSentinel,
93+
},
94+
{
95+
name: "sentinel present, cluster is 4.23 - override to v4.23",
96+
initialTag: catalogVersionSentinel,
97+
clusterTag: "v4.23",
98+
expectedTag: "v4.23",
99+
},
100+
{
101+
name: "Helm pins v4.23 - not the sentinel, no override",
102+
initialTag: "v4.23",
103+
clusterTag: "v4.23",
104+
expectedTag: "v4.23",
105+
},
106+
{
107+
name: "version key absent in Helm values - no override",
108+
initialTag: "",
109+
clusterTag: "v4.23",
110+
expectedTag: "",
111+
},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
hv := helmvalues.NewHelmValues()
117+
if tt.initialTag != "" {
118+
require.NoError(t, hv.SetStringValue(versionKey, tt.initialTag))
119+
}
120+
121+
require.NoError(t, applyCatalogImageTagOverride(hv, tt.clusterTag))
122+
123+
got, _ := hv.GetStringValue(versionKey)
124+
require.Equal(t, tt.expectedTag, got)
125+
})
126+
}
127+
}
128+
78129
func TestSplitYAMLDocuments(t *testing.T) {
79130
tests := []struct {
80131
name string

pkg/helmvalues/helmvalues.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ func (v *HelmValues) HasEnabledFeatureGates() (bool, error) {
7373
return false, nil
7474
}
7575

76+
// GetStringValue reads a dot-delimited string value from the Helm values map.
77+
// Returns the value and true if found; returns "", false if the key is absent or not a string.
78+
func (v *HelmValues) GetStringValue(location string) (string, bool) {
79+
ss := strings.Split(location, ".")
80+
val, found, err := unstructured.NestedString(v.values, ss...)
81+
if err != nil || !found {
82+
return "", false
83+
}
84+
return val, true
85+
}
86+
7687
func (v *HelmValues) SetStringValue(location string, newValue string) error {
7788
if location == "" {
7889
return errors.New("location string has no locations")

pkg/helmvalues/helmvalues_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,69 @@ func TestHasEnabledFeatureGates(t *testing.T) {
257257
}
258258
}
259259

260+
func TestGetStringValue(t *testing.T) {
261+
tests := []struct {
262+
name string
263+
values map[string]interface{}
264+
location string
265+
wantVal string
266+
wantFound bool
267+
}{
268+
{
269+
name: "missing key",
270+
values: map[string]interface{}{},
271+
location: "options.openshift.catalogs.version",
272+
wantFound: false,
273+
},
274+
{
275+
name: "simple key",
276+
values: map[string]interface{}{"key": "value"},
277+
location: "key",
278+
wantVal: "value",
279+
wantFound: true,
280+
},
281+
{
282+
name: "nested key",
283+
values: map[string]interface{}{
284+
"options": map[string]interface{}{
285+
"openshift": map[string]interface{}{
286+
"catalogs": map[string]interface{}{
287+
"version": "v5.0",
288+
},
289+
},
290+
},
291+
},
292+
location: "options.openshift.catalogs.version",
293+
wantVal: "v5.0",
294+
wantFound: true,
295+
},
296+
{
297+
name: "non-string value returns not found",
298+
values: map[string]interface{}{
299+
"options": map[string]interface{}{
300+
"replicas": int64(2),
301+
},
302+
},
303+
location: "options.replicas",
304+
wantFound: false,
305+
},
306+
}
307+
308+
for _, tt := range tests {
309+
t.Run(tt.name, func(t *testing.T) {
310+
hv := NewHelmValues()
311+
hv.values = tt.values
312+
got, found := hv.GetStringValue(tt.location)
313+
if got != tt.wantVal {
314+
t.Errorf("GetStringValue() val = %q, want %q", got, tt.wantVal)
315+
}
316+
if found != tt.wantFound {
317+
t.Errorf("GetStringValue() found = %v, want %v", found, tt.wantFound)
318+
}
319+
})
320+
}
321+
}
322+
260323
func TestSetStringValue(t *testing.T) {
261324
tests := []struct {
262325
name string

0 commit comments

Comments
 (0)