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  }