Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ca9ea6b
ROX-35435: Support differing versions of Central and SecuredCluster
vladbologa Jul 17, 2026
563b361
Derive the single operator from the effective component version
vladbologa Jul 18, 2026
3c1e9b6
Use 'mixed' instead of 'split' versions
vladbologa Jul 21, 2026
9cfb545
Replace 'unwanted' with 'stale'
vladbologa Jul 21, 2026
3be5d41
reject invalid configuration
vladbologa Jul 21, 2026
e0951ee
Restructure version overrides to use central.operator/securedCluster.…
vladbologa Jul 22, 2026
690f227
Simplify Konflux handling
vladbologa Jul 28, 2026
b51e01d
Make OperatorConfig non-pointer + remove merging
vladbologa Jul 28, 2026
52cb864
Drop Effective version naming
vladbologa Jul 28, 2026
cdd2a71
Remove dead code
vladbologa Jul 28, 2026
0b4f021
Simplify imagesForConfig & uniqueMainVersions
vladbologa Jul 28, 2026
e722b1d
Apply suggestions from code review
vladbologa Jul 28, 2026
596519f
Apply suggestions from code review
vladbologa Jul 28, 2026
6476db3
Move code from runDeploy to deployValidate
vladbologa Jul 28, 2026
0db0eab
Simplify error messages
vladbologa Jul 28, 2026
e6bebed
Clarify operator tags / main tags mixup
vladbologa Jul 29, 2026
dc168d9
Address CodeRabbit comment
vladbologa Jul 29, 2026
8f09943
Extract OperatorInstanceConfig out of OperatorConfig
vladbologa Jul 30, 2026
cde6154
Remove OperatorInstance type
vladbologa Jul 30, 2026
55de33a
Require main tags in <component>.operator.version
vladbologa Jul 30, 2026
879ae62
Apply code review suggestions
vladbologa Jul 31, 2026
780994c
Do checkEarlyReadinessSupport per component
vladbologa Jul 31, 2026
b34e56f
Tags type safety
vladbologa Aug 3, 2026
15d408a
Constants for role name suffixes
vladbologa Aug 3, 2026
dcb8379
Propage logger to deployValidate
vladbologa Aug 3, 2026
2dfa903
Move image tag logic to imagetag ns
vladbologa Aug 3, 2026
20f93e8
Add OperatorImage method
vladbologa Aug 3, 2026
2896d20
Check component earlyReadiness support only if it's being installed
vladbologa Aug 3, 2026
d1eec45
Fix CRD bundle using global Konflux setting instead of per-instance s…
vladbologa Aug 3, 2026
e27161f
Use constants for role name suffixes
vladbologa Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 55 additions & 27 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/imagetag"
"github.com/stackrox/roxie/internal/k8s"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/manifest"
Expand Down Expand Up @@ -134,7 +135,21 @@ this flag can be used to tell roxie how to pre-load images for the current clust
registerFlag(cmd, settings, "tag", "Main image tag to use for deployment (takes precedence over MAIN_IMAGE_TAG environment variable)",
withShortName("t"),
withApplyFn("version", func(config *deployer.Config, mainImageTag string) error {
config.Roxie.Version = mainImageTag
config.Roxie.Version = imagetag.MainTag(mainImageTag)
return nil
}),
)

registerFlag(cmd, settings, "central-tag", "Image tag for Central (overrides --tag for Central)",
Comment thread
mclasmeier marked this conversation as resolved.
withApplyFn("version", func(config *deployer.Config, tag string) error {
config.Central.Operator.Version = imagetag.MainTag(tag)
return nil
}),
)

registerFlag(cmd, settings, "secured-cluster-tag", "Image tag for SecuredCluster (overrides --tag for SecuredCluster)",
withApplyFn("version", func(config *deployer.Config, tag string) error {
config.SecuredCluster.Operator.Version = imagetag.MainTag(tag)
return nil
}),
)
Expand Down Expand Up @@ -236,7 +251,7 @@ func runDeploy(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("looking up main image tag: %w", err)
}
deploySettings.Roxie.Version = mainImageTag
deploySettings.Roxie.Version = imagetag.MainTag(mainImageTag)
}

