Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2263be6
ROX-35434: Add support for overriding image repository
vladbologa Aug 13, 2026
1724e1d
Make VerifyCredentials work with registries other than quay.io
vladbologa Aug 13, 2026
95928d5
Rewrite logic that determines if the cluster needs pull secrets
vladbologa Aug 13, 2026
2023295
simplify RegistryRequiresAuth to a best-effort bool check
vladbologa Aug 13, 2026
47fc50c
don't hardcode https scheme in auth probe URL
vladbologa Aug 13, 2026
c91e9d5
Move RegistryRequiresAuth and NeedsPullSecrets to Deployer
vladbologa Aug 16, 2026
d2bb928
Use DefaultRoxieConfig
vladbologa Aug 16, 2026
bdd6e9f
Rename test
vladbologa Aug 16, 2026
0b7a10d
Return error from RegistryRequiresAuth
vladbologa Aug 17, 2026
ae44c99
Handle http codes explicitly
vladbologa Aug 17, 2026
e55f1f8
Rename isOperatorVersionCorrect to isOperatorImageCorrect
vladbologa Aug 17, 2026
df8a90b
Memoize custom registry auth probe
vladbologa Aug 17, 2026
cbb709f
e2e test - verify Central registry too
vladbologa Aug 17, 2026
cf23372
Fix bug in RegistryRequiresAuth
vladbologa Aug 17, 2026
99dc02e
Make ImageRegistry a member of OperatorInstanceConfig
vladbologa Aug 20, 2026
c2d09c8
Apply code review suggestions
vladbologa Aug 20, 2026
84a7ce8
Avoid redundant UsesCustomRegistry check in needsOperatorPullSecrets
vladbologa Aug 20, 2026
4b93396
Move helpers after their calls sites
vladbologa Aug 21, 2026
10538a1
Add ctx to customRegistryRequiresAuth
vladbologa Aug 21, 2026
534ec57
Add clarification comment
vladbologa Aug 21, 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
57 changes: 46 additions & 11 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"fmt"
"math/big"
"os"
"strings"
"time"

