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