| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package vnodectl |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "encoding/json" |
| 8 | "fmt" |
| 9 | "regexp" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/netdata/netdata/go/plugins/logger" |
| 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 | const testPluginName = "test" |
| 24 | |
| 25 | func TestControllerSeqExec(t *testing.T) { |
| 26 | tests := map[string]struct { |
| 27 | initial map[string]*vnodes.VirtualNode |
| 28 | run func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) |
| 29 | }{ |
| 30 | "schema dispatch": { |
| 31 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 32 | fn := dyncfg.NewFunction(functions.Function{UID: "vn-schema", Args: []string{ctl.Prefix(), string(dyncfg.CommandSchema)}}) |
| 33 | ctl.SeqExec(fn) |
| 34 | |
| 35 | assert.Contains(t, out.String(), "FUNCTION_RESULT_BEGIN vn-schema 200 application/json") |
| 36 | assert.Empty(t, seams.affectedJobsCalls) |
| 37 | assert.Empty(t, seams.applyCalls) |
| 38 | }, |
| 39 | }, |
| 40 | "userconfig generation": { |
| 41 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 42 | fn := dyncfg.NewFunction(functions.Function{ |
| 43 | UID: "vn-userconfig", |
| 44 | ContentType: "application/json", |
| 45 | Payload: mustJSON(t, map[string]any{ |
| 46 | "guid": "11111111-1111-1111-1111-111111111111", |
| 47 | "labels": map[string]string{"env": "prod"}, |
| 48 | }), |
| 49 | Args: []string{ctl.Prefix(), string(dyncfg.CommandUserconfig), "db"}, |
| 50 | }) |
| 51 | ctl.SeqExec(fn) |
| 52 | |
| 53 | body := mustFunctionBody(t, out.String(), "vn-userconfig") |
| 54 | assert.Contains(t, out.String(), "FUNCTION_RESULT_BEGIN vn-userconfig 200 application/yaml") |
| 55 | assert.Contains(t, body, "name: db") |
| 56 | assert.Contains(t, body, "hostname: db") |
| 57 | assert.Empty(t, seams.affectedJobsCalls) |
| 58 | assert.Empty(t, seams.applyCalls) |
| 59 | }, |
| 60 | }, |
| 61 | "get returns stored config": { |
| 62 | initial: map[string]*vnodes.VirtualNode{ |
| 63 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 64 | }, |
| 65 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 66 | fn := dyncfg.NewFunction(functions.Function{UID: "vn-get", Args: []string{ctl.configID("db"), string(dyncfg.CommandGet)}}) |
| 67 | ctl.SeqExec(fn) |
| 68 | |
| 69 | var payload map[string]any |
| 70 | mustDecodeFunctionPayload(t, out.String(), "vn-get", &payload) |
| 71 | assert.Equal(t, "db", payload["name"]) |
| 72 | assert.Equal(t, "11111111-1111-1111-1111-111111111111", payload["guid"]) |
| 73 | assert.Empty(t, seams.affectedJobsCalls) |
| 74 | assert.Empty(t, seams.applyCalls) |
| 75 | }, |
| 76 | }, |
| 77 | "add applies vnode update seam": { |
| 78 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 79 | fn := dyncfg.NewFunction(functions.Function{ |
| 80 | UID: "vn-add", |
| 81 | ContentType: "application/json", |
| 82 | Payload: mustJSON(t, map[string]any{ |
| 83 | "guid": "11111111-1111-1111-1111-111111111111", |
| 84 | "labels": map[string]string{"env": "prod"}, |
| 85 | }), |
| 86 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), "db"}, |
| 87 | }) |
| 88 | ctl.SeqExec(fn) |
| 89 | |
| 90 | var payload map[string]any |
| 91 | mustDecodeFunctionPayload(t, out.String(), "vn-add", &payload) |
| 92 | assert.Equal(t, float64(202), payload["status"]) |
| 93 | assert.Equal(t, []string{"db"}, seams.applyCalls) |
| 94 | cfg, ok := ctl.Lookup("db") |
| 95 | require.True(t, ok) |
| 96 | assert.Equal(t, confgroup.TypeDyncfg, cfg.SourceType) |
| 97 | }, |
| 98 | }, |
| 99 | "add no-op keeps apply seam unused": { |
| 100 | initial: map[string]*vnodes.VirtualNode{ |
| 101 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 102 | }, |
| 103 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 104 | fn := dyncfg.NewFunction(functions.Function{ |
| 105 | UID: "vn-add-noop", |
| 106 | ContentType: "application/json", |
| 107 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 108 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), "db"}, |
| 109 | }) |
| 110 | ctl.SeqExec(fn) |
| 111 | |
| 112 | var payload map[string]any |
| 113 | mustDecodeFunctionPayload(t, out.String(), "vn-add-noop", &payload) |
| 114 | assert.Equal(t, float64(202), payload["status"]) |
| 115 | assert.Empty(t, seams.applyCalls) |
| 116 | }, |
| 117 | }, |
| 118 | "add equal user vnode rewrites stored source metadata to dyncfg": { |
| 119 | initial: map[string]*vnodes.VirtualNode{ |
| 120 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeUser), |
| 121 | }, |
| 122 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 123 | fn := dyncfg.NewFunction(functions.Function{ |
| 124 | UID: "vn-add-promote-user", |
| 125 | Source: "user=alice", |
| 126 | ContentType: "application/json", |
| 127 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 128 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), "db"}, |
| 129 | }) |
| 130 | ctl.SeqExec(fn) |
| 131 | |
| 132 | var payload map[string]any |
| 133 | mustDecodeFunctionPayload(t, out.String(), "vn-add-promote-user", &payload) |
| 134 | assert.Equal(t, float64(202), payload["status"]) |
| 135 | assert.Equal(t, []string{"db"}, seams.applyCalls) |
| 136 | |
| 137 | cfg, ok := ctl.Lookup("db") |
| 138 | require.True(t, ok) |
| 139 | assert.Equal(t, confgroup.TypeDyncfg, cfg.SourceType) |
| 140 | assert.Equal(t, "user=alice", cfg.Source) |
| 141 | }, |
| 142 | }, |
| 143 | "duplicate hostname is rejected": { |
| 144 | initial: map[string]*vnodes.VirtualNode{ |
| 145 | "other": testVnode("other", "shared", "22222222-2222-2222-2222-222222222222", confgroup.TypeUser), |
| 146 | }, |
| 147 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 148 | fn := dyncfg.NewFunction(functions.Function{ |
| 149 | UID: "vn-add-dup-host", |
| 150 | ContentType: "application/json", |
| 151 | Payload: mustJSON(t, map[string]any{ |
| 152 | "guid": "33333333-3333-3333-3333-333333333333", |
| 153 | "hostname": "shared", |
| 154 | }), |
| 155 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), "db"}, |
| 156 | }) |
| 157 | ctl.SeqExec(fn) |
| 158 | |
| 159 | var payload map[string]any |
| 160 | mustDecodeFunctionPayload(t, out.String(), "vn-add-dup-host", &payload) |
| 161 | assert.Equal(t, float64(400), payload["status"]) |
| 162 | assert.Contains(t, fmt.Sprint(payload["errorMessage"]), "duplicate virtual node hostname") |
| 163 | assert.Empty(t, seams.applyCalls) |
| 164 | }, |
| 165 | }, |
| 166 | "update applies vnode update seam": { |
| 167 | initial: map[string]*vnodes.VirtualNode{ |
| 168 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 169 | }, |
| 170 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 171 | fn := dyncfg.NewFunction(functions.Function{ |
| 172 | UID: "vn-update", |
| 173 | ContentType: "application/json", |
| 174 | Payload: mustJSON(t, map[string]any{ |
| 175 | "guid": "11111111-1111-1111-1111-111111111111", |
| 176 | "labels": map[string]string{"team": "db"}, |
| 177 | }), |
| 178 | Args: []string{ctl.configID("db"), string(dyncfg.CommandUpdate)}, |
| 179 | }) |
| 180 | ctl.SeqExec(fn) |
| 181 | |
| 182 | var payload map[string]any |
| 183 | mustDecodeFunctionPayload(t, out.String(), "vn-update", &payload) |
| 184 | assert.Equal(t, float64(202), payload["status"]) |
| 185 | assert.Equal(t, []string{"db"}, seams.applyCalls) |
| 186 | }, |
| 187 | }, |
| 188 | "update no-op keeps apply seam unused": { |
| 189 | initial: map[string]*vnodes.VirtualNode{ |
| 190 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 191 | }, |
| 192 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 193 | fn := dyncfg.NewFunction(functions.Function{ |
| 194 | UID: "vn-update-noop", |
| 195 | ContentType: "application/json", |
| 196 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 197 | Args: []string{ctl.configID("db"), string(dyncfg.CommandUpdate)}, |
| 198 | }) |
| 199 | ctl.SeqExec(fn) |
| 200 | |
| 201 | var payload map[string]any |
| 202 | mustDecodeFunctionPayload(t, out.String(), "vn-update-noop", &payload) |
| 203 | assert.Equal(t, float64(202), payload["status"]) |
| 204 | assert.Empty(t, seams.applyCalls) |
| 205 | }, |
| 206 | }, |
| 207 | "update equal user vnode rewrites stored source metadata to dyncfg": { |
| 208 | initial: map[string]*vnodes.VirtualNode{ |
| 209 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeUser), |
| 210 | }, |
| 211 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 212 | fn := dyncfg.NewFunction(functions.Function{ |
| 213 | UID: "vn-update-promote-user", |
| 214 | Source: "user=alice", |
| 215 | ContentType: "application/json", |
| 216 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 217 | Args: []string{ctl.configID("db"), string(dyncfg.CommandUpdate)}, |
| 218 | }) |
| 219 | ctl.SeqExec(fn) |
| 220 | |
| 221 | var payload map[string]any |
| 222 | mustDecodeFunctionPayload(t, out.String(), "vn-update-promote-user", &payload) |
| 223 | assert.Equal(t, float64(202), payload["status"]) |
| 224 | assert.Equal(t, []string{"db"}, seams.applyCalls) |
| 225 | |
| 226 | cfg, ok := ctl.Lookup("db") |
| 227 | require.True(t, ok) |
| 228 | assert.Equal(t, confgroup.TypeDyncfg, cfg.SourceType) |
| 229 | assert.Equal(t, "user=alice", cfg.Source) |
| 230 | }, |
| 231 | }, |
| 232 | "update rejects duplicate hostname": { |
| 233 | initial: map[string]*vnodes.VirtualNode{ |
| 234 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 235 | "other": testVnode("other", "shared", "22222222-2222-2222-2222-222222222222", confgroup.TypeUser), |
| 236 | }, |
| 237 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 238 | fn := dyncfg.NewFunction(functions.Function{ |
| 239 | UID: "vn-update-dup-host", |
| 240 | ContentType: "application/json", |
| 241 | Payload: mustJSON(t, map[string]any{ |
| 242 | "guid": "11111111-1111-1111-1111-111111111111", |
| 243 | "hostname": "shared", |
| 244 | }), |
| 245 | Args: []string{ctl.configID("db"), string(dyncfg.CommandUpdate)}, |
| 246 | }) |
| 247 | ctl.SeqExec(fn) |
| 248 | |
| 249 | var payload map[string]any |
| 250 | mustDecodeFunctionPayload(t, out.String(), "vn-update-dup-host", &payload) |
| 251 | assert.Equal(t, float64(400), payload["status"]) |
| 252 | assert.Contains(t, fmt.Sprint(payload["errorMessage"]), "duplicate virtual node hostname") |
| 253 | assert.Empty(t, seams.applyCalls) |
| 254 | }, |
| 255 | }, |
| 256 | "update rejects duplicate guid": { |
| 257 | initial: map[string]*vnodes.VirtualNode{ |
| 258 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 259 | "other": testVnode("other", "other", "22222222-2222-2222-2222-222222222222", confgroup.TypeUser), |
| 260 | }, |
| 261 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 262 | fn := dyncfg.NewFunction(functions.Function{ |
| 263 | UID: "vn-update-dup-guid", |
| 264 | ContentType: "application/json", |
| 265 | Payload: mustJSON(t, map[string]any{ |
| 266 | "guid": "22222222-2222-2222-2222-222222222222", |
| 267 | }), |
| 268 | Args: []string{ctl.configID("db"), string(dyncfg.CommandUpdate)}, |
| 269 | }) |
| 270 | ctl.SeqExec(fn) |
| 271 | |
| 272 | var payload map[string]any |
| 273 | mustDecodeFunctionPayload(t, out.String(), "vn-update-dup-guid", &payload) |
| 274 | assert.Equal(t, float64(400), payload["status"]) |
| 275 | assert.Contains(t, fmt.Sprint(payload["errorMessage"]), "duplicate virtual node guid") |
| 276 | assert.Empty(t, seams.applyCalls) |
| 277 | }, |
| 278 | }, |
| 279 | "invalid guid is rejected": { |
| 280 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 281 | fn := dyncfg.NewFunction(functions.Function{ |
| 282 | UID: "vn-invalid-guid", |
| 283 | ContentType: "application/json", |
| 284 | Payload: mustJSON(t, map[string]any{"guid": "bad-guid"}), |
| 285 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), "db"}, |
| 286 | }) |
| 287 | ctl.SeqExec(fn) |
| 288 | |
| 289 | var payload map[string]any |
| 290 | mustDecodeFunctionPayload(t, out.String(), "vn-invalid-guid", &payload) |
| 291 | assert.Equal(t, float64(400), payload["status"]) |
| 292 | assert.Empty(t, seams.applyCalls) |
| 293 | }, |
| 294 | }, |
| 295 | "empty vnode name is rejected": { |
| 296 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 297 | fn := dyncfg.NewFunction(functions.Function{ |
| 298 | UID: "vn-empty-name", |
| 299 | ContentType: "application/json", |
| 300 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 301 | Args: []string{ctl.Prefix(), string(dyncfg.CommandAdd), ""}, |
| 302 | }) |
| 303 | ctl.SeqExec(fn) |
| 304 | |
| 305 | var payload map[string]any |
| 306 | mustDecodeFunctionPayload(t, out.String(), "vn-empty-name", &payload) |
| 307 | assert.Equal(t, float64(400), payload["status"]) |
| 308 | assert.Contains(t, fmt.Sprint(payload["errorMessage"]), "Missing vnode name") |
| 309 | assert.Empty(t, seams.applyCalls) |
| 310 | }, |
| 311 | }, |
| 312 | "remove rejects non dyncfg source": { |
| 313 | initial: map[string]*vnodes.VirtualNode{ |
| 314 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeUser), |
| 315 | }, |
| 316 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 317 | fn := dyncfg.NewFunction(functions.Function{UID: "vn-remove-user", Args: []string{ctl.configID("db"), string(dyncfg.CommandRemove)}}) |
| 318 | ctl.SeqExec(fn) |
| 319 | |
| 320 | var payload map[string]any |
| 321 | mustDecodeFunctionPayload(t, out.String(), "vn-remove-user", &payload) |
| 322 | assert.Equal(t, float64(405), payload["status"]) |
| 323 | assert.Empty(t, seams.affectedJobsCalls) |
| 324 | }, |
| 325 | }, |
| 326 | "remove uses affected jobs seam": { |
| 327 | initial: map[string]*vnodes.VirtualNode{ |
| 328 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 329 | }, |
| 330 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 331 | seams.affectedJobs["db"] = []string{"mysql:prod"} |
| 332 | |
| 333 | fn := dyncfg.NewFunction(functions.Function{UID: "vn-remove-running", Args: []string{ctl.configID("db"), string(dyncfg.CommandRemove)}}) |
| 334 | ctl.SeqExec(fn) |
| 335 | |
| 336 | var payload map[string]any |
| 337 | mustDecodeFunctionPayload(t, out.String(), "vn-remove-running", &payload) |
| 338 | assert.Equal(t, float64(409), payload["status"]) |
| 339 | assert.Contains(t, fmt.Sprint(payload["errorMessage"]), "referenced by configs") |
| 340 | assert.Equal(t, []string{"db"}, seams.affectedJobsCalls) |
| 341 | _, ok := ctl.Lookup("db") |
| 342 | assert.True(t, ok) |
| 343 | }, |
| 344 | }, |
| 345 | "test preview uses affected jobs seam": { |
| 346 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer, seams *controllerSeams) { |
| 347 | seams.affectedJobs["db"] = []string{"mysql:prod"} |
| 348 | |
| 349 | fn := dyncfg.NewFunction(functions.Function{ |
| 350 | UID: "vn-test", |
| 351 | ContentType: "application/json", |
| 352 | Payload: mustJSON(t, map[string]any{"guid": "11111111-1111-1111-1111-111111111111"}), |
| 353 | Args: []string{ctl.Prefix(), string(dyncfg.CommandTest), "db"}, |
| 354 | }) |
| 355 | ctl.SeqExec(fn) |
| 356 | |
| 357 | var payload map[string]any |
| 358 | mustDecodeFunctionPayload(t, out.String(), "vn-test", &payload) |
| 359 | assert.Equal(t, float64(202), payload["status"]) |
| 360 | assert.Equal(t, "Updated configuration will affect configs: mysql:prod.", payload["message"]) |
| 361 | assert.Equal(t, []string{"db"}, seams.affectedJobsCalls) |
| 362 | assert.Empty(t, seams.applyCalls) |
| 363 | }, |
| 364 | }, |
| 365 | } |
| 366 | |
| 367 | for name, tc := range tests { |
| 368 | t.Run(name, func(t *testing.T) { |
| 369 | ctl, out, seams := newControllerTestSubject(tc.initial) |
| 370 | tc.run(t, ctl, out, seams) |
| 371 | }) |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | func TestControllerPublicationAndLookup(t *testing.T) { |
| 376 | tests := map[string]struct { |
| 377 | run func(t *testing.T, ctl *Controller, out *bytes.Buffer) |
| 378 | }{ |
| 379 | "create module and publish existing": { |
| 380 | run: func(t *testing.T, ctl *Controller, out *bytes.Buffer) { |
| 381 | ctl.CreateTemplates() |
| 382 | ctl.PublishExisting(dyncfg.StatusRunning) |
| 383 | |
| 384 | assert.Contains(t, out.String(), "CONFIG test:vnode create accepted template /collectors/test/Vnodes") |
| 385 | assert.Contains(t, out.String(), "CONFIG test:vnode:db create running job /collectors/test/Vnodes") |
| 386 | |
| 387 | cfg, ok := ctl.Lookup("db") |
| 388 | require.True(t, ok) |
| 389 | assert.Equal(t, "db", cfg.Name) |
| 390 | }, |
| 391 | }, |
| 392 | } |
| 393 | |
| 394 | for name, tc := range tests { |
| 395 | t.Run(name, func(t *testing.T) { |
| 396 | ctl, out, _ := newControllerTestSubject(map[string]*vnodes.VirtualNode{ |
| 397 | "db": testVnode("db", "db", "11111111-1111-1111-1111-111111111111", confgroup.TypeDyncfg), |
| 398 | }) |
| 399 | tc.run(t, ctl, out) |
| 400 | }) |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | func TestControllerSetAPI_NilPreservesResponder(t *testing.T) { |
| 405 | tests := map[string]struct { |
| 406 | uid string |
| 407 | }{ |
| 408 | "nil SetAPI keeps existing responder for schema responses": { |
| 409 | uid: "vn-schema-nil-rebind", |
| 410 | }, |
| 411 | } |
| 412 | |
| 413 | for name, tc := range tests { |
| 414 | t.Run(name, func(t *testing.T) { |
| 415 | ctl, out, _ := newControllerTestSubject(nil) |
| 416 | ctl.SetAPI(nil) |
| 417 | |
| 418 | fn := dyncfg.NewFunction(functions.Function{ |
| 419 | UID: tc.uid, |
| 420 | Args: []string{ctl.Prefix(), string(dyncfg.CommandSchema)}, |
| 421 | }) |
| 422 | ctl.SeqExec(fn) |
| 423 | |
| 424 | assert.Contains(t, out.String(), "FUNCTION_RESULT_BEGIN "+tc.uid+" 200 application/json") |
| 425 | }) |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | type controllerSeams struct { |
| 430 | affectedJobs map[string][]string |
| 431 | affectedJobsCalls []string |
| 432 | applyCalls []string |
| 433 | } |
| 434 | |
| 435 | func newControllerTestSubject(initial map[string]*vnodes.VirtualNode) (*Controller, *bytes.Buffer, *controllerSeams) { |
| 436 | var out bytes.Buffer |
| 437 | seams := &controllerSeams{affectedJobs: make(map[string][]string)} |
| 438 | ctl := New(Options{ |
| 439 | Logger: logger.New(), |
| 440 | API: dyncfg.NewResponder(netdataapi.New(safewriter.New(&out))), |
| 441 | Plugin: testPluginName, |
| 442 | Initial: initial, |
| 443 | AffectedJobs: func(vnode string) []string { |
| 444 | seams.affectedJobsCalls = append(seams.affectedJobsCalls, vnode) |
| 445 | return seams.affectedJobs[vnode] |
| 446 | }, |
| 447 | ApplyVnodeUpdate: func(name string, _ *vnodes.VirtualNode) { |
| 448 | seams.applyCalls = append(seams.applyCalls, name) |
| 449 | }, |
| 450 | }) |
| 451 | return ctl, &out, seams |
| 452 | } |
| 453 | |
| 454 | func testVnode(name, hostname, guid, sourceType string) *vnodes.VirtualNode { |
| 455 | return &vnodes.VirtualNode{Name: name, Hostname: hostname, GUID: guid, Source: sourceType, SourceType: sourceType} |
| 456 | } |
| 457 | |
| 458 | func mustJSON(t *testing.T, v any) []byte { |
| 459 | t.Helper() |
| 460 | bs, err := json.Marshal(v) |
| 461 | require.NoError(t, err) |
| 462 | return bs |
| 463 | } |
| 464 | |
| 465 | func mustFunctionBody(t *testing.T, output, uid string) string { |
| 466 | t.Helper() |
| 467 | re := regexp.MustCompile("(?s)FUNCTION_RESULT_BEGIN " + regexp.QuoteMeta(uid) + " [^\\n]+\\n(.*?)\\nFUNCTION_RESULT_END") |
| 468 | match := re.FindStringSubmatch(output) |
| 469 | require.Len(t, match, 2, "function result for uid '%s' not found in output:\n%s", uid, output) |
| 470 | return match[1] |
| 471 | } |
| 472 | |
| 473 | func mustDecodeFunctionPayload(t *testing.T, output, uid string, dst any) { |
| 474 | t.Helper() |
| 475 | require.NoError(t, json.Unmarshal([]byte(mustFunctionBody(t, output, uid)), dst)) |
| 476 | } |