if components.IncludesSensor() {
Expand All @@ -249,29 +264,10 @@ func runDeploy(cmd *cobra.Command, args []string) error {
return err
}

if err := deployValidate(components, &deploySettings); err != nil {
if err := deployValidate(log, components, &deploySettings); err != nil {
return err
}

if !deploySettings.Central.EarlyReadinessEnabled() || !deploySettings.SecuredCluster.EarlyReadinessEnabled() {
// Explanation on the versions involved here:
// Deploying StackRox begins with picking a "main image tag" -- this is a version identifier, which cannot be reliably parsed as a semver.
// But there is a derived version from that -- the operator version -- which can be parsed as a semver.
//
// The invocation of deploySettings.Operator.Configure() above in this function prepares the operator deployment config in the sense
// that top-level roxie configuration options are propagated to the concrete operator deployment configuration. This includes also
// storing of the derived operator version within the operator configuration.
//
// This is why we use the operator version here when checking version constraints.
hasSupport, err := stackroxversions.SupportsAdditionalPrinterColumns(deploySettings.Operator.Version)
if err != nil {
return fmt.Errorf("checking version constraint on main image tag %s: %w", deploySettings.Roxie.Version, err)
}
if !hasSupport {
return fmt.Errorf("--early-readiness=false can only be used for StackRox versions satisfying %s", stackroxversions.SupportsAdditionalPrinterColumnsConstraint.String())
}
}

d, err := deployer.New(log)
if err != nil {
return fmt.Errorf("failed to create deployer: %w", err)
Expand Down Expand Up @@ -427,10 +423,6 @@ func configureConfig(log *logger.Logger, components component.Component, deployS
return fmt.Errorf("configuring operator configuration: %w", err)
}

if deploySettings.Roxie.KonfluxImagesEnabled() {
deployer.PopulateKonfluxEnvVars(deploySettings)
}

if components.IncludesCentral() {
if err := deploySettings.Central.ConfigureSpec(&deploySettings.Roxie); err != nil {
return fmt.Errorf("configuring Central spec: %w", err)
Expand All @@ -454,7 +446,7 @@ func configureConfig(log *logger.Logger, components component.Component, deployS
return nil
}

func deployValidate(components component.Component, deploySettings *deployer.Config) error {
func deployValidate(log *logger.Logger, components component.Component, deploySettings *deployer.Config) error {
if components.IncludesCentral() && os.Getenv("ROXIE_SHELL") != "" {
return errors.New("already in a roxie sub-shell (ROXIE_SHELL environment variable is set), please exit the shell and try again")
}
Expand Down Expand Up @@ -495,6 +487,42 @@ func deployValidate(components component.Component, deploySettings *deployer.Con
}
}

if deploySettings.HasMixedVersions() {
log.Dimf("Mixed versions detected (configured via --central-tag / --secured-cluster-tag or central.operator / securedCluster.operator)")
if components.IncludesOperatorExplicitly() {
return errors.New("mixed versions are not supported with operator-only deploy")
}
if deploySettings.Operator.DeployViaOlmEnabled() {
return errors.New("mixed versions are not supported with OLM deployment mode")
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if components.IncludesCentral() && !deploySettings.Central.EarlyReadinessEnabled() {
if err := checkEarlyReadinessSupport("Central", deploySettings.CentralVersion()); err != nil {
return err
}
}
if components.IncludesSensor() && !deploySettings.SecuredCluster.EarlyReadinessEnabled() {
if err := checkEarlyReadinessSupport("SecuredCluster", deploySettings.SecuredClusterVersion()); err != nil {
return err
}
}

return nil
}

func checkEarlyReadinessSupport(componentName string, tag imagetag.MainTag) error {
// The main image tag is not reliably parseable as semver, so we derive the operator
// tag (via ToOperator) for the constraint check.
version := tag.ToOperatorTag().String()
hasSupport, err := stackroxversions.SupportsAdditionalPrinterColumns(version)
if err != nil {
return fmt.Errorf("checking version constraint on %s operator version %s: %w", componentName, version, err)
}
if !hasSupport {
return fmt.Errorf("--early-readiness=false can only be used for StackRox versions satisfying %s (%s version %s does not)",
stackroxversions.SupportsAdditionalPrinterColumnsConstraint.String(), componentName, version)
}
return nil
}

Expand Down
11 changes: 6 additions & 5 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"dario.cat/mergo"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/imagetag"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/paths"
"github.com/stackrox/roxie/internal/types"
Expand Down Expand Up @@ -46,14 +47,14 @@ func TestNewDeployCmd_Flags(t *testing.T) {
name: "tag short flag",
args: []string{"-t", "4.7.0"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.Equal(t, "4.7.0", cfg.Roxie.Version, "Roxie.Version mismatch")
assert.Equal(t, imagetag.MainTag("4.7.0"), cfg.Roxie.Version, "Roxie.Version mismatch")
},
},
{
name: "tag long flag",
args: []string{"--tag", "4.7.0"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.Equal(t, "4.7.0", cfg.Roxie.Version, "Roxie.Version mismatch")
assert.Equal(t, imagetag.MainTag("4.7.0"), cfg.Roxie.Version, "Roxie.Version mismatch")
},
},
{
Expand Down Expand Up @@ -136,7 +137,7 @@ func TestNewDeployCmd_Flags(t *testing.T) {
name: "multiple flags combined",
args: []string{"--tag", "4.7.0", "--exposure", "loadbalancer", "--early-readiness", "--resources", "small"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.Equal(t, "4.7.0", cfg.Roxie.Version, "Roxie.Version mismatch")
assert.Equal(t, imagetag.MainTag("4.7.0"), cfg.Roxie.Version, "Roxie.Version mismatch")
require.NotNil(t, cfg.Central.Exposure, "Central.Exposure should be set")
assert.Equal(t, types.ExposureLoadBalancer, *cfg.Central.Exposure, "Central.Exposure mismatch")
assert.True(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch")
Expand Down Expand Up @@ -181,7 +182,7 @@ securedCluster:
`,
args: []string{"--config", configFilePath},
assert: func(t *testing.T, cfg deployer.Config) {
assert.Equal(t, "1.2.3", cfg.Roxie.Version, "Roxie.Version mismatch")
assert.Equal(t, imagetag.MainTag("1.2.3"), cfg.Roxie.Version, "Roxie.Version mismatch")
assert.True(t,
reflect.DeepEqual(cfg.SecuredCluster.Spec,
map[string]interface{}{
Expand Down Expand Up @@ -239,7 +240,7 @@ central:
name: "set expressions can be used",
args: []string{"--set", "roxie.version=0.99.1", "--set", "central.deployTimeout=4m", "--set", "securedCluster.spec.clusterName=foo"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.Equal(t, "0.99.1", cfg.Roxie.Version, "version mismatch")
assert.Equal(t, imagetag.MainTag("0.99.1"), cfg.Roxie.Version, "version mismatch")
assert.Equal(t, 4*time.Minute, cfg.Central.DeployTimeout, "Central.DeployTimeout mismatch")
assert.Equal(t,
map[string]interface{}{
Expand Down
37 changes: 15 additions & 22 deletions internal/deployer/acs_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,23 @@ import (
)

func imagesForConfig(config Config) []string {
images := make([]string, 0)
prefix := ""
if config.Roxie.KonfluxImagesEnabled() {
prefix = "release-"
}

var images []string
imageRegistry := constants.DefaultRegistry
images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "main", config.Roxie.Version))
images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "central-db", config.Roxie.Version))
images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4-db", config.Roxie.Version))
images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4", config.Roxie.Version))
if !config.Roxie.KonfluxImagesEnabled() {
prefix = "stackrox-"

for _, instance := range config.OperatorInstances() {
prefix := ""
if instance.KonfluxImagesEnabled() {
prefix = "release-"
}
images = append(images,
fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "main", instance.Version),
fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "central-db", instance.Version),
fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4-db", instance.Version),
fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "scanner-v4", instance.Version),
instance.OperatorImage(),
instance.BundleImage(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why we have an instance.BundleImage() method, but not instance.OperatorImage(), we assemble that inline in L25.
What's the reason for this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 20f93e8. Also the other fmt.Sprintf calls could be extracted, but they don't fit in OperatorInstanceConfig.

)
}
images = append(images, fmt.Sprintf("%s/%s%s:%s", imageRegistry, prefix, "operator", config.Operator.Version))
images = append(images, OperatorBundleImage(config))

return images
}

func OperatorBundleImage(config Config) string {
imageRegistry := constants.DefaultRegistry
if config.Roxie.KonfluxImagesEnabled() {
return fmt.Sprintf("%s/release-operator-bundle:v%s", imageRegistry, config.Operator.Version)
}
return fmt.Sprintf("%s/stackrox-operator-bundle:v%s", imageRegistry, config.Operator.Version)
}
77 changes: 70 additions & 7 deletions internal/deployer/config.go
Comment thread
mclasmeier marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"time"

"github.com/stackrox/roxie/internal/constants"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/imagetag"
"github.com/stackrox/roxie/internal/types"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -53,7 +55,7 @@ func (c *Config) DeepCopy() (*Config, error) {

// RoxieConfig holds roxie-level settings such as version and feature flags.
type RoxieConfig struct {
Version string `yaml:"version,omitempty"`
Version imagetag.MainTag `yaml:"version,omitempty"`
KonfluxImages *bool `yaml:"konfluxImages,omitempty"`
FeatureFlags map[string]bool `yaml:"featureFlags,omitempty"`
ClusterType types.ClusterType `yaml:"clusterType,omitempty"`
Expand All @@ -75,12 +77,66 @@ func NewRoxieConfig() RoxieConfig {
}
}

// OperatorConfig controls how the ACS operator is deployed.
type OperatorConfig struct {
SkipDeployment *bool `yaml:"skipDeployment,omitempty"`
DeployViaOlm *bool `yaml:"deployViaOlm,omitempty"`
Version string `yaml:"version,omitempty"`
// OperatorInstanceConfig describes how to deploy a single operator instance.
// In the common single-operator mode, the top-level OperatorConfig (which embeds this)
// is used directly. In mixed-operator mode, CentralConfig.Operator and
// SecuredClusterConfig.Operator override the top-level defaults.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
type OperatorInstanceConfig struct {
Version imagetag.MainTag `yaml:"version,omitempty"`
EnvVars map[string]string `yaml:"envVars,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
RoleNameSuffix string `yaml:"roleNameSuffix,omitempty"`
KonfluxImages *bool `yaml:"konfluxImages,omitempty"`
}

func (c *OperatorInstanceConfig) KonfluxImagesSet() bool {
return c.KonfluxImages != nil
}

func (c *OperatorInstanceConfig) KonfluxImagesEnabled() bool {
return c.KonfluxImages != nil && *c.KonfluxImages
}

// ClusterRoleName returns the ClusterRole name for this operator instance.
func (c *OperatorInstanceConfig) ClusterRoleName() string {
if c.RoleNameSuffix == "" {
return "rhacs-operator-manager-role"
}
return "rhacs-operator-manager-role-" + c.RoleNameSuffix
}

// ClusterRoleBindingName returns the ClusterRoleBinding name for this operator instance.
func (c *OperatorInstanceConfig) ClusterRoleBindingName() string {
if c.RoleNameSuffix == "" {
return "rhacs-operator-manager-rolebinding"
}
return "rhacs-operator-manager-rolebinding-" + c.RoleNameSuffix
}

// BundleImage returns the operator bundle image for this operator instance.
func (c *OperatorInstanceConfig) BundleImage() string {
imageRegistry := constants.DefaultRegistry
Comment thread
mclasmeier marked this conversation as resolved.
operatorTag := c.Version.ToOperatorTag()
if c.KonfluxImagesEnabled() {
return fmt.Sprintf("%s/release-operator-bundle:v%s", imageRegistry, operatorTag)
}
return fmt.Sprintf("%s/stackrox-operator-bundle:v%s", imageRegistry, operatorTag)
}

func (c *OperatorInstanceConfig) OperatorImage() string {
imageRegistry := constants.DefaultRegistry
operatorTag := c.Version.ToOperatorTag()
if c.KonfluxImagesEnabled() {
return fmt.Sprintf("%s/release-operator:%s", imageRegistry, operatorTag)
}
return fmt.Sprintf("%s/stackrox-operator:%s", imageRegistry, operatorTag)
}

// OperatorConfig is the top-level operator configuration used in single-operator mode.
type OperatorConfig struct {
SkipDeployment *bool `yaml:"skipDeployment,omitempty"`
DeployViaOlm *bool `yaml:"deployViaOlm,omitempty"`
OperatorInstanceConfig `yaml:",inline"`
}

func (c *OperatorConfig) SkipDeploymentSet() bool {
Expand All @@ -101,7 +157,10 @@ func (c *OperatorConfig) DeployViaOlmEnabled() bool {

// Configure derives the operator version from the roxie configuration.
func (c *OperatorConfig) Configure(roxieConfig *RoxieConfig) error {
c.Version = helpers.ConvertMainTagToOperatorTag(roxieConfig.Version)
c.Version = roxieConfig.Version
if c.KonfluxImages == nil {
c.KonfluxImages = roxieConfig.KonfluxImages
}
return nil
}

Expand All @@ -115,6 +174,8 @@ type WaitConfig struct {

// CentralConfig holds deployment settings for the Central component.
type CentralConfig struct {
// Operator allows per-component operator overrides; only used in dual-operator mode.
Operator OperatorInstanceConfig `yaml:"operator,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
ResourceProfile types.ResourceProfile `yaml:"resourceProfile,omitempty"`
PauseReconciliation *bool `yaml:"pauseReconciliation,omitempty"`
Expand Down Expand Up @@ -265,6 +326,8 @@ func (c *CentralConfig) CustomResource() (map[string]interface{}, error) {

// SecuredClusterConfig holds deployment settings for the SecuredCluster component.
type SecuredClusterConfig struct {
// Operator allows per-component operator overrides; only used in dual-operator mode.
Operator OperatorInstanceConfig `yaml:"operator,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
ResourceProfile types.ResourceProfile `yaml:"resourceProfile,omitempty"`
PauseReconciliation *bool `yaml:"pauseReconciliation,omitempty"`
Expand Down
Loading
Loading