master
go 306 lines 9.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package sd
4
5 import (
6 "testing"
7
8 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/pipeline"
9 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
10 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
11
12 "gopkg.in/yaml.v2"
13 )
14
15 func TestServiceDiscovery_Run(t *testing.T) {
16 tests := map[string]discoverySim{
17 "add pipeline": {
18 configs: []confFile{
19 prepareConfigFile("name.conf", "name"),
20 },
21 wantPipelines: []*mockPipeline{
22 {name: "name", started: true, stopped: false},
23 },
24 },
25 "add disabled pipeline": {
26 configs: []confFile{
27 prepareDisabledConfigFile("name.conf", "name"),
28 },
29 wantPipelines: nil,
30 },
31 "add pipeline without raw name uses basename": {
32 configs: []confFile{
33 prepareUnnamedConfigFile("basename.conf"),
34 },
35 wantPipelines: []*mockPipeline{
36 {name: "basename", started: true, stopped: false},
37 },
38 },
39 "raw file name overrides basename": {
40 configs: []confFile{
41 prepareConfigFile("basename.conf", "custom-name"),
42 },
43 wantPipelines: []*mockPipeline{
44 {name: "custom-name", started: true, stopped: false},
45 },
46 },
47 "remove pipeline": {
48 configs: []confFile{
49 prepareConfigFile("name.conf", "name"),
50 prepareEmptyConfigFile("name.conf"),
51 },
52 wantPipelines: []*mockPipeline{
53 {name: "name", started: true, stopped: true},
54 },
55 },
56 "re-add pipeline multiple times": {
57 // With the new stability logic, re-adding the same config from the same source
58 // when it's already running is a no-op. Only 1 pipeline should be created.
59 configs: []confFile{
60 prepareConfigFile("name.conf", "name"),
61 prepareConfigFile("name.conf", "name"),
62 prepareConfigFile("name.conf", "name"),
63 },
64 wantPipelines: []*mockPipeline{
65 {name: "name", started: true, stopped: false},
66 },
67 },
68 "restart pipeline": {
69 configs: []confFile{
70 prepareConfigFile("name1.conf", "name1"),
71 prepareEmptyConfigFile("name1.conf"),
72 prepareConfigFile("name2.conf", "name2"),
73 },
74 wantPipelines: []*mockPipeline{
75 {name: "name1", started: true, stopped: true},
76 {name: "name2", started: true, stopped: false},
77 },
78 },
79 "invalid pipeline config": {
80 configs: []confFile{
81 prepareInvalidConfigFile("invalid.conf"),
82 },
83 wantPipelines: nil,
84 },
85 "invalid config for running pipeline with same basename is ignored": {
86 configs: []confFile{
87 prepareConfigFile("name.conf", "name"),
88 prepareInvalidConfigFile("name.conf"),
89 },
90 wantPipelines: []*mockPipeline{
91 {name: "name", started: true, stopped: false},
92 },
93 },
94 }
95
96 for name, sim := range tests {
97 t.Run(name, func(t *testing.T) {
98 sim.run(t)
99 })
100 }
101 }
102
103 func TestServiceDiscovery_UnsupportedDiscovererConfigIsIgnored(t *testing.T) {
104 sim := &discoverySimExt{
105 configs: []confFile{
106 prepareUnsupportedDiscovererConfigFile("/usr/lib/netdata/conf.d/sd/unsupported.conf", "unsupported"),
107 },
108 wantPipelines: nil,
109 wantExposedCount: 0,
110 }
111 sim.run(t)
112 }
113
114 func prepareConfigFile(source, name string) confFile {
115 disc, _ := pipeline.NewDiscovererPayload(testDiscovererTypeNetListeners, testNetListenersConfig{})
116 cfg := pipeline.Config{
117 Name: name,
118 Discoverer: disc,
119 Services: defaultTestServices(),
120 }
121 bs, _ := yaml.Marshal(cfg)
122
123 return confFile{
124 source: source,
125 content: bs,
126 }
127 }
128
129 func prepareUnsupportedDiscovererConfigFile(source, name string) confFile {
130 disc, _ := pipeline.NewDiscovererPayload("unsupported", map[string]any{})
131 cfg := pipeline.Config{
132 Name: name,
133 Discoverer: disc,
134 Services: defaultTestServices(),
135 }
136 bs, _ := yaml.Marshal(cfg)
137
138 return confFile{
139 source: source,
140 content: bs,
141 }
142 }
143
144 func prepareUnnamedConfigFile(source string) confFile {
145 disc, _ := pipeline.NewDiscovererPayload(testDiscovererTypeNetListeners, testNetListenersConfig{})
146 cfg := pipeline.Config{
147 Discoverer: disc,
148 Services: defaultTestServices(),
149 }
150 bs, _ := yaml.Marshal(cfg)
151
152 return confFile{
153 source: source,
154 content: bs,
155 }
156 }
157
158 func prepareEmptyConfigFile(source string) confFile {
159 return confFile{
160 source: source,
161 }
162 }
163
164 func prepareDisabledConfigFile(source, name string) confFile {
165 disc, _ := pipeline.NewDiscovererPayload(testDiscovererTypeNetListeners, testNetListenersConfig{})
166 cfg := pipeline.Config{
167 Name: name,
168 Disabled: true,
169 Discoverer: disc,
170 Services: defaultTestServices(),
171 }
172 bs, _ := yaml.Marshal(cfg)
173
174 return confFile{
175 source: source,
176 content: bs,
177 }
178 }
179
180 func prepareInvalidConfigFile(source string) confFile {
181 disc, _ := pipeline.NewDiscovererPayload(testDiscovererTypeNetListeners, testNetListenersConfig{})
182 cfg := pipeline.Config{
183 Discoverer: disc,
184 }
185 bs, _ := yaml.Marshal(cfg)
186
187 return confFile{
188 source: source,
189 content: bs,
190 }
191 }
192
193 // prepareStockConfigFile creates a config from a stock path (priority 2)
194 func prepareStockConfigFile(name string) confFile {
195 return prepareConfigFile("/usr/lib/netdata/conf.d/sd/"+name+".conf", name)
196 }
197
198 // prepareUserConfigFile creates a config from a user path (priority 8)
199 // User paths contain ".d/" pattern
200 func prepareUserConfigFile(name string) confFile {
201 return prepareConfigFile("/etc/netdata/sd.d/"+name+".conf", name)
202 }
203
204 func TestServiceDiscovery_Priority(t *testing.T) {
205 tests := map[string]discoverySimExt{
206 "stock then user with same name - user wins": {
207 // Stock config arrives first, then user config with same name
208 // User has higher priority, should replace stock
209 configs: []confFile{
210 prepareStockConfigFile("myconfig"),
211 prepareUserConfigFile("myconfig"),
212 },
213 wantPipelines: []*mockPipeline{
214 {name: "myconfig", started: true, stopped: true}, // stock stopped
215 {name: "myconfig", started: true, stopped: false}, // user running
216 },
217 wantExposedCount: 1,
218 wantExposed: []wantExposedCfg{
219 {discovererType: "net_listeners", name: "myconfig", sourceType: confgroup.TypeUser, status: dyncfg.StatusRunning},
220 },
221 },
222 "user then stock with same name - user keeps": {
223 // User config arrives first, then stock config with same name
224 // User has higher priority, should keep user
225 configs: []confFile{
226 prepareUserConfigFile("myconfig"),
227 prepareStockConfigFile("myconfig"),
228 },
229 wantPipelines: []*mockPipeline{
230 {name: "myconfig", started: true, stopped: false}, // user keeps running
231 },
232 wantExposedCount: 1,
233 wantExposed: []wantExposedCfg{
234 {discovererType: "net_listeners", name: "myconfig", sourceType: confgroup.TypeUser, status: dyncfg.StatusRunning},
235 },
236 },
237 "stock then stock with same name - existing keeps if running": {
238 // Two stock configs with same name from different files
239 // Same priority + running = keep existing
240 configs: []confFile{
241 prepareConfigFile("/usr/lib/netdata/conf.d/sd/dir1/myconfig.conf", "myconfig"),
242 prepareConfigFile("/usr/lib/netdata/conf.d/sd/dir2/myconfig.conf", "myconfig"),
243 },
244 wantPipelines: []*mockPipeline{
245 {name: "myconfig", started: true, stopped: false}, // first stock keeps running
246 },
247 wantExposedCount: 1,
248 wantExposed: []wantExposedCfg{
249 {discovererType: "net_listeners", name: "myconfig", sourceType: confgroup.TypeStock, status: dyncfg.StatusRunning},
250 },
251 },
252 "user then user with same name - existing keeps if running": {
253 // Two user configs with same name from different files
254 // Same priority + running = keep existing
255 configs: []confFile{
256 prepareConfigFile("/etc/netdata/sd.d/dir1/myconfig.conf", "myconfig"),
257 prepareConfigFile("/etc/netdata/sd.d/dir2/myconfig.conf", "myconfig"),
258 },
259 wantPipelines: []*mockPipeline{
260 {name: "myconfig", started: true, stopped: false}, // first user keeps running
261 },
262 wantExposedCount: 1,
263 wantExposed: []wantExposedCfg{
264 {discovererType: "net_listeners", name: "myconfig", sourceType: confgroup.TypeUser, status: dyncfg.StatusRunning},
265 },
266 },
267 "remove non-exposed config - no dyncfg change": {
268 // User config exposed, then stock config arrives (not exposed due to lower priority)
269 // When stock file is "removed" (empty content), nothing should change
270 configs: []confFile{
271 prepareUserConfigFile("myconfig"),
272 prepareStockConfigFile("myconfig"),
273 prepareEmptyConfigFile("/usr/lib/netdata/conf.d/sd/myconfig.conf"), // remove stock
274 },
275 wantPipelines: []*mockPipeline{
276 {name: "myconfig", started: true, stopped: false}, // user keeps running
277 },
278 wantExposedCount: 1,
279 wantExposed: []wantExposedCfg{
280 {discovererType: "net_listeners", name: "myconfig", sourceType: confgroup.TypeUser, status: dyncfg.StatusRunning},
281 },
282 },
283 "multiple configs different names": {
284 // Stock and user configs with different names - both should run
285 configs: []confFile{
286 prepareStockConfigFile("stock-config"),
287 prepareUserConfigFile("user-config"),
288 },
289 wantPipelines: []*mockPipeline{
290 {name: "stock-config", started: true, stopped: false},
291 {name: "user-config", started: true, stopped: false},
292 },
293 wantExposedCount: 2,
294 wantExposed: []wantExposedCfg{
295 {discovererType: "net_listeners", name: "stock-config", sourceType: confgroup.TypeStock, status: dyncfg.StatusRunning},
296 {discovererType: "net_listeners", name: "user-config", sourceType: confgroup.TypeUser, status: dyncfg.StatusRunning},
297 },
298 },
299 }
300
301 for name, sim := range tests {
302 t.Run(name, func(t *testing.T) {
303 sim.run(t)
304 })
305 }
306 }