master
go 322 lines 9.6 KB
Raw
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/buildinfo"
19 "github.com/netdata/netdata/go/plugins/pkg/executable"
20 "github.com/netdata/netdata/go/plugins/pkg/multipath"
21 "github.com/netdata/netdata/go/plugins/pkg/terminal"
22 )
23
24 var (
25 initOnce sync.Once
26 env envData
27 dirs directories
28 )
29
30 type envData struct {
31 cygwinBase string
32 userDir string
33 stockDir string
34 varLibDir string
35 watchPath string
36 logLevel string
37 }
38
39 type directories struct {
40 // Root config
41 userConfigDirs multipath.MultiPath // includes CLI dirs (+ env user + fallback)
42 stockConfigDir string
43
44 // Collectors (derived from roots)
45 collectorsUserDirs multipath.MultiPath
46 collectorsStockDir string
47
48 // Service discovery (derived from roots)
49 sdUserDirs multipath.MultiPath
50 sdStockDir string
51
52 // Misc
53 collectorsWatch []string
54 varLibDir string
55 }
56
57 // InitInput is the minimal CLI-derived input required to initialize paths.
58 type InitInput struct {
59 ConfDir []string
60 WatchPath []string
61 }
62
63 func IsStock(path string) bool {
64 stock := StockConfigDir()
65 if stock == "" {
66 // Fallback for contexts that haven't called MustInit yet (mostly unit tests).
67 return !strings.Contains(path, "/etc/")
68 }
69 return strings.HasPrefix(path, stock)
70 }
71
72 // MustInit parses env, applies CLI overrides, discovers directories, and stores them.
73 // Safe to call multiple times; only the first call has effect.
74 func MustInit(input InitInput) {
75 initOnce.Do(func() {
76 env = readEnvFromOS(executable.Directory)
77 var d directories
78 if err := d.build(input, env, executable.Name, executable.Directory); err != nil {
79 // fail fast during startup (internal invariant was broken)
80 panic(fmt.Errorf("pluginconfig initialization failed: %w", err))
81 }
82 dirs = d
83 })
84 }
85
86 func EnvLogLevel() string { return env.logLevel }
87
88 func UserConfigDirs() multipath.MultiPath { return dirs.userConfigDirsClone() }
89 func StockConfigDir() string { return dirs.stockConfigDir }
90 func ConfigDir() multipath.MultiPath { return dirs.configDir() }
91
92 func CollectorsUserDirs() multipath.MultiPath { return dirs.collectorsUserDirsClone() }
93 func CollectorsStockDir() string { return dirs.collectorsStockDir }
94 func CollectorsDir() multipath.MultiPath { return dirs.collectorsDir() }
95
96 func ServiceDiscoveryUserDirs() multipath.MultiPath { return dirs.sdUserDirsClone() }
97 func ServiceDiscoveryStockDir() string { return dirs.sdStockDir }
98 func ServiceDiscoveryDir() multipath.MultiPath { return dirs.serviceDiscoveryDir() }
99
100 func CollectorsConfigWatchPaths() []string { return slices.Clone(dirs.collectorsWatch) }
101 func VarLibDir() string { return dirs.varLibDir }
102
103 func (d *directories) userConfigDirsClone() multipath.MultiPath {
104 return slices.Clone(d.userConfigDirs)
105 }
106 func (d *directories) collectorsUserDirsClone() multipath.MultiPath {
107 return slices.Clone(d.collectorsUserDirs)
108 }
109 func (d *directories) sdUserDirsClone() multipath.MultiPath { return slices.Clone(d.sdUserDirs) }
110
111 func (d *directories) configDir() multipath.MultiPath {
112 combined := append(d.userConfigDirsClone(), d.stockConfigDir)
113 return multipath.New(combined...)
114 }
115 func (d *directories) collectorsDir() multipath.MultiPath {
116 combined := append(d.collectorsUserDirsClone(), d.collectorsStockDir)
117 return multipath.New(combined...)
118 }
119 func (d *directories) serviceDiscoveryDir() multipath.MultiPath {
120 combined := append(d.sdUserDirsClone(), d.sdStockDir)
121 return multipath.New(combined...)
122 }
123
124 func (d *directories) build(input InitInput, env envData, execName, execDir string) error {
125 d.initUserRoots(input, env, execDir)
126 d.initStockRoot(env, execDir)
127 d.deriveCollectors(execName)
128 d.deriveServiceDiscovery(execName)
129 d.initWatchPaths(input, env)
130 d.initVarLib(env)
131 return d.validate()
132 }
133
134 // Build step 1: initialize "user" roots as a multipath: CLI (highest), env, fallback.
135 func (d *directories) initUserRoots(input InitInput, env envData, execDir string) {
136 var roots multipath.MultiPath
137
138 // 1) CLI dirs
139 for _, p := range input.ConfDir {
140 p = safePathClean(handleDirOnWin(env.cygwinBase, p, execDir))
141 roots = append(roots, p)
142 }
143
144 // 2) NETDATA_USER_CONFIG_DIR (env has priority over buildinfo)
145 if dir := safePathClean(env.userDir); dir != "" {
146 roots = append(roots, dir)
147 } else if buildinfo.UserConfigDir != "" {
148 roots = append(roots, safePathClean(buildinfo.UserConfigDir))
149 }
150
151 if len(roots) != 0 {
152 d.userConfigDirs = multipath.New(roots...)
153 return
154 }
155
156 relDir := safePathClean(filepath.Join(execDir, "..", "..", "..", "..", "etc", "netdata"))
157 relDir = handleDirOnWin(env.cygwinBase, relDir, execDir)
158 if isDirExists(relDir) {
159 d.userConfigDirs = multipath.New(relDir)
160 return
161 }
162
163 // 3) Fallback if empty
164 for _, dir := range []string{
165 handleDirOnWin(env.cygwinBase, "/etc/netdata", execDir),
166 handleDirOnWin(env.cygwinBase, "/opt/netdata/etc/netdata", execDir),
167 } {
168 if isDirExists(dir) {
169 d.userConfigDirs = multipath.New(dir)
170 return
171 }
172 }
173
174 d.userConfigDirs = multipath.New(relDir)
175 }
176
177 // Build step 2: initialize single "stock" root: env, common locations, build-relative fallback.
178 func (d *directories) initStockRoot(env envData, execDir string) {
179 // env.stockDir has priority
180 if stock := safePathClean(env.stockDir); stock != "" {
181 d.stockConfigDir = stock
182 return
183 }
184 if buildinfo.StockConfigDir != "" {
185 d.stockConfigDir = safePathClean(buildinfo.StockConfigDir)
186 return
187 }
188
189 relDir := safePathClean(filepath.Join(execDir, "..", "..", "..", "..", "usr", "lib", "netdata", "conf.d"))
190 relDir = handleDirOnWin(env.cygwinBase, relDir, execDir)
191 if isDirExists(relDir) {
192 d.stockConfigDir = relDir
193 return
194 }
195
196 for _, dir := range []string{
197 handleDirOnWin(env.cygwinBase, "/usr/lib/netdata/conf.d", execDir),
198 handleDirOnWin(env.cygwinBase, "/opt/netdata/usr/lib/netdata/conf.d", execDir),
199 } {
200 if isDirExists(dir) {
201 d.stockConfigDir = safePathClean(dir)
202 return
203 }
204 }
205
206 d.stockConfigDir = relDir
207 }
208
209 // Build step 3: derive collectors dirs from roots.
210 func (d *directories) deriveCollectors(execName string) {
211 var user multipath.MultiPath
212 for _, r := range d.userConfigDirs {
213 user = append(user, filepath.Join(safePathClean(r), execName))
214 }
215 d.collectorsUserDirs = multipath.New(user...)
216
217 if d.stockConfigDir != "" {
218 d.collectorsStockDir = filepath.Join(d.stockConfigDir, execName)
219 }
220 }
221
222 // Build step 4: derive service-discovery dirs from roots.
223 func (d *directories) deriveServiceDiscovery(execName string) {
224 var user multipath.MultiPath
225 for _, r := range d.userConfigDirs {
226 user = append(user, filepath.Join(safePathClean(r), execName, "sd"))
227 }
228 d.sdUserDirs = multipath.New(user...)
229
230 if d.stockConfigDir != "" {
231 d.sdStockDir = filepath.Join(d.stockConfigDir, execName, "sd")
232 }
233 }
234
235 // Build step 5: init watchers (normalize + dedupe via multipath.New)
236 func (d *directories) initWatchPaths(input InitInput, env envData) {
237 in := append([]string{}, input.WatchPath...)
238 if env.watchPath != "" {
239 in = append(in, env.watchPath)
240 }
241 d.collectorsWatch = multipath.New(in...)
242 }
243
244 // Build step 6: carry varlib
245 func (d *directories) initVarLib(env envData) {
246 d.varLibDir = env.varLibDir
247 }
248
249 func (d *directories) validate() error {
250 if len(d.userConfigDirs) == 0 {
251 return fmt.Errorf("pluginconfig: user config dirs not initialized")
252 }
253 if d.stockConfigDir == "" {
254 return fmt.Errorf("pluginconfig: stock config dir not initialized")
255 }
256 if len(d.collectorsUserDirs) == 0 {
257 return fmt.Errorf("pluginconfig: collectors user dirs not derived")
258 }
259 if d.collectorsStockDir == "" {
260 return fmt.Errorf("pluginconfig: collectors stock dir not derived")
261 }
262 if len(d.sdUserDirs) == 0 {
263 return fmt.Errorf("pluginconfig: sd user dirs not derived")
264 }
265 if d.sdStockDir == "" {
266 return fmt.Errorf("pluginconfig: sd stock dir not derived")
267 }
268 return nil
269 }
270
271 var isTerm = terminal.IsTerminal()
272
273 func readEnvFromOS(execDir string) envData {
274 e := envData{
275 cygwinBase: os.Getenv("NETDATA_CYGWIN_BASE_PATH"),
276 userDir: os.Getenv("NETDATA_USER_CONFIG_DIR"),
277 stockDir: os.Getenv("NETDATA_STOCK_CONFIG_DIR"),
278 watchPath: os.Getenv("NETDATA_PLUGINS_GOD_WATCH_PATH"),
279 varLibDir: os.Getenv("NETDATA_LIB_DIR"),
280 logLevel: os.Getenv("NETDATA_LOG_LEVEL"),
281 }
282 e.userDir = handleDirOnWin(e.cygwinBase, safePathClean(e.userDir), execDir)
283 e.stockDir = handleDirOnWin(e.cygwinBase, safePathClean(e.stockDir), execDir)
284 e.varLibDir = handleDirOnWin(e.cygwinBase, safePathClean(e.varLibDir), execDir)
285 e.watchPath = handleDirOnWin(e.cygwinBase, safePathClean(e.watchPath), execDir)
286
287 if isTerm {
288 e.userDir = ""
289 e.stockDir = ""
290 e.watchPath = ""
291 }
292
293 return e
294 }
295
296 // Convert a POSIX absolute (/foo) to Windows under base (e.g., C:\msys64\foo).
297 // If base is empty or p doesn’t start with '/', return p unchanged.
298 func handleDirOnWin(base, p string, execDir string) string {
299 // TODO: Temporary workaround to preserve existing behavior for debug builds running under msys64.
300 if base == "" && strings.HasPrefix(execDir, "C:\\msys64") {
301 base = "C:\\msys64"
302 }
303 if base == "" || !strings.HasPrefix(p, "/") {
304 return p
305 }
306 return filepath.Join(base, strings.TrimPrefix(p, "/"))
307 }
308
309 func isDirExists(dir string) bool {
310 fi, err := os.Stat(dir)
311 if err != nil {
312 return false
313 }
314 return fi.Mode().IsDir()
315 }
316
317 func safePathClean(p string) string {
318 if p == "" {
319 return ""
320 }
321 return filepath.Clean(p)
322 }