master
go 198 lines 6.42 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package sd
4
5 import (
6 "encoding/json"
7 "fmt"
8 "path/filepath"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
12 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/pipeline"
13 "github.com/netdata/netdata/go/plugins/plugin/agent/internal/naming"
14 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
15
16 "github.com/gohugoio/hashstructure"
17 "gopkg.in/yaml.v2"
18 )
19
20 // Internal metadata keys (excluded from JSON output, same pattern as confgroup.Config)
21 const (
22 ikeySource = "__source__"
23 ikeySourceType = "__source_type__"
24 ikeyDiscovererType = "__discoverer_type__"
25 ikeyPipelineKey = "__pipeline_key__"
26 )
27
28 // sdConfig represents a service discovery pipeline configuration.
29 // Uses map[string]any with __ metadata fields, same pattern as confgroup.Config.
30 // The actual config data is stored alongside metadata and parsed to pipeline.Config only when needed.
31 type sdConfig map[string]any
32
33 func (c sdConfig) Source() string { v, _ := c[ikeySource].(string); return v }
34 func (c sdConfig) SourceType() string { v, _ := c[ikeySourceType].(string); return v }
35 func (c sdConfig) DiscovererType() string { v, _ := c[ikeyDiscovererType].(string); return v }
36 func (c sdConfig) PipelineKey() string { v, _ := c[ikeyPipelineKey].(string); return v }
37 func (c sdConfig) Name() string { v, _ := c["name"].(string); return v }
38
39 // HashIncludeMap implements hashstructure.HashIncludeMap to exclude __ metadata keys from hashing.
40 // Same pattern as confgroup.Config.
41 func (c sdConfig) HashIncludeMap(_ string, k, _ any) (bool, error) {
42 s := k.(string)
43 return !strings.HasPrefix(s, "__") && !strings.HasSuffix(s, "__"), nil
44 }
45
46 // Hash returns a hash of the config data (excluding __ metadata keys).
47 // Used for comparing configs to detect changes.
48 func (c sdConfig) Hash() uint64 {
49 hash, _ := hashstructure.Hash(c, nil)
50 return hash
51 }
52
53 func (c sdConfig) SetSource(v string) sdConfig { c[ikeySource] = v; return c }
54 func (c sdConfig) SetSourceType(v string) sdConfig { c[ikeySourceType] = v; return c }
55 func (c sdConfig) SetDiscovererType(v string) sdConfig { c[ikeyDiscovererType] = v; return c }
56 func (c sdConfig) SetPipelineKey(v string) sdConfig { c[ikeyPipelineKey] = v; return c }
57
58 // ExposedKey returns the logical key for ExposedCache: "discovererType:name"
59 func (c sdConfig) ExposedKey() string {
60 return c.DiscovererType() + ":" + c.Name()
61 }
62
63 // UID returns the unique key for seenConfigs: "source:discovererType:name"
64 func (c sdConfig) UID() string {
65 return c.Source() + ":" + c.ExposedKey()
66 }
67
68 // SourceTypePriority returns priority based on source type.
69 // Higher value = higher priority. Matches confgroup.Config pattern.
70 func (c sdConfig) SourceTypePriority() int {
71 switch c.SourceType() {
72 case confgroup.TypeDyncfg:
73 return 16
74 case confgroup.TypeUser:
75 return 8
76 case confgroup.TypeStock:
77 return 2
78 default:
79 return 0
80 }
81 }
82
83 // ToPipelineConfig converts sdConfig to pipeline.Config for actually running the pipeline.
84 // This parses the config data (excluding __ fields) into the typed struct.
85 func (c sdConfig) ToPipelineConfig(configDefaults confgroup.Registry) (pipeline.Config, error) {
86 // Marshal without __ fields, then unmarshal to pipeline.Config
87 data := c.DataJSON()
88
89 var cfg pipeline.Config
90 if err := json.Unmarshal(data, &cfg); err != nil {
91 return pipeline.Config{}, fmt.Errorf("unmarshal pipeline config: %w", err)
92 }
93 cfg.Name = c.Name()
94
95 cfg.ConfigDefaults = configDefaults
96
97 // Set source based on source type
98 switch c.SourceType() {
99 case confgroup.TypeDyncfg:
100 cfg.Source = fmt.Sprintf("dyncfg=%s", c.Source())
101 default:
102 cfg.Source = fmt.Sprintf("file=%s", c.Source())
103 }
104
105 return cfg, nil
106 }
107
108 // DataJSON returns JSON representation of config data (excluding __ metadata fields).
109 // Used for dyncfg get command and for converting to pipeline.Config.
110 func (c sdConfig) DataJSON() []byte {
111 data := make(map[string]any, len(c))
112 for k, v := range c {
113 if !strings.HasPrefix(k, "__") {
114 data[k] = v
115 }
116 }
117 b, _ := json.Marshal(data)
118 return b
119 }
120
121 // newSDConfigFromYAML creates an sdConfig from YAML bytes.
122 // Used when loading file configs. The stored name prefers raw config content,
123 // falling back to the file basename, and is cleaned for dyncfg compatibility.
124 func newSDConfigFromYAML(data []byte, source, sourceType, pipelineKey string) (sdConfig, error) {
125 // First unmarshal to pipeline.Config to get discoverer type and apply YAML processing
126 var cfg pipeline.Config
127 if err := yaml.Unmarshal(data, &cfg); err != nil {
128 return nil, fmt.Errorf("unmarshal yaml: %w", err)
129 }
130
131 // Now marshal to JSON and unmarshal to map for sdConfig
132 jsonData, err := json.Marshal(cfg)
133 if err != nil {
134 return nil, fmt.Errorf("marshal to json: %w", err)
135 }
136
137 var m sdConfig
138 if err := json.Unmarshal(jsonData, &m); err != nil {
139 return nil, fmt.Errorf("unmarshal to map: %w", err)
140 }
141
142 name := strings.TrimSpace(cfg.Name)
143 if name == "" {
144 name = configNameFromSource(source)
145 }
146 if name != "" {
147 m["name"] = naming.Sanitize(name)
148 }
149
150 // Add metadata
151 m.SetSource(source)
152 m.SetSourceType(sourceType)
153 m.SetDiscovererType(cfg.Discoverer.Type())
154 m.SetPipelineKey(pipelineKey)
155
156 return m, nil
157 }
158
159 // newSDConfigFromJSON creates an sdConfig from JSON payload.
160 // Used when receiving dyncfg add/update commands.
161 // The name parameter is forced onto the config (from dyncfg job ID), matching jobmgr pattern.
162 func newSDConfigFromJSON(data []byte, name, source, sourceType, discovererType, pipelineKey string) (sdConfig, error) {
163 var m sdConfig
164 if err := json.Unmarshal(data, &m); err != nil {
165 return nil, fmt.Errorf("unmarshal json: %w", err)
166 }
167 if m == nil {
168 return nil, fmt.Errorf("unmarshal json: got nil map")
169 }
170
171 // Force name from dyncfg job ID (matching jobmgr pattern: cfg.SetName(name))
172 // This ensures sdConfig.ExposedKey() matches the dyncfg job ID regardless of payload content
173 m["name"] = naming.Sanitize(name)
174
175 // Add metadata
176 m.SetSource(source)
177 m.SetSourceType(sourceType)
178 m.SetDiscovererType(discovererType)
179 m.SetPipelineKey(pipelineKey)
180
181 return m, nil
182 }
183
184 // sourceTypeFromPath determines the source type (stock/user) from a file path.
185 func sourceTypeFromPath(path string) string {
186 if pluginconfig.IsStock(path) {
187 return confgroup.TypeStock
188 }
189 return confgroup.TypeUser
190 }
191
192 func configNameFromSource(source string) string {
193 base := filepath.Base(strings.TrimSpace(source))
194 if before, ok := strings.CutSuffix(base, ".conf"); ok {
195 base = before
196 }
197 return base
198 }