refactor(go.d): move nd directories to dedicated pluginconfig package (#20827)
Ilya Mashchenko committed
Aug 16, 2025 at 10:33 UTC
ea44b664f759844a0f74b1ed989836b445261b80
5 files changed
+640
-181
src/go/cmd/godplugin/config.go
deleted
-171
@@ -1,171 +0,0 @@
1
-// SPDX-License-Identifier: GPL-3.0-or-later
2
-
3
-package main
4
-
5
-import (
6
- "errors"
7
- "io/fs"
8
- "os"
9
- "path/filepath"
10
- "strings"
11
-
12
- "github.com/netdata/netdata/go/plugins/pkg/executable"
13
- "github.com/netdata/netdata/go/plugins/pkg/multipath"
14
- "github.com/netdata/netdata/go/plugins/plugin/go.d/cli"
15
-)
16
-
17
-type envConfig struct {
18
- cygwinBase string
19
- userDir string
20
- stockDir string
21
- varLibDir string
22
- watchPath string
23
- logLevel string
24
-}
25
-
26
-func newEnvConfig() *envConfig {
27
- cfg := &envConfig{
28
- cygwinBase: os.Getenv("NETDATA_CYGWIN_BASE_PATH"),
29
- userDir: os.Getenv("NETDATA_USER_CONFIG_DIR"),
30
- stockDir: os.Getenv("NETDATA_STOCK_CONFIG_DIR"),
31
- varLibDir: os.Getenv("NETDATA_LIB_DIR"),
32
- watchPath: os.Getenv("NETDATA_PLUGINS_GOD_WATCH_PATH"),
33
- logLevel: os.Getenv("NETDATA_LOG_LEVEL"),
34
- }
35
-
36
- cfg.userDir = cfg.handleDirOnWin(cfg.userDir)
37
- cfg.stockDir = cfg.handleDirOnWin(cfg.stockDir)
38
- cfg.varLibDir = cfg.handleDirOnWin(cfg.varLibDir)
39
- cfg.watchPath = cfg.handleDirOnWin(cfg.watchPath)
40
-
41
- return cfg
42
-}
43
-
44
-func (c *envConfig) handleDirOnWin(path string) string {
45
- base := c.cygwinBase
46
-
47
- // TODO: temp workaround for debug mode
48
- if base == "" && strings.HasPrefix(executable.Directory, "C:\\msys64") {
49
- base = "C:\\msys64"
50
- }
51
-
52
- if base == "" || !strings.HasPrefix(path, "/") {
53
- return path
54
- }
55
-
56
- return filepath.Join(base, path)
57
-}
58
-
59
-type config struct {
60
- name string
61
- pluginDir multipath.MultiPath
62
- collectorsDir multipath.MultiPath
63
- collectorsWatchPath []string
64
- serviceDiscoveryDir multipath.MultiPath
65
- varLibDir string
66
-}
67
-
68
-func newConfig(opts *cli.Option, env *envConfig) *config {
69
- cfg := &config{
70
- name: "go.d",
71
- }
72
-
73
- cfg.pluginDir = cfg.initPluginDir(opts, env)
74
- cfg.collectorsDir = cfg.initCollectorsDir(opts)
75
- cfg.collectorsWatchPath = cfg.initCollectorsWatchPaths(opts, env)
76
- cfg.serviceDiscoveryDir = cfg.initServiceDiscoveryConfigDir()
77
- cfg.varLibDir = env.varLibDir
78
-
79
- return cfg
80
-}
81
-
82
-func (c *config) initPluginDir(opts *cli.Option, env *envConfig) multipath.MultiPath {
83
- if len(opts.ConfDir) > 0 {
84
- return opts.ConfDir
85
- }
86
-
87
- if env.userDir != "" || env.stockDir != "" {
88
- return multipath.New(env.userDir, env.stockDir)
89
- }
90
-
91
- dirs := []string{
92
- filepath.Join(executable.Directory, "/../../../../etc/netdata"),
93
- }
94
-
95
- // Find the first existing standard directory
96
- standardDirs := []string{
97
- env.handleDirOnWin("/etc/netdata"),
98
- env.handleDirOnWin("/opt/netdata/etc/netdata"),
99
- }
100
- for _, dir := range standardDirs {
101
- if isDirExists(dir) {
102
- dirs = append(dirs, dir)
103
- break
104
- }
105
- }
106
-
107
- dirs = append(dirs, filepath.Join(executable.Directory, "/../../../../usr/lib/netdata/conf.d"))
108
-
109
- // Find the first existing lib directory
110
- libDirs := []string{
111
- env.handleDirOnWin("/usr/lib/netdata/conf.d"),
112
- env.handleDirOnWin("/opt/netdata/usr/lib/netdata/conf.d"),
113
- }
114
- for _, dir := range libDirs {
115
- if isDirExists(dir) {
116
- dirs = append(dirs, dir)
117
- break
118
- }
119
- }
120
-
121
- return multipath.New(dirs...)
122
-}
123
-
124
-func (c *config) initCollectorsDir(opts *cli.Option) multipath.MultiPath {
125
- if len(opts.ConfDir) > 0 {
126
- return opts.ConfDir
127
- }
128
-
129
- c.mustPluginDir()
130
-
131
- var mpath multipath.MultiPath
132
-
133
- for _, dir := range c.pluginDir {
134
- mpath = append(mpath, filepath.Join(dir, c.name))
135
- }
136
-
137
- return multipath.New(mpath...)
138
-}
139
-
140
-func (c *config) initServiceDiscoveryConfigDir() multipath.MultiPath {
141
- c.mustPluginDir()
142
-
143
- var mpath multipath.MultiPath
144
-
145
- for _, v := range c.pluginDir {
146
- mpath = append(mpath, filepath.Join(v, c.name, "sd"))
147
- }
148
-
149
- return mpath
150
-}
151
-
152
-func (c *config) initCollectorsWatchPaths(opts *cli.Option, env *envConfig) []string {
153
- if env.watchPath == "" {
154
- return opts.WatchPath
155
- }
156
- return append(opts.WatchPath, env.watchPath)
157
-}
158
-
159
-func (c *config) mustPluginDir() {
160
- if len(c.pluginDir) == 0 {
161
- panic("plugin config init: plugin dir is empty")
162
- }
163
-}
164
-
165
-func isDirExists(dir string) bool {
166
- fi, err := os.Stat(dir)
167
- if err != nil {
168
- return !errors.Is(err, fs.ErrNotExist)
169
- }
170
- return fi.Mode().IsDir()
171
-}
src/go/cmd/godplugin/main.go
+14
-10
@@ -11,8 +11,10 @@ import (
11
12
"github.com/netdata/netdata/go/plugins/logger"
13
"github.com/netdata/netdata/go/plugins/pkg/buildinfo"
14
+ "github.com/netdata/netdata/go/plugins/pkg/executable"
15
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent"
16
"github.com/netdata/netdata/go/plugins/plugin/go.d/cli"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pluginconfig"
18
19
"go.uber.org/automaxprocs/maxprocs"
20
"golang.org/x/net/http/httpproxy"
@@ -37,23 +39,22 @@ func main() {
39
return
40
}
41
40
- env := newEnvConfig()
41
- cfg := newConfig(opts, env)
42
+ pluginconfig.MustInit(opts)
43
43
- if env.logLevel != "" {
44
- logger.Level.SetByName(env.logLevel)
44
+ if lvl := pluginconfig.EnvLogLevel(); lvl != "" {
45
+ logger.Level.SetByName(lvl)
46
}
47
if opts.Debug {
48
logger.Level.Set(slog.LevelDebug)
49
}
50
51
a := agent.New(agent.Config{
51
- Name: cfg.name,
52
- PluginConfigDir: cfg.pluginDir,
53
- CollectorsConfigDir: cfg.collectorsDir,
54
- ServiceDiscoveryConfigDir: cfg.serviceDiscoveryDir,
55
- CollectorsConfigWatchPath: cfg.collectorsWatchPath,
56
- VarLibDir: cfg.varLibDir,
52
+ Name: executable.Name,
53
+ PluginConfigDir: pluginconfig.ConfigDir(),
54
+ CollectorsConfigDir: pluginconfig.CollectorsDir(),
55
+ ServiceDiscoveryConfigDir: pluginconfig.ServiceDiscoveryDir(),
56
+ CollectorsConfigWatchPath: pluginconfig.CollectorsConfigWatchPaths(),
57
+ VarLibDir: pluginconfig.VarLibDir(),
58
RunModule: opts.Module,
59
RunJob: opts.Job,
60
MinUpdateEvery: opts.UpdateEvery,
@@ -67,6 +68,9 @@ func main() {
68
proxyCfg := httpproxy.FromEnvironment()
69
a.Infof("env HTTP_PROXY '%s', HTTPS_PROXY '%s'", proxyCfg.HTTPProxy, proxyCfg.HTTPSProxy)
70
71
+ a.Infof("directories → config: %s | collectors: %s | sd: %s | varlib: %s",
72
+ a.ConfigDir, a.CollectorsConfDir, a.ServiceDiscoveryConfigDir, a.VarLibDir)
73
+
74
a.Run()
75
}
76
src/go/pkg/executable/executable.go
+3
@@ -25,6 +25,9 @@ func init() {
25
if strings.HasSuffix(Name, ".test") {
26
Name = "test"
27
}
28
+ if Name == "godplugin" {
29
+ Name = "go.d"
30
+ }
31
32
fi, err := os.Lstat(path)
33
if err != nil {
src/go/plugin/go.d/pkg/pluginconfig/pluginconfig.go
new
+276
@@ -0,0 +1,276 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+// Package pluginconfig centralizes runtime configuration for the plugin,
4
+// including environment-derived values, CLI overrides, and computed directory
5
+// paths. Precedence inside "user" paths: CLI --config-dir (first), then
6
+// NETDATA_USER_CONFIG_DIR, then fallback. Overall precedence:
7
+// User (multipath) → Stock (single).
8
+package pluginconfig
9
+
10
+import (
11
+ "fmt"
12
+ "os"
13
+ "path/filepath"
14
+ "slices"
15
+ "strings"
16
+ "sync"
17
+
18
+ "github.com/netdata/netdata/go/plugins/pkg/executable"
19
+ "github.com/netdata/netdata/go/plugins/pkg/multipath"
20
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/cli"
21
+)
22
+
23
+var (
24
+ initOnce sync.Once
25
+ env envData
26
+ dirs directories
27
+)
28
+
29
+type envData struct {
30
+ cygwinBase string
31
+ userDir string
32
+ stockDir string
33
+ varLibDir string
34
+ watchPath string
35
+ logLevel string
36
+}
37
+
38
+type directories struct {
39
+ // Root config
40
+ userConfigDirs multipath.MultiPath // includes CLI dirs (+ env user + fallback)
41
+ stockConfigDir string
42
+
43
+ // Collectors (derived from roots)
44
+ collectorsUserDirs multipath.MultiPath
45
+ collectorsStockDir string
46
+
47
+ // Service discovery (derived from roots)
48
+ sdUserDirs multipath.MultiPath
49
+ sdStockDir string
50
+
51
+ // Misc
52
+ collectorsWatch []string
53
+ varLibDir string
54
+}
55
+
56
+// MustInit parses env, applies CLI overrides, discovers directories, and stores them.
57
+// Safe to call multiple times; only the first call has effect.
58
+func MustInit(opts *cli.Option) {
59
+ initOnce.Do(func() {
60
+ env = readEnvFromOS(executable.Directory)
61
+ var d directories
62
+ if err := d.build(opts, env, executable.Name, executable.Directory); err != nil {
63
+ // fail fast during startup (internal invariant was broken)
64
+ panic(fmt.Errorf("pluginconfig initialization failed: %w", err))
65
+ }
66
+ dirs = d
67
+ })
68
+}
69
+
70
+func EnvLogLevel() string { return env.logLevel }
71
+
72
+func UserConfigDirs() multipath.MultiPath { return dirs.userConfigDirsClone() }
73
+func StockConfigDir() string { return dirs.stockConfigDir }
74
+func ConfigDir() multipath.MultiPath { return dirs.configDir() }
75
+
76
+func CollectorsUserDirs() multipath.MultiPath { return dirs.collectorsUserDirsClone() }
77
+func CollectorsStockDir() string { return dirs.collectorsStockDir }
78
+func CollectorsDir() multipath.MultiPath { return dirs.collectorsDir() }
79
+
80
+func ServiceDiscoveryUserDirs() multipath.MultiPath { return dirs.sdUserDirsClone() }
81
+func ServiceDiscoveryStockDir() string { return dirs.sdStockDir }
82
+func ServiceDiscoveryDir() multipath.MultiPath { return dirs.serviceDiscoveryDir() }
83
+
84
+func CollectorsConfigWatchPaths() []string { return slices.Clone(dirs.collectorsWatch) }
85
+func VarLibDir() string { return dirs.varLibDir }
86
+
87
+func (d *directories) userConfigDirsClone() multipath.MultiPath {
88
+ return slices.Clone(d.userConfigDirs)
89
+}
90
+func (d *directories) collectorsUserDirsClone() multipath.MultiPath {
91
+ return slices.Clone(d.collectorsUserDirs)
92
+}
93
+func (d *directories) sdUserDirsClone() multipath.MultiPath { return slices.Clone(d.sdUserDirs) }
94
+
95
+func (d *directories) configDir() multipath.MultiPath {
96
+ combined := append(d.userConfigDirsClone(), d.stockConfigDir)
97
+ return multipath.New(combined...)
98
+}
99
+func (d *directories) collectorsDir() multipath.MultiPath {
100
+ combined := append(d.collectorsUserDirsClone(), d.collectorsStockDir)
101
+ return multipath.New(combined...)
102
+}
103
+func (d *directories) serviceDiscoveryDir() multipath.MultiPath {
104
+ combined := append(d.sdUserDirsClone(), d.sdStockDir)
105
+ return multipath.New(combined...)
106
+}
107
+
108
+func (d *directories) build(opts *cli.Option, env envData, execName, execDir string) error {
109
+ d.initUserRoots(opts, env, execDir)
110
+ d.initStockRoot(env, execDir)
111
+ d.deriveCollectors(execName)
112
+ d.deriveServiceDiscovery(execName)
113
+ d.initWatchPaths(opts, env)
114
+ d.initVarLib(env)
115
+ return d.validate()
116
+}
117
+
118
+// Build step 1: initialize "user" roots as a multipath: CLI (highest), env, fallback.
119
+func (d *directories) initUserRoots(opts *cli.Option, env envData, execDir string) {
120
+ var roots multipath.MultiPath
121
+
122
+ // 1) CLI dirs
123
+ for _, p := range opts.ConfDir {
124
+ p = safePathClean(handleDirOnWin(env.cygwinBase, p, execDir))
125
+ roots = append(roots, p)
126
+ }
127
+
128
+ // 2) NETDATA_USER_CONFIG_DIR
129
+ if env.userDir != "" {
130
+ roots = append(roots, safePathClean(env.userDir))
131
+ }
132
+
133
+ if len(roots) != 0 {
134
+ d.userConfigDirs = multipath.New(roots...)
135
+ return
136
+ }
137
+
138
+ // 3) Fallback if empty
139
+ for _, dir := range []string{
140
+ handleDirOnWin(env.cygwinBase, "/etc/netdata", execDir),
141
+ handleDirOnWin(env.cygwinBase, "/opt/netdata/etc/netdata", execDir),
142
+ } {
143
+ if isDirExists(dir) {
144
+ d.userConfigDirs = multipath.New(dir)
145
+ return
146
+ }
147
+ }
148
+
149
+ d.userConfigDirs = multipath.New(filepath.Join(execDir, "..", "..", "..", "..", "etc", "netdata"))
150
+}
151
+
152
+// Build step 2: initialize single "stock" root: env, common locations, build-relative fallback.
153
+func (d *directories) initStockRoot(env envData, execDir string) {
154
+ if stock := safePathClean(env.stockDir); stock != "" {
155
+ d.stockConfigDir = stock
156
+ return
157
+ }
158
+
159
+ for _, dir := range []string{
160
+ handleDirOnWin(env.cygwinBase, "/usr/lib/netdata/conf.d", execDir),
161
+ handleDirOnWin(env.cygwinBase, "/opt/netdata/usr/lib/netdata/conf.d", execDir),
162
+ } {
163
+ if isDirExists(dir) {
164
+ d.stockConfigDir = safePathClean(dir)
165
+ return
166
+ }
167
+ }
168
+
169
+ d.stockConfigDir = filepath.Join(execDir, "..", "..", "..", "..", "usr", "lib", "netdata", "conf.d")
170
+}
171
+
172
+// Build step 3: derive collectors dirs from roots.
173
+func (d *directories) deriveCollectors(execName string) {
174
+ var user multipath.MultiPath
175
+ for _, r := range d.userConfigDirs {
176
+ user = append(user, filepath.Join(safePathClean(r), execName))
177
+ }
178
+ d.collectorsUserDirs = multipath.New(user...)
179
+
180
+ if d.stockConfigDir != "" {
181
+ d.collectorsStockDir = filepath.Join(d.stockConfigDir, execName)
182
+ }
183
+}
184
+
185
+// Build step 4: derive service-discovery dirs from roots.
186
+func (d *directories) deriveServiceDiscovery(execName string) {
187
+ var user multipath.MultiPath
188
+ for _, r := range d.userConfigDirs {
189
+ user = append(user, filepath.Join(safePathClean(r), execName, "sd"))
190
+ }
191
+ d.sdUserDirs = multipath.New(user...)
192
+
193
+ if d.stockConfigDir != "" {
194
+ d.sdStockDir = filepath.Join(d.stockConfigDir, execName, "sd")
195
+ }
196
+}
197
+
198
+// Build step 5: init watchers (normalize + dedupe via multipath.New)
199
+func (d *directories) initWatchPaths(opts *cli.Option, env envData) {
200
+ in := append([]string{}, opts.WatchPath...)
201
+ if env.watchPath != "" {
202
+ in = append(in, env.watchPath)
203
+ }
204
+ d.collectorsWatch = multipath.New(in...)
205
+}
206
+
207
+// Build step 6: carry varlib
208
+func (d *directories) initVarLib(env envData) {
209
+ d.varLibDir = env.varLibDir
210
+}
211
+
212
+func (d *directories) validate() error {
213
+ if len(d.userConfigDirs) == 0 {
214
+ return fmt.Errorf("pluginconfig: user config dirs not initialized")
215
+ }
216
+ if d.stockConfigDir == "" {
217
+ return fmt.Errorf("pluginconfig: stock config dir not initialized")
218
+ }
219
+ if len(d.collectorsUserDirs) == 0 {
220
+ return fmt.Errorf("pluginconfig: collectors user dirs not derived")
221
+ }
222
+ if d.collectorsStockDir == "" {
223
+ return fmt.Errorf("pluginconfig: collectors stock dir not derived")
224
+ }
225
+ if len(d.sdUserDirs) == 0 {
226
+ return fmt.Errorf("pluginconfig: sd user dirs not derived")
227
+ }
228
+ if d.sdStockDir == "" {
229
+ return fmt.Errorf("pluginconfig: sd stock dir not derived")
230
+ }
231
+ return nil
232
+}
233
+
234
+func readEnvFromOS(execDir string) envData {
235
+ e := envData{
236
+ cygwinBase: os.Getenv("NETDATA_CYGWIN_BASE_PATH"),
237
+ userDir: os.Getenv("NETDATA_USER_CONFIG_DIR"),
238
+ stockDir: os.Getenv("NETDATA_STOCK_CONFIG_DIR"),
239
+ varLibDir: os.Getenv("NETDATA_LIB_DIR"),
240
+ watchPath: os.Getenv("NETDATA_PLUGINS_GOD_WATCH_PATH"),
241
+ logLevel: os.Getenv("NETDATA_LOG_LEVEL"),
242
+ }
243
+ e.userDir = handleDirOnWin(e.cygwinBase, safePathClean(e.userDir), execDir)
244
+ e.stockDir = handleDirOnWin(e.cygwinBase, safePathClean(e.stockDir), execDir)
245
+ e.varLibDir = handleDirOnWin(e.cygwinBase, safePathClean(e.varLibDir), execDir)
246
+ e.watchPath = handleDirOnWin(e.cygwinBase, safePathClean(e.watchPath), execDir)
247
+ return e
248
+}
249
+
250
+// Convert a POSIX absolute (/foo) to Windows under base (e.g., C:\msys64\foo).
251
+// If base is empty or p doesn’t start with '/', return p unchanged.
252
+func handleDirOnWin(base, p string, execDir string) string {
253
+ // TODO: Temporary workaround to preserve existing behavior for debug builds running under msys64.
254
+ if base == "" && strings.HasPrefix(execDir, "C:\\msys64") {
255
+ base = "C:\\msys64"
256
+ }
257
+ if base == "" || !strings.HasPrefix(p, "/") {
258
+ return p
259
+ }
260
+ return filepath.Join(base, p)
261
+}
262
+
263
+func isDirExists(dir string) bool {
264
+ fi, err := os.Stat(dir)
265
+ if err != nil {
266
+ return false
267
+ }
268
+ return fi.Mode().IsDir()
269
+}
270
+
271
+func safePathClean(p string) string {
272
+ if p == "" {
273
+ return ""
274
+ }
275
+ return filepath.Clean(p)
276
+}
src/go/plugin/go.d/pkg/pluginconfig/pluginconfig_test.go
new
+347
@@ -0,0 +1,347 @@
1
+package pluginconfig
2
+
3
+import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+
8
+ "github.com/stretchr/testify/assert"
9
+ "github.com/stretchr/testify/require"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/cli"
12
+)
13
+
14
+// helpers
15
+
16
+func mkdir(t *testing.T, p string) {
17
+ t.Helper()
18
+ require.NoError(t, os.MkdirAll(p, 0o755))
19
+}
20
+
21
+const (
22
+ testPluginName = "test.plugin"
23
+ testExecDir = "/opt/netdata/bin"
24
+)
25
+
26
+func TestDirectoriesBuild(t *testing.T) {
27
+ tmp := t.TempDir()
28
+
29
+ // Probe locations (used ONLY when build() falls back to cygwinBase discovery).
30
+ // These dirs must exist for probe-based selection to succeed; otherwise,
31
+ // build() will skip them and fall back to build-relative defaults.
32
+ probeUser := filepath.Join(tmp, "etc", "netdata")
33
+ probeStock := filepath.Join(tmp, "usr", "lib", "netdata", "conf.d")
34
+ mkdir(t, probeUser)
35
+ mkdir(t, probeStock)
36
+
37
+ tests := map[string]struct {
38
+ opts *cli.Option
39
+ env envData
40
+ want directories
41
+ wantErr bool
42
+ }{
43
+ // ─────────────────────────────────── core (no cygwinBase) ───────────────────────────────────
44
+ "cli_dirs_only": {
45
+ opts: &cli.Option{
46
+ ConfDir: []string{"/tmp/user1", "/tmp/user2"},
47
+ },
48
+ env: envData{
49
+ stockDir: probeStock, // avoid probe/mapping; keep paths clean in expectations
50
+ },
51
+ want: directories{
52
+ userConfigDirs: []string{"/tmp/user1", "/tmp/user2"},
53
+ stockConfigDir: probeStock,
54
+ collectorsUserDirs: []string{"/tmp/user1/" + testPluginName, "/tmp/user2/" + testPluginName},
55
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
56
+ sdUserDirs: []string{"/tmp/user1/" + testPluginName + "/sd", "/tmp/user2/" + testPluginName + "/sd"},
57
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
58
+ collectorsWatch: []string{},
59
+ varLibDir: "",
60
+ },
61
+ },
62
+ "env_dirs_only": {
63
+ opts: &cli.Option{},
64
+ env: envData{
65
+ userDir: "/tmp/env/user",
66
+ stockDir: "/tmp/env/stock",
67
+ },
68
+ want: directories{
69
+ userConfigDirs: []string{"/tmp/env/user"},
70
+ stockConfigDir: "/tmp/env/stock",
71
+ collectorsUserDirs: []string{"/tmp/env/user/" + testPluginName},
72
+ collectorsStockDir: "/tmp/env/stock/" + testPluginName,
73
+ sdUserDirs: []string{"/tmp/env/user/" + testPluginName + "/sd"},
74
+ sdStockDir: "/tmp/env/stock/" + testPluginName + "/sd",
75
+ collectorsWatch: []string{},
76
+ varLibDir: "",
77
+ },
78
+ },
79
+ "cli_overrides_env": {
80
+ opts: &cli.Option{
81
+ ConfDir: []string{"/tmp/cli/dir"},
82
+ },
83
+ env: envData{
84
+ userDir: "/tmp/env/user",
85
+ stockDir: "/tmp/env/stock",
86
+ },
87
+ want: directories{
88
+ userConfigDirs: []string{"/tmp/cli/dir", "/tmp/env/user"},
89
+ stockConfigDir: "/tmp/env/stock",
90
+ collectorsUserDirs: []string{"/tmp/cli/dir/" + testPluginName, "/tmp/env/user/" + testPluginName},
91
+ collectorsStockDir: "/tmp/env/stock/" + testPluginName,
92
+ sdUserDirs: []string{"/tmp/cli/dir/" + testPluginName + "/sd", "/tmp/env/user/" + testPluginName + "/sd"},
93
+ sdStockDir: "/tmp/env/stock/" + testPluginName + "/sd",
94
+ collectorsWatch: []string{},
95
+ varLibDir: "",
96
+ },
97
+ },
98
+ "fallback_dirs_when_no_config": {
99
+ opts: &cli.Option{},
100
+ env: envData{},
101
+ // build-relative fallback from testExecDir
102
+ want: directories{
103
+ userConfigDirs: []string{filepath.Join(testExecDir, "..", "..", "..", "..", "etc", "netdata")},
104
+ stockConfigDir: filepath.Join(testExecDir, "..", "..", "..", "..", "usr", "lib", "netdata", "conf.d"),
105
+ collectorsUserDirs: []string{filepath.Join(testExecDir, "..", "..", "..", "..", "etc", "netdata", testPluginName)},
106
+ collectorsStockDir: filepath.Join(testExecDir, "..", "..", "..", "..", "usr", "lib", "netdata", "conf.d", testPluginName),
107
+ sdUserDirs: []string{filepath.Join(testExecDir, "..", "..", "..", "..", "etc", "netdata", testPluginName, "sd")},
108
+ sdStockDir: filepath.Join(testExecDir, "..", "..", "..", "..", "usr", "lib", "netdata", "conf.d", testPluginName, "sd"),
109
+ collectorsWatch: []string{},
110
+ varLibDir: "",
111
+ },
112
+ },
113
+ "multiple_cli_dirs": {
114
+ opts: &cli.Option{
115
+ ConfDir: []string{"/tmp/dir1", "/tmp/dir2", "/tmp/dir3"},
116
+ },
117
+ env: envData{
118
+ stockDir: probeStock,
119
+ },
120
+ want: directories{
121
+ userConfigDirs: []string{"/tmp/dir1", "/tmp/dir2", "/tmp/dir3"},
122
+ stockConfigDir: probeStock,
123
+ collectorsUserDirs: []string{
124
+ "/tmp/dir1/" + testPluginName,
125
+ "/tmp/dir2/" + testPluginName,
126
+ "/tmp/dir3/" + testPluginName,
127
+ },
128
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
129
+ sdUserDirs: []string{
130
+ "/tmp/dir1/" + testPluginName + "/sd",
131
+ "/tmp/dir2/" + testPluginName + "/sd",
132
+ "/tmp/dir3/" + testPluginName + "/sd",
133
+ },
134
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
135
+ collectorsWatch: []string{},
136
+ varLibDir: "",
137
+ },
138
+ },
139
+
140
+ // ─────────────────────────────── explicit cygwinBase cases ───────────────────────────────
141
+ "watch_paths_from_cli_and_env (cygwinBase)": {
142
+ opts: &cli.Option{
143
+ WatchPath: []string{"/tmp/watch1", "/tmp/watch2"},
144
+ },
145
+ env: envData{
146
+ cygwinBase: tmp, // exercise probe discovery
147
+ watchPath: "/tmp/watch/env",
148
+ },
149
+ want: directories{
150
+ userConfigDirs: []string{probeUser}, // from probe discovery
151
+ stockConfigDir: probeStock,
152
+ collectorsUserDirs: []string{filepath.Join(probeUser, testPluginName)},
153
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
154
+ sdUserDirs: []string{filepath.Join(probeUser, testPluginName, "sd")},
155
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
156
+ collectorsWatch: []string{"/tmp/watch1", "/tmp/watch2", "/tmp/watch/env"}, // dedup preserved
157
+ varLibDir: "",
158
+ },
159
+ },
160
+ "varlib_from_env (cygwinBase + probes)": {
161
+ opts: &cli.Option{},
162
+ env: envData{
163
+ cygwinBase: tmp,
164
+ varLibDir: "/var/lib/netdata",
165
+ },
166
+ want: directories{
167
+ userConfigDirs: []string{probeUser},
168
+ stockConfigDir: probeStock,
169
+ collectorsUserDirs: []string{filepath.Join(probeUser, testPluginName)},
170
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
171
+ sdUserDirs: []string{filepath.Join(probeUser, testPluginName, "sd")},
172
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
173
+ collectorsWatch: []string{},
174
+ varLibDir: "/var/lib/netdata",
175
+ },
176
+ },
177
+ "empty_stock_dir_from_env (cygwinBase + probes)": {
178
+ opts: &cli.Option{},
179
+ env: envData{
180
+ userDir: "/tmp/user", // explicit user
181
+ stockDir: "", // missing -> probe for stock
182
+ cygwinBase: tmp,
183
+ },
184
+ want: directories{
185
+ userConfigDirs: []string{"/tmp/user"},
186
+ stockConfigDir: probeStock, // discovered
187
+ collectorsUserDirs: []string{"/tmp/user/" + testPluginName},
188
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
189
+ sdUserDirs: []string{"/tmp/user/" + testPluginName + "/sd"},
190
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
191
+ collectorsWatch: []string{},
192
+ varLibDir: "",
193
+ },
194
+ },
195
+ "env_user_pre_normalized (cygwinBase set)": {
196
+ opts: &cli.Option{},
197
+ env: envData{
198
+ cygwinBase: tmp, // present but build() expects env already normalized
199
+ userDir: filepath.Join(tmp, "etc", "netdata"), // pre-normalized
200
+ stockDir: filepath.Join(tmp, "custom", "stock"), // pre-normalized
201
+ },
202
+ want: directories{
203
+ userConfigDirs: []string{filepath.Join(tmp, "etc", "netdata")},
204
+ stockConfigDir: filepath.Join(tmp, "custom", "stock"),
205
+ collectorsUserDirs: []string{filepath.Join(tmp, "etc", "netdata", testPluginName)},
206
+ collectorsStockDir: filepath.Join(tmp, "custom", "stock", testPluginName),
207
+ sdUserDirs: []string{filepath.Join(tmp, "etc", "netdata", testPluginName, "sd")},
208
+ sdStockDir: filepath.Join(tmp, "custom", "stock", testPluginName, "sd"),
209
+ collectorsWatch: []string{},
210
+ varLibDir: "",
211
+ },
212
+ },
213
+ "cli_dirs_remapped (cygwinBase)": {
214
+ opts: &cli.Option{
215
+ ConfDir: []string{"/etc/netdata", "/opt/netdata/etc/netdata"},
216
+ },
217
+ env: envData{
218
+ cygwinBase: tmp, // remap CLI POSIX to <tmp> paths
219
+ stockDir: probeStock,
220
+ },
221
+ want: directories{
222
+ userConfigDirs: []string{
223
+ filepath.Join(tmp, "etc", "netdata"),
224
+ filepath.Join(tmp, "opt", "netdata", "etc", "netdata"),
225
+ },
226
+ stockConfigDir: probeStock,
227
+ collectorsUserDirs: []string{
228
+ filepath.Join(tmp, "etc", "netdata", testPluginName),
229
+ filepath.Join(tmp, "opt", "netdata", "etc", "netdata", testPluginName),
230
+ },
231
+ collectorsStockDir: filepath.Join(probeStock, testPluginName),
232
+ sdUserDirs: []string{
233
+ filepath.Join(tmp, "etc", "netdata", testPluginName, "sd"),
234
+ filepath.Join(tmp, "opt", "netdata", "etc", "netdata", testPluginName, "sd"),
235
+ },
236
+ sdStockDir: filepath.Join(probeStock, testPluginName, "sd"),
237
+ collectorsWatch: []string{},
238
+ varLibDir: "",
239
+ },
240
+ },
241
+ }
242
+
243
+ for name, tc := range tests {
244
+ t.Run(name, func(t *testing.T) {
245
+ t.Parallel() // safe: build() uses only inputs, no globals
246
+ var got directories
247
+ err := got.build(tc.opts, tc.env, testPluginName, testExecDir)
248
+
249
+ if tc.wantErr {
250
+ require.Error(t, err)
251
+ return
252
+ }
253
+ require.NoError(t, err)
254
+ assert.Equal(t, tc.want, got)
255
+ })
256
+ }
257
+}
258
+
259
+func TestDirectoriesBuildValidation(t *testing.T) {
260
+ tests := map[string]struct {
261
+ dirs directories
262
+ wantErr bool
263
+ }{
264
+ "empty_user_config_dirs": {
265
+ wantErr: true,
266
+ dirs: directories{
267
+ userConfigDirs: nil,
268
+ stockConfigDir: "/stock",
269
+ }},
270
+ "empty_stock_config_dir": {
271
+ wantErr: true,
272
+ dirs: directories{
273
+ userConfigDirs: []string{"/user"},
274
+ stockConfigDir: "",
275
+ collectorsUserDirs: []string{"/user/plugin"},
276
+ collectorsStockDir: "/stock/plugin",
277
+ sdUserDirs: []string{"/user/plugin/sd"},
278
+ sdStockDir: "/stock/plugin/sd",
279
+ }},
280
+ "empty_collectors_user_dirs": {
281
+ wantErr: true,
282
+ dirs: directories{
283
+ userConfigDirs: []string{"/user"},
284
+ stockConfigDir: "/stock",
285
+ collectorsUserDirs: nil,
286
+ collectorsStockDir: "/stock/plugin",
287
+ sdUserDirs: []string{"/user/plugin/sd"},
288
+ sdStockDir: "/stock/plugin/sd",
289
+ },
290
+ },
291
+ "empty_collectors_stock_dir": {
292
+ wantErr: true,
293
+ dirs: directories{
294
+ userConfigDirs: []string{"/user"},
295
+ stockConfigDir: "/stock",
296
+ collectorsUserDirs: []string{"/user/plugin"},
297
+ collectorsStockDir: "",
298
+ sdUserDirs: []string{"/user/plugin/sd"},
299
+ sdStockDir: "/stock/plugin/sd",
300
+ },
301
+ },
302
+ "empty_sd_user_dirs": {
303
+ wantErr: true,
304
+ dirs: directories{
305
+ userConfigDirs: []string{"/user"},
306
+ stockConfigDir: "/stock",
307
+ collectorsUserDirs: []string{"/user/plugin"},
308
+ collectorsStockDir: "/stock/plugin",
309
+ sdUserDirs: nil,
310
+ sdStockDir: "/stock/plugin/sd",
311
+ },
312
+ },
313
+ "empty_sd_stock_dir": {
314
+ wantErr: true,
315
+ dirs: directories{
316
+ userConfigDirs: []string{"/user"},
317
+ stockConfigDir: "/stock",
318
+ collectorsUserDirs: []string{"/user/plugin"},
319
+ collectorsStockDir: "/stock/plugin",
320
+ sdUserDirs: []string{"/user/plugin/sd"},
321
+ sdStockDir: "",
322
+ },
323
+ },
324
+ "valid_directories": {
325
+ wantErr: false,
326
+ dirs: directories{
327
+ userConfigDirs: []string{"/user"},
328
+ stockConfigDir: "/stock",
329
+ collectorsUserDirs: []string{"/user/plugin"},
330
+ collectorsStockDir: "/stock/plugin",
331
+ sdUserDirs: []string{"/user/plugin/sd"},
332
+ sdStockDir: "/stock/plugin/sd",
333
+ },
334
+ },
335
+ }
336
+
337
+ for name, tc := range tests {
338
+ t.Run(name, func(t *testing.T) {
339
+ err := tc.dirs.validate()
340
+ if tc.wantErr {
341
+ require.Error(t, err)
342
+ } else {
343
+ require.NoError(t, err)
344
+ }
345
+ })
346
+ }
347
+}