From 9461bd2697b0d81997c12c853843a0c25e9b6244 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:31:26 +0200 Subject: [PATCH 01/13] New module for retrieving config and cache directory --- internal/paths/paths.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 internal/paths/paths.go diff --git a/internal/paths/paths.go b/internal/paths/paths.go new file mode 100644 index 0000000..5dda1fb --- /dev/null +++ b/internal/paths/paths.go @@ -0,0 +1,36 @@ +package paths + +import ( + "fmt" + "os" + "path/filepath" +) + +const appName = "roxie" + +func UserConfigPath() (string, error) { + dir, err := configDir() + if err != nil { + return "", fmt.Errorf("retrieving user config path: %w", err) + } + return filepath.Join(dir, "config.yaml"), nil +} + +func configDir() (string, error) { + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, appName), nil +} + +// CacheDir returns the cache directory to be used by roxie. +// This directory might not yet exist, it is the responsibility of the caller +// to make sure this directory exists before writing to it. +func CacheDir() (string, error) { + dir, err := os.UserCacheDir() + if err != nil { + return "", fmt.Errorf("retrieving user cache path: %w", err) + } + return filepath.Join(dir, appName), nil +} From 5db93ca1361fe8edb640c41d9364342f154121c1 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:32:12 +0200 Subject: [PATCH 02/13] Use new cache directory in imagecache --- internal/imagecache/imagecache.go | 12 ++++++------ internal/imagecache/imagecache_test.go | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index 0fa6ee9..c8e6d17 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -10,6 +10,7 @@ import ( "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/ocihelper" + "github.com/stackrox/roxie/internal/paths" ) // ImageCache manages cache of verified pullable Docker images @@ -27,14 +28,13 @@ type CacheData struct { } // New creates a new ImageCache instance -func New(log *logger.Logger, cacheFile string, maxEntries int) *ImageCache { +func New(log *logger.Logger, cacheFile string, maxEntries int) (*ImageCache, error) { if cacheFile == "" { - home, err := os.UserHomeDir() + cacheDir, err := paths.CacheDir() if err != nil { - home = "." + return nil, err } - // TODO(#91): how about using something XDG-compliant like ~/.cache/roxie/images? - cacheFile = filepath.Join(home, ".roxie.image_cache") + cacheFile = filepath.Join(cacheDir, "image_cache") } if maxEntries <= 0 { @@ -48,7 +48,7 @@ func New(log *logger.Logger, cacheFile string, maxEntries int) *ImageCache { } ic.cache = ic.loadCache() - return ic + return ic, nil } // loadCache loads image cache from file diff --git a/internal/imagecache/imagecache_test.go b/internal/imagecache/imagecache_test.go index c3ca3de..25aeebc 100644 --- a/internal/imagecache/imagecache_test.go +++ b/internal/imagecache/imagecache_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stackrox/roxie/internal/logger" + "github.com/stretchr/testify/require" ) func TestImageCacheLoadSaveRoundtrip(t *testing.T) { @@ -14,7 +15,8 @@ func TestImageCacheLoadSaveRoundtrip(t *testing.T) { cachePath := filepath.Join(tmpDir, ".roxie.image_cache") log := logger.New() - c := New(log, cachePath, 20) + c, err := New(log, cachePath, 20) + require.NoError(t, err, "creating ImageCache failed") if len(c.cache) != 0 { t.Errorf("Expected empty cache, got %d entries", len(c.cache)) @@ -28,7 +30,8 @@ func TestImageCacheLoadSaveRoundtrip(t *testing.T) { } // Reopen cache and verify persistence - c2 := New(log, cachePath, 20) + c2, err := New(log, cachePath, 20) + require.NoError(t, err, "creating ImageCache failed") if !c2.IsCached("quay.io/example/app:1") { t.Error("Image should be cached after reopening") } @@ -50,7 +53,8 @@ func TestImageCacheHandlesOldFormat(t *testing.T) { } log := logger.New() - c := New(log, cachePath, 20) + c, err := New(log, cachePath, 20) + require.NoError(t, err, "creating ImageCache failed") if !c.IsCached("a") { t.Error("Should load 'a' from old format") @@ -66,7 +70,8 @@ func TestImageCacheMaxEntries(t *testing.T) { log := logger.New() maxEntries := 5 - c := New(log, cachePath, maxEntries) + c, err := New(log, cachePath, maxEntries) + require.NoError(t, err, "creating ImageCache failed") // Add more than maxEntries for i := 0; i < 10; i++ { @@ -88,7 +93,8 @@ func TestImageCacheMoveToEnd(t *testing.T) { cachePath := filepath.Join(tmpDir, ".roxie.image_cache") log := logger.New() - c := New(log, cachePath, 5) + c, err := New(log, cachePath, 5) + require.NoError(t, err, "creating ImageCache failed") c.AddToCache("image1") c.AddToCache("image2") From 0a8c428d5001f2e14038bb0415e7ad4140dda8e3 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 18:43:54 +0200 Subject: [PATCH 03/13] New global logger --- cmd/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/main.go b/cmd/main.go index 81b92f7..da0cec7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,6 +6,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/deployer" + "github.com/stackrox/roxie/internal/logger" ) var ( @@ -15,6 +16,8 @@ var ( envrc string dryRun bool + globalLogger = logger.New() + // We need this set up before command line flags are parsed. deploySettings = deployer.NewConfig() ) From 1c36d9e3612215c47cd386aa57bc2cfcdbc6e1a6 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:33:43 +0200 Subject: [PATCH 04/13] Use new global logger --- cmd/deploy.go | 2 +- cmd/env.go | 3 +-- cmd/teardown.go | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 3a1d2b6..b394269 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -246,7 +246,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 diff --git a/cmd/env.go b/cmd/env.go index 0e59264..129356b 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -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 { @@ -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 } diff --git a/cmd/teardown.go b/cmd/teardown.go index 2b8a834..94b7906 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -9,7 +9,6 @@ import ( "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/deployer" "github.com/stackrox/roxie/internal/env" - "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/manifest" ) @@ -39,7 +38,7 @@ func newTeardownCmd(settings *deployer.Config) *cobra.Command { } func runTeardown(cmd *cobra.Command, args []string) error { - log := logger.New() + log := globalLogger if err := env.Initialize(log); err != nil { return err } From 0cd893fae2ac439330da4c308f791c0c9c3a778e Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:35:08 +0200 Subject: [PATCH 05/13] internal/deployer/deployer.go imagecache --- internal/deployer/deployer.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index ee4c553..38ed448 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -239,7 +239,10 @@ func New(log *logger.Logger) (*Deployer, error) { } d.dockerAuth = dockerauth.New(log) - d.imageCache = imagecache.New(log, "", 20) + d.imageCache, err = imagecache.New(log, "", 20) + if err != nil { + return nil, err + } d.portForward = portforward.New(k8s.GetKubectl(), log) if password := os.Getenv("ROX_ADMIN_PASSWORD"); password != "" { From 0b0e7788079ceecb9a364f1b41a12a81f2429b93 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 18:45:18 +0200 Subject: [PATCH 06/13] Introduce global variable for saving settings from arg parsing. Introduce function for applying user defaults. --- cmd/main.go | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index da0cec7..afaef11 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,12 +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 ( @@ -19,17 +23,46 @@ var ( 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(). +// Note: the user config should only contain reasonable fields, which +// are not already handled by roxies smart defaulting like cluster-dependent +// resource profiles. +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", @@ -42,8 +75,8 @@ 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.AddCommand(newDeployCmd(&deploySettingsFromArgs)) + rootCmd.AddCommand(newTeardownCmd(&deploySettingsFromArgs)) rootCmd.AddCommand(newShellCmd()) rootCmd.AddCommand(newVersionCmd()) rootCmd.AddCommand(newEnvCmd()) From e6ac8b616c335cc92df8689cdaea08be2488cd3b Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:38:21 +0200 Subject: [PATCH 07/13] var -> const --- cmd/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index b394269..d2d12a4 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -28,7 +28,7 @@ import ( "k8s.io/utils/ptr" ) -var ( +const ( sharedNamespace = "stackrox" ) From d6ca3f1f605a869441d48480cec4a8dffe91f3ad Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Thu, 4 Jun 2026 17:38:33 +0200 Subject: [PATCH 08/13] New tests --- cmd/deploy_test.go | 130 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 945d3a4..026f5be 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -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) { @@ -94,7 +98,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { name: "early-readiness", args: []string{"--early-readiness"}, assert: func(t *testing.T, cfg deployer.Config) { - assert.True(t, cfg.Central.EarlyReadiness, "Central.EarlyReadiness mismatch") + assert.True(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch") assert.True(t, cfg.SecuredCluster.EarlyReadiness, "SecuredCluster.EarlyReadiness mismatch") }, }, @@ -102,7 +106,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { name: "disable early-readiness", args: []string{"--early-readiness=false"}, assert: func(t *testing.T, cfg deployer.Config) { - assert.False(t, cfg.Central.EarlyReadiness, "Central.EarlyReadiness mismatch") + assert.False(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch") assert.False(t, cfg.SecuredCluster.EarlyReadiness, "SecuredCluster.EarlyReadiness mismatch") }, }, @@ -110,7 +114,7 @@ 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.Central.PauseReconciliationEnabled(), "Central.PauseReconciliation mismatch") assert.True(t, cfg.SecuredCluster.PauseReconciliation, "SecuredCluster.PauseReconciliation mismatch") }, }, @@ -118,14 +122,14 @@ func TestNewDeployCmd_Flags(t *testing.T) { 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") }, }, { @@ -135,7 +139,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { assert.Equal(t, "4.7.0", cfg.Roxie.Version, "Roxie.Version mismatch") require.NotNil(t, cfg.Central.Exposure, "Central.Exposure should be set") assert.Equal(t, types.ExposureLoadBalancer, *cfg.Central.Exposure, "Central.Exposure mismatch") - assert.True(t, cfg.Central.EarlyReadiness, "Central.EarlyReadiness mismatch") + assert.True(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch") assert.Equal(t, types.ResourceProfileSmall, cfg.Central.ResourceProfile, "Central.ResourceProfile mismatch") }, }, @@ -205,3 +209,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)) + }) +} From 37d63436237e73a943cbd4fb8f10e9881daf03af Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 8 Jun 2026 07:29:57 +0200 Subject: [PATCH 09/13] Use bool ptrs for more reliable merging of user configs. Otherwise overwriting of bool settings with a user-config would only work in one direction: from false to true. By using pointers we can reliably detect "is this field set or not" and overwrite. Adjusted call sites for new bool ptrs. --- cmd/deploy.go | 29 ++++----- cmd/deploy_test.go | 6 +- internal/deployer/config.go | 82 ++++++++++++++++++++---- internal/deployer/deploy_via_operator.go | 12 ++-- internal/deployer/deployer.go | 4 +- internal/deployer/operator.go | 6 +- 6 files changed, 97 insertions(+), 42 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index d2d12a4..6485539 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -25,7 +25,6 @@ import ( "github.com/stackrox/roxie/internal/stackroxversions" "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/ptr" ) const ( @@ -55,7 +54,7 @@ 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 }), ) @@ -63,7 +62,7 @@ Examples: 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 }), ) @@ -71,7 +70,7 @@ Examples: 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 }), ) @@ -79,7 +78,7 @@ Examples: 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 }), ) @@ -87,8 +86,8 @@ Examples: 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 }), ) @@ -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 }), ) @@ -228,8 +227,8 @@ Examples: registerFlag(cmd, settings, "early-readiness", "Only wait for essential workloads (central/sensor) to be ready", withNoOptDefVal("true"), withApplyFnBool(func(config *deployer.Config, val bool) error { - config.Central.EarlyReadiness = val - config.SecuredCluster.EarlyReadiness = val + config.Central.EarlyReadiness = new(val) + config.SecuredCluster.EarlyReadiness = new(val) return nil }), ) @@ -282,7 +281,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { return err } - if !deploySettings.Central.EarlyReadiness || !deploySettings.SecuredCluster.EarlyReadiness { + if !deploySettings.Central.EarlyReadinessEnabled() || !deploySettings.SecuredCluster.EarlyReadinessEnabled() { // Explanation on the versions involved here: // Deploying StackRox begins with picking a "main image tag" -- this is a version identifier, which cannot be reliably parsed as a semver. // But there is a derived version from that -- the operator version -- which can be parsed as a semver. @@ -411,7 +410,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 @@ -448,12 +447,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() { diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 026f5be..20f6aed 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -99,7 +99,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { args: []string{"--early-readiness"}, assert: func(t *testing.T, cfg deployer.Config) { assert.True(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch") - assert.True(t, cfg.SecuredCluster.EarlyReadiness, "SecuredCluster.EarlyReadiness mismatch") + assert.True(t, cfg.SecuredCluster.EarlyReadinessEnabled(), "SecuredCluster.EarlyReadiness mismatch") }, }, { @@ -107,7 +107,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { args: []string{"--early-readiness=false"}, assert: func(t *testing.T, cfg deployer.Config) { assert.False(t, cfg.Central.EarlyReadinessEnabled(), "Central.EarlyReadiness mismatch") - assert.False(t, cfg.SecuredCluster.EarlyReadiness, "SecuredCluster.EarlyReadiness mismatch") + assert.False(t, cfg.SecuredCluster.EarlyReadinessEnabled(), "SecuredCluster.EarlyReadiness mismatch") }, }, { @@ -115,7 +115,7 @@ func TestNewDeployCmd_Flags(t *testing.T) { args: []string{"--pause-reconciliation"}, assert: func(t *testing.T, cfg deployer.Config) { assert.True(t, cfg.Central.PauseReconciliationEnabled(), "Central.PauseReconciliation mismatch") - assert.True(t, cfg.SecuredCluster.PauseReconciliation, "SecuredCluster.PauseReconciliation mismatch") + assert.True(t, cfg.SecuredCluster.PauseReconciliationEnabled(), "SecuredCluster.PauseReconciliation mismatch") }, }, { diff --git a/internal/deployer/config.go b/internal/deployer/config.go index 358f462..2751f9d 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -45,11 +45,19 @@ func (c *Config) DeepCopy() (*Config, error) { // RoxieConfig holds roxie-level settings such as version and feature flags. type RoxieConfig struct { Version string `yaml:"version,omitempty"` - KonfluxImages bool `yaml:"konfluxImages,omitempty"` + KonfluxImages *bool `yaml:"konfluxImages,omitempty"` FeatureFlags map[string]bool `yaml:"featureFlags,omitempty"` ClusterType types.ClusterType `yaml:"clusterType,omitempty"` } +func (c *RoxieConfig) KonfluxImagesSet() bool { + return c.KonfluxImages != nil +} + +func (c *RoxieConfig) KonfluxImagesEnabled() bool { + return c.KonfluxImages != nil && *c.KonfluxImages +} + // NewRoxieConfig returns a RoxieConfig with initialized defaults. func NewRoxieConfig() RoxieConfig { return RoxieConfig{ @@ -59,11 +67,27 @@ func NewRoxieConfig() RoxieConfig { // OperatorConfig controls how the ACS operator is deployed. type OperatorConfig struct { - SkipDeployment bool `yaml:"skipDeployment,omitempty"` - DeployViaOlm bool `yaml:"deployViaOlm,omitempty"` + SkipDeployment *bool `yaml:"skipDeployment,omitempty"` + DeployViaOlm *bool `yaml:"deployViaOlm,omitempty"` Version string `yaml:"version,omitempty"` } +func (c *OperatorConfig) SkipDeploymentSet() bool { + return c.SkipDeployment != nil +} + +func (c *OperatorConfig) SkipDeploymentEnabled() bool { + return c.SkipDeployment != nil && *c.SkipDeployment +} + +func (c *OperatorConfig) DeployViaOlmSet() bool { + return c.DeployViaOlm != nil +} + +func (c *OperatorConfig) DeployViaOlmEnabled() bool { + return c.DeployViaOlm != nil && *c.DeployViaOlm +} + // Configure derives the operator version from the roxie configuration. func (c *OperatorConfig) Configure(roxieConfig *RoxieConfig) error { c.Version = helpers.ConvertMainTagToOperatorTag(roxieConfig.Version) @@ -82,11 +106,11 @@ type WaitConfig struct { type CentralConfig struct { Namespace string `yaml:"namespace,omitempty"` ResourceProfile types.ResourceProfile `yaml:"resourceProfile,omitempty"` - PauseReconciliation bool `yaml:"pauseReconciliation,omitempty"` + PauseReconciliation *bool `yaml:"pauseReconciliation,omitempty"` Exposure *types.Exposure `yaml:"exposure,omitempty"` DeployTimeout time.Duration `yaml:"deployTimeout,omitempty"` PortForwarding *bool `yaml:"portForwarding,omitempty"` - EarlyReadiness bool `yaml:"earlyReadiness,omitempty"` + EarlyReadiness *bool `yaml:"earlyReadiness,omitempty"` Spec map[string]interface{} `yaml:"spec,omitempty"` } @@ -95,7 +119,7 @@ func DefaultCentralConfig() CentralConfig { return CentralConfig{ DeployTimeout: DefaultCentralWaitTimeout, Namespace: "acs-central", - EarlyReadiness: true, + EarlyReadiness: new(true), Spec: map[string]interface{}{ "central": map[string]interface{}{ "telemetry": map[string]interface{}{ @@ -112,17 +136,33 @@ func (c *CentralConfig) GetWaitConfig() WaitConfig { // With earlyReadiness we just wait for the Available condition of that component's core // Deployment to be True. waitFor := "central/" + centralCrName - if c.EarlyReadiness { + if c.EarlyReadinessEnabled() { waitFor = "deployment/central" } return WaitConfig{ Namespace: c.Namespace, - EarlyReadiness: c.EarlyReadiness, + EarlyReadiness: c.EarlyReadinessEnabled(), WaitFor: waitFor, Timeout: c.DeployTimeout, } } +func (c *CentralConfig) PauseReconciliationSet() bool { + return c.PauseReconciliation != nil +} + +func (c *CentralConfig) PauseReconciliationEnabled() bool { + return c.PauseReconciliation != nil && *c.PauseReconciliation +} + +func (c *CentralConfig) EarlyReadinessSet() bool { + return c.EarlyReadiness != nil +} + +func (c *CentralConfig) EarlyReadinessEnabled() bool { + return c.EarlyReadiness != nil && *c.EarlyReadiness +} + func (c *CentralConfig) PortForwardingSet() bool { return c.PortForwarding != nil } @@ -202,9 +242,9 @@ func (c *CentralConfig) CustomResource() (map[string]interface{}, error) { type SecuredClusterConfig struct { Namespace string `yaml:"namespace,omitempty"` ResourceProfile types.ResourceProfile `yaml:"resourceProfile,omitempty"` - PauseReconciliation bool `yaml:"pauseReconciliation,omitempty"` + PauseReconciliation *bool `yaml:"pauseReconciliation,omitempty"` DeployTimeout time.Duration `yaml:"deployTimeout,omitempty"` - EarlyReadiness bool `yaml:"earlyReadiness,omitempty"` + EarlyReadiness *bool `yaml:"earlyReadiness,omitempty"` Spec map[string]interface{} `yaml:"spec,omitempty"` } @@ -213,19 +253,35 @@ func DefaultSecuredClusterConfig() SecuredClusterConfig { return SecuredClusterConfig{ DeployTimeout: DefaultSecuredClusterWaitTimeout, Namespace: "acs-sensor", - EarlyReadiness: true, + EarlyReadiness: new(true), Spec: make(map[string]interface{}), } } +func (s *SecuredClusterConfig) PauseReconciliationSet() bool { + return s.PauseReconciliation != nil +} + +func (s *SecuredClusterConfig) PauseReconciliationEnabled() bool { + return s.PauseReconciliation != nil && *s.PauseReconciliation +} + +func (s *SecuredClusterConfig) EarlyReadinessSet() bool { + return s.EarlyReadiness != nil +} + +func (s *SecuredClusterConfig) EarlyReadinessEnabled() bool { + return s.EarlyReadiness != nil && *s.EarlyReadiness +} + func (s *SecuredClusterConfig) GetWaitConfig() WaitConfig { waitFor := "securedcluster/" + securedClusterCrName - if s.EarlyReadiness { + if s.EarlyReadinessEnabled() { waitFor = "deployment/sensor" } return WaitConfig{ Namespace: s.Namespace, - EarlyReadiness: s.EarlyReadiness, + EarlyReadiness: s.EarlyReadinessEnabled(), WaitFor: waitFor, Timeout: s.DeployTimeout, } diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index 44b0a47..2820d11 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -46,7 +46,7 @@ func (d *Deployer) deployOperatorOnly(ctx context.Context) error { // ensureOperatorDeployed ensures the operator is deployed with the correct version and mode func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { // Skip operator deployment/checks if flag is set to false - if d.config.Operator.SkipDeployment { + if d.config.Operator.SkipDeploymentEnabled() { d.logger.Info("â„šī¸ Skipping operator deployment checks (--deploy-operator=false)") d.logger.Info(" Assuming operator is already running...") return nil @@ -66,12 +66,12 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { if !operatorExists { needsDeployment = true - } else if d.config.Operator.DeployViaOlm && currentMode == OperatorModeNonOLM { + } else if d.config.Operator.DeployViaOlmEnabled() && currentMode == OperatorModeNonOLM { // Switching from non-OLM to OLM d.logger.Info("🔄 Switching operator from non-OLM to OLM mode...") needsTeardown = true needsDeployment = true - } else if !d.config.Operator.DeployViaOlm && currentMode == OperatorModeOLM { + } else if !d.config.Operator.DeployViaOlmEnabled() && currentMode == OperatorModeOLM { // Switching from OLM to non-OLM d.logger.Info("🔄 Switching operator from OLM to non-OLM mode...") needsTeardown = true @@ -101,7 +101,7 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { } if needsDeployment { - if d.config.Operator.DeployViaOlm { + if d.config.Operator.DeployViaOlmEnabled() { if err := d.deployOperatorViaOLM(ctx); err != nil { return fmt.Errorf("failed to deploy operator via OLM: %w", err) } @@ -141,7 +141,7 @@ func (d *Deployer) deployCentralOperator(ctx context.Context) error { return fmt.Errorf("failed waiting for Central: %w", err) } - if d.config.Central.PauseReconciliation { + if d.config.Central.PauseReconciliationEnabled() { d.logger.Infof("Adding pause-reconcile annotation to Central") err := d.addPauseReconcileAnnotation(ctx, "Central", centralCrName, d.config.Central.Namespace) if err != nil { @@ -691,7 +691,7 @@ func (d *Deployer) deploySecuredClusterOperator(ctx context.Context) error { return fmt.Errorf("failed waiting for SecuredCluster: %w", err) } - if d.config.SecuredCluster.PauseReconciliation { + if d.config.SecuredCluster.PauseReconciliationEnabled() { d.logger.Infof("Adding pause-reconcile annotation to SecuredCluster") err := d.addPauseReconcileAnnotation(ctx, "SecuredCluster", securedClusterCrName, d.config.SecuredCluster.Namespace) if err != nil { diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 38ed448..9e8c231 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -727,7 +727,7 @@ func (d *Deployer) writeEnvrcFile(ctx context.Context) error { func (d *Deployer) PrintCentralDeploymentSummary() { component := "Central" mainImageTag := d.config.Roxie.Version - olm := d.config.Operator.DeployViaOlm + olm := d.config.Operator.DeployViaOlmEnabled() exposure := d.config.Central.GetExposure() portForwarding := d.config.Central.PortForwardingEnabled() log := d.logger @@ -908,7 +908,7 @@ func (d *Deployer) checkPodProgressInNamespace(ctx context.Context, namespace st func (d *Deployer) PrintSecuredClusterDeploymentSummary() { component := "Secured Cluster" mainImageTag := d.config.Roxie.Version - olm := d.config.Operator.DeployViaOlm + olm := d.config.Operator.DeployViaOlmEnabled() log := d.logger kubeContext := d.kubeContext diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index c10b4c3..f271a4a 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -35,7 +35,7 @@ var requiredCRDs = []string{ // deployOperatorNonOLM deploys the RHACS operator without OLM func (d *Deployer) deployOperatorNonOLM(ctx context.Context) error { d.logger.Infof("Operator tag: %s", d.config.Operator.Version) - if d.config.Roxie.KonfluxImages { + if d.config.Roxie.KonfluxImagesEnabled() { if err := d.ensureKonfluxImageRewriting(ctx); err != nil { return fmt.Errorf("failed to configure Konflux image rewriting: %w", err) } @@ -191,7 +191,7 @@ func (d *Deployer) ensureCRDsInstalled(ctx context.Context) error { } func (d *Deployer) getOperatorBundleImage() string { - if d.config.Roxie.KonfluxImages { + if d.config.Roxie.KonfluxImagesEnabled() { d.logger.Infof("Using Konflux-built operator bundle image") return fmt.Sprintf(operatorBundleImageReleaseRepo+":v%s", d.config.Operator.Version) } @@ -318,7 +318,7 @@ func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) } serviceAccountName := deploymentSpec["service_account"].(string) - d.useOperatorPullSecrets = d.config.Roxie.KonfluxImages && d.config.Roxie.ClusterType.NeedsPullSecrets() + d.useOperatorPullSecrets = d.config.Roxie.KonfluxImagesEnabled() && d.config.Roxie.ClusterType.NeedsPullSecrets() d.logger.Info("📋 Operator deployment plan:") d.logger.Dim(fmt.Sprintf(" â€ĸ Namespace: %s", operatorNamespace)) From e7211bcfece42ab83a338b0f2cb18b10a1595ebe Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 8 Jun 2026 14:34:19 +0200 Subject: [PATCH 10/13] Support for creating empty configs, without any default values. We use this for temporarily saving just the result from the translation of command line args to YAML patches. --- internal/deployer/config.go | 56 +++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/internal/deployer/config.go b/internal/deployer/config.go index 2751f9d..913300b 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -18,8 +18,8 @@ type Config struct { SecuredCluster SecuredClusterConfig `yaml:"securedCluster,omitempty"` } -// NewConfig returns a Config populated with default values. -func NewConfig() Config { +// DefaultConfig returns a Config populated with default values. +func DefaultConfig() Config { return Config{ Roxie: NewRoxieConfig(), Central: DefaultCentralConfig(), @@ -27,6 +27,15 @@ func NewConfig() Config { } } +// NewConfig returns a Config populated with default values. +func NewConfig() Config { + return Config{ + Roxie: NewRoxieConfig(), + Central: NewCentralConfig(), + SecuredCluster: NewSecuredClusterConfig(), + } +} + // DeepCopy creates a deep-copy of the provided config using a YAML marshaling/unmarshaling roundtrip. // Due the `omitempty`, this causes empty values (e.g. empty maps) from being discarded (replace with nil // in the resulting copy). @@ -114,20 +123,25 @@ type CentralConfig struct { Spec map[string]interface{} `yaml:"spec,omitempty"` } +// NewCentralConfig returns an emptry CentralConfig, with deep initialization of data structures. +func NewCentralConfig() CentralConfig { + return CentralConfig{ + Spec: make(map[string]interface{}), + } +} + // DefaultCentralConfig returns a CentralConfig with sensible defaults. func DefaultCentralConfig() CentralConfig { - return CentralConfig{ - DeployTimeout: DefaultCentralWaitTimeout, - Namespace: "acs-central", - EarlyReadiness: new(true), - Spec: map[string]interface{}{ - "central": map[string]interface{}{ - "telemetry": map[string]interface{}{ - "enabled": false, - }, - }, + cfg := NewCentralConfig() + cfg.DeployTimeout = DefaultCentralWaitTimeout + cfg.Namespace = "acs-central" + cfg.EarlyReadiness = new(true) + cfg.Spec["central"] = map[string]interface{}{ + "telemetry": map[string]interface{}{ + "enabled": false, }, } + return cfg } func (c *CentralConfig) GetWaitConfig() WaitConfig { @@ -248,16 +262,22 @@ type SecuredClusterConfig struct { Spec map[string]interface{} `yaml:"spec,omitempty"` } -// DefaultSecuredClusterConfig returns a SecuredClusterConfig with sensible defaults. -func DefaultSecuredClusterConfig() SecuredClusterConfig { +// NewSecuredClusterConfig returns an emptry SecuredClusterConfig, with deep initialization of data structures. +func NewSecuredClusterConfig() SecuredClusterConfig { return SecuredClusterConfig{ - DeployTimeout: DefaultSecuredClusterWaitTimeout, - Namespace: "acs-sensor", - EarlyReadiness: new(true), - Spec: make(map[string]interface{}), + Spec: make(map[string]interface{}), } } +// DefaultSecuredClusterConfig returns a SecuredClusterConfig with sensible defaults. +func DefaultSecuredClusterConfig() SecuredClusterConfig { + cfg := NewSecuredClusterConfig() + cfg.DeployTimeout = DefaultSecuredClusterWaitTimeout + cfg.Namespace = "acs-sensor" + cfg.EarlyReadiness = new(true) + return cfg +} + func (s *SecuredClusterConfig) PauseReconciliationSet() bool { return s.PauseReconciliation != nil } From baf3f6c84ab3fbfc50996a4023e9cdf47cc721cd Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 8 Jun 2026 09:25:45 +0200 Subject: [PATCH 11/13] Move imageCache.New to not leak tempDir --- internal/deployer/deployer.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 9e8c231..f86c95a 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -227,22 +227,24 @@ func New(log *logger.Logger) (*Deployer, error) { return nil, err } + imageCache, err := imagecache.New(log, "", 20) + if err != nil { + return nil, err + } + tempDir, err := os.MkdirTemp("", "roxie-deployer-*") if err != nil { return nil, fmt.Errorf("failed to create temporary directory: %w", err) } d := &Deployer{ - logger: log, - startTime: time.Now(), - tempDir: tempDir, + logger: log, + startTime: time.Now(), + tempDir: tempDir, + imageCache: imageCache, } d.dockerAuth = dockerauth.New(log) - d.imageCache, err = imagecache.New(log, "", 20) - if err != nil { - return nil, err - } d.portForward = portforward.New(k8s.GetKubectl(), log) if password := os.Getenv("ROX_ADMIN_PASSWORD"); password != "" { From f77598ee1cc622b2464f5ced444dfadc540534f3 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 8 Jun 2026 14:33:31 +0200 Subject: [PATCH 12/13] Apply user defaults for deploy/teardown commands. --- cmd/deploy.go | 13 +++++++++++++ cmd/teardown.go | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cmd/deploy.go b/cmd/deploy.go index 6485539..ad5e74f 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -263,6 +263,19 @@ 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 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 { diff --git a/cmd/teardown.go b/cmd/teardown.go index 94b7906..919ccef 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "dario.cat/mergo" "github.com/spf13/cobra" "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/deployer" @@ -55,6 +56,19 @@ func runTeardown(cmd *cobra.Command, args []string) error { return nil } + // Start with default configuration. + deploySettings := deployer.DefaultConfig() + + // Apply user config on top (overriding defaults). + 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) + } + d, err := deployer.New(log) if err != nil { return fmt.Errorf("failed to create deployer: %w", err) From f13512e63837f26eb3b602e60a70d8fbe586a913 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Mon, 8 Jun 2026 15:30:09 +0200 Subject: [PATCH 13/13] Fix mergo pointers --- cmd/deploy.go | 2 +- cmd/teardown.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index ad5e74f..0497920 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -272,7 +272,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { } // Apply changes from arg parsing. - if err := mergo.Merge(deploySettings, deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil { + if err := mergo.Merge(&deploySettings, &deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil { return fmt.Errorf("applying config patches from command line argument: %w", err) } diff --git a/cmd/teardown.go b/cmd/teardown.go index 919ccef..809868f 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -65,7 +65,7 @@ func runTeardown(cmd *cobra.Command, args []string) error { } // Apply changes from arg parsing. - if err := mergo.Merge(deploySettings, deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil { + if err := mergo.Merge(&deploySettings, &deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil { return fmt.Errorf("applying config patches from command line argument: %w", err) }