golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/makemac/main_test.go (about)

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	spb "go.chromium.org/luci/swarming/proto/api_v2"
    15  	"golang.org/x/build/internal/macservice"
    16  	"golang.org/x/build/internal/secret"
    17  )
    18  
    19  func init() {
    20  	secret.InitFlagSupport(context.Background())
    21  }
    22  
    23  // recordMacServiceClient is a macserviceClient that records mutating requests.
    24  type recordMacServiceClient struct {
    25  	lease  []macservice.LeaseRequest
    26  	renew  []macservice.RenewRequest
    27  	vacate []macservice.VacateRequest
    28  }
    29  
    30  func (r *recordMacServiceClient) Lease(req macservice.LeaseRequest) (macservice.LeaseResponse, error) {
    31  	r.lease = append(r.lease, req)
    32  	return macservice.LeaseResponse{}, nil // Perhaps fake LeaseResponse.PendingLease.LeaseID?
    33  }
    34  
    35  func (r *recordMacServiceClient) Renew(req macservice.RenewRequest) (macservice.RenewResponse, error) {
    36  	r.renew = append(r.renew, req)
    37  	return macservice.RenewResponse{}, nil // Perhaps fake RenewResponse.Expires?
    38  }
    39  
    40  func (r *recordMacServiceClient) Vacate(req macservice.VacateRequest) error {
    41  	r.vacate = append(r.vacate, req)
    42  	return nil
    43  }
    44  
    45  func (r *recordMacServiceClient) Find(req macservice.FindRequest) (macservice.FindResponse, error) {
    46  	return macservice.FindResponse{}, fmt.Errorf("unimplemented")
    47  }
    48  
    49  func TestHandleMissingBots(t *testing.T) {
    50  	const project = managedProjectPrefix + "/swarming.example.com"
    51  
    52  	// Test leases:
    53  	// * "healthy" connected to LUCI, and is healthy.
    54  	// * "dead" connected to LUCI, but later died.
    55  	// * "newBooting" never connected to LUCI, but was just created 5min ago.
    56  	// * "neverBooted" never connected to LUCI, and was created 5hr ago.
    57  	// * "neverBootedUnmanaged" never connected to LUCI, and was created 5hr ago, but is not managed by makemac.
    58  	//
    59  	// handleMissingBots should vacate neverBooted and none of the others.
    60  	bots := map[string]*spb.BotInfo{
    61  		"healthy": {BotId: "healthy"},
    62  		"dead":    {BotId: "dead", IsDead: true},
    63  	}
    64  	leases := map[string]macservice.Instance{
    65  		"healthy": {
    66  			Lease: macservice.Lease{
    67  				LeaseID:             "healthy",
    68  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
    69  				Expires:             time.Now().Add(createExpirationDuration - 2*time.Hour),
    70  			},
    71  		},
    72  		"newBooting": {
    73  			Lease: macservice.Lease{
    74  				LeaseID:             "newBooting",
    75  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
    76  				Expires:             time.Now().Add(createExpirationDuration - 5*time.Minute),
    77  			},
    78  		},
    79  		"neverBooted": {
    80  			Lease: macservice.Lease{
    81  				LeaseID:             "neverBooted",
    82  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
    83  				Expires:             time.Now().Add(createExpirationDuration - 2*time.Hour),
    84  			},
    85  		},
    86  		"neverBootedUnmanaged": {
    87  			Lease: macservice.Lease{
    88  				LeaseID:             "neverBootedUnmanaged",
    89  				VMResourceNamespace: macservice.Namespace{ProjectName: "other"},
    90  				Expires:             time.Now().Add(createExpirationDuration - 2*time.Hour),
    91  			},
    92  		},
    93  	}
    94  
    95  	var mc recordMacServiceClient
    96  	handleMissingBots(&mc, bots, leases)
    97  
    98  	got := mc.vacate
    99  	want := []macservice.VacateRequest{{LeaseID: "neverBooted"}}
   100  	if diff := cmp.Diff(want, got); diff != "" {
   101  		t.Errorf("Vacated leases mismatch (-want +got):\n%s", diff)
   102  	}
   103  
   104  	if _, ok := leases["neverBooted"]; ok {
   105  		t.Errorf("neverBooted present in leases, want deleted")
   106  	}
   107  }
   108  
   109  func TestHandleDeadBots(t *testing.T) {
   110  	const project = managedProjectPrefix + "/swarming.example.com"
   111  
   112  	// Test leases:
   113  	// * "healthy" connected to LUCI, and is healthy.
   114  	// * "dead" connected to LUCI, but later died, and the lease is gone from MacService.
   115  	// * "deadLeasePresent" connected to LUCI, but later died, and the lease is still present on MacService.
   116  	// * "deadLeasePresentUnmanaged" connected to LUCI, but later died, and the lease is still present on MacService, but is not managed by makemac.
   117  	// * "neverBooted" never connected to LUCI, and was created 5hr ago.
   118  	//
   119  	// handleDeadBots should vacate deadLeasePresent and none of the others.
   120  	bots := map[string]*spb.BotInfo{
   121  		"healthy":          {BotId: "healthy"},
   122  		"dead":             {BotId: "dead", IsDead: true},
   123  		"deadLeasePresent": {BotId: "deadLeasePresent", IsDead: true},
   124  	}
   125  	leases := map[string]macservice.Instance{
   126  		"healthy": {
   127  			Lease: macservice.Lease{
   128  				LeaseID:             "healthy",
   129  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
   130  				Expires:             time.Now().Add(createExpirationDuration - 2*time.Hour),
   131  			},
   132  		},
   133  		"deadLeasePresent": {
   134  			Lease: macservice.Lease{
   135  				LeaseID:             "deadLeasePresent",
   136  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
   137  				// Lease created 5 minutes ago. Doesn't matter;
   138  				// new lease checked don't apply here. See
   139  				// comment in handleDeadBots.
   140  				Expires: time.Now().Add(createExpirationDuration - 5*time.Minute),
   141  			},
   142  		},
   143  		"deadLeasePresentUnmanaged": {
   144  			Lease: macservice.Lease{
   145  				LeaseID:             "deadLeasePresentUnmanaged",
   146  				VMResourceNamespace: macservice.Namespace{ProjectName: "other"},
   147  				Expires:             time.Now().Add(createExpirationDuration - 5*time.Minute),
   148  			},
   149  		},
   150  		"neverBooted": {
   151  			Lease: macservice.Lease{
   152  				LeaseID:             "neverBooted",
   153  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
   154  				Expires:             time.Now().Add(createExpirationDuration - 2*time.Hour),
   155  			},
   156  		},
   157  	}
   158  
   159  	var mc recordMacServiceClient
   160  	handleDeadBots(&mc, bots, leases)
   161  
   162  	got := mc.vacate
   163  	want := []macservice.VacateRequest{{LeaseID: "deadLeasePresent"}}
   164  	if diff := cmp.Diff(want, got); diff != "" {
   165  		t.Errorf("Vacated leases mismatch (-want +got):\n%s", diff)
   166  	}
   167  
   168  	if _, ok := leases["deadLeasePresent"]; ok {
   169  		t.Errorf("deadLeasePresent present in leases, want deleted")
   170  	}
   171  }
   172  
   173  func TestRenewLeases(t *testing.T) {
   174  	const project = managedProjectPrefix + "/swarming.example.com"
   175  
   176  	// Test leases:
   177  	// * "new" was created <1hr ago.
   178  	// * "standard" was created >1hr ago.
   179  	// * "unmanaged" was created >1hr ago, but is not managed by makemac.
   180  	//
   181  	// renewLeases should renew "standard" and none of the others.
   182  	leases := map[string]macservice.Instance{
   183  		"new": {
   184  			Lease: macservice.Lease{
   185  				LeaseID:             "new",
   186  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
   187  				Expires:             time.Now().Add(createExpirationDuration - 5*time.Minute),
   188  			},
   189  		},
   190  		"standard": {
   191  			Lease: macservice.Lease{
   192  				LeaseID:             "standard",
   193  				VMResourceNamespace: macservice.Namespace{ProjectName: project},
   194  				Expires:             time.Now().Add(renewExpirationDuration - 5*time.Minute),
   195  			},
   196  		},
   197  		"unmanaged": {
   198  			Lease: macservice.Lease{
   199  				LeaseID:             "unmanaged",
   200  				VMResourceNamespace: macservice.Namespace{ProjectName: "other"},
   201  				Expires:             time.Now().Add(renewExpirationDuration - 5*time.Minute),
   202  			},
   203  		},
   204  	}
   205  
   206  	var mc recordMacServiceClient
   207  	renewLeases(&mc, leases)
   208  
   209  	got := mc.renew
   210  	want := []macservice.RenewRequest{{LeaseID: "standard", Duration: renewExpirationDurationString}}
   211  	if diff := cmp.Diff(want, got); diff != "" {
   212  		t.Errorf("Renewed leases mismatch (-want +got):\n%s", diff)
   213  	}
   214  }
   215  
   216  func TestHandleObsoleteLeases(t *testing.T) {
   217  	swarming1 := &swarmingConfig{
   218  		Host: "swarming1.example.com",
   219  		Pool: "example.pool",
   220  	}
   221  	project1 := managedProjectPrefix + "/" + swarming1.Host
   222  	swarming2 := &swarmingConfig{
   223  		Host: "swarming2.example.com",
   224  		Pool: "example.pool",
   225  	}
   226  	project2 := managedProjectPrefix + "/" + swarming2.Host
   227  
   228  	// Test leases:
   229  	// * "active" uses image "active-image"
   230  	// * "obsolete" uses image "obsolete-image"
   231  	// * "obsolete-on-swarming2" uses image "obsolete-image" on "swarming2" (as configured)
   232  	// * "unmanaged" uses image "obsolete-image", but is not managed by makemac.
   233  	//
   234  	// handleObsoleteLeases should vacate "obsolete" and none of the others.
   235  	config := map[*swarmingConfig][]imageConfig{
   236  		swarming1: {
   237  			{
   238  				Hostname: "active",
   239  				Cert:     "dummy-cert",
   240  				Key:      "dummy-key",
   241  				Image:    "active-image",
   242  				MinCount: 1,
   243  			},
   244  		},
   245  		swarming2: {
   246  			{
   247  				Hostname: "obsolete-on-swarming2",
   248  				Cert:     "dummy-cert",
   249  				Key:      "dummy-key",
   250  				Image:    "obsolete-image",
   251  				MinCount: 1,
   252  			},
   253  		},
   254  	}
   255  	leases := map[string]macservice.Instance{
   256  		"active": {
   257  			Lease: macservice.Lease{
   258  				LeaseID:             "active",
   259  				VMResourceNamespace: macservice.Namespace{ProjectName: project1},
   260  			},
   261  			InstanceSpecification: macservice.InstanceSpecification{
   262  				DiskSelection: macservice.DiskSelection{
   263  					ImageHashes: macservice.ImageHashes{
   264  						BootSHA256: "active-image",
   265  					},
   266  				},
   267  			},
   268  		},
   269  		"obsolete": {
   270  			Lease: macservice.Lease{
   271  				LeaseID:             "obsolete",
   272  				VMResourceNamespace: macservice.Namespace{ProjectName: project1},
   273  			},
   274  			InstanceSpecification: macservice.InstanceSpecification{
   275  				DiskSelection: macservice.DiskSelection{
   276  					ImageHashes: macservice.ImageHashes{
   277  						BootSHA256: "obsolete-image",
   278  					},
   279  				},
   280  			},
   281  		},
   282  		"obsolete-on-swarming2": {
   283  			Lease: macservice.Lease{
   284  				LeaseID:             "obsolete-on-swarming2",
   285  				VMResourceNamespace: macservice.Namespace{ProjectName: project2},
   286  			},
   287  			InstanceSpecification: macservice.InstanceSpecification{
   288  				DiskSelection: macservice.DiskSelection{
   289  					ImageHashes: macservice.ImageHashes{
   290  						BootSHA256: "obsolete-image",
   291  					},
   292  				},
   293  			},
   294  		},
   295  		"unmanaged": {
   296  			Lease: macservice.Lease{
   297  				LeaseID:             "obsolete",
   298  				VMResourceNamespace: macservice.Namespace{ProjectName: "other"},
   299  			},
   300  			InstanceSpecification: macservice.InstanceSpecification{
   301  				DiskSelection: macservice.DiskSelection{
   302  					ImageHashes: macservice.ImageHashes{
   303  						BootSHA256: "obsolete-image",
   304  					},
   305  				},
   306  			},
   307  		},
   308  	}
   309  
   310  	var mc recordMacServiceClient
   311  	handleObsoleteLeases(&mc, config, leases)
   312  
   313  	got := mc.vacate
   314  	want := []macservice.VacateRequest{{LeaseID: "obsolete"}}
   315  	if diff := cmp.Diff(want, got); diff != "" {
   316  		t.Errorf("Vacated leases mismatch (-want +got):\n%s", diff)
   317  	}
   318  }
   319  
   320  func TestAddNewLeases(t *testing.T) {
   321  	swarming1 := &swarmingConfig{
   322  		Host: "swarming1.example.com",
   323  		Pool: "example.pool",
   324  	}
   325  	project1 := managedProjectPrefix + "/" + swarming1.Host
   326  	swarming2 := &swarmingConfig{
   327  		Host: "swarming2.example.com",
   328  		Pool: "example.pool",
   329  	}
   330  
   331  	// Test leases:
   332  	// * "image-a-1" uses image "image-a" on "swarming1"
   333  	// * "unmanaged" uses image "image-a", but is not managed by makemac.
   334  	//
   335  	// Test images:
   336  	// * On "swarming1":
   337  	//   * "image-a" wants 2 instances.
   338  	//   * "image-b" wants 2 instances.
   339  	// * On "swarming2":
   340  	//   * "image-a" wants 1 instances.
   341  	//
   342  	// addNewLeases should create:
   343  	// * 1 "image-a" instance on "swarming1"
   344  	// * 1 "image-a" instance on "swarming2"
   345  	// * 2 "image-b" instances on "swarming1"
   346  	config := map[*swarmingConfig][]imageConfig{
   347  		swarming1: {
   348  			{
   349  				Hostname: "a",
   350  				Cert:     "dummy-cert",
   351  				Key:      "dummy-key",
   352  				Image:    "image-a",
   353  				MinCount: 2,
   354  			},
   355  			{
   356  				Hostname: "b",
   357  				Cert:     "dummy-cert",
   358  				Key:      "dummy-key",
   359  				Image:    "image-b",
   360  				MinCount: 2,
   361  			},
   362  		},
   363  		swarming2: {
   364  			{
   365  				Hostname: "a",
   366  				Cert:     "dummy-cert",
   367  				Key:      "dummy-key",
   368  				Image:    "image-a",
   369  				MinCount: 1,
   370  			},
   371  		},
   372  	}
   373  	leases := map[string]macservice.Instance{
   374  		"image-a-1": {
   375  			Lease: macservice.Lease{
   376  				LeaseID:             "image-a-1",
   377  				VMResourceNamespace: macservice.Namespace{ProjectName: project1},
   378  			},
   379  			InstanceSpecification: macservice.InstanceSpecification{
   380  				DiskSelection: macservice.DiskSelection{
   381  					ImageHashes: macservice.ImageHashes{
   382  						BootSHA256: "image-a",
   383  					},
   384  				},
   385  			},
   386  		},
   387  		"unmanaged": {
   388  			Lease: macservice.Lease{
   389  				LeaseID:             "unmanaged",
   390  				VMResourceNamespace: macservice.Namespace{ProjectName: "other"},
   391  			},
   392  			InstanceSpecification: macservice.InstanceSpecification{
   393  				DiskSelection: macservice.DiskSelection{
   394  					ImageHashes: macservice.ImageHashes{
   395  						BootSHA256: "image-a",
   396  					},
   397  				},
   398  			},
   399  		},
   400  	}
   401  
   402  	var mc recordMacServiceClient
   403  	addNewLeases(&mc, config, leases)
   404  
   405  	leaseASwarm1, err := makeLeaseRequest(swarming1, &config[swarming1][0])
   406  	if err != nil {
   407  		t.Fatalf("makeLeaseRequest(a, swarm1) got err %v want nil", err)
   408  	}
   409  	leaseBSwarm1, err := makeLeaseRequest(swarming1, &config[swarming1][1])
   410  	if err != nil {
   411  		t.Fatalf("makeLeaseRequest(b, swarm1) got err %v want nil", err)
   412  	}
   413  	leaseASwarm2, err := makeLeaseRequest(swarming2, &config[swarming2][0])
   414  	if err != nil {
   415  		t.Fatalf("makeLeaseRequest(a, swarm2) got err %v want nil", err)
   416  	}
   417  
   418  	got := mc.lease
   419  	want := []macservice.LeaseRequest{leaseASwarm1, leaseBSwarm1, leaseBSwarm1, leaseASwarm2}
   420  	if diff := cmp.Diff(want, got); diff != "" {
   421  		t.Errorf("Lease request mismatch (-want +got):\n%s", diff)
   422  	}
   423  }