| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package file |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | |
| 10 | "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup" |
| 11 | |
| 12 | "gopkg.in/yaml.v2" |
| 13 | ) |
| 14 | |
| 15 | type format int |
| 16 | |
| 17 | const ( |
| 18 | unknownFormat format = iota |
| 19 | unknownEmptyFormat |
| 20 | staticFormat |
| 21 | sdFormat |
| 22 | ) |
| 23 | |
| 24 | func parse(req confgroup.Registry, path string) (*confgroup.Group, error) { |
| 25 | bs, err := os.ReadFile(path) |
| 26 | if err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | if len(bs) == 0 { |
| 30 | return nil, nil |
| 31 | } |
| 32 | |
| 33 | switch cfgFormat(bs) { |
| 34 | case staticFormat: |
| 35 | return parseStaticFormat(req, path, bs) |
| 36 | case sdFormat: |
| 37 | return parseSDFormat(req, path, bs) |
| 38 | case unknownEmptyFormat: |
| 39 | return nil, nil |
| 40 | default: |
| 41 | return nil, fmt.Errorf("unknown file format: '%s'", path) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func parseStaticFormat(reg confgroup.Registry, path string, bs []byte) (*confgroup.Group, error) { |
| 46 | name := fileName(path) |
| 47 | // TODO: properly handle module renaming |
| 48 | // See agent/setup.go buildDiscoveryConf() for details |
| 49 | if name == "wmi" { |
| 50 | name = "windows" |
| 51 | } |
| 52 | modDef, ok := reg.Lookup(name) |
| 53 | if !ok { |
| 54 | return nil, nil |
| 55 | } |
| 56 | |
| 57 | var modCfg staticConfig |
| 58 | if err := yaml.Unmarshal(bs, &modCfg); err != nil { |
| 59 | return nil, err |
| 60 | } |
| 61 | |
| 62 | for _, cfg := range modCfg.Jobs { |
| 63 | cfg.SetModule(name) |
| 64 | def := mergeDef(modCfg.Default, modDef) |
| 65 | cfg.ApplyDefaults(def) |
| 66 | } |
| 67 | |
| 68 | group := &confgroup.Group{ |
| 69 | Configs: modCfg.Jobs, |
| 70 | Source: path, |
| 71 | } |
| 72 | |
| 73 | return group, nil |
| 74 | } |
| 75 | |
| 76 | func parseSDFormat(reg confgroup.Registry, path string, bs []byte) (*confgroup.Group, error) { |
| 77 | var cfgs sdConfig |
| 78 | if err := yaml.Unmarshal(bs, &cfgs); err != nil { |
| 79 | return nil, err |
| 80 | } |
| 81 | |
| 82 | var i int |
| 83 | for _, cfg := range cfgs { |
| 84 | if def, ok := reg.Lookup(cfg.Module()); ok && cfg.Module() != "" { |
| 85 | cfg.ApplyDefaults(def) |
| 86 | cfgs[i] = cfg |
| 87 | i++ |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | group := &confgroup.Group{ |
| 92 | Configs: cfgs[:i], |
| 93 | Source: path, |
| 94 | } |
| 95 | |
| 96 | return group, nil |
| 97 | } |
| 98 | |
| 99 | func cfgFormat(bs []byte) format { |
| 100 | var data any |
| 101 | if err := yaml.Unmarshal(bs, &data); err != nil { |
| 102 | return unknownFormat |
| 103 | } |
| 104 | if data == nil { |
| 105 | return unknownEmptyFormat |
| 106 | } |
| 107 | |
| 108 | type ( |
| 109 | static = map[any]any |
| 110 | sd = []any |
| 111 | ) |
| 112 | switch data.(type) { |
| 113 | case static: |
| 114 | return staticFormat |
| 115 | case sd: |
| 116 | return sdFormat |
| 117 | default: |
| 118 | return unknownFormat |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func mergeDef(a, b confgroup.Default) confgroup.Default { |
| 123 | return confgroup.Default{ |
| 124 | MinUpdateEvery: firstPositive(a.MinUpdateEvery, b.MinUpdateEvery), |
| 125 | UpdateEvery: firstPositive(a.UpdateEvery, b.UpdateEvery), |
| 126 | AutoDetectionRetry: firstPositive(a.AutoDetectionRetry, b.AutoDetectionRetry), |
| 127 | Priority: firstPositive(a.Priority, b.Priority), |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | func firstPositive(value int, others ...int) int { |
| 132 | if value > 0 || len(others) == 0 { |
| 133 | return value |
| 134 | } |
| 135 | return firstPositive(others[0], others[1:]...) |
| 136 | } |
| 137 | |
| 138 | func fileName(path string) string { |
| 139 | _, file := filepath.Split(path) |
| 140 | ext := filepath.Ext(path) |
| 141 | return file[:len(file)-len(ext)] |
| 142 | } |