@cryptotaxi247 / netdata / commits / ea44b664f

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 +}