Skip to content

Commit b91d393

Browse files
mclasmeierMoritz Clasmeier
andauthored
Support user config file (#203)
Co-authored-by: Moritz Clasmeier <mclasmeier@redhat.com>
1 parent abc4071 commit b91d393

16 files changed

Lines changed: 394 additions & 90 deletions

cmd/deploy.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ import (
2525
"github.com/stackrox/roxie/internal/stackroxversions"
2626
"gopkg.in/yaml.v3"
2727
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28-
"k8s.io/utils/ptr"
2928
)
3029

31-
var (
30+
const (
3231
sharedNamespace = "stackrox"
3332
)
3433

@@ -55,40 +54,40 @@ Examples:
5554
registerFlag(cmd, settings, "olm", "Deploy operator via OLM (requires OLM installed)",
5655
withNoOptDefVal("true"),
5756
withApplyFnBool(func(config *deployer.Config, val bool) error {
58-
config.Operator.DeployViaOlm = val
57+
config.Operator.DeployViaOlm = new(val)
5958
return nil
6059
}),
6160
)
6261

6362
registerFlag(cmd, settings, "konflux", "Use Konflux images",
6463
withNoOptDefVal("true"),
6564
withApplyFnBool(func(config *deployer.Config, val bool) error {
66-
config.Roxie.KonfluxImages = val
65+
config.Roxie.KonfluxImages = new(val)
6766
return nil
6867
}),
6968
)
7069

7170
registerFlag(cmd, settings, "deploy-operator", "Whether to deploy and manage the operator",
7271
withNoOptDefVal("true"),
7372
withApplyFnBool(func(config *deployer.Config, val bool) error {
74-
config.Operator.SkipDeployment = !val
73+
config.Operator.SkipDeployment = new(!val)
7574
return nil
7675
}),
7776
)
7877

7978
registerFlag(cmd, settings, "port-forwarding", "Enable localhost port-forward for Central",
8079
withNoOptDefVal("true"),
8180
withApplyFnBool(func(config *deployer.Config, val bool) error {
82-
config.Central.PortForwarding = ptr.To(val)
81+
config.Central.PortForwarding = new(val)
8382
return nil
8483
}),
8584
)
8685

8786
registerFlag(cmd, settings, "pause-reconciliation", "Pause reconciliation after deployment",
8887
withNoOptDefVal("true"),
8988
withApplyFnBool(func(config *deployer.Config, val bool) error {
90-
config.Central.PauseReconciliation = val
91-
config.SecuredCluster.PauseReconciliation = val
89+
config.Central.PauseReconciliation = new(val)
90+
config.SecuredCluster.PauseReconciliation = new(val)
9291
return nil
9392
}),
9493
)
@@ -120,7 +119,7 @@ Examples:
120119
if err := yaml.Unmarshal([]byte(val), &exposure); err != nil {
121120
return err
122121
}
123-
config.Central.Exposure = ptr.To(exposure)
122+
config.Central.Exposure = new(exposure)
124123
return nil
125124
}),
126125
)
@@ -246,7 +245,7 @@ Examples:
246245
}
247246

