Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 41 additions & 5 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ 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/dockerauth"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/helpers"
"github.com/stackrox/roxie/internal/imagetag"
Expand Down Expand Up @@ -264,7 +268,7 @@ func runDeploy(cmd *cobra.Command, args []string) error {
return err
}

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

Expand Down Expand Up @@ -446,7 +450,22 @@ func configureConfig(log *logger.Logger, components component.Component, deployS
return nil
}

func deployValidate(log *logger.Logger, components component.Component, deploySettings *deployer.Config) error {
// 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 deployValidate(ctx context.Context, 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 All @@ -455,6 +474,21 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
return errors.New("running without a controlling terminal requires --envrc to be set")
}

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

requiresAuth := dockerauth.New(log).RegistryRequiresAuth(ctx, registry)
deploySettings.Roxie.RegistryRequiresAuth = requiresAuth
if requiresAuth {
log.Dimf("Registry %s requires authentication", registry)
} else {
log.Dimf("Registry %s is public, no authentication required", registry)
}
}

clusterType := deploySettings.Roxie.ClusterType

if env.RunningInRoxieContainer {
Expand All @@ -466,10 +500,9 @@ func deployValidate(log *logger.Logger, components component.Component, deploySe
return errors.New("containerized mode requires Central exposure")
}

// On infra OpenShift we already get image pull secrets for Quay automatically.
if clusterType.NeedsPullSecrets() {
if deploySettings.Roxie.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)
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)
Expand All @@ -485,6 +518,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 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
8 changes: 3 additions & 5 deletions internal/deployer/acs_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package deployer

