Skip to content
Merged
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
42 changes: 28 additions & 14 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ import (
"github.com/stackrox/roxie/internal/stackroxversions"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/ptr"
)

var (
const (
sharedNamespace = "stackrox"
)

Expand All @@ -55,40 +54,40 @@ Examples:
registerFlag(cmd, settings, "olm", "Deploy operator via OLM (requires OLM installed)",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Operator.DeployViaOlm = val
config.Operator.DeployViaOlm = new(val)
return nil
}),
)

registerFlag(cmd, settings, "konflux", "Use Konflux images",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Roxie.KonfluxImages = val
config.Roxie.KonfluxImages = new(val)
return nil
}),
)

registerFlag(cmd, settings, "deploy-operator", "Whether to deploy and manage the operator",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Operator.SkipDeployment = !val
config.Operator.SkipDeployment = new(!val)
return nil
}),
)

registerFlag(cmd, settings, "port-forwarding", "Enable localhost port-forward for Central",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Central.PortForwarding = ptr.To(val)
config.Central.PortForwarding = new(val)
return nil
}),
)

registerFlag(cmd, settings, "pause-reconciliation", "Pause reconciliation after deployment",
withNoOptDefVal("true"),
withApplyFnBool(func(config *deployer.Config, val bool) error {
config.Central.PauseReconciliation = val
config.SecuredCluster.PauseReconciliation = val
config.Central.PauseReconciliation = new(val)
config.SecuredCluster.PauseReconciliation = new(val)
return nil
}),
)
Expand Down Expand Up @@ -120,7 +119,7 @@ Examples:
if err := yaml.Unmarshal([]byte(val), &exposure); err != nil {
return err
}
config.Central.Exposure = ptr.To(exposure)
config.Central.Exposure = new(exposure)
return nil
}),
)
Expand Down Expand Up @@ -246,7 +245,7 @@ Examples:
}

func runDeploy(cmd *cobra.Command, args []string) error {
log := logger.New()
log := globalLogger
if !dryRun {
if err := env.Initialize(log); err != nil {
return err
Expand All @@ -264,6 +263,21 @@ func runDeploy(cmd *cobra.Command, args []string) error {
return err
}

// Start with default configuration.
deploySettings := deployer.DefaultConfig()

// Apply user config on top (overriding defaults).
if !skipUserConfig {
if err := tryApplyUserDefaults(globalLogger, &deploySettings); err != nil {
return fmt.Errorf("applying user config: %w", err)
}
}

// Apply changes from arg parsing.
if err := mergo.Merge(&deploySettings, &deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil {
return fmt.Errorf("applying config patches from command line argument: %w", err)
}

if deploySettings.Roxie.Version != "" {
log.Dimf("Using main image tag %s", deploySettings.Roxie.Version)
} else {
Expand Down Expand Up @@ -411,7 +425,7 @@ func configureConfig(log *logger.Logger, components component.Component, deployS

if !deploySettings.Central.PortForwardingSet() && !deploySettings.Central.ExposureEnabled() {
log.Info("Enabling port-forwarding due to no exposure")
deploySettings.Central.PortForwarding = ptr.To(true)
deploySettings.Central.PortForwarding = new(true)
}

return nil
Expand Down Expand Up @@ -448,12 +462,12 @@ func deployValidate(components component.Component, deploySettings *deployer.Con
}
}

if deploySettings.Operator.SkipDeployment && deploySettings.Operator.DeployViaOlm {
if deploySettings.Operator.SkipDeploymentEnabled() && deploySettings.Operator.DeployViaOlmEnabled() {
return errors.New("skipping operator deployment while also requesting deploying via OLM at the same time does not make sense")
}

if deploySettings.Roxie.KonfluxImages {
if deploySettings.Operator.DeployViaOlm {
if deploySettings.Roxie.KonfluxImagesEnabled() {
if deploySettings.Operator.DeployViaOlmEnabled() {
return errors.New("using Konflux images while deploying operator via OLM is not supported")
}
if !clusterType.IsOpenShift() {
Expand Down
126 changes: 122 additions & 4 deletions cmd/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import (
"testing"
"time"

"dario.cat/mergo"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/paths"
"github.com/stackrox/roxie/internal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestNewDeployCmd_Flags(t *testing.T) {
Expand Down Expand Up @@ -110,22 +114,22 @@ func TestNewDeployCmd_Flags(t *testing.T) {
name: "pause-reconciliation",
args: []string{"--pause-reconciliation"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.True(t, cfg.Central.PauseReconciliation, "Central.PauseReconciliation mismatch")
assert.True(t, cfg.SecuredCluster.PauseReconciliation, "SecuredCluster.PauseReconciliation mismatch")
assert.True(t, cfg.Central.PauseReconciliationEnabled(), "Central.PauseReconciliation mismatch")
assert.True(t, cfg.SecuredCluster.PauseReconciliationEnabled(), "SecuredCluster.PauseReconciliation mismatch")
},
},
{
name: "olm",
args: []string{"--olm"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.True(t, cfg.Operator.DeployViaOlm, "Operator.DeployViaOlm mismatch")
assert.True(t, cfg.Operator.DeployViaOlmEnabled(), "Operator.DeployViaOlm mismatch")
},
},
{
name: "disable deploy-operator",
args: []string{"--deploy-operator=false"},
assert: func(t *testing.T, cfg deployer.Config) {
assert.True(t, cfg.Operator.SkipDeployment, "Operator.SkipDeployment mismatch")
assert.True(t, cfg.Operator.SkipDeploymentEnabled(), "Operator.SkipDeployment mismatch")
},
},
{
Expand Down Expand Up @@ -218,3 +222,117 @@ central:
})
}
}

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

tests := []struct {
name string
config deployer.Config
user deployer.Config
expected deployer.Config
}{
{
name: "empty user config leaves config unchanged",
config: deployer.Config{
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
},
},
expected: deployer.Config{
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
},
},
},
{
name: "fills empty fields from user defaults",
config: deployer.Config{},
user: deployer.Config{
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
Operator: deployer.OperatorConfig{DeployViaOlm: new(true)},
},
expected: deployer.Config{
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
Operator: deployer.OperatorConfig{DeployViaOlm: new(true)},
},
},
{
name: "user config overrides any config fields including config defaults",
config: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.9.2",
},
Central: deployer.CentralConfig{
EarlyReadiness: new(true),
},
},
user: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.5.0",
},
Operator: deployer.OperatorConfig{
DeployViaOlm: new(true),
},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
EarlyReadiness: new(false),
},
},
expected: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.5.0",
},
Operator: deployer.OperatorConfig{
DeployViaOlm: new(true),
},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
EarlyReadiness: new(false),
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tmpDir)
t.Setenv("HOME", tmpDir) // For non-Unix systems.

if !reflect.DeepEqual(tt.user, deployer.Config{}) {
configPath, err := paths.UserConfigPath()
require.NoError(t, err)
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
data, err := yaml.Marshal(tt.user)
require.NoError(t, err)
require.NoError(t, os.WriteFile(configPath, data, 0o644))
}

cfg := deployer.NewConfig()
require.NoError(t, mergo.Merge(&cfg, &tt.config, mergo.WithOverride, mergo.WithoutDereference))
require.NoError(t, tryApplyUserDefaults(log, &cfg))

expected := deployer.NewConfig()
require.NoError(t, mergo.Merge(&expected, &tt.expected, mergo.WithOverride, mergo.WithoutDereference))

assert.True(t, reflect.DeepEqual(expected, cfg), "expected %+v, got %+v", expected, cfg)
})
}