248247
func runDeploy(cmd *cobra.Command, args []string) error {
249-
log := logger.New()
248+
log := globalLogger
250249
if !dryRun {
251250
if err := env.Initialize(log); err != nil {
252251
return err
@@ -264,6 +263,21 @@ func runDeploy(cmd *cobra.Command, args []string) error {
264263
return err
265264
}
266265

266+
// Start with default configuration.
267+
deploySettings := deployer.DefaultConfig()
268+
269+
// Apply user config on top (overriding defaults).
270+
if !skipUserConfig {
271+
if err := tryApplyUserDefaults(globalLogger, &deploySettings); err != nil {
272+
return fmt.Errorf("applying user config: %w", err)
273+
}
274+
}
275+
276+
// Apply changes from arg parsing.
277+
if err := mergo.Merge(&deploySettings, &deploySettingsFromArgs, mergo.WithOverride, mergo.WithoutDereference); err != nil {
278+
return fmt.Errorf("applying config patches from command line argument: %w", err)
279+
}
280+
267281
if deploySettings.Roxie.Version != "" {
268282
log.Dimf("Using main image tag %s", deploySettings.Roxie.Version)
269283
} else {
@@ -411,7 +425,7 @@ func configureConfig(log *logger.Logger, components component.Component, deployS
411425

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

417431
return nil
@@ -448,12 +462,12 @@ func deployValidate(components component.Component, deploySettings *deployer.Con
448462
}
449463
}
450464

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

455-
if deploySettings.Roxie.KonfluxImages {
456-
if deploySettings.Operator.DeployViaOlm {
469+
if deploySettings.Roxie.KonfluxImagesEnabled() {
470+
if deploySettings.Operator.DeployViaOlmEnabled() {
457471
return errors.New("using Konflux images while deploying operator via OLM is not supported")
458472
}
459473
if !clusterType.IsOpenShift() {

cmd/deploy_test.go

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import (
77
"testing"
88
"time"
99

10+
"dario.cat/mergo"
1011
"github.com/stackrox/roxie/internal/deployer"
12+
"github.com/stackrox/roxie/internal/logger"
13+
"github.com/stackrox/roxie/internal/paths"
1114
"github.com/stackrox/roxie/internal/types"
1215
"github.com/stretchr/testify/assert"
1316
"github.com/stretchr/testify/require"
17+
"gopkg.in/yaml.v3"
1418
)
1519

1620
func TestNewDeployCmd_Flags(t *testing.T) {
@@ -110,22 +114,22 @@ func TestNewDeployCmd_Flags(t *testing.T) {
110114
name: "pause-reconciliation",
111115
args: []string{"--pause-reconciliation"},
112116
assert: func(t *testing.T, cfg deployer.Config) {
113-
assert.True(t, cfg.Central.PauseReconciliation, "Central.PauseReconciliation mismatch")
114-
assert.True(t, cfg.SecuredCluster.PauseReconciliation, "SecuredCluster.PauseReconciliation mismatch")
117+
assert.True(t, cfg.Central.PauseReconciliationEnabled(), "Central.PauseReconciliation mismatch")
118+
assert.True(t, cfg.SecuredCluster.PauseReconciliationEnabled(), "SecuredCluster.PauseReconciliation mismatch")
115119
},
116120
},
117121
{
118122
name: "olm",
119123
args: []string{"--olm"},
120124
assert: func(t *testing.T, cfg deployer.Config) {
121-
assert.True(t, cfg.Operator.DeployViaOlm, "Operator.DeployViaOlm mismatch")
125+
assert.True(t, cfg.Operator.DeployViaOlmEnabled(), "Operator.DeployViaOlm mismatch")
122126
},
123127
},
124128
{
125129
name: "disable deploy-operator",
126130
args: []string{"--deploy-operator=false"},
127131
assert: func(t *testing.T, cfg deployer.Config) {
128-
assert.True(t, cfg.Operator.SkipDeployment, "Operator.SkipDeployment mismatch")
132+
assert.True(t, cfg.Operator.SkipDeploymentEnabled(), "Operator.SkipDeployment mismatch")
129133
},
130134
},
131135
{
@@ -218,3 +222,117 @@ central:
218222
})
219223
}
220224
}
225+
226+
func TestApplyUserDefaults(t *testing.T) {
227+
log := logger.New()
228+
229+
tests := []struct {
230+
name string
231+
config deployer.Config
232+
user deployer.Config
233+
expected deployer.Config
234+
}{
235+
{
236+
name: "empty user config leaves config unchanged",
237+
config: deployer.Config{
238+
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
239+
Central: deployer.CentralConfig{
240+
Namespace: "custom-namespace",
241+
},
242+
},
243+
expected: deployer.Config{
244+
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
245+
Central: deployer.CentralConfig{
246+
Namespace: "custom-namespace",
247+
},
248+
},
249+
},
250+
{
251+
name: "fills empty fields from user defaults",
252+
config: deployer.Config{},
253+
user: deployer.Config{
254+
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
255+
Operator: deployer.OperatorConfig{DeployViaOlm: new(true)},
256+
},
257+
expected: deployer.Config{
258+
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
259+
Operator: deployer.OperatorConfig{DeployViaOlm: new(true)},
260+
},
261+
},
262+
{
263+
name: "user config overrides any config fields including config defaults",
264+
config: deployer.Config{
265+
Roxie: deployer.RoxieConfig{
266+
Version: "4.9.2",
267+
},
268+
Central: deployer.CentralConfig{
269+
EarlyReadiness: new(true),
270+
},
271+
},
272+
user: deployer.Config{
273+
Roxie: deployer.RoxieConfig{
274+
Version: "4.5.0",
275+
},
276+
Operator: deployer.OperatorConfig{
277+
DeployViaOlm: new(true),
278+
},
279+
Central: deployer.CentralConfig{
280+
Namespace: "custom-namespace",
281+
EarlyReadiness: new(false),
282+
},
283+
},
284+
expected: deployer.Config{
285+
Roxie: deployer.RoxieConfig{
286+
Version: "4.5.0",
287+
},
288+
Operator: deployer.OperatorConfig{
289+
DeployViaOlm: new(true),
290+
},
291+
Central: deployer.CentralConfig{
292+
Namespace: "custom-namespace",
293+
EarlyReadiness: new(false),
294+
},
295+
},
296+
},
297+
}
298+
299+
for _, tt := range tests {
300+
t.Run(tt.name, func(t *testing.T) {
301+
tmpDir := t.TempDir()
302+
t.Setenv("XDG_CONFIG_HOME", tmpDir)
303+
t.Setenv("HOME", tmpDir) // For non-Unix systems.
304+
305+
if !reflect.DeepEqual(tt.user, deployer.Config{}) {
306+
configPath, err := paths.UserConfigPath()
307+
require.NoError(t, err)
308+
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
309+
data, err := yaml.Marshal(tt.user)
310+
require.NoError(t, err)
311+
require.NoError(t, os.WriteFile(configPath, data, 0o644))
312+
}
313+
314+
cfg := deployer.NewConfig()
315+
require.NoError(t, mergo.Merge(&cfg, &tt.config, mergo.WithOverride, mergo.WithoutDereference))
316+
require.NoError(t, tryApplyUserDefaults(log, &cfg))
317+
318+
expected := deployer.NewConfig()
319+
require.NoError(t, mergo.Merge(&expected, &tt.expected, mergo.WithOverride, mergo.WithoutDereference))
320+
321+
assert.True(t, reflect.DeepEqual(expected, cfg), "expected %+v, got %+v", expected, cfg)
322+
})
323+
}
324+
325+
t.Run("returns error on invalid yaml", func(t *testing.T) {
326+
tmpDir := t.TempDir()
327+
t.Setenv("XDG_CONFIG_HOME", tmpDir)
328+
t.Setenv("HOME", tmpDir) // For non-Unix systems.
329+
330+
configPath, err := paths.UserConfigPath()
331+
require.NoError(t, err)
332+
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
333+
require.NoError(t, os.WriteFile(configPath, []byte(`invalid: [yaml`), 0o644))
334+
335+
cfg := deployer.NewConfig()
336+
assert.Error(t, tryApplyUserDefaults(log, &cfg))
337+
})
338+
}

cmd/env.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/spf13/cobra"
88
"github.com/stackrox/roxie/internal/env"
9-
"github.com/stackrox/roxie/internal/logger"
109
)
1110

1211
func newEnvCmd() *cobra.Command {
@@ -22,7 +21,7 @@ func newEnvCmd() *cobra.Command {
2221
}
2322

2423
func runEnv(cmd *cobra.Command, args []string) error {
25-
log := logger.New()
24+
log := globalLogger
2625
if err := env.Initialize(log); err != nil {
2726
return err
2827
}

cmd/main.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56

7+
"dario.cat/mergo"
68
"github.com/fatih/color"
79
"github.com/spf13/cobra"
810
"github.com/stackrox/roxie/internal/deployer"
11+
"github.com/stackrox/roxie/internal/logger"
12+
"github.com/stackrox/roxie/internal/paths"
13+
"gopkg.in/yaml.v3"
914
)
1015

1116
var (
@@ -15,18 +20,48 @@ var (
1520
envrc string
1621
dryRun bool
1722

23+
skipUserConfig bool
24+
25+
globalLogger = logger.New()
26+
1827
// We need this set up before command line flags are parsed.
19-
deploySettings = deployer.NewConfig()
28+
deploySettingsFromArgs = deployer.NewConfig()
2029
)
2130

2231
func main() {
32+
red := color.New(color.FgRed, color.Bold)
2333
if err := rootCmd.Execute(); err != nil {
24-
red := color.New(color.FgRed, color.Bold)
2534
red.Fprintf(os.Stderr, "Error: %v\n", err)
2635
os.Exit(1)
2736
}
2837
}
2938

39+
// If a user config file exists, apply those user defaults on top the
40+
// current config. This essentially means, that the user config can
41+
// override values, which are already initialized in NewConfig().
42+
func tryApplyUserDefaults(log *logger.Logger, config *deployer.Config) error {
43+
path, err := paths.UserConfigPath()
44+
if err != nil {
45+
return err
46+
}
47+
data, err := os.ReadFile(path)
48+
if err != nil {
49+
if os.IsNotExist(err) {
50+
return nil
51+
}
52+
return fmt.Errorf("reading user config %q: %w", path, err)
53+
}
54+
var userDefaults deployer.Config
55+
if err := yaml.Unmarshal(data, &userDefaults); err != nil {
56+
return fmt.Errorf("parsing user config %q: %w", path, err)
57+
}
58+
if err := mergo.Merge(config, &userDefaults, mergo.WithOverride, mergo.WithoutDereference); err != nil {
59+
return fmt.Errorf("merging user config %q: %w", path, err)
60+
}
61+
log.Dimf("Applied user config from %s", path)
62+
return nil
63+
}
64+
3065
var rootCmd = &cobra.Command{
3166
Use: "roxie",
3267
Short: "roxie - Advanced Cluster Security Deployment Tool",
@@ -39,8 +74,10 @@ Red Hat Advanced Cluster Security (ACS) on any Kubernetes/OpenShift cluster.`,
3974
func init() {
4075
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output (show CRs)")
4176
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Do not actually modify cluster")
42-
rootCmd.AddCommand(newDeployCmd(&deploySettings))
43-
rootCmd.AddCommand(newTeardownCmd(&deploySettings))
77+
rootCmd.PersistentFlags().BoolVar(&skipUserConfig, "skip-user-config", false,
78+
fmt.Sprintf("Skips reading of user's configuration (%s)", paths.UserConfigPathString()))
79+
rootCmd.AddCommand(newDeployCmd(&deploySettingsFromArgs))
80+
rootCmd.AddCommand(newTeardownCmd(&deploySettingsFromArgs))
4481
rootCmd.AddCommand(newShellCmd())
4582
rootCmd.AddCommand(newVersionCmd())
4683
rootCmd.AddCommand(newEnvCmd())

0 commit comments

Comments
 (0)