import (
"fmt"

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

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

for _, instance := range config.OperatorInstances() {
prefix := ""
Expand All @@ -20,8 +18,8 @@ func imagesForConfig(config Config) []string {
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(),
instance.OperatorImage(imageRegistry),
instance.BundleImage(imageRegistry),
)
}

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.config.Roxie.NeedsPullSecrets()
if err := d.prepareNamespace(ctx, d.config.Central.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down
36 changes: 32 additions & 4 deletions internal/deployer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package deployer

import (
"fmt"
"strings"
"time"

"github.com/stackrox/roxie/internal/constants"
Expand Down Expand Up @@ -56,10 +57,38 @@ 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"`

// RegistryRequiresAuth is computed internally and is not user-configurable.
RegistryRequiresAuth bool `yaml:"-"`

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.

Why did you decide to put this here?
I don't think this belongs into the config struct.
We should not start treating this is some object for tracking internal application state.
This is the config struct, which is the input for the deployer.
State, if needed, belongs into the deployer IMHO.

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.

tbh I didn't know where to put it, and we already did something similar in OperatorInstanceConfig.

Makes sense to move to the Deployer state.

}

// Registry returns the resolved image registry, defaulting to
// constants.DefaultRegistry when ImageRegistry is not set.
func (c *RoxieConfig) Registry() string {
if c.ImageRegistry == "" {
return constants.DefaultRegistry

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.

Given that DefaultRoxieConfig() exists and is used already, I would advise against introducing defaulting logic into getters. That would make it unnecessarily complicated IMHO.

}
return strings.TrimSuffix(c.ImageRegistry, "/")
}

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

// NeedsPullSecrets returns whether roxie needs to set up image pull secrets itself.
// For a custom registry this relies on RegistryRequiresAuth having already been
// resolved during deploy validation (see cmd/deploy.go's deployValidate).
func (c *RoxieConfig) NeedsPullSecrets() bool {

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.

If you agree with my assessment above on the internal state tracking, then this would also be removed, since "Needs pull secrets?" wouldn't be behavior on a config struct any longer.

if c.UsesCustomRegistry() {
return c.RegistryRequiresAuth
}
return c.ClusterType.NeedsDefaultRegistryPullSecrets()
}

func (c *RoxieConfig) KonfluxImagesSet() bool {
Expand Down Expand Up @@ -118,17 +147,16 @@ func (c *OperatorInstanceConfig) ClusterRoleBindingName() string {
}

// BundleImage returns the operator bundle image for this operator instance.
func (c *OperatorInstanceConfig) BundleImage() string {
imageRegistry := constants.DefaultRegistry
func (c *OperatorInstanceConfig) BundleImage(imageRegistry string) string {
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
// OperatorImage returns the operator image for this operator instance.
func (c *OperatorInstanceConfig) OperatorImage(imageRegistry string) string {
operatorTag := c.Version.ToOperatorTag()
if c.KonfluxImagesEnabled() {
return fmt.Sprintf("%s/release-operator:%s", imageRegistry, operatorTag)
Expand Down
32 changes: 13 additions & 19 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 @@ -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.config.Roxie.NeedsPullSecrets()
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.
// isOperatorVersionCorrect checks if the deployed operator matches the desired
// image, comparing the full reference (registry, repository, and tag).
func (d *Deployer) isOperatorVersionCorrect(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(d.config.Roxie.Registry())
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.Registry())
_, 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.config.Roxie.NeedsPullSecrets()
if err := d.prepareNamespace(ctx, d.config.SecuredCluster.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,9 @@ func (d *Deployer) stopDetachedPortForward() {
// Deploy deploys the specified components to the cluster.
func (d *Deployer) Deploy(ctx context.Context, components component.Component) error {
// Prepare and verify credentials early to fail fast.
needPullSecrets := d.config.Roxie.ClusterType.NeedsPullSecrets()
needPullSecrets := d.config.Roxie.NeedsPullSecrets()
if needPullSecrets {
if err := d.prepareCredentials(); err != nil {
if err := d.prepareCredentials(ctx); err != nil {
return fmt.Errorf("failed to prepare credentials: %w", err)
}
}
Expand Down Expand Up @@ -354,11 +354,11 @@ func (d *Deployer) Deploy(ctx context.Context, components component.Component) e

// prepareCredentials prepares and verifies Docker credentials early to allow failing fast.
// The verified credentials are stored in the Deployer object for later use.
func (d *Deployer) prepareCredentials() error {
func (d *Deployer) prepareCredentials(ctx context.Context) error {
d.logger.Dimf("Preparing and verifying Docker credentials...")

// This will retrieve and verify credentials, returning error if invalid
creds, err := d.dockerAuth.GetAndVerifyCredentials()
creds, err := d.dockerAuth.GetAndVerifyCredentials(ctx, d.config.Roxie.Registry())
if err != nil {
return err
}
Expand Down
9 changes: 7 additions & 2 deletions internal/deployer/konflux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ import (
func TestOperatorImage_Konflux(t *testing.T) {
instance := OperatorInstanceConfig{Version: "4.9.2", KonfluxImages: new(true)}
expected := fmt.Sprintf("%s/release-operator:4.9.2", constants.DefaultRegistry)
assert.Equal(t, expected, instance.OperatorImage())
assert.Equal(t, expected, instance.OperatorImage(constants.DefaultRegistry))
}

func TestOperatorImage_NonKonflux(t *testing.T) {
instance := OperatorInstanceConfig{Version: "4.9.2", KonfluxImages: new(false)}
expected := fmt.Sprintf("%s/stackrox-operator:4.9.2", constants.DefaultRegistry)
assert.Equal(t, expected, instance.OperatorImage())
assert.Equal(t, expected, instance.OperatorImage(constants.DefaultRegistry))
}

func TestOperatorImage_RegistryOverride(t *testing.T) {
instance := OperatorInstanceConfig{Version: "4.9.2"}
assert.Equal(t, "quay.io/stackrox-io/stackrox-operator:4.9.2", instance.OperatorImage("quay.io/stackrox-io"))
}

func TestPopulateKonfluxEnvVars_AllEntries(t *testing.T) {
Expand Down
Loading
Loading