go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/backend/instances_test.go (about)

     1  // Copyright 2018 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  	"net/http"
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	"google.golang.org/api/compute/v1"
    25  	"google.golang.org/api/option"
    26  
    27  	"go.chromium.org/luci/appengine/tq"
    28  	"go.chromium.org/luci/appengine/tq/tqtesting"
    29  	"go.chromium.org/luci/common/tsmon"
    30  	"go.chromium.org/luci/gae/impl/memory"
    31  	"go.chromium.org/luci/gae/service/datastore"
    32  
    33  	"go.chromium.org/luci/gce/api/tasks/v1"
    34  	"go.chromium.org/luci/gce/appengine/backend/internal/metrics"
    35  	"go.chromium.org/luci/gce/appengine/model"
    36  	"go.chromium.org/luci/gce/appengine/testing/roundtripper"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  func TestCreate(t *testing.T) {
    43  	t.Parallel()
    44  
    45  	Convey("createInstance", t, func() {
    46  		dsp := &tq.Dispatcher{}
    47  		registerTasks(dsp)
    48  		rt := &roundtripper.JSONRoundTripper{}
    49  		gce, err := compute.New(&http.Client{Transport: rt})
    50  		So(err, ShouldBeNil)
    51  		c, _ := tsmon.WithDummyInMemory(memory.Use(context.Background()))
    52  		c = withCompute(withDispatcher(c, dsp), ComputeService{Stable: gce})
    53  		tqt := tqtesting.GetTestable(c, dsp)
    54  		tqt.CreateQueues()
    55  		s := tsmon.Store(c)
    56  
    57  		Convey("invalid", func() {
    58  			Convey("nil", func() {
    59  				err := createInstance(c, nil)
    60  				So(err, ShouldErrLike, "unexpected payload")
    61  			})
    62  
    63  			Convey("empty", func() {
    64  				err := createInstance(c, &tasks.CreateInstance{})
    65  				So(err, ShouldErrLike, "ID is required")
    66  			})
    67  
    68  			Convey("missing", func() {
    69  				err := createInstance(c, &tasks.CreateInstance{
    70  					Id: "id",
    71  				})
    72  				So(err, ShouldErrLike, "failed to fetch VM")
    73  			})
    74  		})
    75  
    76  		Convey("valid", func() {
    77  			Convey("exists", func() {
    78  				datastore.Put(c, &model.VM{
    79  					ID:       "id",
    80  					Hostname: "name",
    81  					URL:      "url",
    82  				})
    83  				err := createInstance(c, &tasks.CreateInstance{
    84  					Id: "id",
    85  				})
    86  				So(err, ShouldBeNil)
    87  			})
    88  
    89  			Convey("drained", func() {
    90  				rt.Handler = func(req any) (int, any) {
    91  					inst, ok := req.(*compute.Instance)
    92  					So(ok, ShouldBeTrue)
    93  					So(inst.Name, ShouldEqual, "name")
    94  					return http.StatusOK, &compute.Operation{}
    95  				}
    96  				rt.Type = reflect.TypeOf(compute.Instance{})
    97  				datastore.Put(c, &model.VM{
    98  					ID:       "id",
    99  					Drained:  true,
   100  					Hostname: "name",
   101  				})
   102  				err := createInstance(c, &tasks.CreateInstance{
   103  					Id: "id",
   104  				})
   105  				So(err, ShouldBeNil)
   106  			})
   107  
   108  			Convey("error", func() {
   109  				Convey("http", func() {
   110  					Convey("transient", func() {
   111  						rt.Handler = func(req any) (int, any) {
   112  							return http.StatusInternalServerError, nil
   113  						}
   114  						rt.Type = reflect.TypeOf(compute.Instance{})
   115  						datastore.Put(c, &model.VM{
   116  							ID:       "id",
   117  							Hostname: "name",
   118  						})
   119  						err := createInstance(c, &tasks.CreateInstance{
   120  							Id: "id",
   121  						})
   122  						So(err, ShouldErrLike, "transiently failed to create instance")
   123  						v := &model.VM{
   124  							ID: "id",
   125  						}
   126  						So(datastore.Get(c, v), ShouldBeNil)
   127  						So(v.Hostname, ShouldEqual, "name")
   128  					})
   129  
   130  					Convey("permanent", func() {
   131  						rt.Handler = func(req any) (int, any) {
   132  							return http.StatusConflict, nil
   133  						}
   134  						rt.Type = reflect.TypeOf(compute.Instance{})
   135  						datastore.Put(c, &model.VM{
   136  							ID:       "id",
   137  							Hostname: "name",
   138  						})
   139  						err := createInstance(c, &tasks.CreateInstance{
   140  							Id: "id",
   141  						})
   142  						So(err, ShouldErrLike, "failed to create instance")
   143  						v := &model.VM{
   144  							ID: "id",
   145  						}
   146  						So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity)
   147  					})
   148  				})
   149  
   150  				Convey("operation", func() {
   151  					rt.Handler = func(req any) (int, any) {
   152  						return http.StatusOK, &compute.Operation{
   153  							Error: &compute.OperationError{
   154  								Errors: []*compute.OperationErrorErrors{
   155  									{},
   156  								},
   157  							},
   158  						}
   159  					}
   160  					rt.Type = reflect.TypeOf(compute.Instance{})
   161  					datastore.Put(c, &model.VM{
   162  						ID:       "id",
   163  						Hostname: "name",
   164  					})
   165  					err := createInstance(c, &tasks.CreateInstance{
   166  						Id: "id",
   167  					})
   168  					So(err, ShouldErrLike, "failed to create instance")
   169  					v := &model.VM{
   170  						ID: "id",
   171  					}
   172  					So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity)
   173  				})
   174  			})
   175  
   176  			Convey("created", func() {
   177  				confFields := []any{"", "", "", "name"}
   178  				So(s.Get(c, metrics.CreatedInstanceChecked, time.Time{}, confFields), ShouldBeNil)
   179  				Convey("pending", func() {
   180  					rt.Handler = func(req any) (int, any) {
   181  						inst, ok := req.(*compute.Instance)
   182  						So(ok, ShouldBeTrue)
   183  						So(inst.Name, ShouldEqual, "name")
   184  						return http.StatusOK, &compute.Operation{}
   185  					}
   186  					rt.Type = reflect.TypeOf(compute.Instance{})
   187  					datastore.Put(c, &model.VM{
   188  						ID:       "id",
   189  						Hostname: "name",
   190  					})
   191  					err := createInstance(c, &tasks.CreateInstance{
   192  						Id: "id",
   193  					})
   194  					So(err, ShouldBeNil)
   195  					v := &model.VM{
   196  						ID: "id",
   197  					}
   198  					So(datastore.Get(c, v), ShouldBeNil)
   199  					So(v.Created, ShouldEqual, 0)
   200  					So(v.URL, ShouldBeEmpty)
   201  				})
   202  
   203  				Convey("done", func() {
   204  					rt.Handler = func(req any) (int, any) {
   205  						switch rt.Type {
   206  						case reflect.TypeOf(compute.Instance{}):
   207  							// First call, to create the instance.
   208  							inst, ok := req.(*compute.Instance)
   209  							So(ok, ShouldBeTrue)
   210  							So(inst.Name, ShouldEqual, "name")
   211  							rt.Type = reflect.TypeOf(map[string]string{})
   212  							return http.StatusOK, &compute.Operation{
   213  								EndTime:    "2018-12-14T15:07:48.200-08:00",
   214  								Status:     "DONE",
   215  								TargetLink: "url",
   216  							}
   217  						default:
   218  							// Second call, to check the reason for the conflict.
   219  							// This request should have no body.
   220  							So(*(req.(*map[string]string)), ShouldHaveLength, 0)
   221  							return http.StatusOK, &compute.Instance{
   222  								CreationTimestamp: "2018-12-14T15:07:48.200-08:00",
   223  								NetworkInterfaces: []*compute.NetworkInterface{
   224  									{
   225  										NetworkIP: "0.0.0.1",
   226  									},
   227  									{
   228  										AccessConfigs: []*compute.AccessConfig{
   229  											{
   230  												NatIP: "2.0.0.0",
   231  											},
   232  										},
   233  										NetworkIP: "0.0.0.2",
   234  									},
   235  									{
   236  										AccessConfigs: []*compute.AccessConfig{
   237  											{
   238  												NatIP: "3.0.0.0",
   239  											},
   240  											{
   241  												NatIP: "3.0.0.1",
   242  											},
   243  										},
   244  										NetworkIP: "0.0.0.3",
   245  									},
   246  								},
   247  								SelfLink: "url",
   248  							}
   249  						}
   250  					}
   251  					rt.Type = reflect.TypeOf(compute.Instance{})
   252  					datastore.Put(c, &model.VM{
   253  						ID:       "id",
   254  						Hostname: "name",
   255  					})
   256  					err := createInstance(c, &tasks.CreateInstance{
   257  						Id: "id",
   258  					})
   259  					So(err, ShouldBeNil)
   260  					v := &model.VM{
   261  						ID: "id",
   262  					}
   263  					So(datastore.Get(c, v), ShouldBeNil)
   264  					So(v.Created, ShouldNotEqual, 0)
   265  					So(v.NetworkInterfaces, ShouldResemble, []model.NetworkInterface{
   266  						{
   267  							InternalIP: "0.0.0.1",
   268  						},
   269  						{
   270  							ExternalIP: "2.0.0.0",
   271  							InternalIP: "0.0.0.2",
   272  						},
   273  						{
   274  							ExternalIP: "3.0.0.0",
   275  							InternalIP: "0.0.0.3",
   276  						},
   277  					})
   278  					So(v.URL, ShouldEqual, "url")
   279  					So(s.Get(c, metrics.CreatedInstanceChecked, time.Time{}, confFields), ShouldEqual, 1)
   280  				})
   281  			})
   282  		})
   283  	})
   284  }
   285  
   286  func TestDestroyInstance(t *testing.T) {
   287  	t.Parallel()
   288  
   289  	Convey("destroyInstance", t, func() {
   290  		dsp := &tq.Dispatcher{}
   291  		registerTasks(dsp)
   292  		rt := &roundtripper.JSONRoundTripper{}
   293  		gce, err := compute.New(&http.Client{Transport: rt})
   294  		So(err, ShouldBeNil)
   295  		c := withCompute(withDispatcher(memory.Use(context.Background()), dsp), ComputeService{Stable: gce})
   296  		tqt := tqtesting.GetTestable(c, dsp)
   297  		tqt.CreateQueues()
   298  
   299  		Convey("invalid", func() {
   300  			Convey("nil", func() {
   301  				err := destroyInstance(c, nil)
   302  				So(err, ShouldErrLike, "unexpected payload")
   303  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   304  			})
   305  
   306  			Convey("empty", func() {
   307  				err := destroyInstance(c, &tasks.DestroyInstance{})
   308  				So(err, ShouldErrLike, "ID is required")
   309  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   310  			})
   311  
   312  			Convey("url", func() {
   313  				err := destroyInstance(c, &tasks.DestroyInstance{
   314  					Id: "id",
   315  				})
   316  				So(err, ShouldErrLike, "URL is required")
   317  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   318  			})
   319  		})
   320  
   321  		Convey("valid", func() {
   322  			Convey("missing", func() {
   323  				err := destroyInstance(c, &tasks.DestroyInstance{
   324  					Id:  "id",
   325  					Url: "url",
   326  				})
   327  				So(err, ShouldBeNil)
   328  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   329  			})
   330  
   331  			Convey("replaced", func() {
   332  				datastore.Put(c, &model.VM{
   333  					ID:  "id",
   334  					URL: "new",
   335  				})
   336  				err := destroyInstance(c, &tasks.DestroyInstance{
   337  					Id:  "id",
   338  					Url: "old",
   339  				})
   340  				So(err, ShouldBeNil)
   341  				v := &model.VM{
   342  					ID: "id",
   343  				}
   344  				So(datastore.Get(c, v), ShouldBeNil)
   345  				So(v.URL, ShouldEqual, "new")
   346  			})
   347  
   348  			Convey("error", func() {
   349  				Convey("http", func() {
   350  					rt.Handler = func(req any) (int, any) {
   351  						return http.StatusInternalServerError, nil
   352  					}
   353  					datastore.Put(c, &model.VM{
   354  						ID:  "id",
   355  						URL: "url",
   356  					})
   357  					err := destroyInstance(c, &tasks.DestroyInstance{
   358  						Id:  "id",
   359  						Url: "url",
   360  					})
   361  					So(err, ShouldErrLike, "failed to destroy instance")
   362  					v := &model.VM{
   363  						ID: "id",
   364  					}
   365  					datastore.Get(c, v)
   366  					So(v.URL, ShouldEqual, "url")
   367  					So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   368  				})
   369  
   370  				Convey("operation", func() {
   371  					rt.Handler = func(req any) (int, any) {
   372  						return http.StatusOK, &compute.Operation{
   373  							Error: &compute.OperationError{
   374  								Errors: []*compute.OperationErrorErrors{
   375  									{},
   376  								},
   377  							},
   378  						}
   379  					}
   380  					datastore.Put(c, &model.VM{
   381  						ID:  "id",
   382  						URL: "url",
   383  					})
   384  					err := destroyInstance(c, &tasks.DestroyInstance{
   385  						Id:  "id",
   386  						Url: "url",
   387  					})
   388  					So(err, ShouldErrLike, "failed to destroy instance")
   389  					v := &model.VM{
   390  						ID: "id",
   391  					}
   392  					datastore.Get(c, v)
   393  					So(v.URL, ShouldEqual, "url")
   394  					So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   395  				})
   396  			})
   397  
   398  			Convey("destroys", func() {
   399  				Convey("pending", func() {
   400  					rt.Handler = func(req any) (int, any) {
   401  						return http.StatusOK, &compute.Operation{}
   402  					}
   403  					datastore.Put(c, &model.VM{
   404  						ID:       "id",
   405  						Created:  1,
   406  						Hostname: "name",
   407  						URL:      "url",
   408  					})
   409  					err := destroyInstance(c, &tasks.DestroyInstance{
   410  						Id:  "id",
   411  						Url: "url",
   412  					})
   413  					So(err, ShouldBeNil)
   414  					v := &model.VM{
   415  						ID: "id",
   416  					}
   417  					datastore.Get(c, v)
   418  					So(v.Created, ShouldEqual, 1)
   419  					So(v.Hostname, ShouldEqual, "name")
   420  					So(v.URL, ShouldEqual, "url")
   421  					So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   422  				})
   423  
   424  				Convey("done", func() {
   425  					rt.Handler = func(req any) (int, any) {
   426  						return http.StatusOK, &compute.Operation{
   427  							Status:     "DONE",
   428  							TargetLink: "url",
   429  						}
   430  					}
   431  					datastore.Put(c, &model.VM{
   432  						ID:       "id",
   433  						Created:  1,
   434  						Hostname: "name",
   435  						URL:      "url",
   436  					})
   437  					err := destroyInstance(c, &tasks.DestroyInstance{
   438  						Id:  "id",
   439  						Url: "url",
   440  					})
   441  					So(err, ShouldBeNil)
   442  					v := &model.VM{
   443  						ID: "id",
   444  					}
   445  					datastore.Get(c, v)
   446  					So(v.Created, ShouldEqual, 1)
   447  					So(v.Hostname, ShouldEqual, "name")
   448  					So(v.URL, ShouldEqual, "url")
   449  					So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
   450  				})
   451  			})
   452  		})
   453  	})
   454  }
   455  
   456  func TestAuditInstanceInZone(t *testing.T) {
   457  	t.Parallel()
   458  
   459  	Convey("auditInstanceInZone", t, func() {
   460  		dsp := &tq.Dispatcher{}
   461  		registerTasks(dsp)
   462  		rt := &roundtripper.JSONRoundTripper{}
   463  		c := context.Background()
   464  		gce, err := compute.NewService(c, option.WithHTTPClient(&http.Client{Transport: rt}))
   465  		So(err, ShouldBeNil)
   466  		c = withCompute(withDispatcher(memory.Use(c), dsp), ComputeService{Stable: gce})
   467  		datastore.GetTestable(c).Consistent(true)
   468  		tqt := tqtesting.GetTestable(c, dsp)
   469  		tqt.CreateQueues()
   470  
   471  		Convey("invalid", func() {
   472  			Convey("nil", func() {
   473  				err := auditInstanceInZone(c, nil)
   474  				So(err, ShouldErrLike, "Unexpected payload")
   475  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   476  			})
   477  
   478  			Convey("empty", func() {
   479  				err := auditInstanceInZone(c, &tasks.AuditProject{})
   480  				So(err, ShouldErrLike, "Project is required")
   481  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   482  			})
   483  
   484  			Convey("empty region", func() {
   485  				err := auditInstanceInZone(c, &tasks.AuditProject{
   486  					Project: "libreboot",
   487  				})
   488  				So(err, ShouldErrLike, "Zone is required")
   489  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   490  			})
   491  		})
   492  
   493  		Convey("valid", func() {
   494  			Convey("VM entry exists", func() {
   495  				count := 0
   496  				// The first request must be to the List API. Should not
   497  				// do any consequent requests
   498  				rt.Handler = func(req any) (int, any) {
   499  					switch count {
   500  					case 0:
   501  						count += 1
   502  						return http.StatusOK, &compute.InstanceList{
   503  							Items: []*compute.Instance{{
   504  								Name: "double-11-puts",
   505  							}},
   506  						}
   507  					default:
   508  						count += 1
   509  						return http.StatusInternalServerError, nil
   510  					}
   511  				}
   512  				err := datastore.Put(c, &model.VM{
   513  					ID:       "double-11",
   514  					Hostname: "double-11-puts",
   515  					Prefix:   "double",
   516  				})
   517  				So(err, ShouldBeNil)
   518  				err = auditInstanceInZone(c, &tasks.AuditProject{
   519  					Project: "libreboot",
   520  					Zone:    "us-mex-1",
   521  				})
   522  				So(err, ShouldBeNil)
   523  				So(count, ShouldEqual, 1)
   524  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   525  			})
   526  
   527  			Convey("VM entry exists double, page token", func() {
   528  				count := 0
   529  				rt.Handler = func(req any) (int, any) {
   530  					switch count {
   531  					case 0:
   532  						count += 1
   533  						return http.StatusOK, &compute.InstanceList{
   534  							Items: []*compute.Instance{{
   535  								Name: "double-11-puts",
   536  							}, {
   537  								Name: "thes-1-puts",
   538  							}},
   539  							NextPageToken: "next-page",
   540  						}
   541  					default:
   542  						count += 1
   543  						return http.StatusInternalServerError, nil
   544  					}
   545  				}
   546  				err := datastore.Put(c, &model.VM{
   547  					ID:       "double-11",
   548  					Hostname: "double-11-puts",
   549  					Prefix:   "double",
   550  				})
   551  				So(err, ShouldBeNil)
   552  				err = datastore.Put(c, &model.VM{
   553  					ID:       "thes-1",
   554  					Hostname: "thes-1-puts",
   555  					Prefix:   "thes",
   556  				})
   557  				So(err, ShouldBeNil)
   558  				err = auditInstanceInZone(c, &tasks.AuditProject{
   559  					Project: "libreboot",
   560  					Zone:    "us-mex-1",
   561  				})
   562  				So(err, ShouldBeNil)
   563  				So(count, ShouldEqual, 1)
   564  				// The next token should schedule a job
   565  				So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
   566  			})
   567  
   568  			Convey("VM leaked (single)", func() {
   569  				count := 0
   570  				rt.Handler = func(req any) (int, any) {
   571  					switch count {
   572  					case 0:
   573  						count += 1
   574  						return http.StatusOK, &compute.InstanceList{
   575  							Items: []*compute.Instance{{
   576  								Name: "double-11-acrd",
   577  							}},
   578  						}
   579  					case 1:
   580  						count += 1
   581  						return http.StatusOK, &compute.Operation{}
   582  					default:
   583  						count += 1
   584  						return http.StatusInternalServerError, nil
   585  					}
   586  				}
   587  				err := datastore.Put(c, &model.VM{
   588  					ID:       "double-11",
   589  					Hostname: "double-11-puts",
   590  					Prefix:   "double",
   591  				})
   592  				So(err, ShouldBeNil)
   593  				err = auditInstanceInZone(c, &tasks.AuditProject{
   594  					Project: "libreboot",
   595  					Zone:    "us-mex-1",
   596  				})
   597  				So(err, ShouldBeNil)
   598  				So(count, ShouldEqual, 2)
   599  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   600  			})
   601  
   602  			Convey("VM leaked (double)", func() {
   603  				count := 0
   604  				rt.Handler = func(req any) (int, any) {
   605  					switch count {
   606  					case 0:
   607  						count += 1
   608  						return http.StatusOK, &compute.InstanceList{
   609  							Items: []*compute.Instance{{
   610  								Name: "double-12-acrd",
   611  							}, {
   612  								Name: "thes-1-acrd",
   613  							}},
   614  						}
   615  					case 1:
   616  						count += 1
   617  						return http.StatusOK, &compute.Operation{}
   618  					case 2:
   619  						count += 1
   620  						return http.StatusOK, &compute.Operation{}
   621  					default:
   622  						count += 1
   623  						return http.StatusInternalServerError, nil
   624  					}
   625  				}
   626  				err := datastore.Put(c, &model.VM{
   627  					ID:       "double-11",
   628  					Hostname: "double-11-puts",
   629  					Prefix:   "double",
   630  				})
   631  				So(err, ShouldBeNil)
   632  				err = datastore.Put(c, &model.VM{
   633  					ID:       "thes-1",
   634  					Hostname: "thes-1-puts",
   635  					Prefix:   "thes",
   636  				})
   637  				So(err, ShouldBeNil)
   638  				err = auditInstanceInZone(c, &tasks.AuditProject{
   639  					Project: "libreboot",
   640  					Zone:    "us-mex-1",
   641  				})
   642  				So(count, ShouldEqual, 3)
   643  				So(err, ShouldBeNil)
   644  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   645  			})
   646  
   647  			Convey("VM leaked (mix)", func() {
   648  				count := 0
   649  				rt.Handler = func(req any) (int, any) {
   650  					switch count {
   651  					case 0:
   652  						count += 1
   653  						return http.StatusOK, &compute.InstanceList{
   654  							Items: []*compute.Instance{{
   655  								Name: "double-12-acrd",
   656  							}, {
   657  								Name: "thes-1-puts",
   658  							}},
   659  						}
   660  					case 1:
   661  						count += 1
   662  						return http.StatusOK, &compute.Operation{
   663  							Status: "Done",
   664  						}
   665  					default:
   666  						count += 1
   667  						return http.StatusInternalServerError, nil
   668  					}
   669  				}
   670  				err := datastore.Put(c, &model.VM{
   671  					ID:       "double-11",
   672  					Hostname: "double-11-puts",
   673  					Prefix:   "double",
   674  				})
   675  				err = auditInstanceInZone(c, &tasks.AuditProject{
   676  					Project: "libreboot",
   677  					Zone:    "us-mex-1",
   678  				})
   679  				So(count, ShouldEqual, 2)
   680  				So(err, ShouldBeNil)
   681  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   682  			})
   683  		})
   684  
   685  		Convey("error", func() {
   686  			Convey("list failure", func() {
   687  				rt.Handler = func(req any) (int, any) {
   688  					return http.StatusInternalServerError, nil
   689  				}
   690  				err := auditInstanceInZone(c, &tasks.AuditProject{
   691  					Project: "libreboot",
   692  					Zone:    "us-mex-1",
   693  				})
   694  				So(err, ShouldErrLike, "failed to list")
   695  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   696  			})
   697  			Convey("VM delete failure", func() {
   698  				count := 0
   699  				rt.Handler = func(req any) (int, any) {
   700  					switch count {
   701  					case 0:
   702  						count += 1
   703  						return http.StatusOK, &compute.InstanceList{
   704  							Items: []*compute.Instance{{
   705  								Name: "double-12-acrd",
   706  							}},
   707  						}
   708  					case 1:
   709  						count += 1
   710  						return http.StatusOK, &compute.Operation{
   711  							Error: &compute.OperationError{
   712  								Errors: []*compute.OperationErrorErrors{
   713  									{},
   714  								},
   715  							},
   716  						}
   717  					default:
   718  						count += 1
   719  						return http.StatusInternalServerError, nil
   720  					}
   721  				}
   722  				err := datastore.Put(c, &model.VM{
   723  					ID:       "double-11",
   724  					Hostname: "double-11-puts",
   725  					Prefix:   "double",
   726  				})
   727  				So(err, ShouldBeNil)
   728  				err = auditInstanceInZone(c, &tasks.AuditProject{
   729  					Project: "libreboot",
   730  					Zone:    "us-mex-1",
   731  				})
   732  				So(count, ShouldEqual, 2)
   733  				So(err, ShouldBeNil)
   734  				So(tqt.GetScheduledTasks(), ShouldBeEmpty)
   735  			})
   736  		})
   737  	})
   738  }
   739  
   740  func TestIsLeakHuerestic(t *testing.T) {
   741  	t.Parallel()
   742  
   743  	Convey("isLeakHuerestic", t, func() {
   744  		c := context.Background()
   745  		c = memory.Use(c)
   746  		datastore.GetTestable(c).Consistent(true)
   747  
   748  		err := datastore.Put(c, &model.VM{
   749  			ID:       "host-vm-test-time-10",
   750  			Hostname: "host-vm-test-time-10-xyz3",
   751  			Prefix:   "host-vm-test-time",
   752  		})
   753  		So(err, ShouldBeNil)
   754  		err = datastore.Put(c, &model.VM{
   755  			ID:       "host-vm-test-time-11",
   756  			Hostname: "host-vm-test-time-11-xwz2",
   757  			Prefix:   "host-vm-test-time",
   758  		})
   759  		So(err, ShouldBeNil)
   760  		Convey("positive results", func() {
   761  			Convey("valid leak with replacement", func() {
   762  				// Leak replaced by host-vm-test-time-10-xyz3
   763  				leak := isLeakHuerestic(c, "host-vm-test-time-10-ijk1", "project", "us-numba-1")
   764  				So(leak, ShouldBeTrue)
   765  			})
   766  			Convey("valid leak resized pool", func() {
   767  				// Leak and pool resized
   768  				leak := isLeakHuerestic(c, "host-vm-test-time-12-abc2", "project", "us-numba-1")
   769  				So(leak, ShouldBeTrue)
   770  			})
   771  		})
   772  		Convey("negative results", func() {
   773  			Convey("non gce-provider instance", func() {
   774  				// gce-provider didn't create this instance
   775  				leak := isLeakHuerestic(c, "sha512-collision-detect", "project", "us-numba-1")
   776  				So(leak, ShouldBeFalse)
   777  			})
   778  			Convey("instance without a current config", func() {
   779  				// Config is deleted for this prefix
   780  				leak := isLeakHuerestic(c, "dut-12-abc2", "project", "us-numba-1")
   781  				So(leak, ShouldBeFalse)
   782  			})
   783  		})
   784  	})
   785  }