t.Run("returns error on invalid yaml", func(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tmpDir)
t.Setenv("HOME", tmpDir) // For non-Unix systems.

configPath, err := paths.UserConfigPath()
require.NoError(t, err)
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
require.NoError(t, os.WriteFile(configPath, []byte(`invalid: [yaml`), 0o644))

cfg := deployer.NewConfig()
assert.Error(t, tryApplyUserDefaults(log, &cfg))
})
}
3 changes: 1 addition & 2 deletions cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/spf13/cobra"
"github.com/stackrox/roxie/internal/env"
"github.com/stackrox/roxie/internal/logger"
)

func newEnvCmd() *cobra.Command {
Expand All @@ -22,7 +21,7 @@ func newEnvCmd() *cobra.Command {
}

func runEnv(cmd *cobra.Command, args []string) error {
log := logger.New()
log := globalLogger
if err := env.Initialize(log); err != nil {
return err
}
Expand Down
45 changes: 41 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package main

import (
"fmt"
"os"

"dario.cat/mergo"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/stackrox/roxie/internal/deployer"
"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/paths"
"gopkg.in/yaml.v3"
)

var (
Expand All @@ -15,18 +20,48 @@ var (
envrc string
dryRun bool

skipUserConfig bool

globalLogger = logger.New()

// We need this set up before command line flags are parsed.
deploySettings = deployer.NewConfig()
deploySettingsFromArgs = deployer.NewConfig()
)

func main() {
red := color.New(color.FgRed, color.Bold)
if err := rootCmd.Execute(); err != nil {
red := color.New(color.FgRed, color.Bold)
red.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

// If a user config file exists, apply those user defaults on top the
// current config. This essentially means, that the user config can
// override values, which are already initialized in NewConfig().
func tryApplyUserDefaults(log *logger.Logger, config *deployer.Config) error {
path, err := paths.UserConfigPath()
if err != nil {
return err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("reading user config %q: %w", path, err)
}
var userDefaults deployer.Config
if err := yaml.Unmarshal(data, &userDefaults); err != nil {
return fmt.Errorf("parsing user config %q: %w", path, err)
}
if err := mergo.Merge(config, &userDefaults, mergo.WithOverride, mergo.WithoutDereference); err != nil {
return fmt.Errorf("merging user config %q: %w", path, err)
}
log.Dimf("Applied user config from %s", path)
return nil
}

var rootCmd = &cobra.Command{
Use: "roxie",
Short: "roxie - Advanced Cluster Security Deployment Tool",
Expand All @@ -39,8 +74,10 @@ Red Hat Advanced Cluster Security (ACS) on any Kubernetes/OpenShift cluster.`,
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output (show CRs)")
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Do not actually modify cluster")
rootCmd.AddCommand(newDeployCmd(&deploySettings))
rootCmd.AddCommand(newTeardownCmd(&deploySettings))
rootCmd.PersistentFlags().BoolVar(&skipUserConfig, "skip-user-config", false,
fmt.Sprintf("Skips reading of user's configuration (%s)", paths.UserConfigPathString()))
rootCmd.AddCommand(newDeployCmd(&deploySettingsFromArgs))
rootCmd.AddCommand(newTeardownCmd(&deploySettingsFromArgs))
rootCmd.AddCommand(newShellCmd())
rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newEnvCmd())
Expand Down
Loading
Loading