go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/backend/bots_test.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package backend 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 "google.golang.org/grpc" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/appengine/tq" 29 "go.chromium.org/luci/appengine/tq/tqtesting" 30 "go.chromium.org/luci/common/clock/testclock" 31 "go.chromium.org/luci/gae/impl/memory" 32 "go.chromium.org/luci/gae/service/datastore" 33 swarmingpb "go.chromium.org/luci/swarming/proto/api_v2" 34 35 "go.chromium.org/luci/gce/api/config/v1" 36 "go.chromium.org/luci/gce/api/tasks/v1" 37 "go.chromium.org/luci/gce/appengine/model" 38 39 . "github.com/smartystreets/goconvey/convey" 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 var someTimeAgo = timestamppb.New(time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)) 44 45 func TestDeleteBot(t *testing.T) { 46 t.Parallel() 47 48 Convey("deleteBot", t, func() { 49 dsp := &tq.Dispatcher{} 50 registerTasks(dsp) 51 52 swr := &mockSwarmingBotsClient{} 53 54 c := withDispatcher(memory.Use(context.Background()), dsp) 55 c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr }) 56 57 tqt := tqtesting.GetTestable(c, dsp) 58 tqt.CreateQueues() 59 60 Convey("invalid", func() { 61 Convey("nil", func() { 62 err := deleteBot(c, nil) 63 So(err, ShouldErrLike, "unexpected payload") 64 }) 65 66 Convey("empty", func() { 67 err := deleteBot(c, &tasks.DeleteBot{}) 68 So(err, ShouldErrLike, "ID is required") 69 }) 70 71 Convey("hostname", func() { 72 err := deleteBot(c, &tasks.DeleteBot{ 73 Id: "id", 74 }) 75 So(err, ShouldErrLike, "hostname is required") 76 }) 77 }) 78 79 Convey("valid", func() { 80 Convey("missing", func() { 81 err := deleteBot(c, &tasks.DeleteBot{ 82 Id: "id", 83 Hostname: "name", 84 }) 85 So(err, ShouldBeNil) 86 }) 87 88 Convey("error", func() { 89 swr.err = status.Errorf(codes.Internal, "boom") 90 So(datastore.Put(c, &model.VM{ 91 ID: "id", 92 Created: 1, 93 Hostname: "name", 94 Lifetime: 1, 95 URL: "url", 96 }), ShouldBeNil) 97 err := deleteBot(c, &tasks.DeleteBot{ 98 Id: "id", 99 Hostname: "name", 100 }) 101 So(err, ShouldErrLike, "failed to delete bot") 102 v := &model.VM{ 103 ID: "id", 104 } 105 So(datastore.Get(c, v), ShouldBeNil) 106 So(v.Created, ShouldEqual, 1) 107 So(v.Hostname, ShouldEqual, "name") 108 So(v.URL, ShouldEqual, "url") 109 }) 110 111 Convey("deleted", func() { 112 swr.err = status.Errorf(codes.NotFound, "not found") 113 So(datastore.Put(c, &model.VM{ 114 ID: "id", 115 Created: 1, 116 Hostname: "name", 117 Lifetime: 1, 118 URL: "url", 119 }), ShouldBeNil) 120 err := deleteBot(c, &tasks.DeleteBot{ 121 Id: "id", 122 Hostname: "name", 123 }) 124 So(err, ShouldBeNil) 125 v := &model.VM{ 126 ID: "id", 127 } 128 So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity) 129 }) 130 131 Convey("deletes", func() { 132 swr.deleteBotResponse = &swarmingpb.DeleteResponse{} 133 So(datastore.Put(c, &model.VM{ 134 ID: "id", 135 Created: 1, 136 Hostname: "name", 137 Lifetime: 1, 138 URL: "url", 139 }), ShouldBeNil) 140 err := deleteBot(c, &tasks.DeleteBot{ 141 Id: "id", 142 Hostname: "name", 143 }) 144 So(err, ShouldBeNil) 145 v := &model.VM{ 146 ID: "id", 147 } 148 So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity) 149 }) 150 }) 151 }) 152 } 153 154 func TestDeleteVM(t *testing.T) { 155 t.Parallel() 156 157 Convey("deleteVM", t, func() { 158 c := memory.Use(context.Background()) 159 160 Convey("deletes", func() { 161 So(datastore.Put(c, &model.VM{ 162 ID: "id", 163 Hostname: "name", 164 }), ShouldBeNil) 165 So(deleteVM(c, "id", "name"), ShouldBeNil) 166 v := &model.VM{ 167 ID: "id", 168 } 169 So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity) 170 }) 171 172 Convey("deleted", func() { 173 So(deleteVM(c, "id", "name"), ShouldBeNil) 174 }) 175 176 Convey("replaced", func() { 177 So(datastore.Put(c, &model.VM{ 178 ID: "id", 179 Hostname: "name-2", 180 }), ShouldBeNil) 181 So(deleteVM(c, "id", "name-1"), ShouldBeNil) 182 v := &model.VM{ 183 ID: "id", 184 } 185 So(datastore.Get(c, v), ShouldBeNil) 186 So(v.Hostname, ShouldEqual, "name-2") 187 }) 188 }) 189 } 190 191 func TestManageBot(t *testing.T) { 192 t.Parallel() 193 194 Convey("manageBot", t, func() { 195 dsp := &tq.Dispatcher{} 196 registerTasks(dsp) 197 198 swr := &mockSwarmingBotsClient{} 199 200 c := withDispatcher(memory.Use(context.Background()), dsp) 201 c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr }) 202 203 tqt := tqtesting.GetTestable(c, dsp) 204 tqt.CreateQueues() 205 206 Convey("invalid", func() { 207 Convey("nil", func() { 208 err := manageBot(c, nil) 209 So(err, ShouldErrLike, "unexpected payload") 210 }) 211 212 Convey("empty", func() { 213 err := manageBot(c, &tasks.ManageBot{}) 214 So(err, ShouldErrLike, "ID is required") 215 }) 216 }) 217 218 Convey("valid", func() { 219 So(datastore.Put(c, &model.Config{ 220 ID: "config", 221 Config: &config.Config{ 222 CurrentAmount: 1, 223 }, 224 }), ShouldBeNil) 225 226 Convey("deleted", func() { 227 err := manageBot(c, &tasks.ManageBot{ 228 Id: "id", 229 }) 230 So(err, ShouldBeNil) 231 }) 232 233 Convey("creating", func() { 234 So(datastore.Put(c, &model.VM{ 235 ID: "id", 236 Config: "config", 237 }), ShouldBeNil) 238 err := manageBot(c, &tasks.ManageBot{ 239 Id: "id", 240 }) 241 So(err, ShouldBeNil) 242 }) 243 244 Convey("error", func() { 245 swr.err = status.Errorf(codes.InvalidArgument, "unexpected error") 246 So(datastore.Put(c, &model.VM{ 247 ID: "id", 248 Config: "config", 249 URL: "url", 250 }), ShouldBeNil) 251 err := manageBot(c, &tasks.ManageBot{ 252 Id: "id", 253 }) 254 So(err, ShouldErrLike, "failed to fetch bot") 255 }) 256 257 Convey("missing", func() { 258 Convey("deadline", func() { 259 swr.err = status.Errorf(codes.NotFound, "not found") 260 So(datastore.Put(c, &model.VM{ 261 ID: "id", 262 Config: "config", 263 Created: 1, 264 Hostname: "name", 265 Lifetime: 1, 266 URL: "url", 267 }), ShouldBeNil) 268 err := manageBot(c, &tasks.ManageBot{ 269 Id: "id", 270 }) 271 So(err, ShouldBeNil) 272 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 273 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{}) 274 v := &model.VM{ 275 ID: "id", 276 } 277 So(datastore.Get(c, v), ShouldBeNil) 278 }) 279 280 Convey("drained & new bots", func() { 281 swr.err = status.Errorf(codes.NotFound, "not found") 282 So(datastore.Put(c, &model.VM{ 283 ID: "id", 284 Config: "config", 285 Drained: true, 286 Hostname: "name", 287 URL: "url", 288 Created: time.Now().Unix() - 100, 289 }), ShouldBeNil) 290 err := manageBot(c, &tasks.ManageBot{ 291 Id: "id", 292 }) 293 So(err, ShouldBeNil) 294 // For now, won't destroy a instance if it's set to drained or newly created but 295 // hasn't connected to swarming yet 296 So(tqt.GetScheduledTasks(), ShouldHaveLength, 0) 297 }) 298 299 Convey("timeout", func() { 300 swr.err = status.Errorf(codes.NotFound, "not found") 301 So(datastore.Put(c, &model.VM{ 302 ID: "id", 303 Config: "config", 304 Created: 1, 305 Hostname: "name", 306 Timeout: 1, 307 URL: "url", 308 }), ShouldBeNil) 309 err := manageBot(c, &tasks.ManageBot{ 310 Id: "id", 311 }) 312 So(err, ShouldBeNil) 313 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 314 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{}) 315 }) 316 317 Convey("wait", func() { 318 swr.err = status.Errorf(codes.NotFound, "not found") 319 So(datastore.Put(c, &model.VM{ 320 ID: "id", 321 Config: "config", 322 Hostname: "name", 323 URL: "url", 324 }), ShouldBeNil) 325 err := manageBot(c, &tasks.ManageBot{ 326 Id: "id", 327 }) 328 So(err, ShouldBeNil) 329 }) 330 }) 331 332 Convey("found", func() { 333 Convey("deleted", func() { 334 swr.getBotResponse = &swarmingpb.BotInfo{ 335 BotId: "id", 336 Deleted: true, 337 } 338 So(datastore.Put(c, &model.VM{ 339 ID: "id", 340 Config: "config", 341 Hostname: "name", 342 URL: "url", 343 // Has to be older than time.Now().Unix() - minPendingMinutesForBotConnected * 10 344 Created: time.Now().Unix() - 10000, 345 }), ShouldBeNil) 346 err := manageBot(c, &tasks.ManageBot{ 347 Id: "id", 348 }) 349 So(err, ShouldBeNil) 350 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 351 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{}) 352 v := &model.VM{ 353 ID: "id", 354 } 355 So(datastore.Get(c, v), ShouldBeNil) 356 }) 357 358 Convey("deleted but newly created", func() { 359 swr.getBotResponse = &swarmingpb.BotInfo{ 360 BotId: "id", 361 Deleted: true, 362 } 363 So(datastore.Put(c, &model.VM{ 364 ID: "id", 365 Config: "config", 366 Hostname: "name", 367 URL: "url", 368 Created: time.Now().Unix() - 10, 369 }), ShouldBeNil) 370 err := manageBot(c, &tasks.ManageBot{ 371 Id: "id", 372 }) 373 So(err, ShouldBeNil) 374 // Won't destroy the instance if it's a newly created VM 375 So(tqt.GetScheduledTasks(), ShouldHaveLength, 0) 376 }) 377 378 Convey("dead", func() { 379 swr.getBotResponse = &swarmingpb.BotInfo{ 380 BotId: "id", 381 FirstSeenTs: someTimeAgo, 382 IsDead: true, 383 } 384 So(datastore.Put(c, &model.VM{ 385 ID: "id", 386 Config: "config", 387 Hostname: "name", 388 URL: "url", 389 // Has to be older than time.Now().Unix() - minPendingMinutesForBotConnected * 10 390 Created: time.Now().Unix() - 10000, 391 }), ShouldBeNil) 392 err := manageBot(c, &tasks.ManageBot{ 393 Id: "id", 394 }) 395 So(err, ShouldBeNil) 396 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 397 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{}) 398 v := &model.VM{ 399 ID: "id", 400 } 401 So(datastore.Get(c, v), ShouldBeNil) 402 }) 403 404 Convey("dead but newly created", func() { 405 swr.getBotResponse = &swarmingpb.BotInfo{ 406 BotId: "id", 407 FirstSeenTs: someTimeAgo, 408 IsDead: true, 409 } 410 So(datastore.Put(c, &model.VM{ 411 ID: "id", 412 Config: "config", 413 Hostname: "name", 414 URL: "url", 415 Created: time.Now().Unix() - 10, 416 }), ShouldBeNil) 417 err := manageBot(c, &tasks.ManageBot{ 418 Id: "id", 419 }) 420 So(err, ShouldBeNil) 421 // won't destroy the instance if it's a newly created VM 422 So(tqt.GetScheduledTasks(), ShouldHaveLength, 0) 423 }) 424 425 Convey("terminated", func() { 426 swr.getBotResponse = &swarmingpb.BotInfo{ 427 BotId: "id", 428 FirstSeenTs: someTimeAgo, 429 } 430 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{ 431 Items: []*swarmingpb.BotEventResponse{ 432 { 433 EventType: "bot_terminate", 434 }, 435 }, 436 } 437 So(datastore.Put(c, &model.VM{ 438 ID: "id", 439 Config: "config", 440 Hostname: "name", 441 URL: "url", 442 }), ShouldBeNil) 443 err := manageBot(c, &tasks.ManageBot{ 444 Id: "id", 445 }) 446 So(err, ShouldBeNil) 447 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 448 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{}) 449 v := &model.VM{ 450 ID: "id", 451 } 452 So(datastore.Get(c, v), ShouldBeNil) 453 So(v.Connected, ShouldNotEqual, 0) 454 }) 455 456 Convey("deadline", func() { 457 swr.getBotResponse = &swarmingpb.BotInfo{ 458 BotId: "id", 459 FirstSeenTs: someTimeAgo, 460 } 461 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{} 462 So(datastore.Put(c, &model.VM{ 463 ID: "id", 464 Config: "config", 465 Created: 1, 466 Lifetime: 1, 467 Hostname: "name", 468 URL: "url", 469 }), ShouldBeNil) 470 err := manageBot(c, &tasks.ManageBot{ 471 Id: "id", 472 }) 473 So(err, ShouldBeNil) 474 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 475 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.TerminateBot{}) 476 v := &model.VM{ 477 ID: "id", 478 } 479 So(datastore.Get(c, v), ShouldBeNil) 480 So(v.Connected, ShouldNotEqual, 0) 481 }) 482 483 Convey("drained", func() { 484 swr.getBotResponse = &swarmingpb.BotInfo{ 485 BotId: "id", 486 FirstSeenTs: someTimeAgo, 487 } 488 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{} 489 So(datastore.Put(c, &model.VM{ 490 ID: "id", 491 Config: "config", 492 Drained: true, 493 Hostname: "name", 494 URL: "url", 495 }), ShouldBeNil) 496 err := manageBot(c, &tasks.ManageBot{ 497 Id: "id", 498 }) 499 So(err, ShouldBeNil) 500 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 501 So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.TerminateBot{}) 502 v := &model.VM{ 503 ID: "id", 504 } 505 So(datastore.Get(c, v), ShouldBeNil) 506 So(v.Connected, ShouldNotEqual, 0) 507 }) 508 509 Convey("alive", func() { 510 swr.getBotResponse = &swarmingpb.BotInfo{ 511 BotId: "id", 512 FirstSeenTs: someTimeAgo, 513 } 514 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{} 515 So(datastore.Put(c, &model.VM{ 516 ID: "id", 517 Config: "config", 518 Hostname: "name", 519 URL: "url", 520 }), ShouldBeNil) 521 err := manageBot(c, &tasks.ManageBot{ 522 Id: "id", 523 }) 524 So(err, ShouldBeNil) 525 So(tqt.GetScheduledTasks(), ShouldBeEmpty) 526 v := &model.VM{ 527 ID: "id", 528 } 529 So(datastore.Get(c, v), ShouldBeNil) 530 So(v.Connected, ShouldNotEqual, 0) 531 }) 532 }) 533 }) 534 }) 535 } 536 537 func TestTerminateBot(t *testing.T) { 538 t.Parallel() 539 540 Convey("terminateBot", t, func() { 541 dsp := &tq.Dispatcher{} 542 registerTasks(dsp) 543 544 swr := &mockSwarmingBotsClient{} 545 546 c := withDispatcher(memory.Use(context.Background()), dsp) 547 c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr }) 548 549 tqt := tqtesting.GetTestable(c, dsp) 550 tqt.CreateQueues() 551 552 Convey("invalid", func() { 553 Convey("nil", func() { 554 err := terminateBot(c, nil) 555 So(err, ShouldErrLike, "unexpected payload") 556 }) 557 558 Convey("empty", func() { 559 err := terminateBot(c, &tasks.TerminateBot{}) 560 So(err, ShouldErrLike, "ID is required") 561 }) 562 563 Convey("hostname", func() { 564 err := terminateBot(c, &tasks.TerminateBot{ 565 Id: "id", 566 }) 567 So(err, ShouldErrLike, "hostname is required") 568 }) 569 }) 570 571 Convey("valid", func() { 572 Convey("missing", func() { 573 err := terminateBot(c, &tasks.TerminateBot{ 574 Id: "id", 575 Hostname: "name", 576 }) 577 So(err, ShouldBeNil) 578 }) 579 580 Convey("replaced", func() { 581 So(datastore.Put(c, &model.VM{ 582 ID: "id", 583 Hostname: "new", 584 }), ShouldBeNil) 585 err := terminateBot(c, &tasks.TerminateBot{ 586 Id: "id", 587 Hostname: "old", 588 }) 589 So(err, ShouldBeNil) 590 v := &model.VM{ 591 ID: "id", 592 } 593 So(datastore.Get(c, v), ShouldBeNil) 594 So(v.Hostname, ShouldEqual, "new") 595 }) 596 597 Convey("error", func() { 598 swr.err = status.Errorf(codes.Internal, "internal error") 599 So(datastore.Put(c, &model.VM{ 600 ID: "id", 601 Hostname: "name", 602 }), ShouldBeNil) 603 err := terminateBot(c, &tasks.TerminateBot{ 604 Id: "id", 605 Hostname: "name", 606 }) 607 So(err, ShouldErrLike, "failed to terminate bot") 608 v := &model.VM{ 609 ID: "id", 610 } 611 So(datastore.Get(c, v), ShouldBeNil) 612 So(v.Hostname, ShouldEqual, "name") 613 }) 614 615 Convey("terminates", func() { 616 c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC) 617 swr.terminateBotResponse = &swarmingpb.TerminateResponse{} 618 So(datastore.Put(c, &model.VM{ 619 ID: "id", 620 Hostname: "name", 621 }), ShouldBeNil) 622 terminateTask := tasks.TerminateBot{ 623 Id: "id", 624 Hostname: "name", 625 } 626 So(terminateBot(c, &terminateTask), ShouldBeNil) 627 So(swr.calls, ShouldEqual, 1) 628 629 Convey("wait 1 hour before sending another terminate task", func() { 630 c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC.Add(time.Hour-time.Second)) 631 So(terminateBot(c, &terminateTask), ShouldBeNil) 632 So(swr.calls, ShouldEqual, 1) 633 634 c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC.Add(time.Hour)) 635 So(terminateBot(c, &terminateTask), ShouldBeNil) 636 So(swr.calls, ShouldEqual, 2) 637 }) 638 }) 639 }) 640 }) 641 } 642 643 func TestInspectSwarming(t *testing.T) { 644 t.Parallel() 645 646 Convey("inspectSwarmingAsync", t, func() { 647 dsp := &tq.Dispatcher{} 648 registerTasks(dsp) 649 c := withDispatcher(memory.Use(context.Background()), dsp) 650 tqt := tqtesting.GetTestable(c, dsp) 651 tqt.CreateQueues() 652 datastore.GetTestable(c).Consistent(true) 653 654 Convey("none", func() { 655 err := inspectSwarmingAsync(c) 656 So(err, ShouldBeNil) 657 So(tqt.GetScheduledTasks(), ShouldHaveLength, 0) 658 }) 659 660 Convey("one", func() { 661 So(datastore.Put(c, &model.Config{ 662 ID: "config-1", 663 Config: &config.Config{ 664 Swarming: "https://gce-swarming.appspot.com", 665 }, 666 }), ShouldBeNil) 667 err := inspectSwarmingAsync(c) 668 So(err, ShouldBeNil) 669 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 670 }) 671 672 Convey("two", func() { 673 So(datastore.Put(c, &model.Config{ 674 ID: "config-1", 675 Config: &config.Config{ 676 Swarming: "https://gce-swarming.appspot.com", 677 }, 678 }), ShouldBeNil) 679 So(datastore.Put(c, &model.Config{ 680 ID: "config-2", 681 Config: &config.Config{ 682 Swarming: "https://vmleaser-swarming.appspot.com", 683 }, 684 }), ShouldBeNil) 685 err := inspectSwarmingAsync(c) 686 So(err, ShouldBeNil) 687 So(tqt.GetScheduledTasks(), ShouldHaveLength, 2) 688 }) 689 }) 690 691 Convey("inspectSwarming", t, func() { 692 dsp := &tq.Dispatcher{} 693 registerTasks(dsp) 694 695 swr := &mockSwarmingBotsClient{} 696 697 c := withDispatcher(memory.Use(context.Background()), dsp) 698 c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr }) 699 700 tqt := tqtesting.GetTestable(c, dsp) 701 tqt.CreateQueues() 702 datastore.GetTestable(c).Consistent(true) 703 704 Convey("BadInputs", func() { 705 Convey("nil", func() { 706 err := inspectSwarming(c, nil) 707 So(err, ShouldNotBeNil) 708 }) 709 Convey("empty", func() { 710 err := inspectSwarming(c, &tasks.InspectSwarming{}) 711 So(err, ShouldNotBeNil) 712 }) 713 }) 714 715 Convey("Swarming error", func() { 716 swr.err = status.Errorf(codes.Internal, "internal server error") 717 err := inspectSwarming(c, &tasks.InspectSwarming{ 718 Swarming: "https://gce-swarming.appspot.com", 719 }) 720 So(err, ShouldNotBeNil) 721 }) 722 Convey("Ignore non-gce bot", func() { 723 So(datastore.Put(c, &model.VM{ 724 ID: "vm-1", 725 Hostname: "vm-1-abcd", 726 Swarming: "https://gce-swarming.appspot.com", 727 }), ShouldBeNil) 728 So(datastore.Put(c, &model.VM{ 729 ID: "vm-2", 730 Hostname: "vm-2-abcd", 731 Swarming: "https://gce-swarming.appspot.com", 732 }), ShouldBeNil) 733 swr.listBotsResponse = &swarmingpb.BotInfoListResponse{ 734 Cursor: "", 735 Items: []*swarmingpb.BotInfo{ 736 { 737 BotId: "vm-1-abcd", 738 FirstSeenTs: someTimeAgo, 739 }, 740 { 741 BotId: "vm-2-abcd", 742 FirstSeenTs: someTimeAgo, 743 }, 744 // We don't have a record for this bot in datastore 745 { 746 BotId: "vm-3-abcd", 747 FirstSeenTs: someTimeAgo, 748 }, 749 }, 750 } 751 err := inspectSwarming(c, &tasks.InspectSwarming{ 752 Swarming: "https://gce-swarming.appspot.com", 753 }) 754 So(err, ShouldBeNil) 755 // ignoring vm-3-abcd as we didn't see it in datastore 756 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 757 }) 758 Convey("Delete dead or deleted bots", func() { 759 So(datastore.Put(c, &model.VM{ 760 ID: "vm-1", 761 Hostname: "vm-1-abcd", 762 Swarming: "https://gce-swarming.appspot.com", 763 }), ShouldBeNil) 764 So(datastore.Put(c, &model.VM{ 765 ID: "vm-2", 766 Hostname: "vm-2-abcd", 767 Swarming: "https://gce-swarming.appspot.com", 768 }), ShouldBeNil) 769 swr.listBotsResponse = &swarmingpb.BotInfoListResponse{ 770 Cursor: "", 771 Items: []*swarmingpb.BotInfo{ 772 { 773 BotId: "vm-1-abcd", 774 FirstSeenTs: someTimeAgo, 775 IsDead: true, 776 }, 777 { 778 BotId: "vm-2-abcd", 779 FirstSeenTs: someTimeAgo, 780 Deleted: true, 781 }, 782 }, 783 } 784 err := inspectSwarming(c, &tasks.InspectSwarming{ 785 Swarming: "https://gce-swarming.appspot.com", 786 }) 787 So(err, ShouldBeNil) 788 So(tqt.GetScheduledTasks(), ShouldHaveLength, 2) 789 }) 790 Convey("HappyPath-1", func() { 791 So(datastore.Put(c, &model.VM{ 792 ID: "vm-1", 793 Hostname: "vm-1-abcd", 794 Swarming: "https://gce-swarming.appspot.com", 795 }), ShouldBeNil) 796 So(datastore.Put(c, &model.VM{ 797 ID: "vm-2", 798 Hostname: "vm-2-abcd", 799 Swarming: "https://gce-swarming.appspot.com", 800 }), ShouldBeNil) 801 So(datastore.Put(c, &model.VM{ 802 ID: "vm-3", 803 Hostname: "vm-3-abcd", 804 Swarming: "https://vmleaser-swarming.appspot.com", 805 }), ShouldBeNil) 806 swr.listBotsResponse = &swarmingpb.BotInfoListResponse{ 807 Cursor: "", 808 Items: []*swarmingpb.BotInfo{ 809 { 810 BotId: "vm-1-abcd", 811 FirstSeenTs: someTimeAgo, 812 }, 813 { 814 BotId: "vm-2-abcd", 815 FirstSeenTs: someTimeAgo, 816 }, 817 }, 818 } 819 err := inspectSwarming(c, &tasks.InspectSwarming{ 820 Swarming: "https://gce-swarming.appspot.com", 821 }) 822 So(err, ShouldBeNil) 823 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 824 }) 825 Convey("HappyPath-2", func() { 826 So(datastore.Put(c, &model.VM{ 827 ID: "vm-1", 828 Hostname: "vm-1-abcd", 829 Swarming: "https://gce-swarming.appspot.com", 830 }), ShouldBeNil) 831 So(datastore.Put(c, &model.VM{ 832 ID: "vm-2", 833 Hostname: "vm-2-abcd", 834 Swarming: "https://gce-swarming.appspot.com", 835 }), ShouldBeNil) 836 So(datastore.Put(c, &model.VM{ 837 ID: "vm-3", 838 Hostname: "vm-3-abcd", 839 Swarming: "https://vmleaser-swarming.appspot.com", 840 }), ShouldBeNil) 841 swr.listBotsResponse = &swarmingpb.BotInfoListResponse{ 842 Cursor: "", 843 Items: []*swarmingpb.BotInfo{ 844 { 845 BotId: "vm-3-abcd", 846 FirstSeenTs: someTimeAgo, 847 }, 848 }, 849 } 850 err := inspectSwarming(c, &tasks.InspectSwarming{ 851 Swarming: "https://vmleaser-swarming.appspot.com", 852 }) 853 So(err, ShouldBeNil) 854 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 855 }) 856 Convey("HappyPath-3-pagination", func() { 857 So(datastore.Put(c, &model.VM{ 858 ID: "vm-1", 859 Hostname: "vm-1-abcd", 860 Swarming: "https://gce-swarming.appspot.com", 861 }), ShouldBeNil) 862 So(datastore.Put(c, &model.VM{ 863 ID: "vm-2", 864 Hostname: "vm-2-abcd", 865 Swarming: "https://gce-swarming.appspot.com", 866 }), ShouldBeNil) 867 So(datastore.Put(c, &model.VM{ 868 ID: "vm-3", 869 Hostname: "vm-3-abcd", 870 Swarming: "https://gce-swarming.appspot.com", 871 }), ShouldBeNil) 872 swr.listBotsResponse = &swarmingpb.BotInfoListResponse{ 873 Cursor: "cursor", 874 Items: []*swarmingpb.BotInfo{ 875 { 876 BotId: "vm-1-abcd", 877 FirstSeenTs: someTimeAgo, 878 }, 879 { 880 BotId: "vm-2-abcd", 881 FirstSeenTs: someTimeAgo, 882 }, 883 }, 884 } 885 err := inspectSwarming(c, &tasks.InspectSwarming{ 886 Swarming: "https://gce-swarming.appspot.com", 887 }) 888 So(err, ShouldBeNil) 889 // One DeleteStaleSwarmingBots tasks and one inspectSwarming task with cursor 890 So(tqt.GetScheduledTasks(), ShouldHaveLength, 2) 891 }) 892 893 }) 894 } 895 896 func TestDeleteStaleSwarmingBot(t *testing.T) { 897 t.Parallel() 898 899 Convey("deleteStaleSwarmingBot", t, func() { 900 dsp := &tq.Dispatcher{} 901 registerTasks(dsp) 902 903 swr := &mockSwarmingBotsClient{} 904 905 c := withDispatcher(memory.Use(context.Background()), dsp) 906 c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr }) 907 908 tqt := tqtesting.GetTestable(c, dsp) 909 tqt.CreateQueues() 910 datastore.GetTestable(c).Consistent(true) 911 912 Convey("BadInputs", func() { 913 Convey("nil", func() { 914 err := deleteStaleSwarmingBot(c, nil) 915 So(err, ShouldNotBeNil) 916 }) 917 Convey("empty", func() { 918 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{}) 919 So(err, ShouldNotBeNil) 920 }) 921 Convey("missing timestamp", func() { 922 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 923 Id: "id-1", 924 }) 925 So(err, ShouldNotBeNil) 926 }) 927 }) 928 Convey("VM issues", func() { 929 Convey("Missing VM", func() { 930 // Don't err if the VM is missing, prob deleted already 931 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 932 Id: "id-1", 933 FirstSeenTs: "onceUponATime", 934 }) 935 So(err, ShouldBeNil) 936 }) 937 Convey("Missing URL in VM", func() { 938 So(datastore.Put(c, &model.VM{ 939 ID: "vm-3", 940 Hostname: "vm-3-abcd", 941 Swarming: "https://gce-swarming.appspot.com", 942 }), ShouldBeNil) 943 // Don't err if the URL in VM is missing 944 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 945 Id: "id-1", 946 FirstSeenTs: "onceUponATime", 947 }) 948 So(err, ShouldBeNil) 949 }) 950 }) 951 Convey("Swarming Issues", func() { 952 Convey("Failed to fetch", func() { 953 So(datastore.Put(c, &model.VM{ 954 ID: "vm-3", 955 Hostname: "vm-3-abcd", 956 URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd", 957 Swarming: "https://gce-swarming.appspot.com", 958 }), ShouldBeNil) 959 swr.err = status.Errorf(codes.NotFound, "not found") 960 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 961 Id: "id-1", 962 FirstSeenTs: "onceUponATime", 963 }) 964 So(err, ShouldBeNil) 965 }) 966 }) 967 Convey("Happy paths", func() { 968 Convey("Bot terminated", func() { 969 So(datastore.Put(c, &model.VM{ 970 ID: "vm-3", 971 Hostname: "vm-3-abcd", 972 URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd", 973 Swarming: "https://gce-swarming.appspot.com", 974 }), ShouldBeNil) 975 swr.getBotResponse = &swarmingpb.BotInfo{ 976 BotId: "vm-3-abcd", 977 FirstSeenTs: someTimeAgo, 978 } 979 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{ 980 Items: []*swarmingpb.BotEventResponse{ 981 {EventType: "bot_terminate"}, 982 }, 983 } 984 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 985 Id: "vm-3", 986 FirstSeenTs: "2019-03-13T00:12:29.882948", 987 }) 988 So(err, ShouldBeNil) 989 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 990 }) 991 Convey("Bot retirement", func() { 992 So(datastore.Put(c, &model.VM{ 993 ID: "vm-3", 994 Hostname: "vm-3-abcd", 995 URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd", 996 Swarming: "https://gce-swarming.appspot.com", 997 Lifetime: 99, 998 Created: time.Now().Unix() - 100, 999 }), ShouldBeNil) 1000 swr.getBotResponse = &swarmingpb.BotInfo{ 1001 BotId: "vm-3-abcd", 1002 FirstSeenTs: someTimeAgo, 1003 } 1004 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{ 1005 Items: []*swarmingpb.BotEventResponse{}, 1006 } 1007 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 1008 Id: "vm-3", 1009 FirstSeenTs: "2019-03-13T00:12:29.882948", 1010 }) 1011 So(err, ShouldBeNil) 1012 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 1013 }) 1014 Convey("Bot drained", func() { 1015 So(datastore.Put(c, &model.VM{ 1016 ID: "vm-3", 1017 Hostname: "vm-3-abcd", 1018 URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd", 1019 Swarming: "https://gce-swarming.appspot.com", 1020 Lifetime: 100000000, 1021 Created: time.Now().Unix(), 1022 Drained: true, 1023 }), ShouldBeNil) 1024 swr.getBotResponse = &swarmingpb.BotInfo{ 1025 BotId: "vm-3-abcd", 1026 FirstSeenTs: someTimeAgo, 1027 } 1028 swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{ 1029 Items: []*swarmingpb.BotEventResponse{}, 1030 } 1031 err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{ 1032 Id: "vm-3", 1033 FirstSeenTs: "2019-03-13T00:12:29.882948", 1034 }) 1035 So(err, ShouldBeNil) 1036 So(tqt.GetScheduledTasks(), ShouldHaveLength, 1) 1037 }) 1038 }) 1039 1040 }) 1041 } 1042 1043 type mockSwarmingBotsClient struct { 1044 err error 1045 calls int 1046 1047 deleteBotResponse *swarmingpb.DeleteResponse 1048 getBotResponse *swarmingpb.BotInfo 1049 listBotEventsResponse *swarmingpb.BotEventsResponse 1050 terminateBotResponse *swarmingpb.TerminateResponse 1051 listBotsResponse *swarmingpb.BotInfoListResponse 1052 1053 swarmingpb.BotsClient // "implements" remaining RPCs by nil panicking 1054 } 1055 1056 func handleCall[R any](mc *mockSwarmingBotsClient, method string, resp *R) (*R, error) { 1057 mc.calls++ 1058 if mc.err != nil { 1059 return nil, mc.err 1060 } 1061 if resp == nil { 1062 panic(fmt.Sprintf("unexpected call to %s", method)) 1063 } 1064 return resp, nil 1065 } 1066 1067 func (mc *mockSwarmingBotsClient) GetBot(context.Context, *swarmingpb.BotRequest, ...grpc.CallOption) (*swarmingpb.BotInfo, error) { 1068 return handleCall(mc, "GetBot", mc.getBotResponse) 1069 } 1070 1071 func (mc *mockSwarmingBotsClient) TerminateBot(context.Context, *swarmingpb.TerminateRequest, ...grpc.CallOption) (*swarmingpb.TerminateResponse, error) { 1072 return handleCall(mc, "TerminateBot", mc.terminateBotResponse) 1073 } 1074 1075 func (mc *mockSwarmingBotsClient) DeleteBot(context.Context, *swarmingpb.BotRequest, ...grpc.CallOption) (*swarmingpb.DeleteResponse, error) { 1076 return handleCall(mc, "DeleteBot", mc.deleteBotResponse) 1077 } 1078 1079 func (mc *mockSwarmingBotsClient) ListBotEvents(context.Context, *swarmingpb.BotEventsRequest, ...grpc.CallOption) (*swarmingpb.BotEventsResponse, error) { 1080 return handleCall(mc, "ListBotEvents", mc.listBotEventsResponse) 1081 } 1082 1083 func (mc *mockSwarmingBotsClient) ListBots(context.Context, *swarmingpb.BotsRequest, ...grpc.CallOption) (*swarmingpb.BotInfoListResponse, error) { 1084 return handleCall(mc, "ListBots", mc.listBotsResponse) 1085 }