master
go 338 lines 10.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package jobmgr
4
5 import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
14 "github.com/netdata/netdata/go/plugins/pkg/safewriter"
15 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
16 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
17 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
18 "github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
19 "github.com/stretchr/testify/assert"
20 "github.com/stretchr/testify/require"
21 )
22
23 func TestDyncfgVnodeExec_Dispatch(t *testing.T) {
24 tests := map[string]struct {
25 fn func(t *testing.T, mgr *Manager) dyncfg.Function
26 wantQueuedCmd dyncfg.Command
27 assertDirect func(t *testing.T, out string)
28 }{
29 "schema stays direct": {
30 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
31 return dyncfg.NewFunction(functions.Function{UID: "vn-schema", Args: []string{mgr.dyncfgVnodePrefixValue(), string(dyncfg.CommandSchema)}})
32 },
33 assertDirect: func(t *testing.T, out string) {
34 assert.Contains(t, out, "FUNCTION_RESULT_BEGIN vn-schema 200 application/json")
35 },
36 },
37 "userconfig stays direct": {
38 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
39 return dyncfg.NewFunction(functions.Function{
40 UID: "vn-userconfig",
41 ContentType: "application/json",
42 Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}),
43 Args: []string{mgr.dyncfgVnodePrefixValue(), string(dyncfg.CommandUserconfig), "db"},
44 })
45 },
46 assertDirect: func(t *testing.T, out string) {
47 assert.Contains(t, out, "FUNCTION_RESULT_BEGIN vn-userconfig 200 application/yaml")
48 assert.Contains(t, out, "name: db")
49 },
50 },
51 "add is queued": {
52 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
53 return dyncfg.NewFunction(functions.Function{
54 UID: "vn-add",
55 ContentType: "application/json",
56 Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}),
57 Args: []string{mgr.dyncfgVnodePrefixValue(), string(dyncfg.CommandAdd), "db"},
58 })
59 },
60 wantQueuedCmd: dyncfg.CommandAdd,
61 },
62 "update is queued": {
63 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
64 return dyncfg.NewFunction(functions.Function{
65 UID: "vn-update",
66 ContentType: "application/json",
67 Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}),
68 Args: []string{mgr.dyncfgVnodePrefixValue() + ":db", string(dyncfg.CommandUpdate)},
69 })
70 },
71 wantQueuedCmd: dyncfg.CommandUpdate,
72 },
73 "remove is queued": {
74 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
75 return dyncfg.NewFunction(functions.Function{UID: "vn-remove", Args: []string{mgr.dyncfgVnodePrefixValue() + ":db", string(dyncfg.CommandRemove)}})
76 },
77 wantQueuedCmd: dyncfg.CommandRemove,
78 },
79 "test is queued": {
80 fn: func(t *testing.T, mgr *Manager) dyncfg.Function {
81 return dyncfg.NewFunction(functions.Function{
82 UID: "vn-test",
83 ContentType: "application/json",
84 Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}),
85 Args: []string{mgr.dyncfgVnodePrefixValue(), string(dyncfg.CommandTest), "db"},
86 })
87 },
88 wantQueuedCmd: dyncfg.CommandTest,
89 },
90 }
91
92 for name, tc := range tests {
93 t.Run(name, func(t *testing.T) {
94 var buf bytes.Buffer
95 mgr := New(Config{PluginName: testPluginName})
96 mgr.ctx = context.Background()
97 mgr.dyncfgCh = make(chan dyncfg.Function, 1)
98 mgr.SetDyncfgResponder(dyncfg.NewResponder(netdataapi.New(safewriter.New(&buf))))
99
100 fn := tc.fn(t, mgr)
101 mgr.dyncfgVnodeExec(fn)
102
103 if tc.wantQueuedCmd != "" {
104 select {
105 case queued := <-mgr.dyncfgCh:
106 assert.Equal(t, tc.wantQueuedCmd, queued.Command())
107 assert.Equal(t, fn.UID(), queued.UID())
108 assert.Equal(t, "", strings.TrimSpace(buf.String()))
109 case <-time.After(time.Second):
110 t.Fatal("vnode command was not queued")
111 }
112 return
113 }
114
115 select {
116 case queued := <-mgr.dyncfgCh:
117 t.Fatalf("unexpected queued vnode command: %s", queued.Command())
118 default:
119 }
120 tc.assertDirect(t, buf.String())
121 })
122 }
123 }
124
125 func TestRun_PublishesExistingVnodesThroughController(t *testing.T) {
126 tests := map[string]struct {
127 run func(t *testing.T, out *bytes.Buffer, fnReg *recordingFunctionRegistry)
128 }{
129 "startup publishes vnode module and existing job": {
130 run: func(t *testing.T, out *bytes.Buffer, fnReg *recordingFunctionRegistry) {
131 assert.Contains(t, out.String(), "CONFIG test:vnode create accepted template /collectors/test/Vnodes")
132 assert.Contains(t, out.String(), "CONFIG test:vnode:db create running job /collectors/test/Vnodes")
133 prefixes := fnReg.registeredPrefixes()
134 assert.Contains(t, prefixes, registeredPrefix{name: "config", prefix: "test:vnode"})
135 },
136 },
137 }
138
139 for name, tc := range tests {
140 t.Run(name, func(t *testing.T) {
141 var buf bytes.Buffer
142 fnReg := &recordingFunctionRegistry{}
143 mgr := New(Config{
144 PluginName: testPluginName,
145 Out: &buf,
146 FnReg: fnReg,
147 Vnodes: map[string]*vnodes.VirtualNode{
148 "db": {
149 Name: "db",
150 Hostname: "db",
151 GUID: "11111111-1111-1111-1111-111111111111",
152 SourceType: confgroup.TypeDyncfg,
153 Source: confgroup.TypeDyncfg,
154 },
155 },
156 })
157
158 ctx, cancel := context.WithCancel(context.Background())
159 in := make(chan []*confgroup.Group)
160 done := make(chan struct{})
161 go func() {
162 mgr.Run(ctx, in)
163 close(done)
164 }()
165
166 waitCtx, waitCancel := context.WithTimeout(context.Background(), time.Second)
167 defer waitCancel()
168 require.True(t, mgr.WaitStarted(waitCtx))
169
170 cancel()
171 close(in)
172 select {
173 case <-done:
174 case <-time.After(2 * time.Second):
175 t.Fatal("manager did not stop after cancel")
176 }
177
178 tc.run(t, &buf, fnReg)
179 })
180 }
181 }
182
183 func TestCreateCollectorJob_UsesVnodeControllerLookup(t *testing.T) {
184 tests := map[string]struct {
185 cfg confgroup.Config
186 vnodes map[string]*vnodes.VirtualNode
187 wantErr string
188 run func(t *testing.T, job runtimeJob)
189 }{
190 "existing vnode is copied into created job": {
191 cfg: prepareDyncfgCfg("success", "mysql").Set("vnode", "db"),
192 vnodes: map[string]*vnodes.VirtualNode{
193 "db": {
194 Name: "db",
195 Hostname: "db",
196 GUID: "11111111-1111-1111-1111-111111111111",
197 SourceType: confgroup.TypeDyncfg,
198 Source: confgroup.TypeDyncfg,
199 },
200 },
201 run: func(t *testing.T, job runtimeJob) {
202 assert.Equal(t, "db", job.Vnode().Name)
203 },
204 },
205 "missing vnode returns an error": {
206 cfg: prepareDyncfgCfg("success", "mysql").Set("vnode", "db"),
207 wantErr: "vnode 'db' is not found",
208 },
209 }
210
211 for name, tc := range tests {
212 t.Run(name, func(t *testing.T) {
213 mgr := New(Config{PluginName: testPluginName, Vnodes: tc.vnodes})
214 mgr.modules = prepareMockRegistry()
215
216 job, err := mgr.createCollectorJob(tc.cfg)
217 if tc.wantErr != "" {
218 require.Error(t, err)
219 assert.Contains(t, err.Error(), tc.wantErr)
220 return
221 }
222
223 require.NoError(t, err)
224 tc.run(t, job)
225 })
226 }
227 }
228
229 func TestDyncfgCmdTest_ValidatesVnodeThroughControllerLookup(t *testing.T) {
230 tests := map[string]struct {
231 run func(t *testing.T, mgr *Manager, out *bytes.Buffer)
232 }{
233 "missing vnode returns 400 before worker execution": {
234 run: func(t *testing.T, mgr *Manager, out *bytes.Buffer) {
235 cfg := prepareDyncfgCfg("success", "job").Set("vnode", "missing")
236 payload, err := json.Marshal(cfg)
237 require.NoError(t, err)
238
239 fn := dyncfg.NewFunction(functions.Function{
240 UID: "collector-vnode-missing",
241 ContentType: "application/json",
242 Payload: payload,
243 Args: []string{mgr.dyncfgModID("success"), string(dyncfg.CommandTest), "job"},
244 })
245
246 mgr.dyncfgCmdTest(fn)
247
248 var resp map[string]any
249 mustDecodeFunctionPayload(t, out.String(), "collector-vnode-missing", &resp)
250 assert.Equal(t, float64(400), resp["status"])
251 assert.Contains(t, resp["errorMessage"], "missing")
252 },
253 },
254 }
255
256 for name, tc := range tests {
257 t.Run(name, func(t *testing.T) {
258 var buf bytes.Buffer
259 mgr := New(Config{PluginName: testPluginName})
260 mgr.modules = prepareMockRegistry()
261 mgr.ctx = context.Background()
262 mgr.SetDyncfgResponder(dyncfg.NewResponder(netdataapi.New(safewriter.New(&buf))))
263 tc.run(t, mgr, &buf)
264 })
265 }
266 }
267
268 func TestApplyVnodeUpdate_UpdatesMatchingRunningJobs(t *testing.T) {
269 mgr := New(Config{PluginName: testPluginName})
270
271 dbJob := &vnodeUpdateProbeJob{
272 fullName: "success_db",
273 module: "success",
274 name: "db",
275 vnode: vnodes.VirtualNode{Name: "db"},
276 }
277 otherJob := &vnodeUpdateProbeJob{
278 fullName: "success_other",
279 module: "success",
280 name: "other",
281 vnode: vnodes.VirtualNode{Name: "other"},
282 }
283
284 mgr.runningJobs.lock()
285 mgr.runningJobs.add(dbJob.FullName(), dbJob)
286 mgr.runningJobs.add(otherJob.FullName(), otherJob)
287 mgr.runningJobs.unlock()
288
289 next := &vnodes.VirtualNode{
290 Name: "db",
291 Hostname: "db-new",
292 GUID: "11111111-1111-1111-1111-111111111111",
293 SourceType: confgroup.TypeDyncfg,
294 Source: confgroup.TypeDyncfg,
295 }
296
297 mgr.applyVnodeUpdate("db", next)
298
299 require.NotNil(t, dbJob.updated)
300 assert.Equal(t, "db-new", dbJob.updated.Hostname)
301 assert.Nil(t, otherJob.updated)
302 }
303
304 type vnodeUpdateProbeJob struct {
305 fullName string
306 module string
307 name string
308 vnode vnodes.VirtualNode
309 updated *vnodes.VirtualNode
310 }
311
312 func (j *vnodeUpdateProbeJob) FullName() string { return j.fullName }
313 func (j *vnodeUpdateProbeJob) ModuleName() string { return j.module }
314 func (j *vnodeUpdateProbeJob) Name() string { return j.name }
315 func (j *vnodeUpdateProbeJob) Collector() any { return nil }
316 func (j *vnodeUpdateProbeJob) Start() {}
317 func (j *vnodeUpdateProbeJob) Stop() {}
318 func (j *vnodeUpdateProbeJob) Tick(int) {}
319 func (j *vnodeUpdateProbeJob) AutoDetection() error {
320 return nil
321 }
322 func (j *vnodeUpdateProbeJob) AutoDetectionEvery() int { return 0 }
323 func (j *vnodeUpdateProbeJob) RetryAutoDetection() bool {
324 return false
325 }
326 func (j *vnodeUpdateProbeJob) Cleanup() {}
327 func (j *vnodeUpdateProbeJob) IsRunning() bool { return true }
328 func (j *vnodeUpdateProbeJob) Panicked() bool { return false }
329 func (j *vnodeUpdateProbeJob) Vnode() vnodes.VirtualNode { return j.vnode }
330 func (j *vnodeUpdateProbeJob) UpdateVnode(vnode *vnodes.VirtualNode) {
331 if vnode == nil {
332 j.updated = nil
333 return
334 }
335 copy := *vnode
336 j.updated = &copy
337 j.vnode = copy
338 }