From 3810af2eeffb0c5c94c8b721987ee8c8e71e5107 Mon Sep 17 00:00:00 2001 From: Marcin Owsiany Date: Mon, 19 Oct 2026 09:29:36 +0200 Subject: [PATCH 1/2] Resolve operator version at read time instead of baking it OperatorConfig.Configure set operator.version = roxie.version unconditionally, discarding an explicitly configured operator.version. In CI this caused the operator bundle to be fetched with the main image's "-rcd" tag (roxie.version) rather than the configured operator.version, yielding MANIFEST_UNKNOWN since no "-rcd" operator bundle is published. Only assigning when empty would fix that symptom, but it introduces a new bug: the post-Configure config is persisted into the cluster manifest, so the resolved version gets baked in and wins on subsequent runs. A later `roxie deploy --tag=` onto the same cluster would then be a no-op for the operator, because operator.version (baked) is non-empty and --tag only sets roxie.version. Instead of baking, resolve lazily. Configure no longer touches the version; CentralVersion/SecuredClusterVersion now fall back central/securedCluster.operator.version -> operator.version -> roxie.version. Nothing version-related is persisted, an explicit operator.version still wins, and --tag always takes effect on redeploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/deployer/config.go | 3 +-- internal/deployer/operator_instances.go | 14 ++++++++---- internal/deployer/operator_instances_test.go | 23 ++++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/internal/deployer/config.go b/internal/deployer/config.go index 54fe922..fb62b4c 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -167,9 +167,8 @@ func (c *OperatorConfig) DeployViaOlmEnabled() bool { return c.DeployViaOlm != nil && *c.DeployViaOlm } -// Configure derives the operator version from the roxie configuration. +// Configure applies roxie-level Konflux-image defaults to the operator configuration. func (c *OperatorConfig) Configure(roxieConfig *RoxieConfig) error { - c.Version = roxieConfig.Version if c.KonfluxImages == nil { c.KonfluxImages = roxieConfig.KonfluxImages } diff --git a/internal/deployer/operator_instances.go b/internal/deployer/operator_instances.go index 10fbcd1..92db6de 100644 --- a/internal/deployer/operator_instances.go +++ b/internal/deployer/operator_instances.go @@ -31,21 +31,27 @@ var AllOperatorNamespaces = []string{ } // CentralVersion returns the main image tag for Central. -// Uses central.operator.version if set, otherwise falls back to operator.version. +// Resolution order: central.operator.version, then operator.version, then roxie.version. func (c *Config) CentralVersion() imagetag.MainTag { if c.Central.Operator.Version != "" { return c.Central.Operator.Version } - return c.Operator.Version + if c.Operator.Version != "" { + return c.Operator.Version + } + return c.Roxie.Version } // SecuredClusterVersion returns the main image tag for SecuredCluster. -// Uses securedCluster.operator.version if set, otherwise falls back to operator.version. +// Resolution order: securedCluster.operator.version, then operator.version, then roxie.version. func (c *Config) SecuredClusterVersion() imagetag.MainTag { if c.SecuredCluster.Operator.Version != "" { return c.SecuredCluster.Operator.Version } - return c.Operator.Version + if c.Operator.Version != "" { + return c.Operator.Version + } + return c.Roxie.Version } // HasMixedVersions reports whether Central and SecuredCluster use different operator versions. diff --git a/internal/deployer/operator_instances_test.go b/internal/deployer/operator_instances_test.go index 697284f..be2f761 100644 --- a/internal/deployer/operator_instances_test.go +++ b/internal/deployer/operator_instances_test.go @@ -9,18 +9,37 @@ import ( "github.com/stretchr/testify/require" ) -func TestConfigure_PropagatesRoxieVersion(t *testing.T) { +func TestConfigure_DoesNotBakeVersion_ResolvesAtReadTime(t *testing.T) { cfg := Config{ Roxie: RoxieConfig{Version: "4.11.1"}, } err := cfg.Operator.Configure(&cfg.Roxie) require.NoError(t, err) - assert.Equal(t, imagetag.MainTag("4.11.1"), cfg.Operator.Version) + // Configure must not bake roxie.version into operator.version. + assert.Empty(t, cfg.Operator.Version) + + // The version still resolves lazily via the accessors. assert.Equal(t, imagetag.MainTag("4.11.1"), cfg.CentralVersion()) assert.Equal(t, imagetag.MainTag("4.11.1"), cfg.SecuredClusterVersion()) } +// An explicit operator.version must take precedence over roxie.version. +func TestExplicitOperatorVersionHonoredOverRoxieVersion(t *testing.T) { + cfg := Config{ + Roxie: RoxieConfig{Version: "5.1.0-rcd", ImageRegistry: constants.DefaultRegistry}, + Operator: OperatorConfig{OperatorInstanceConfig: OperatorInstanceConfig{Version: "5.1.0"}}, + } + // configureConfig runs this before building operator instances. + require.NoError(t, cfg.Operator.Configure(&cfg.Roxie)) + + assert.Equal(t, imagetag.MainTag("5.1.0"), cfg.CentralVersion()) + assert.Equal(t, imagetag.MainTag("5.1.0"), cfg.SecuredClusterVersion()) + + inst := cfg.NewestOperatorInstance() + assert.Equal(t, constants.DefaultRegistry+"/stackrox-operator-bundle:v5.1.0", inst.BundleImage()) +} + func TestVersions_DefaultToRoxieVersion(t *testing.T) { cfg := Config{ Roxie: RoxieConfig{Version: "4.9.0"}, From 3341562aa35d32cd53ac31546a9cd36e59a6d187 Mon Sep 17 00:00:00 2001 From: Marcin Owsiany Date: Mon, 19 Oct 2026 09:37:32 +0200 Subject: [PATCH 2/2] Resolve Konflux image selection at read time; drop OperatorConfig.Configure Apply the same read-time resolution to KonfluxImages as was done for the operator version: resolveKonfluxImages now falls back per-component -> operator.konfluxImages -> roxie.konfluxImages, instead of relying on Configure to bake roxie.konfluxImages into operator.konfluxImages. As with the version, baking persisted the resolved value into the cluster manifest, so toggling --konflux on an existing cluster was a no-op on redeploy. With both the version and Konflux fallbacks resolved lazily, Configure had nothing left to do, so it and its call site in configureConfig are removed. The comment at that call site was already stale: it claimed to perform the "transformation of main image tags to semver compatible versions", which is actually done by MainTag.ToOperatorTag() at image-reference build time, not in Configure. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/deploy.go | 7 ----- internal/deployer/config.go | 8 ------ internal/deployer/operator_instances.go | 10 +++++--- internal/deployer/operator_instances_test.go | 27 ++++++++++++-------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 1fc3cfa..4106850 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -462,13 +462,6 @@ func configureConfig(components component.Component, deploySettings *deployer.Co log.Warning("You are deploying SecuredCluster to a local cluster, it is recommended to specify a resource profile (or --resources=auto)") } - // We need to do this regardless of whether the operator is deployed or not, because - // this includes the transformation of StackRox main image tags to semver compatible versions, - // which we will make use of later for checking version constraints. - if err := deploySettings.Operator.Configure(&deploySettings.Roxie); err != nil { - return fmt.Errorf("configuring operator configuration: %w", err) - } - if components.IncludesCentral() { if err := deploySettings.Central.ConfigureSpec(&deploySettings.Roxie); err != nil { return fmt.Errorf("configuring Central spec: %w", err) diff --git a/internal/deployer/config.go b/internal/deployer/config.go index fb62b4c..54916de 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -167,14 +167,6 @@ func (c *OperatorConfig) DeployViaOlmEnabled() bool { return c.DeployViaOlm != nil && *c.DeployViaOlm } -// Configure applies roxie-level Konflux-image defaults to the operator configuration. -func (c *OperatorConfig) Configure(roxieConfig *RoxieConfig) error { - if c.KonfluxImages == nil { - c.KonfluxImages = roxieConfig.KonfluxImages - } - return nil -} - // WaitConfig describes how to wait for a component to become ready. type WaitConfig struct { Namespace string diff --git a/internal/deployer/operator_instances.go b/internal/deployer/operator_instances.go index 92db6de..8beadcc 100644 --- a/internal/deployer/operator_instances.go +++ b/internal/deployer/operator_instances.go @@ -119,13 +119,17 @@ func (c *Config) OperatorInstances() ([]OperatorInstanceConfig, bool, error) { }, false, err } -// resolveKonfluxImages returns the effective KonfluxImages setting for a per-component -// operator config, falling back to Operator.KonfluxImages if not set at the component level. +// resolveKonfluxImages returns the effective KonfluxImages setting for an operator +// instance. Resolution order: the per-component value, then operator.konfluxImages, +// then roxie.konfluxImages. func (c *Config) resolveKonfluxImages(instanceCfg *OperatorInstanceConfig) *bool { if instanceCfg.KonfluxImagesSet() { return instanceCfg.KonfluxImages } - return c.Operator.KonfluxImages + if c.Operator.KonfluxImagesSet() { + return c.Operator.KonfluxImages + } + return c.Roxie.KonfluxImages } // NewestOperatorInstance returns the operator instance with the highest version. diff --git a/internal/deployer/operator_instances_test.go b/internal/deployer/operator_instances_test.go index be2f761..edf4f3c 100644 --- a/internal/deployer/operator_instances_test.go +++ b/internal/deployer/operator_instances_test.go @@ -9,17 +9,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestConfigure_DoesNotBakeVersion_ResolvesAtReadTime(t *testing.T) { +// With nothing but roxie.version set, the operator versions resolve to it. +func TestVersionResolvesFromRoxieVersion(t *testing.T) { cfg := Config{ Roxie: RoxieConfig{Version: "4.11.1"}, } - err := cfg.Operator.Configure(&cfg.Roxie) - require.NoError(t, err) - - // Configure must not bake roxie.version into operator.version. - assert.Empty(t, cfg.Operator.Version) - - // The version still resolves lazily via the accessors. assert.Equal(t, imagetag.MainTag("4.11.1"), cfg.CentralVersion()) assert.Equal(t, imagetag.MainTag("4.11.1"), cfg.SecuredClusterVersion()) } @@ -30,8 +24,6 @@ func TestExplicitOperatorVersionHonoredOverRoxieVersion(t *testing.T) { Roxie: RoxieConfig{Version: "5.1.0-rcd", ImageRegistry: constants.DefaultRegistry}, Operator: OperatorConfig{OperatorInstanceConfig: OperatorInstanceConfig{Version: "5.1.0"}}, } - // configureConfig runs this before building operator instances. - require.NoError(t, cfg.Operator.Configure(&cfg.Roxie)) assert.Equal(t, imagetag.MainTag("5.1.0"), cfg.CentralVersion()) assert.Equal(t, imagetag.MainTag("5.1.0"), cfg.SecuredClusterVersion()) @@ -40,6 +32,21 @@ func TestExplicitOperatorVersionHonoredOverRoxieVersion(t *testing.T) { assert.Equal(t, constants.DefaultRegistry+"/stackrox-operator-bundle:v5.1.0", inst.BundleImage()) } +// KonfluxImages must resolve from roxie.konfluxImages when not set at the +// operator or per-component level. +func TestKonfluxImagesResolveFromRoxie(t *testing.T) { + konflux := true + cfg := Config{ + Roxie: RoxieConfig{Version: "4.8.0", KonfluxImages: &konflux}, + Operator: OperatorConfig{OperatorInstanceConfig: OperatorInstanceConfig{Version: "4.8.0"}}, + } + + instances, _, err := cfg.OperatorInstances() + require.NoError(t, err) + require.Len(t, instances, 1) + assert.True(t, instances[0].KonfluxImagesEnabled()) +} + func TestVersions_DefaultToRoxieVersion(t *testing.T) { cfg := Config{ Roxie: RoxieConfig{Version: "4.9.0"},