Skip to content

ROX-35435: Support differing versions of Central and SecuredCluster#246

Draft
vladbologa wants to merge 2 commits into
mainfrom
vb/differing-central-sc-versions
Draft

ROX-35435: Support differing versions of Central and SecuredCluster#246
vladbologa wants to merge 2 commits into
mainfrom
vb/differing-central-sc-versions

Conversation

@vladbologa

@vladbologa vladbologa commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Roxie can deploy Central and SecuredCluster at different ACS versions on the same cluster (for compatibility testing), while keeping the single-version path unchanged.

Config and CLI

  • roxie.version / --tag remains the global default.
  • Optional overrides: central.version / --central-tag and securedCluster.version / --secured-cluster-tag.
  • Effective versions are resolved via EffectiveCentralVersion() / EffectiveSecuredClusterVersion(). When they differ, NeedsSplitOperators() is true.
  • Precedence: component-specific tag > --tag / roxie.version.

Operators

When versions match (common case):

  • One operator in rhacs-operator-system (unchanged).

When versions differ:

  • Operator for Central in rhacs-operator-central with SECURED_CLUSTER_RECONCILER_ENABLED=false.
  • Operator for SecuredCluster in rhacs-operator-sensor with CENTRAL_RECONCILER_ENABLED=false.
  • Distinct ClusterRole / ClusterRoleBinding names per instance (…-central / …-sensor).

This is derived automatically from version mismatch; users do not configure reconciler toggles or operator namespaces.

Limitations

  • Split versions with --olm / OLM deploy are not supported (hard error).
  • Split versions with roxie deploy operator (operator-only) are not supported (hard error).

Approaches considered

Approach Why not
Pause-reconcile annotation Leaves either Central or the Secured Cluster without an active operator.
Two separate roxie deploy invocations Awkward UX, problems with roxie shell.
Label selectors (CENTRAL_LABEL_SELECTOR / …) Designed for multi-tenant multi-CR setups. Overkill when there is one Central and one SecuredCluster; reconciler toggles are simpler.

Manual testing on an infra cluster

  1. Fresh split deploy — Central 4.11.1, SecuredCluster 4.10.0. Confirmed dual operators, correct reconciler toggles, matching product images; rhacs-operator-system unused.
  2. Teardown after splitroxie teardown removed both split operator namespaces and component CRs/workloads.
  3. Same-version deploy--tag 4.11.1 only. Single operator in rhacs-operator-system, no reconciler toggles, no split namespaces.
  4. In-place single → split — Without prior teardown, switched to Central 4.11.1 / SecuredCluster 4.10.0. rhacs-operator-system removed; split operators stood up correctly.

