Skip to content

Commit 5302781

Browse files
authored
Merge pull request #1907 from dgageot/override-global-paths
Add --config-dir and --data-dir global CLI flags to override default paths
2 parents f589056 + 9fabf37 commit 5302781

3 files changed

Lines changed: 136 additions & 16 deletions

File tree

cmd/root/root.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type rootFlags struct {
3030
debugMode bool
3131
logFilePath string
3232
logFile io.Closer
33+
cacheDir string
34+
configDir string
35+
dataDir string
3336
}
3437

3538
func isDockerAgent() bool {
@@ -51,6 +54,18 @@ func NewRootCmd() *cobra.Command {
5154
cagent run ./agent.yaml
5255
cagent run agentcatalog/pirate`,
5356
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
57+
// Apply directory overrides before anything else so that
58+
// logging, telemetry, and config loading honour them.
59+
if flags.cacheDir != "" {
60+
paths.SetCacheDir(flags.cacheDir)
61+
}
62+
if flags.configDir != "" {
63+
paths.SetConfigDir(flags.configDir)
64+
}
65+
if flags.dataDir != "" {
66+
paths.SetDataDir(flags.dataDir)
67+
}
68+
5469
// Initialize logging before anything else so logs don't break TUI
5570
if err := flags.setupLogging(); err != nil {
5671
// If logging setup fails, fall back to stderr so we still get logs
@@ -96,6 +111,9 @@ func NewRootCmd() *cobra.Command {
96111
cmd.PersistentFlags().BoolVarP(&flags.debugMode, "debug", "d", false, "Enable debug logging")
97112
cmd.PersistentFlags().BoolVarP(&flags.enableOtel, "otel", "o", false, "Enable OpenTelemetry tracing")
98113
cmd.PersistentFlags().StringVar(&flags.logFilePath, "log-file", "", "Path to debug log file (default: ~/.cagent/cagent.debug.log; only used with --debug)")
114+
cmd.PersistentFlags().StringVar(&flags.cacheDir, "cache-dir", "", "Override the cache directory (default: ~/Library/Caches/cagent on macOS)")
115+
cmd.PersistentFlags().StringVar(&flags.configDir, "config-dir", "", "Override the config directory (default: ~/.config/cagent)")
116+
cmd.PersistentFlags().StringVar(&flags.dataDir, "data-dir", "", "Override the data directory (default: ~/.cagent)")
99117

100118
cmd.AddCommand(newVersionCmd())
101119
cmd.AddCommand(newRunCmd())

pkg/paths/paths.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,101 @@ package paths
33
import (
44
"os"
55
"path/filepath"
6+
"sync/atomic"
67
)
78

9+
// overridable holds an optional directory override backed by an atomic pointer.
10+
// A nil pointer (the zero value) means "use the default".
11+
type overridable struct{ p atomic.Pointer[string] }
12+
13+
// Set stores an override directory. An empty value clears the override.
14+
func (o *overridable) Set(dir string) {
15+
if dir == "" {
16+
o.p.Store(nil)
17+
} else {
18+
o.p.Store(&dir)
19+
}
20+
}
21+
22+
// get returns the override if set, or falls back to the result of defaultFn.
23+
func (o *overridable) get(defaultFn func() string) string {
24+
if p := o.p.Load(); p != nil {
25+
return filepath.Clean(*p)
26+
}
27+
return defaultFn()
28+
}
29+
30+
var (
31+
cacheDirOverride overridable
32+
configDirOverride overridable
33+
dataDirOverride overridable
34+
)
35+
36+
// SetCacheDir overrides the default cache directory returned by [GetCacheDir].
37+
// An empty value restores the default behaviour.
38+
// This should be called early (e.g. during CLI flag processing) before any
39+
// goroutine calls the corresponding getter.
40+
func SetCacheDir(dir string) { cacheDirOverride.Set(dir) }
41+
42+
// SetConfigDir overrides the default config directory returned by [GetConfigDir].
43+
// An empty value restores the default behaviour.
44+
func SetConfigDir(dir string) { configDirOverride.Set(dir) }
45+
46+
// SetDataDir overrides the default data directory returned by [GetDataDir].
47+
// An empty value restores the default behaviour.
48+
func SetDataDir(dir string) { dataDirOverride.Set(dir) }
49+
850
// GetCacheDir returns the user's cache directory for cagent.
951
//
52+
// If an override has been set via [SetCacheDir] it is returned instead.
53+
//
1054
// On Linux this follows XDG: $XDG_CACHE_HOME/cagent (default ~/.cache/cagent).
1155
// On macOS this uses ~/Library/Caches/cagent.
1256
// On Windows this uses %LocalAppData%/cagent.
1357
//
1458
// If the cache directory cannot be determined, it falls back to a directory
1559
// under the system temporary directory.
1660
func GetCacheDir() string {
17-
cacheDir, err := os.UserCacheDir()
18-
if err != nil {
19-
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent-cache"))
20-
}
21-
return filepath.Clean(filepath.Join(cacheDir, "cagent"))
61+
return cacheDirOverride.get(func() string {
62+
cacheDir, err := os.UserCacheDir()
63+
if err != nil {
64+
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent-cache"))
65+
}
66+
return filepath.Clean(filepath.Join(cacheDir, "cagent"))
67+
})
2268
}
2369

2470
// GetConfigDir returns the user's config directory for cagent.
2571
//
72+
// If an override has been set via [SetConfigDir] it is returned instead.
73+
//
2674
// If the home directory cannot be determined, it falls back to a directory
2775
// under the system temporary directory. This is a best-effort fallback and
2876
// not intended to be a security boundary.
2977
func GetConfigDir() string {
30-
homeDir, err := os.UserHomeDir()
31-
if err != nil {
32-
// Fallback to temp directory
33-
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent-config"))
34-
}
35-
return filepath.Clean(filepath.Join(homeDir, ".config", "cagent"))
78+
return configDirOverride.get(func() string {
79+
homeDir, err := os.UserHomeDir()
80+
if err != nil {
81+
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent-config"))
82+
}
83+
return filepath.Clean(filepath.Join(homeDir, ".config", "cagent"))
84+
})
3685
}
3786

3887
// GetDataDir returns the user's data directory for cagent (caches, content, logs).
3988
//
89+
// If an override has been set via [SetDataDir] it is returned instead.
90+
//
4091
// If the home directory cannot be determined, it falls back to a directory
4192
// under the system temporary directory.
4293
func GetDataDir() string {
43-
homeDir, err := os.UserHomeDir()
44-
if err != nil {
45-
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent"))
46-
}
47-
return filepath.Clean(filepath.Join(homeDir, ".cagent"))
94+
return dataDirOverride.get(func() string {
95+
homeDir, err := os.UserHomeDir()
96+
if err != nil {
97+
return filepath.Clean(filepath.Join(os.TempDir(), ".cagent"))
98+
}
99+
return filepath.Clean(filepath.Join(homeDir, ".cagent"))
100+
})
48101
}
49102

50103
// GetHomeDir returns the user's home directory.

pkg/paths/paths_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package paths_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/docker/cagent/pkg/paths"
9+
)
10+
11+
func TestOverrides(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
set func(string)
17+
get func() string
18+
custom string
19+
}{
20+
{"CacheDir", paths.SetCacheDir, paths.GetCacheDir, "/custom/cache"},
21+
{"ConfigDir", paths.SetConfigDir, paths.GetConfigDir, "/custom/config"},
22+
{"DataDir", paths.SetDataDir, paths.GetDataDir, "/custom/data"},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
t.Parallel()
28+
29+
// Restore default after the test.
30+
t.Cleanup(func() { tt.set("") })
31+
32+
original := tt.get()
33+
assert.NotEmpty(t, original)
34+
35+
tt.set(tt.custom)
36+
assert.Equal(t, tt.custom, tt.get())
37+
38+
// Empty string restores the default.
39+
tt.set("")
40+
assert.Equal(t, original, tt.get())
41+
})
42+
}
43+
}
44+
45+
func TestGetHomeDir(t *testing.T) {
46+
t.Parallel()
47+
48+
assert.NotEmpty(t, paths.GetHomeDir())
49+
}

0 commit comments

Comments
 (0)