"dario.cat/mergo"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stackrox/roxie/internal/clusterdefaults"
"github.com/stackrox/roxie/internal/component"
"github.com/stackrox/roxie/internal/constants"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
Expand Down Expand Up @@ -280,6 +283,12 @@ func runDeploy(cmd *cobra.Command, args []string) error {
d.SetVerbose(verbose)
d.SetConfig(deploySettings)

if d.NeedsPullSecrets(ctx) {
if err := validateContainerizedCredentials(deploySettings.Roxie.ImageRegistry, deploySettings.Roxie.ClusterType); err != nil {
return err
}
}

if dryRun {
log.Info("Exiting because of enabled dry run mode.")
return nil
Expand Down Expand Up @@ -353,6 +362,19 @@ func runDeploy(cmd *cobra.Command, args []string) error {
return nil
}

func validateContainerizedCredentials(registry string, clusterType types.ClusterType) error {
if !env.RunningInRoxieContainer {
return nil
}
if os.Getenv("REGISTRY_USERNAME") == "" || os.Getenv("REGISTRY_PASSWORD") == "" {
return fmt.Errorf("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables for registry %s on clusters of type %s", registry, clusterType)
}
if _, err := os.Stat("/kubeconfig"); err != nil {
return fmt.Errorf("containerized mode requires /kubeconfig file: %w", err)
}
return nil
}

func retrieveClusterConfigForComponents(
ctx context.Context,
log *logger.Logger,
Expand Down Expand Up @@ -455,7 +477,12 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
return errors.New("running without a controlling terminal requires --envrc to be set")
}

clusterType := deploySettings.Roxie.ClusterType
registry := deploySettings.Roxie.ImageRegistry
if deploySettings.Roxie.UsesCustomRegistry() {
if err := validateImageRegistry(registry); err != nil {
return err
}
}

if env.RunningInRoxieContainer {
// For running containerized we have specific requirements.
Expand All @@ -465,16 +492,6 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
if !deploySettings.Central.ExposureEnabled() {
return errors.New("containerized mode requires Central exposure")
}

// On infra OpenShift we already get image pull secrets for Quay automatically.
if clusterType.NeedsPullSecrets() {
if os.Getenv("REGISTRY_USERNAME") == "" || os.Getenv("REGISTRY_PASSWORD") == "" {
return fmt.Errorf("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables for clusters of type %s", clusterType)
}
if _, err := os.Stat("/kubeconfig"); err != nil {
return fmt.Errorf("containerized mode requires /kubeconfig file: %w", err)
}
}
}

if deploySettings.Operator.SkipDeploymentEnabled() && deploySettings.Operator.DeployViaOlmEnabled() {
Expand All @@ -485,6 +502,9 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
if deploySettings.Operator.DeployViaOlmEnabled() {
return errors.New("using Konflux images while deploying operator via OLM is not supported")
}
if registry != constants.DefaultRegistry {
return fmt.Errorf("using Konflux images with a custom image registry (%s) is not supported", registry)
}
}

if deploySettings.HasMixedVersions() {
Expand All @@ -508,6 +528,21 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
return nil
}

// validateImageRegistry checks that registry is a well-formed "host/repository-path" string, e.g. "quay.io/rhacs-eng".
func validateImageRegistry(registry string) error {
host, repoPath, hasPath := strings.Cut(registry, "/")
if !hasPath || repoPath == "" {
return fmt.Errorf("roxie.imageRegistry must include a repository path (e.g. %s), got: %s", constants.DefaultRegistry, registry)
}
if _, err := name.NewRegistry(host); err != nil {
return fmt.Errorf("roxie.imageRegistry has an invalid registry host %q: %w", host, err)
}
if _, err := name.NewRepository(repoPath); err != nil {
return fmt.Errorf("roxie.imageRegistry has an invalid repository path %q: %w", repoPath, 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.
Expand Down
50 changes: 50 additions & 0 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"dario.cat/mergo"
"github.com/stackrox/roxie/internal/constants"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/imagetag"
"github.com/stackrox/roxie/internal/logger"
Expand Down Expand Up @@ -314,6 +315,55 @@ func TestNewDeployCmd_SetRejectsSpec(t *testing.T) {
}
}

func TestValidateImageRegistry(t *testing.T) {
tests := []struct {
name string
registry string
expectError bool
errorContains string
}{
{name: "default registry", registry: constants.DefaultRegistry},
{name: "valid host/path registry", registry: "quay.io/stackrox-io"},
{name: "registry host with port", registry: "localhost:5000/rhacs-eng"},
{
name: "bare host with no path is rejected",
registry: "justahost",
expectError: true,
errorContains: "must include a repository path",
},
{
name: "trailing slash with no path is rejected",
registry: "quay.io/",
expectError: true,
errorContains: "must include a repository path",
},
{
name: "invalid registry host",
registry: "quay io/rhacs-eng",
expectError: true,
errorContains: "invalid registry host",
},
{
name: "invalid repository path characters",
registry: "quay.io/RHACS-ENG",
expectError: true,
errorContains: "invalid repository path",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateImageRegistry(tt.registry)
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorContains)
return
}
require.NoError(t, err)
})
}
}

func TestApplyUserDefaults(t *testing.T) {
log := logger.New()

Expand Down
11 changes: 4 additions & 7 deletions internal/deployer/acs_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ package deployer

import (
"fmt"

"github.com/stackrox/roxie/internal/constants"
)

func imagesForConfig(config Config) []string {
var images []string
imageRegistry := constants.DefaultRegistry

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),
fmt.Sprintf("%s/%s%s:%s", instance.ImageRegistry, prefix, "main", instance.Version),
fmt.Sprintf("%s/%s%s:%s", instance.ImageRegistry, prefix, "central-db", instance.Version),
fmt.Sprintf("%s/%s%s:%s", instance.ImageRegistry, prefix, "scanner-v4-db", instance.Version),
fmt.Sprintf("%s/%s%s:%s", instance.ImageRegistry, prefix, "scanner-v4", instance.Version),
instance.OperatorImage(),
instance.BundleImage(),
)
Expand Down
2 changes: 1 addition & 1 deletion internal/deployer/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (d *Deployer) deployAddOns(ctx context.Context, addOns []AddOn) error {
return nil
}

needPullSecrets := d.config.Roxie.ClusterType.NeedsPullSecrets()
needPullSecrets := d.NeedsPullSecrets(ctx)
if err := d.prepareNamespace(ctx, d.config.Central.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down
19 changes: 13 additions & 6 deletions internal/deployer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ func (c *Config) DeepCopy() (*Config, error) {
// RoxieConfig holds roxie-level settings such as version and feature flags.
type RoxieConfig struct {
Version imagetag.MainTag `yaml:"version,omitempty"`
ImageRegistry string `yaml:"imageRegistry,omitempty"`
KonfluxImages *bool `yaml:"konfluxImages,omitempty"`
FeatureFlags map[string]bool `yaml:"featureFlags,omitempty"`
ClusterType types.ClusterType `yaml:"clusterType,omitempty"`
HAProxy HAProxyConfig `yaml:"haProxy,omitempty"`
}

// UsesCustomRegistry returns whether a custom image registry was configured.
func (c *RoxieConfig) UsesCustomRegistry() bool {
return c.ImageRegistry != constants.DefaultRegistry
}

func (c *RoxieConfig) KonfluxImagesSet() bool {
return c.KonfluxImages != nil
}
Expand Down Expand Up @@ -89,6 +95,7 @@ type OperatorInstanceConfig struct {
// The following fields are computed internally and are not user-configurable.
Namespace string `yaml:"-"`
RoleNameSuffix string `yaml:"-"`
ImageRegistry string `yaml:"-"`
}

func (c *OperatorInstanceConfig) KonfluxImagesSet() bool {
Expand Down Expand Up @@ -119,21 +126,20 @@ func (c *OperatorInstanceConfig) ClusterRoleBindingName() string {

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

// OperatorImage returns the operator image for this operator instance.
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/release-operator:%s", c.ImageRegistry, operatorTag)
}
return fmt.Sprintf("%s/stackrox-operator:%s", imageRegistry, operatorTag)
return fmt.Sprintf("%s/stackrox-operator:%s", c.ImageRegistry, operatorTag)
}

// OperatorConfig is the top-level operator configuration used in single-operator mode.
Expand Down Expand Up @@ -207,6 +213,7 @@ func NewCentralConfig() CentralConfig {
func DefaultRoxieConfig() RoxieConfig {
cfg := NewRoxieConfig()
cfg.HAProxy.BindPort = defaultHAProxyBindPort
cfg.ImageRegistry = constants.DefaultRegistry
return cfg
}

Expand Down
46 changes: 20 additions & 26 deletions internal/deployer/deploy_via_operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import (
"strings"
"time"

"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/stackrox/roxie/internal/component"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/k8s"
"github.com/stackrox/roxie/internal/types"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

var (
Expand Down Expand Up @@ -129,11 +130,11 @@ func (d *Deployer) ensureOperatorInstanceNonOLM(ctx context.Context, instance Op
needsTeardown := false

if exists {
if d.isOperatorVersionCorrect(ctx, instance) {
d.logger.Infof("✓ Operator already deployed with correct version in namespace %s", instance.Namespace)
if d.isOperatorImageCorrect(ctx, instance) {
d.logger.Infof("✓ Operator already deployed with correct image in namespace %s", instance.Namespace)
return nil
}
d.logger.Infof("🔄 Operator version mismatch in namespace %s, redeploying...", instance.Namespace)
d.logger.Infof("🔄 Operator image mismatch in namespace %s, redeploying...", instance.Namespace)
needsTeardown = true
needsDeployment = true
}
Expand Down Expand Up @@ -180,10 +181,10 @@ func (d *Deployer) ensureOperatorDeployedOLM(ctx context.Context) error {
Namespace: operatorNamespaceSystem,
EnvVars: d.config.Operator.EnvVars,
}
if d.isOperatorVersionCorrect(ctx, instance) {
d.logger.Info("✓ Operator already deployed with correct version")
if d.isOperatorImageCorrect(ctx, instance) {
d.logger.Info("✓ Operator already deployed with correct image")
} else {
d.logger.Info("🔄 Operator version mismatch, redeploying...")
d.logger.Info("🔄 Operator image mismatch, redeploying...")
needsTeardown = true
needsDeployment = true
}
Expand Down Expand Up @@ -214,7 +215,7 @@ func (d *Deployer) ensureOperatorDeployedOLM(ctx context.Context) error {
func (d *Deployer) deployCentralOperator(ctx context.Context) error {
d.logger.Info("🚀 Deploying Central via Operator...")

needPullSecrets := d.config.Roxie.ClusterType.NeedsPullSecrets()
needPullSecrets := d.NeedsPullSecrets(ctx)
if err := d.prepareNamespace(ctx, d.config.Central.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down Expand Up @@ -247,27 +248,20 @@ func (d *Deployer) deployCentralOperator(ctx context.Context) error {
return d.configureCentralEndpoint(ctx)
}

// isOperatorVersionCorrect checks if the deployed operator matches the desired version.
func (d *Deployer) isOperatorVersionCorrect(ctx context.Context, instance OperatorInstanceConfig) bool {
// isOperatorImageCorrect checks if the deployed operator matches the desired
// image, comparing the full reference (registry, repository, and tag).
func (d *Deployer) isOperatorImageCorrect(ctx context.Context, instance OperatorInstanceConfig) bool {
currentImage, err := d.getDeployedOperatorImage(ctx, instance.Namespace)
if err != nil {
d.logger.Warningf("Could not retrieve operator image: %v", err)
return false
}

// Extract the tag from the current image
parts := strings.SplitN(currentImage, ":", 2)
if len(parts) < 2 {
d.logger.Warningf("Could not parse operator image tag from: %s", currentImage)
return false
}
currentTag := parts[1]

desiredTag := instance.Version.ToOperatorTag().String()
if currentTag != desiredTag {
d.logger.Info("Operator version mismatch detected:")
d.logger.Infof(" Current: %s", currentTag)
d.logger.Infof(" Desired: %s", desiredTag)
desiredImage := instance.OperatorImage()
if currentImage != desiredImage {
d.logger.Info("Operator image mismatch detected:")
d.logger.Infof(" Current: %s", currentImage)
d.logger.Infof(" Desired: %s", desiredImage)
return false
}
return true
Expand Down Expand Up @@ -309,7 +303,7 @@ func (d *Deployer) ensurePullSecretExists(ctx context.Context, namespace string)
return errors.New("no pull secrets available to set up on the cluster")
}

pullSecretYAML := d.dockerAuth.CreatePullSecretYAMLFromCredentials(*d.dockerCreds, namespace)
pullSecretYAML := d.dockerAuth.CreatePullSecretYAMLFromCredentials(*d.dockerCreds, namespace, d.config.Roxie.ImageRegistry)
_, err := d.runKubectl(ctx, k8s.KubectlOptions{
Args: []string{"apply", "-f", "-"},
Stdin: strings.NewReader(pullSecretYAML),
Expand Down Expand Up @@ -828,7 +822,7 @@ func (d *Deployer) configureCentralEndpoint(ctx context.Context) error {
func (d *Deployer) deploySecuredClusterOperator(ctx context.Context) error {
d.logger.Info("🚀 Deploying SecuredCluster via Operator...")

needPullSecrets := d.config.Roxie.ClusterType.NeedsPullSecrets()
needPullSecrets := d.NeedsPullSecrets(ctx)
if err := d.prepareNamespace(ctx, d.config.SecuredCluster.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down
Loading
Loading