Summary by CodeRabbit

  • New Features

    • Added support for independent Central and Secured Cluster image versions.
    • Added per-component image tag options for deployments.
    • Added support for multiple operator instances with component-specific versions, namespaces, and settings.
    • Deployment summaries now display the effective component versions.
  • Bug Fixes

    • Improved image selection to avoid duplicates and use the correct versions.
    • Improved operator upgrades and transitions between deployment modes.
    • Added validation to reject unsupported split-operator configurations.
    • Improved readiness validation across all deployed operator versions.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 36a8283b-789a-40b9-9346-c1fbe599ecbb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vb/differing-central-sc-versions

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/deployer/acs_images.go`:
- Around line 38-40: Update the image collection loop in the deployer flow
around OperatorInstances so single-operator configurations consistently use the
explicit Operator.Version override instead of mixing it with the resolved
instance version; alternatively, unify OperatorBundleImageForVersion and the OLM
Konflux environment-variable path on the same resolved version. Add a regression
test covering both Roxie.Version and Operator.Version being set, ensuring all
generated image references use one consistent operator version.

In `@internal/deployer/deploy_via_operator.go`:
- Around line 138-141: Update the early-return logic in the deployment flow
around isOperatorVersionCorrect so it also verifies the manager environment
matches instance.EnvVars, including component-specific reconciler flags and
Konflux/custom variables. Only return when both the image version and
environment are correct; otherwise reapply the desired Deployment using the
existing operator deployment path, preserving the split-mode flags defined by
the operator instance plan.
- Around line 110-125: The cleanup loop in deploy_via_operator.go around
AllOperatorNamespaces must attempt teardown for every undesired known namespace
without gating on operatorDeploymentExists or namespaceExists, allowing
teardownOperatorNonOLMInNamespace to remove namespaced and cluster-scoped
resources idempotently. In operator.go around the non-OLM cleanup flow, invoke
cleanup even when no Deployment is detected so partial resources are removed;
apply the change at both affected sites.

In `@internal/deployer/operator_instance.go`:
- Around line 78-82: Update the single-operator path in NeedsSplitOperators to
derive the operator version from the effective component version, honoring equal
Roxie and Operator overrides instead of using Roxie.Version directly. Preserve
the fallback behavior when no converted version is available, and extend
TestEffectiveVersions_BothOverridesSame_NoSplit with an assertion that the
deployed operator uses the override version.

In `@internal/deployer/operator.go`:
- Around line 575-598: Update the teardown flow around the kubectl deletion
calls and waitForNamespaceDeletion to propagate genuine deletion or timeout
errors instead of ignoring them or returning nil. Keep --ignore-not-found=true
for idempotent cleanup, but capture each runKubectl error and return it
immediately; likewise return the waitForNamespaceDeletion error before logging
success. Only log the success message and return nil after all cleanup steps
complete successfully.
- Around line 217-222: Update the EnvVars logging block in the operator
deployment flow to stop emitting environment values, since entries may contain
credentials or tokens. Preserve the existing count log and, if individual
entries are still needed, log only each variable’s name from envVarsToSortedList
without accessing or formatting ev["value"].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: b2693e61-392e-405e-a4bb-6dd14e9c6436

📥 Commits

Reviewing files that changed from the base of the PR and between 4f94203 and 9500218.

📒 Files selected for processing (10)
  • cmd/deploy.go
  • internal/deployer/acs_images.go
  • internal/deployer/config.go
  • internal/deployer/deploy_via_operator.go
  • internal/deployer/deployer.go
  • internal/deployer/konflux.go
  • internal/deployer/konflux_test.go
  • internal/deployer/operator.go
  • internal/deployer/operator_instance.go
  • internal/deployer/operator_instance_test.go

Comment on lines +38 to +40
for _, instance := range config.OperatorInstances() {
add(fmt.Sprintf("%s/%s%s:%s", imageRegistry, operatorPrefix, "operator", instance.Version))
add(OperatorBundleImageForVersion(instance.Version, config.Roxie.KonfluxImagesEnabled()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep single-operator version resolution consistent.

For Roxie.Version="4.9.x" and Operator.Version="4.8.0", OperatorInstances() resolves 4.9.0, so this image list contains 4.9.0; OperatorBundleImage and the OLM Konflux env-var path use 4.8.0. Preserve the explicit operator-version override here or unify all paths on one resolved version, with a regression test for both fields being set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/acs_images.go` around lines 38 - 40, Update the image
collection loop in the deployer flow around OperatorInstances so single-operator
configurations consistently use the explicit Operator.Version override instead
of mixing it with the resolved instance version; alternatively, unify
OperatorBundleImageForVersion and the OLM Konflux environment-variable path on
the same resolved version. Add a regression test covering both Roxie.Version and
Operator.Version being set, ensuring all generated image references use one
consistent operator version.

