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
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() {
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 @@ -446,6 +455,34 @@ func configureConfig(log *logger.Logger, components component.Component, deployS
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 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 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 All @@ -455,7 +492,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 +507,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 +517,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.ImageRegistry

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.NeedsPullSecrets()
if err := d.prepareNamespace(ctx, d.config.Central.Namespace, needPullSecrets); err != nil {
return fmt.Errorf("failed to prepare namespace: %w", err)
}
Expand Down
14 changes: 10 additions & 4 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 @@ -118,17 +124,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 Expand Up @@ -207,6 +212,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()
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(d.config.Roxie.ImageRegistry)
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()
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