golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/pool/ec2_test.go (about) 1 // Copyright 2020 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 //go:build linux || darwin 6 7 package pool 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "sort" 14 "testing" 15 "time" 16 17 "github.com/google/go-cmp/cmp" 18 "golang.org/x/build/buildenv" 19 "golang.org/x/build/buildlet" 20 "golang.org/x/build/dashboard" 21 "golang.org/x/build/internal/cloud" 22 "golang.org/x/build/internal/coordinator/pool/queue" 23 "golang.org/x/build/internal/spanlog" 24 ) 25 26 func TestEC2BuildletGetBuildlet(t *testing.T) { 27 host := "host-type-x" 28 29 l := newLedger() 30 l.UpdateInstanceTypes([]*cloud.InstanceType{ 31 // set to default gce type because there is no way to set the machine 32 // type from outside of the buildenv package. 33 { 34 Type: "e2-standard-16", 35 CPU: 16, 36 }, 37 }) 38 l.SetCPULimit(20) 39 40 bp := &EC2Buildlet{ 41 buildletClient: &fakeEC2BuildletClient{ 42 createVMRequestSuccess: true, 43 VMCreated: true, 44 buildletCreated: true, 45 }, 46 buildEnv: &buildenv.Environment{}, 47 ledger: l, 48 hosts: map[string]*dashboard.HostConfig{ 49 host: { 50 VMImage: "ami-15", 51 ContainerImage: "bar-arm64:latest", 52 SSHUsername: "foo", 53 }, 54 }, 55 } 56 _, err := bp.GetBuildlet(context.Background(), host, noopEventTimeLogger{}, new(queue.SchedItem)) 57 if err != nil { 58 t.Errorf("EC2Buildlet.GetBuildlet(ctx, %q, %+v) = _, %s; want no error", host, noopEventTimeLogger{}, err) 59 } 60 } 61 62 func TestEC2BuildletGetBuildletError(t *testing.T) { 63 host := "host-type-x" 64 testCases := []struct { 65 desc string 66 hostType string 67 logger Logger 68 ledger *ledger 69 types []*cloud.InstanceType 70 buildletClient ec2BuildletClient 71 hosts map[string]*dashboard.HostConfig 72 }{ 73 { 74 desc: "invalid-host-type", 75 hostType: host, 76 ledger: newLedger(), 77 types: []*cloud.InstanceType{ 78 { 79 Type: "e2-highcpu-2", 80 CPU: 4, 81 }, 82 }, 83 hosts: map[string]*dashboard.HostConfig{ 84 "wrong-host-type": {}, 85 }, 86 logger: noopEventTimeLogger{}, 87 buildletClient: &fakeEC2BuildletClient{ 88 createVMRequestSuccess: true, 89 VMCreated: true, 90 }, 91 }, 92 { 93 desc: "buildlet-client-failed-instance-created", 94 hostType: host, 95 ledger: newLedger(), 96 types: []*cloud.InstanceType{ 97 { 98 Type: "e2-highcpu-2", 99 CPU: 4, 100 }, 101 }, 102 hosts: map[string]*dashboard.HostConfig{ 103 host: {}, 104 }, 105 logger: noopEventTimeLogger{}, 106 buildletClient: &fakeEC2BuildletClient{ 107 createVMRequestSuccess: false, 108 VMCreated: false, 109 }, 110 }, 111 { 112 desc: "buildlet-client-failed-instance-not-created", 113 hostType: host, 114 ledger: newLedger(), 115 types: []*cloud.InstanceType{ 116 { 117 Type: "e2-highcpu-2", 118 CPU: 4, 119 }, 120 }, 121 hosts: map[string]*dashboard.HostConfig{ 122 host: {}, 123 }, 124 logger: noopEventTimeLogger{}, 125 buildletClient: &fakeEC2BuildletClient{ 126 createVMRequestSuccess: true, 127 VMCreated: false, 128 }, 129 }, 130 } 131 for _, tt := range testCases { 132 t.Run(tt.desc, func(t *testing.T) { 133 bp := &EC2Buildlet{ 134 buildletClient: tt.buildletClient, 135 buildEnv: &buildenv.Environment{}, 136 ledger: tt.ledger, 137 hosts: tt.hosts, 138 } 139 tt.ledger.UpdateInstanceTypes(tt.types) 140 _, gotErr := bp.GetBuildlet(context.Background(), tt.hostType, tt.logger, new(queue.SchedItem)) 141 if gotErr == nil { 142 t.Errorf("EC2Buildlet.GetBuildlet(ctx, %q, %+v) = _, %s", tt.hostType, tt.logger, gotErr) 143 } 144 }) 145 } 146 } 147 148 func TestEC2BuildletGetBuildletLogger(t *testing.T) { 149 host := "host-type-x" 150 testCases := []struct { 151 desc string 152 buildletClient ec2BuildletClient 153 hostType string 154 hosts map[string]*dashboard.HostConfig 155 ledger *ledger 156 types []*cloud.InstanceType 157 wantLogs []string 158 wantSpans []string 159 wantSpansErr []string 160 }{ 161 { 162 desc: "buildlet-client-failed-instance-create-request-failed", 163 hostType: host, 164 ledger: newLedger(), 165 types: []*cloud.InstanceType{ 166 { 167 Type: "e2-standard-8", 168 CPU: 8, 169 }, 170 }, 171 hosts: map[string]*dashboard.HostConfig{ 172 host: {}, 173 }, 174 buildletClient: &fakeEC2BuildletClient{ 175 createVMRequestSuccess: false, 176 VMCreated: false, 177 buildletCreated: false, 178 }, 179 wantSpans: []string{"create_ec2_instance", "awaiting_ec2_quota", "create_ec2_buildlet"}, 180 wantSpansErr: []string{"create_ec2_buildlet", "create_ec2_instance"}, 181 }, 182 { 183 desc: "buildlet-client-failed-instance-not-created", 184 hostType: host, 185 ledger: newLedger(), 186 types: []*cloud.InstanceType{ 187 { 188 Type: "e2-standard-8", 189 CPU: 8, 190 }, 191 }, 192 hosts: map[string]*dashboard.HostConfig{ 193 host: {}, 194 }, 195 buildletClient: &fakeEC2BuildletClient{ 196 createVMRequestSuccess: true, 197 VMCreated: false, 198 buildletCreated: false, 199 }, 200 wantSpans: []string{"create_ec2_instance", "awaiting_ec2_quota", "create_ec2_buildlet"}, 201 wantSpansErr: []string{"create_ec2_buildlet", "create_ec2_instance"}, 202 }, 203 { 204 desc: "buildlet-client-failed-instance-created", 205 hostType: host, 206 ledger: newLedger(), 207 types: []*cloud.InstanceType{ 208 { 209 Type: "e2-standard-8", 210 CPU: 8, 211 }, 212 }, 213 hosts: map[string]*dashboard.HostConfig{ 214 host: {}, 215 }, 216 buildletClient: &fakeEC2BuildletClient{ 217 createVMRequestSuccess: true, 218 VMCreated: true, 219 buildletCreated: false, 220 }, 221 wantSpans: []string{"create_ec2_instance", "awaiting_ec2_quota", "create_ec2_buildlet", "wait_buildlet_start"}, 222 wantSpansErr: []string{"create_ec2_buildlet", "wait_buildlet_start"}, 223 }, 224 { 225 desc: "success", 226 hostType: host, 227 ledger: newLedger(), 228 types: []*cloud.InstanceType{ 229 { 230 Type: "e2-standard-8", 231 CPU: 8, 232 }, 233 }, 234 hosts: map[string]*dashboard.HostConfig{ 235 host: {}, 236 }, 237 buildletClient: &fakeEC2BuildletClient{ 238 createVMRequestSuccess: true, 239 VMCreated: true, 240 buildletCreated: true, 241 }, 242 wantSpans: []string{"create_ec2_instance", "create_ec2_buildlet", "awaiting_ec2_quota", "wait_buildlet_start"}, 243 wantSpansErr: []string{}, 244 }, 245 } 246 for _, tc := range testCases { 247 t.Run(tc.desc, func(t *testing.T) { 248 bp := &EC2Buildlet{ 249 buildletClient: tc.buildletClient, 250 buildEnv: &buildenv.Environment{}, 251 ledger: tc.ledger, 252 hosts: tc.hosts, 253 } 254 bp.ledger.SetCPULimit(20) 255 bp.ledger.UpdateInstanceTypes(tc.types) 256 l := newTestLogger() 257 _, _ = bp.GetBuildlet(context.Background(), tc.hostType, l, new(queue.SchedItem)) 258 if !cmp.Equal(l.spanEvents(), tc.wantSpans, cmp.Transformer("sort", func(in []string) []string { 259 out := append([]string(nil), in...) 260 sort.Strings(out) 261 return out 262 })) { 263 t.Errorf("span events = %+v; want %+v", l.spanEvents(), tc.wantSpans) 264 } 265 for _, spanErr := range tc.wantSpansErr { 266 s, ok := l.spans[spanErr] 267 if !ok { 268 t.Fatalf("log span %q does not exist", spanErr) 269 } 270 if s.err == nil { 271 t.Fatalf("testLogger.span[%q].err is nil", spanErr) 272 } 273 } 274 }) 275 } 276 } 277 278 func TestEC2BuildletString(t *testing.T) { 279 testCases := []struct { 280 desc string 281 instCount int64 282 cpuCount int64 283 cpuLimit int64 284 }{ 285 {"default", 0, 0, 0}, 286 {"non-default", 2, 2, 3}, 287 } 288 for _, tc := range testCases { 289 t.Run(tc.desc, func(t *testing.T) { 290 es := make([]*entry, tc.instCount) 291 entries := make(map[string]*entry) 292 for i, e := range es { 293 entries[fmt.Sprintf("%d", i)] = e 294 } 295 l := newLedger() 296 eb := &EC2Buildlet{ledger: l} 297 l.entries = entries 298 eb.ledger.cpuQueue.UpdateQuotas(int(tc.cpuCount), int(tc.cpuLimit)) 299 want := fmt.Sprintf("EC2 pool capacity: %d instances; %d/%d CPUs", tc.instCount, tc.cpuCount, tc.cpuLimit) 300 got := eb.String() 301 if got != want { 302 t.Errorf("EC2Buildlet.String() = %s; want %s", got, want) 303 } 304 }) 305 } 306 } 307 308 func TestEC2BuildletCapacityString(t *testing.T) { 309 testCases := []struct { 310 desc string 311 instCount int64 312 cpuCount int64 313 cpuLimit int64 314 }{ 315 {"defaults", 0, 0, 0}, 316 {"non-default", 2, 2, 3}, 317 } 318 for _, tc := range testCases { 319 t.Run(tc.desc, func(t *testing.T) { 320 es := make([]*entry, tc.instCount) 321 entries := make(map[string]*entry) 322 for i, e := range es { 323 entries[fmt.Sprintf("%d", i)] = e 324 } 325 l := newLedger() 326 l.entries = entries 327 eb := &EC2Buildlet{ledger: l} 328 eb.ledger.cpuQueue.UpdateQuotas(int(tc.cpuCount), int(tc.cpuLimit)) 329 want := fmt.Sprintf("%d instances; %d/%d CPUs", tc.instCount, tc.cpuCount, tc.cpuLimit) 330 got := eb.capacityString() 331 if got != want { 332 t.Errorf("EC2Buildlet.capacityString() = %s; want %s", got, want) 333 } 334 }) 335 } 336 } 337 338 func TestEC2BuildletbuildletDone(t *testing.T) { 339 t.Run("done-successful", func(t *testing.T) { 340 instName := "instance-name-x" 341 342 awsC := cloud.NewFakeAWSClient() 343 inst, err := awsC.CreateInstance(context.Background(), &cloud.EC2VMConfiguration{ 344 Description: "test instance", 345 ImageID: "image-x", 346 Name: instName, 347 SSHKeyID: "key-14", 348 Tags: map[string]string{}, 349 Type: "type-x", 350 Zone: "zone-1", 351 }) 352 if err != nil { 353 t.Errorf("unable to create instance: %s", err) 354 } 355 356 l := newLedger() 357 pool := &EC2Buildlet{ 358 awsClient: awsC, 359 ledger: l, 360 } 361 l.entries = map[string]*entry{ 362 instName: { 363 createdAt: time.Now(), 364 instanceID: inst.ID, 365 instanceName: instName, 366 vCPUCount: 5, 367 quota: new(queue.Item), 368 }, 369 } 370 pool.buildletDone(instName) 371 if gotID := pool.ledger.InstanceID(instName); gotID != "" { 372 t.Errorf("ledger.instanceID = %q; want %q", gotID, "") 373 } 374 gotInsts, err := awsC.RunningInstances(context.Background()) 375 if err != nil || len(gotInsts) != 0 { 376 t.Errorf("awsClient.RunningInstances(ctx) = %+v, %s; want [], nil", gotInsts, err) 377 } 378 }) 379 t.Run("instance-not-in-ledger", func(t *testing.T) { 380 instName := "instance-name-x" 381 382 awsC := cloud.NewFakeAWSClient() 383 inst, err := awsC.CreateInstance(context.Background(), &cloud.EC2VMConfiguration{ 384 Description: "test instance", 385 ImageID: "image-x", 386 Name: instName, 387 SSHKeyID: "key-14", 388 Tags: map[string]string{}, 389 Type: "type-x", 390 Zone: "zone-1", 391 }) 392 if err != nil { 393 t.Errorf("unable to create instance: %s", err) 394 } 395 396 pool := &EC2Buildlet{ 397 awsClient: awsC, 398 ledger: newLedger(), 399 } 400 pool.buildletDone(inst.Name) 401 gotInsts, err := awsC.RunningInstances(context.Background()) 402 if err != nil || len(gotInsts) != 1 { 403 t.Errorf("awsClient.RunningInstances(ctx) = %+v, %s; want 1 instance, nil", gotInsts, err) 404 } 405 }) 406 t.Run("instance-not-in-ec2", func(t *testing.T) { 407 instName := "instance-name-x" 408 l := newLedger() 409 pool := &EC2Buildlet{ 410 awsClient: cloud.NewFakeAWSClient(), 411 ledger: l, 412 } 413 l.entries = map[string]*entry{ 414 instName: { 415 createdAt: time.Now(), 416 instanceID: "instance-id-14", 417 instanceName: instName, 418 vCPUCount: 5, 419 quota: new(queue.Item), 420 }, 421 } 422 pool.buildletDone(instName) 423 if gotID := pool.ledger.InstanceID(instName); gotID != "" { 424 t.Errorf("ledger.instanceID = %q; want %q", gotID, "") 425 } 426 }) 427 } 428 429 func TestEC2BuildletClose(t *testing.T) { 430 cancelled := false 431 pool := &EC2Buildlet{ 432 cancelPoll: func() { cancelled = true }, 433 } 434 pool.Close() 435 if !cancelled { 436 t.Error("EC2Buildlet.pollCancel not called") 437 } 438 } 439 440 func TestEC2BuildletRetrieveAndSetQuota(t *testing.T) { 441 pool := &EC2Buildlet{ 442 awsClient: cloud.NewFakeAWSClient(), 443 ledger: newLedger(), 444 } 445 err := pool.retrieveAndSetQuota(context.Background()) 446 if err != nil { 447 t.Errorf("EC2Buildlet.retrieveAndSetQuota(ctx) = %s; want nil", err) 448 } 449 usage := pool.ledger.cpuQueue.Quotas() 450 if usage.Limit == 0 { 451 t.Errorf("ledger.cpuLimit = %d; want non-zero", usage.Limit) 452 } 453 } 454 455 func TestEC2BuildletRetrieveAndSetInstanceTypes(t *testing.T) { 456 pool := &EC2Buildlet{ 457 awsClient: cloud.NewFakeAWSClient(), 458 ledger: newLedger(), 459 } 460 err := pool.retrieveAndSetInstanceTypes() 461 if err != nil { 462 t.Errorf("EC2Buildlet.retrieveAndSetInstanceTypes() = %s; want nil", err) 463 } 464 if len(pool.ledger.types) == 0 { 465 t.Errorf("len(pool.ledger.types) = %d; want non-zero", len(pool.ledger.types)) 466 } 467 } 468 469 func TestEC2BuildeletDestroyUntrackedInstances(t *testing.T) { 470 awsC := cloud.NewFakeAWSClient() 471 create := func(name string) *cloud.Instance { 472 inst, err := awsC.CreateInstance(context.Background(), &cloud.EC2VMConfiguration{ 473 Description: "test instance", 474 ImageID: "image-x", 475 Name: name, 476 SSHKeyID: "key-14", 477 Tags: map[string]string{}, 478 Type: "type-x", 479 Zone: "zone-1", 480 }) 481 if err != nil { 482 t.Errorf("unable to create instance: %s", err) 483 } 484 return inst 485 } 486 // create untracked instances 487 for it := 0; it < 10; it++ { 488 _ = create(instanceName("host-test-type", 10)) 489 } 490 wantTrackedInst := create(instanceName("host-test-type", 10)) 491 wantRemoteInst := create(instanceName("host-test-type", 10)) 492 _ = create("debug-tiger-host-14") // non buildlet instance 493 494 l := newLedger() 495 pool := &EC2Buildlet{ 496 awsClient: awsC, 497 isRemoteBuildlet: func(name string) bool { 498 if name == wantRemoteInst.Name { 499 return true 500 } 501 return false 502 }, 503 ledger: l, 504 } 505 l.entries = map[string]*entry{ 506 wantTrackedInst.Name: { 507 createdAt: time.Now(), 508 instanceID: wantTrackedInst.ID, 509 instanceName: wantTrackedInst.Name, 510 vCPUCount: 4, 511 }, 512 } 513 pool.destroyUntrackedInstances(context.Background()) 514 wantInstCount := 3 515 gotInsts, err := awsC.RunningInstances(context.Background()) 516 if err != nil || len(gotInsts) != wantInstCount { 517 t.Errorf("awsClient.RunningInstances(ctx) = %+v, %s; want %d instances and no error", gotInsts, err, wantInstCount) 518 } 519 } 520 521 // fakeEC2BuildletClient is the client used to create buildlets on EC2. 522 type fakeEC2BuildletClient struct { 523 createVMRequestSuccess bool 524 VMCreated bool 525 buildletCreated bool 526 } 527 528 // StartNewVM boots a new VM on EC2, waits until the client is accepting connections 529 // on the configured port and returns a buildlet client configured communicate with it. 530 func (f *fakeEC2BuildletClient) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *buildlet.VMOpts) (buildlet.Client, error) { 531 // check required params 532 if opts == nil || opts.TLS.IsZero() { 533 return nil, errors.New("TLS keypair is not set") 534 } 535 if buildEnv == nil { 536 return nil, errors.New("invalid build environment") 537 } 538 if hconf == nil { 539 return nil, errors.New("invalid host configuration") 540 } 541 if vmName == "" || hostType == "" { 542 return nil, fmt.Errorf("invalid vmName: %q and hostType: %q", vmName, hostType) 543 } 544 if opts.DeleteIn == 0 { 545 // Note: This implements a short default in the rare case the caller doesn't care. 546 opts.DeleteIn = 30 * time.Minute 547 } 548 if !f.createVMRequestSuccess { 549 return nil, fmt.Errorf("unable to create instance %s: creation disabled", vmName) 550 } 551 condRun := func(fn func()) { 552 if fn != nil { 553 fn() 554 } 555 } 556 condRun(opts.OnInstanceRequested) 557 if !f.VMCreated { 558 return nil, errors.New("error waiting for instance to exist: vm existence disabled") 559 } 560 561 condRun(opts.OnInstanceCreated) 562 563 if !f.buildletCreated { 564 return nil, errors.New("error waiting for buildlet: buildlet creation disabled") 565 } 566 567 if opts.OnGotEC2InstanceInfo != nil { 568 opts.OnGotEC2InstanceInfo(&cloud.Instance{ 569 CPUCount: 4, 570 CreatedAt: time.Time{}, 571 Description: "sample vm", 572 ID: "id-" + instanceName("random", 4), 573 IPAddressExternal: "127.0.0.1", 574 IPAddressInternal: "127.0.0.1", 575 ImageID: "image-x", 576 Name: vmName, 577 SSHKeyID: "key-15", 578 SecurityGroups: nil, 579 State: "running", 580 Tags: map[string]string{ 581 "foo": "bar", 582 }, 583 Type: "yy.large", 584 Zone: "zone-a", 585 }) 586 } 587 return &buildlet.FakeClient{}, nil 588 } 589 590 type testLogger struct { 591 eventTimes []eventTime 592 spans map[string]*span 593 } 594 595 type eventTime struct { 596 event string 597 opt []string 598 } 599 600 type span struct { 601 event string 602 opt []string 603 err error 604 calledDone bool 605 } 606 607 func (s *span) Done(err error) error { 608 s.err = err 609 s.calledDone = true 610 return nil 611 } 612 613 func newTestLogger() *testLogger { 614 return &testLogger{ 615 eventTimes: make([]eventTime, 0, 5), 616 spans: make(map[string]*span), 617 } 618 } 619 620 func (l *testLogger) LogEventTime(event string, optText ...string) { 621 l.eventTimes = append(l.eventTimes, eventTime{ 622 event: event, 623 opt: optText, 624 }) 625 } 626 627 func (l *testLogger) CreateSpan(event string, optText ...string) spanlog.Span { 628 s := &span{ 629 event: event, 630 opt: optText, 631 } 632 l.spans[event] = s 633 return s 634 } 635 636 func (l *testLogger) spanEvents() []string { 637 se := make([]string, 0, len(l.spans)) 638 for k, s := range l.spans { 639 if !s.calledDone { 640 continue 641 } 642 se = append(se, k) 643 } 644 return se 645 } 646 647 type noopEventTimeLogger struct{} 648 649 func (l noopEventTimeLogger) LogEventTime(event string, optText ...string) {} 650 func (l noopEventTimeLogger) CreateSpan(event string, optText ...string) spanlog.Span { 651 return noopSpan{} 652 } 653 654 type noopSpan struct{} 655 656 func (s noopSpan) Done(err error) error { return nil }