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
4 changes: 2 additions & 2 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"k8s.io/utils/ptr"
)

var (
const (
sharedNamespace = "stackrox"
)

Expand Down Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 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/types"
"github.com/stackrox/roxie/internal/xdg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestNewDeployCmd_Flags(t *testing.T) {
Expand Down Expand Up @@ -205,3 +209,109 @@ 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: true},
},
expected: deployer.Config{
Roxie: deployer.RoxieConfig{Version: "4.5.0"},
Operator: deployer.OperatorConfig{DeployViaOlm: true},
},
},
{
name: "user config overrides any config fields including config defaults",
config: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.9.2",
},
},
user: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.5.0",
},
Operator: deployer.OperatorConfig{
DeployViaOlm: true,
},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
},
},
expected: deployer.Config{
Roxie: deployer.RoxieConfig{
Version: "4.5.0",
},
Operator: deployer.OperatorConfig{
DeployViaOlm: true,
},
Central: deployer.CentralConfig{
Namespace: "custom-namespace",
},
},
},
}

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 := xdg.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) {
t.Setenv("HOME", t.TempDir())
configPath, err := xdg.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
47 changes: 45 additions & 2 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/xdg"
"gopkg.in/yaml.v3"
)

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

globalLogger = logger.New()

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

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

func run() error {
if err := tryApplyUserDefaults(globalLogger, &deploySettings); err != nil {
return err
}
return rootCmd.Execute()
}

// 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 := xdg.UserConfigPath()
if err != nil {
return err
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 Down
3 changes: 1 addition & 2 deletions cmd/teardown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion internal/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
12 changes: 6 additions & 6 deletions internal/imagecache/imagecache.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/stackrox/roxie/internal/logger"
"github.com/stackrox/roxie/internal/ocihelper"
"github.com/stackrox/roxie/internal/xdg"
)

// ImageCache manages cache of verified pullable Docker images
Expand All @@ -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 := xdg.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 {
Expand All @@ -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
Expand Down
16 changes: 11 additions & 5 deletions internal/imagecache/imagecache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"testing"

"github.com/stackrox/roxie/internal/logger"
"github.com/stretchr/testify/require"
)

func TestImageCacheLoadSaveRoundtrip(t *testing.T) {
tmpDir := t.TempDir()
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))
Expand All @@ -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")
}
Expand All @@ -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")
Expand All @@ -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++ {
Expand All @@ -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")
Expand Down
36 changes: 36 additions & 0 deletions internal/xdg/xdg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package xdg

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 "", err
}
return filepath.Join(dir, appName), nil
}
Loading