Comment on lines +110 to +125
for _, ns := range AllOperatorNamespaces {
if desiredNS[ns] {
continue
}
if !d.operatorDeploymentExists(ctx, ns) && !d.namespaceExists(ns) {
continue
}
d.logger.Infof("🔄 Removing operator from unwanted namespace %s...", ns)
instance := OperatorInstance{Namespace: ns}
switch ns {
case operatorNamespaceCentral:
instance.RoleNameSuffix = "central"
case operatorNamespaceSensor:
instance.RoleNameSuffix = "sensor"
}
if err := d.teardownOperatorNonOLMInNamespace(ctx, instance); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Make operator cleanup unconditional and idempotent.

Both paths infer that no cleanup is required from missing namespaced Deployments, but partially failed or manually altered deployments may still leave namespaces or cluster-scoped RBAC.

  • internal/deployer/deploy_via_operator.go#L110-L125: attempt cleanup for every undesired known instance, including its ClusterRole and ClusterRoleBinding.
  • internal/deployer/operator.go#L652-L664: run non-OLM cleanup even when no Deployment is detected so partial resources are removed.
📍 Affects 2 files
  • internal/deployer/deploy_via_operator.go#L110-L125 (this comment)
  • internal/deployer/operator.go#L652-L664
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/deploy_via_operator.go` around lines 110 - 125, The cleanup
loop in deploy_via_operator.go around AllOperatorNamespaces must attempt
teardown for every undesired known namespace without gating on
operatorDeploymentExists or namespaceExists, allowing
teardownOperatorNonOLMInNamespace to remove namespaced and cluster-scoped
resources idempotently. In operator.go around the non-OLM cleanup flow, invoke
cleanup even when no Deployment is detected so partial resources are removed;
apply the change at both affected sites.

Comment on lines +138 to +141
if exists {
if d.isOperatorVersionCorrect(ctx, instance) {
d.logger.Infof("✓ Operator already deployed with correct version in %s", instance.Namespace)
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Reconcile instance environment, not just its image version.

A matching image causes an early return even if the manager is missing the component-specific reconciler flags or has stale Konflux/custom variables. In split mode, missing flags can allow both operators to reconcile the same component.

Compare the manager environment with instance.EnvVars, or reapply the desired Deployment when it differs. The split plan in internal/deployer/operator_instance.go lines 77-110 relies on these flags.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/deploy_via_operator.go` around lines 138 - 141, Update the
early-return logic in the deployment flow around isOperatorVersionCorrect so it
also verifies the manager environment matches instance.EnvVars, including
component-specific reconciler flags and Konflux/custom variables. Only return
when both the image version and environment are correct; otherwise reapply the
desired Deployment using the existing operator deployment path, preserving the
split-mode flags defined by the operator instance plan.

Comment on lines +78 to +82
if !c.NeedsSplitOperators() {
version := helpers.ConvertMainTagToOperatorTag(c.Roxie.Version)
if version == "" {
version = c.Operator.Version
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Derive the single operator from the effective component version.

When both overrides are equal, split mode is disabled, but this branch still uses Roxie.Version. Thus global 4.9.0 plus two 4.8.0 overrides deploys a 4.9.0 operator, violating override precedence.

Proposed fix
 	if !c.NeedsSplitOperators() {
-		version := helpers.ConvertMainTagToOperatorTag(c.Roxie.Version)
+		version := helpers.ConvertMainTagToOperatorTag(c.EffectiveCentralVersion())
 		if version == "" {
 			version = c.Operator.Version
 		}

Add an operator-instance assertion to TestEffectiveVersions_BothOverridesSame_NoSplit to prevent regression.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !c.NeedsSplitOperators() {
version := helpers.ConvertMainTagToOperatorTag(c.Roxie.Version)
if version == "" {
version = c.Operator.Version
}
if !c.NeedsSplitOperators() {
version := helpers.ConvertMainTagToOperatorTag(c.EffectiveCentralVersion())
if version == "" {
version = c.Operator.Version
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/operator_instance.go` around lines 78 - 82, Update the
single-operator path in NeedsSplitOperators to derive the operator version from
the effective component version, honoring equal Roxie and Operator overrides
instead of using Roxie.Version directly. Preserve the fallback behavior when no
converted version is available, and extend
TestEffectiveVersions_BothOverridesSame_NoSplit with an assertion that the
deployed operator uses the override version.

Comment on lines +217 to 222
if len(instance.EnvVars) > 0 {
d.logger.Dimf(" • Custom operator env vars: %d", len(instance.EnvVars))
for _, envVar := range envVarsToSortedList(instance.EnvVars) {
ev := envVar.(map[string]any)
d.logger.Dimf(" %s=%s", ev["name"], ev["value"])
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not log operator environment values.

Operator.EnvVars is arbitrary configuration and may contain credentials or tokens. Log names or only the count, not values.

Proposed fix
 	for _, envVar := range envVarsToSortedList(instance.EnvVars) {
 		ev := envVar.(map[string]any)
-		d.logger.Dimf("    %s=%s", ev["name"], ev["value"])
+		d.logger.Dimf("    %s=<redacted>", ev["name"])
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(instance.EnvVars) > 0 {
d.logger.Dimf(" • Custom operator env vars: %d", len(instance.EnvVars))
for _, envVar := range envVarsToSortedList(instance.EnvVars) {
ev := envVar.(map[string]any)
d.logger.Dimf(" %s=%s", ev["name"], ev["value"])
}
if len(instance.EnvVars) > 0 {
d.logger.Dimf(" • Custom operator env vars: %d", len(instance.EnvVars))
for _, envVar := range envVarsToSortedList(instance.EnvVars) {
ev := envVar.(map[string]any)
d.logger.Dimf(" %s=<redacted>", ev["name"])
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/operator.go` around lines 217 - 222, Update the EnvVars
logging block in the operator deployment flow to stop emitting environment
values, since entries may contain credentials or tokens. Preserve the existing
count log and, if individual entries are still needed, log only each variable’s
name from envVarsToSortedList without accessing or formatting ev["value"].

Comment on lines 575 to +598
d.runKubectl(ctx, k8s.KubectlOptions{
Args: []string{"delete", "namespace", operatorNamespace, "--wait=false"},
Args: []string{"delete", "namespace", instance.Namespace, "--wait=false"},
IgnoreErrors: true,
})

// Delete cluster-scoped resources created by non-OLM flow.
clusterResources := []struct {
for _, resource := range []struct {
name string
kind string
}{
{name: "rhacs-operator-manager-rolebinding", kind: "clusterrolebinding"},
{name: "rhacs-operator-manager-role", kind: "clusterrole"},
}
for _, resource := range clusterResources {
{name: instance.ClusterRoleBindingName(), kind: "clusterrolebinding"},
{name: instance.ClusterRoleName(), kind: "clusterrole"},
} {
d.runKubectl(ctx, k8s.KubectlOptions{
Args: []string{"delete", resource.kind, resource.name, "--ignore-not-found=true"},
IgnoreErrors: true,
})
}

if err := d.waitForNamespaceDeletion(operatorNamespace); err != nil {
d.logger.Warningf("Namespace %s deletion incomplete: %v", operatorNamespace, err)
if err := d.waitForNamespaceDeletion(instance.Namespace); err != nil {
d.logger.Warningf("Namespace %s deletion incomplete: %v", instance.Namespace, err)
}

d.logger.Successf("✓ Non-OLM operator resources removed from %s", instance.Namespace)
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate teardown failures instead of reporting false success.

Every delete failure is ignored, namespace-deletion failure is reduced to a warning, and the function always returns nil. Callers therefore cannot detect incomplete cleanup despite explicitly handling its error.

Use --ignore-not-found=true, but return genuine deletion and timeout failures before logging success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/deployer/operator.go` around lines 575 - 598, Update the teardown
flow around the kubectl deletion calls and waitForNamespaceDeletion to propagate
genuine deletion or timeout errors instead of ignoring them or returning nil.
Keep --ignore-not-found=true for idempotent cleanup, but capture each runKubectl
error and return it immediately; likewise return the waitForNamespaceDeletion
error before logging success. Only log the success message and return nil after
all cleanup steps complete successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant