golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/gomote/gomote_test.go (about) 1 // Copyright 2021 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 gomote 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 "net/http" 15 "net/http/httptest" 16 "testing" 17 "time" 18 19 "cloud.google.com/go/storage" 20 "github.com/google/go-cmp/cmp" 21 "golang.org/x/build/internal/access" 22 "golang.org/x/build/internal/coordinator/remote" 23 "golang.org/x/build/internal/coordinator/schedule" 24 "golang.org/x/build/internal/gomote/protos" 25 "golang.org/x/crypto/ssh" 26 "golang.org/x/net/nettest" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/status" 30 "google.golang.org/protobuf/testing/protocmp" 31 ) 32 33 const testBucketName = "unit-testing-bucket" 34 35 func fakeGomoteServer(t *testing.T, ctx context.Context) protos.GomoteServiceServer { 36 signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate)) 37 if err != nil { 38 t.Fatalf("unable to parse raw certificate authority private key into signer=%s", err) 39 } 40 return &Server{ 41 bucket: &fakeBucketHandler{bucketName: testBucketName}, 42 buildlets: remote.NewSessionPool(ctx), 43 gceBucketName: testBucketName, 44 scheduler: schedule.NewFake(), 45 sshCertificateAuthority: signer, 46 } 47 } 48 49 func setupGomoteTest(t *testing.T, ctx context.Context) protos.GomoteServiceClient { 50 lis, err := nettest.NewLocalListener("tcp") 51 if err != nil { 52 t.Fatalf("unable to create net listener: %s", err) 53 } 54 sopts := access.FakeIAPAuthInterceptorOptions() 55 s := grpc.NewServer(sopts...) 56 protos.RegisterGomoteServiceServer(s, fakeGomoteServer(t, ctx)) 57 go s.Serve(lis) 58 59 // create GRPC client 60 copts := []grpc.DialOption{ 61 grpc.WithInsecure(), 62 grpc.WithBlock(), 63 grpc.WithTimeout(5 * time.Second), 64 } 65 conn, err := grpc.Dial(lis.Addr().String(), copts...) 66 if err != nil { 67 lis.Close() 68 t.Fatalf("unable to create GRPC client: %s", err) 69 } 70 gc := protos.NewGomoteServiceClient(conn) 71 t.Cleanup(func() { 72 conn.Close() 73 s.Stop() 74 lis.Close() 75 }) 76 return gc 77 } 78 79 func TestAuthenticate(t *testing.T) { 80 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 81 client := setupGomoteTest(t, context.Background()) 82 got, err := client.Authenticate(ctx, &protos.AuthenticateRequest{}) 83 if err != nil { 84 t.Fatalf("client.Authenticate(ctx, request) = %v, %s; want no error", got, err) 85 } 86 } 87 88 func TestAuthenticateError(t *testing.T) { 89 wantCode := codes.Unauthenticated 90 client := setupGomoteTest(t, context.Background()) 91 _, err := client.Authenticate(context.Background(), &protos.AuthenticateRequest{}) 92 if status.Code(err) != wantCode { 93 t.Fatalf("client.Authenticate(ctx, request) = _, %s; want %s", status.Code(err), wantCode) 94 } 95 } 96 97 func TestAddBootstrap(t *testing.T) { 98 client := setupGomoteTest(t, context.Background()) 99 gomoteID := mustCreateInstance(t, client, fakeIAP()) 100 req := &protos.AddBootstrapRequest{ 101 GomoteId: gomoteID, 102 } 103 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 104 got, err := client.AddBootstrap(ctx, req) 105 if err != nil { 106 t.Fatalf("client.AddBootstrap(ctx, %v) = %v, %s; want no error", req, got, err) 107 } 108 } 109 110 func TestAddBootstrapError(t *testing.T) { 111 // This test will create a gomote instance and attempt to call AddBootstrap. 112 // If overrideID is set to true, the test will use a different gomoteID than 113 // the one created for the test. 114 testCases := []struct { 115 desc string 116 ctx context.Context 117 overrideID bool 118 gomoteID string // Used iff overrideID is true. 119 wantCode codes.Code 120 }{ 121 { 122 desc: "unauthenticated request", 123 ctx: context.Background(), 124 wantCode: codes.Unauthenticated, 125 }, 126 { 127 desc: "missing gomote id", 128 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 129 overrideID: true, 130 wantCode: codes.NotFound, 131 }, 132 { 133 desc: "gomote does not exist", 134 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 135 overrideID: true, 136 gomoteID: "xyz", 137 wantCode: codes.NotFound, 138 }, 139 { 140 desc: "gomote is not owned by caller", 141 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("user-x", "email-y")), 142 wantCode: codes.PermissionDenied, 143 }, 144 } 145 for _, tc := range testCases { 146 t.Run(tc.desc, func(t *testing.T) { 147 client := setupGomoteTest(t, context.Background()) 148 gomoteID := mustCreateInstance(t, client, fakeIAP()) 149 if tc.overrideID { 150 gomoteID = tc.gomoteID 151 } 152 req := &protos.AddBootstrapRequest{ 153 GomoteId: gomoteID, 154 } 155 got, err := client.AddBootstrap(tc.ctx, req) 156 if err != nil && status.Code(err) != tc.wantCode { 157 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 158 } 159 if err == nil { 160 t.Fatalf("client.AddBootstrap(ctx, %v) = %v, nil; want error", req, got) 161 } 162 }) 163 } 164 } 165 166 func TestCreateInstance(t *testing.T) { 167 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 168 req := &protos.CreateInstanceRequest{BuilderType: "linux-amd64"} 169 client := setupGomoteTest(t, context.Background()) 170 stream, err := client.CreateInstance(ctx, req) 171 if err != nil { 172 t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", req, stream, err) 173 } 174 var updateComplete bool 175 for { 176 update, err := stream.Recv() 177 if err == io.EOF && !updateComplete { 178 t.Fatal("stream.Recv = stream, io.EOF; want no EOF") 179 } 180 if err == io.EOF { 181 break 182 } 183 if err != nil { 184 t.Fatalf("stream.Recv() = nil, %s; want no error", err) 185 } 186 if update.GetStatus() == protos.CreateInstanceResponse_COMPLETE { 187 updateComplete = true 188 } 189 } 190 } 191 192 func TestCreateInstanceError(t *testing.T) { 193 testCases := []struct { 194 desc string 195 ctx context.Context 196 request *protos.CreateInstanceRequest 197 wantCode codes.Code 198 }{ 199 { 200 desc: "unauthenticated request", 201 ctx: context.Background(), 202 request: &protos.CreateInstanceRequest{}, 203 wantCode: codes.Unauthenticated, 204 }, 205 { 206 desc: "missing builder type", 207 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 208 request: &protos.CreateInstanceRequest{}, 209 wantCode: codes.InvalidArgument, 210 }, 211 { 212 desc: "invalid builder type", 213 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 214 request: &protos.CreateInstanceRequest{ 215 BuilderType: "funky-time-builder", 216 }, 217 wantCode: codes.InvalidArgument, 218 }, 219 } 220 for _, tc := range testCases { 221 t.Run(tc.desc, func(t *testing.T) { 222 client := setupGomoteTest(t, context.Background()) 223 224 stream, err := client.CreateInstance(tc.ctx, tc.request) 225 if err != nil { 226 t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", tc.request, stream, err) 227 } 228 for { 229 _, got := stream.Recv() 230 if got == io.EOF { 231 t.Fatal("stream.Recv = stream, io.EOF; want no EOF") 232 } 233 if got != nil && status.Code(got) != tc.wantCode { 234 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 235 } 236 return 237 } 238 }) 239 } 240 } 241 242 func TestInstanceAlive(t *testing.T) { 243 client := setupGomoteTest(t, context.Background()) 244 gomoteID := mustCreateInstance(t, client, fakeIAP()) 245 req := &protos.InstanceAliveRequest{ 246 GomoteId: gomoteID, 247 } 248 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 249 got, err := client.InstanceAlive(ctx, req) 250 if err != nil { 251 t.Fatalf("client.InstanceAlive(ctx, %v) = %v, %s; want no error", req, got, err) 252 } 253 } 254 255 func TestInstanceAliveError(t *testing.T) { 256 // This test will create a gomote instance and attempt to call InstanceAlive. 257 // If overrideID is set to true, the test will use a different gomoteID than 258 // the one created for the test. 259 testCases := []struct { 260 desc string 261 ctx context.Context 262 overrideID bool 263 gomoteID string // Used iff overrideID is true. 264 wantCode codes.Code 265 }{ 266 { 267 desc: "unauthenticated request", 268 ctx: context.Background(), 269 wantCode: codes.Unauthenticated, 270 }, 271 { 272 desc: "missing gomote id", 273 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 274 overrideID: true, 275 wantCode: codes.InvalidArgument, 276 }, 277 { 278 desc: "gomote does not exist", 279 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 280 overrideID: true, 281 gomoteID: "xyz", 282 wantCode: codes.NotFound, 283 }, 284 { 285 desc: "gomote is not owned by caller", 286 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("user-x", "email-y")), 287 wantCode: codes.PermissionDenied, 288 }, 289 } 290 for _, tc := range testCases { 291 t.Run(tc.desc, func(t *testing.T) { 292 client := setupGomoteTest(t, context.Background()) 293 gomoteID := mustCreateInstance(t, client, fakeIAP()) 294 if tc.overrideID { 295 gomoteID = tc.gomoteID 296 } 297 req := &protos.InstanceAliveRequest{ 298 GomoteId: gomoteID, 299 } 300 got, err := client.InstanceAlive(tc.ctx, req) 301 if err != nil && status.Code(err) != tc.wantCode { 302 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 303 } 304 if err == nil { 305 t.Fatalf("client.InstanceAlive(ctx, %v) = %v, nil; want error", req, got) 306 } 307 }) 308 } 309 } 310 311 func TestListDirectory(t *testing.T) { 312 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 313 client := setupGomoteTest(t, context.Background()) 314 gomoteID := mustCreateInstance(t, client, fakeIAP()) 315 if _, err := client.ListDirectory(ctx, &protos.ListDirectoryRequest{ 316 GomoteId: gomoteID, 317 Directory: "/foo", 318 }); err != nil { 319 t.Fatalf("client.RemoveFiles(ctx, req) = response, %s; want no error", err) 320 } 321 } 322 323 func TestListDirectoryError(t *testing.T) { 324 // This test will create a gomote instance and attempt to call ListDirectory. 325 // If overrideID is set to true, the test will use a different gomoteID than 326 // the one created for the test. 327 testCases := []struct { 328 desc string 329 ctx context.Context 330 overrideID bool 331 gomoteID string // Used iff overrideID is true. 332 directory string 333 recursive bool 334 skipFiles []string 335 digest bool 336 wantCode codes.Code 337 }{ 338 { 339 desc: "unauthenticated request", 340 ctx: context.Background(), 341 wantCode: codes.Unauthenticated, 342 }, 343 { 344 desc: "missing gomote id", 345 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 346 overrideID: true, 347 gomoteID: "", 348 wantCode: codes.InvalidArgument, 349 }, 350 { 351 desc: "missing directory", 352 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 353 wantCode: codes.InvalidArgument, 354 }, 355 { 356 desc: "gomote does not exist", 357 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 358 overrideID: true, 359 gomoteID: "chucky", 360 directory: "/foo", 361 wantCode: codes.NotFound, 362 }, 363 { 364 desc: "wrong gomote id", 365 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 366 overrideID: false, 367 directory: "/foo", 368 wantCode: codes.PermissionDenied, 369 }, 370 } 371 for _, tc := range testCases { 372 t.Run(tc.desc, func(t *testing.T) { 373 client := setupGomoteTest(t, context.Background()) 374 gomoteID := mustCreateInstance(t, client, fakeIAP()) 375 if tc.overrideID { 376 gomoteID = tc.gomoteID 377 } 378 req := &protos.ListDirectoryRequest{ 379 GomoteId: gomoteID, 380 Directory: tc.directory, 381 Recursive: false, 382 SkipFiles: []string{}, 383 Digest: false, 384 } 385 got, err := client.ListDirectory(tc.ctx, req) 386 if err != nil && status.Code(err) != tc.wantCode { 387 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 388 } 389 if err == nil { 390 t.Fatalf("client.RemoveFiles(ctx, %v) = %v, nil; want error", req, got) 391 } 392 }) 393 } 394 } 395 396 func TestListInstance(t *testing.T) { 397 client := setupGomoteTest(t, context.Background()) 398 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 399 var want []*protos.Instance 400 for i := 0; i < 3; i++ { 401 want = append(want, &protos.Instance{ 402 GomoteId: mustCreateInstance(t, client, fakeIAP()), 403 BuilderType: "linux-amd64", 404 }) 405 } 406 mustCreateInstance(t, client, fakeIAPWithUser("user-x", "uuid-user-x")) 407 response, err := client.ListInstances(ctx, &protos.ListInstancesRequest{}) 408 if err != nil { 409 t.Fatalf("client.ListInstances = nil, %s; want no error", err) 410 } 411 got := response.GetInstances() 412 if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields(&protos.Instance{}, "expires", "host_type")); diff != "" { 413 t.Errorf("ListInstances() mismatch (-want, +got):\n%s", diff) 414 } 415 } 416 417 func TestDestroyInstance(t *testing.T) { 418 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 419 client := setupGomoteTest(t, context.Background()) 420 gomoteID := mustCreateInstance(t, client, fakeIAP()) 421 if _, err := client.DestroyInstance(ctx, &protos.DestroyInstanceRequest{ 422 GomoteId: gomoteID, 423 }); err != nil { 424 t.Fatalf("client.DestroyInstance(ctx, req) = response, %s; want no error", err) 425 } 426 } 427 428 func TestDestroyInstanceError(t *testing.T) { 429 // This test will create a gomote instance and attempt to call DestroyInstance. 430 // If overrideID is set to true, the test will use a different gomoteID than 431 // the one created for the test. 432 testCases := []struct { 433 desc string 434 ctx context.Context 435 overrideID bool 436 gomoteID string // Used iff overrideID is true. 437 wantCode codes.Code 438 }{ 439 { 440 desc: "unauthenticated request", 441 ctx: context.Background(), 442 wantCode: codes.Unauthenticated, 443 }, 444 { 445 desc: "missing gomote id", 446 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 447 overrideID: true, 448 gomoteID: "", 449 wantCode: codes.InvalidArgument, 450 }, 451 { 452 desc: "gomote does not exist", 453 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 454 overrideID: true, 455 gomoteID: "chucky", 456 wantCode: codes.NotFound, 457 }, 458 { 459 desc: "wrong gomote id", 460 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 461 overrideID: false, 462 wantCode: codes.PermissionDenied, 463 }, 464 } 465 for _, tc := range testCases { 466 t.Run(tc.desc, func(t *testing.T) { 467 client := setupGomoteTest(t, context.Background()) 468 gomoteID := mustCreateInstance(t, client, fakeIAP()) 469 if tc.overrideID { 470 gomoteID = tc.gomoteID 471 } 472 req := &protos.DestroyInstanceRequest{ 473 GomoteId: gomoteID, 474 } 475 got, err := client.DestroyInstance(tc.ctx, req) 476 if err != nil && status.Code(err) != tc.wantCode { 477 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 478 } 479 if err == nil { 480 t.Fatalf("client.DestroyInstance(ctx, %v) = %v, nil; want error", req, got) 481 } 482 }) 483 } 484 } 485 486 func TestExecuteCommand(t *testing.T) { 487 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 488 client := setupGomoteTest(t, context.Background()) 489 gomoteID := mustCreateInstance(t, client, fakeIAP()) 490 stream, err := client.ExecuteCommand(ctx, &protos.ExecuteCommandRequest{ 491 GomoteId: gomoteID, 492 Command: "ls", 493 SystemLevel: false, 494 Debug: false, 495 AppendEnvironment: nil, 496 Path: nil, 497 Directory: "/workdir", 498 Args: []string{"-alh"}, 499 }) 500 if err != nil { 501 t.Fatalf("client.ExecuteCommand(ctx, req) = response, %s; want no error", err) 502 } 503 var out []byte 504 for { 505 res, err := stream.Recv() 506 if err != nil && err == io.EOF { 507 break 508 } 509 if err != nil { 510 t.Fatalf("stream.Recv() = _, %s; want no error", err) 511 } 512 out = append(out, res.GetOutput()...) 513 } 514 if len(out) == 0 { 515 t.Fatalf("output: %q, expected non-empty", out) 516 } 517 } 518 519 func TestExecuteCommandError(t *testing.T) { 520 // This test will create a gomote instance and attempt to call TestExecuteCommand. 521 // If overrideID is set to true, the test will use a different gomoteID than 522 // the one created for the test. 523 testCases := []struct { 524 desc string 525 ctx context.Context 526 overrideID bool 527 gomoteID string // Used iff overrideID is true. 528 cmd string 529 wantCode codes.Code 530 }{ 531 { 532 desc: "unauthenticated request", 533 ctx: context.Background(), 534 wantCode: codes.Unauthenticated, 535 }, 536 { 537 desc: "missing gomote id", 538 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 539 overrideID: true, 540 gomoteID: "", 541 wantCode: codes.NotFound, 542 }, 543 { 544 desc: "missing command", 545 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 546 wantCode: codes.Aborted, 547 }, 548 { 549 desc: "gomote does not exist", 550 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 551 overrideID: true, 552 gomoteID: "chucky", 553 cmd: "ls", 554 wantCode: codes.NotFound, 555 }, 556 { 557 desc: "wrong gomote id", 558 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 559 overrideID: false, 560 cmd: "ls", 561 wantCode: codes.PermissionDenied, 562 }, 563 } 564 for _, tc := range testCases { 565 t.Run(tc.desc, func(t *testing.T) { 566 client := setupGomoteTest(t, context.Background()) 567 gomoteID := mustCreateInstance(t, client, fakeIAP()) 568 if tc.overrideID { 569 gomoteID = tc.gomoteID 570 } 571 stream, err := client.ExecuteCommand(tc.ctx, &protos.ExecuteCommandRequest{ 572 GomoteId: gomoteID, 573 Command: tc.cmd, 574 SystemLevel: false, 575 Debug: false, 576 AppendEnvironment: nil, 577 Path: nil, 578 Directory: "/workdir", 579 Args: []string{"-alh"}, 580 }) 581 if err != nil { 582 t.Fatalf("unexpected error: %s", err) 583 } 584 res, err := stream.Recv() 585 if err != nil && status.Code(err) != tc.wantCode { 586 t.Fatalf("unexpected error: %s", err) 587 } 588 if err == nil { 589 t.Fatalf("client.ExecuteCommand(ctx, req) = %v, nil; want error", res) 590 } 591 }) 592 } 593 } 594 595 func TestReadTGZToURLError(t *testing.T) { 596 // This test will create a gomote instance and attempt to call ReadTGZToURL. 597 // If overrideID is set to true, the test will use a different gomoteID than 598 // the one created for the test. 599 testCases := []struct { 600 desc string 601 ctx context.Context 602 overrideID bool 603 gomoteID string // Used iff overrideID is true. 604 directory string 605 wantCode codes.Code 606 }{ 607 { 608 desc: "unauthenticated request", 609 ctx: context.Background(), 610 wantCode: codes.Unauthenticated, 611 }, 612 { 613 desc: "missing gomote id", 614 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 615 overrideID: true, 616 gomoteID: "", 617 wantCode: codes.NotFound, 618 }, 619 { 620 desc: "gomote does not exist", 621 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 622 overrideID: true, 623 gomoteID: "chucky", 624 wantCode: codes.NotFound, 625 }, 626 { 627 desc: "wrong gomote id", 628 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 629 overrideID: false, 630 wantCode: codes.PermissionDenied, 631 }, 632 } 633 for _, tc := range testCases { 634 t.Run(tc.desc, func(t *testing.T) { 635 client := setupGomoteTest(t, context.Background()) 636 gomoteID := mustCreateInstance(t, client, fakeIAP()) 637 if tc.overrideID { 638 gomoteID = tc.gomoteID 639 } 640 req := &protos.ReadTGZToURLRequest{ 641 GomoteId: gomoteID, 642 Directory: tc.directory, 643 } 644 got, err := client.ReadTGZToURL(tc.ctx, req) 645 if err != nil && status.Code(err) != tc.wantCode { 646 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 647 } 648 if err == nil { 649 t.Fatalf("client.ReadTGZToURL(ctx, %v) = %v, nil; want error", req, got) 650 } 651 }) 652 } 653 } 654 655 func TestRemoveFiles(t *testing.T) { 656 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 657 client := setupGomoteTest(t, context.Background()) 658 gomoteID := mustCreateInstance(t, client, fakeIAP()) 659 if _, err := client.RemoveFiles(ctx, &protos.RemoveFilesRequest{ 660 GomoteId: gomoteID, 661 Paths: []string{"temp_file.log"}, 662 }); err != nil { 663 t.Fatalf("client.RemoveFiles(ctx, req) = response, %s; want no error", err) 664 } 665 } 666 667 func TestRemoveFilesError(t *testing.T) { 668 // This test will create a gomote instance and attempt to call RemoveFiles. 669 // If overrideID is set to true, the test will use a different gomoteID than 670 // the one created for the test. 671 testCases := []struct { 672 desc string 673 ctx context.Context 674 overrideID bool 675 gomoteID string // Used iff overrideID is true. 676 paths []string 677 wantCode codes.Code 678 }{ 679 { 680 desc: "unauthenticated request", 681 ctx: context.Background(), 682 wantCode: codes.Unauthenticated, 683 }, 684 { 685 desc: "missing gomote id", 686 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 687 overrideID: true, 688 gomoteID: "", 689 wantCode: codes.InvalidArgument, 690 }, 691 { 692 desc: "missing paths", 693 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 694 paths: []string{}, 695 wantCode: codes.InvalidArgument, 696 }, 697 { 698 desc: "gomote does not exist", 699 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 700 overrideID: true, 701 gomoteID: "chucky", 702 paths: []string{"file.a"}, 703 wantCode: codes.NotFound, 704 }, 705 { 706 desc: "wrong gomote id", 707 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 708 overrideID: false, 709 paths: []string{"file.a"}, 710 wantCode: codes.PermissionDenied, 711 }, 712 } 713 for _, tc := range testCases { 714 t.Run(tc.desc, func(t *testing.T) { 715 client := setupGomoteTest(t, context.Background()) 716 gomoteID := mustCreateInstance(t, client, fakeIAP()) 717 if tc.overrideID { 718 gomoteID = tc.gomoteID 719 } 720 req := &protos.RemoveFilesRequest{ 721 GomoteId: gomoteID, 722 Paths: tc.paths, 723 } 724 got, err := client.RemoveFiles(tc.ctx, req) 725 if err != nil && status.Code(err) != tc.wantCode { 726 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 727 } 728 if err == nil { 729 t.Fatalf("client.RemoveFiles(ctx, %v) = %v, nil; want error", req, got) 730 } 731 }) 732 } 733 } 734 735 func TestSignSSHKey(t *testing.T) { 736 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 737 client := setupGomoteTest(t, context.Background()) 738 gomoteID := mustCreateInstance(t, client, fakeIAP()) 739 if _, err := client.SignSSHKey(ctx, &protos.SignSSHKeyRequest{ 740 GomoteId: gomoteID, 741 PublicSshKey: []byte(devCertCAPublic), 742 }); err != nil { 743 t.Fatalf("client.SignSSHKey(ctx, req) = response, %s; want no error", err) 744 } 745 } 746 747 func TestSignSSHKeyError(t *testing.T) { 748 // This test will create a gomote instance and attempt to call SignSSHKey. 749 // If overrideID is set to true, the test will use a different gomoteID than 750 // the one created for the test. 751 testCases := []struct { 752 desc string 753 ctx context.Context 754 overrideID bool 755 gomoteID string // Used iff overrideID is true. 756 publickSSHKey []byte 757 wantCode codes.Code 758 }{ 759 { 760 desc: "unauthenticated request", 761 ctx: context.Background(), 762 wantCode: codes.Unauthenticated, 763 }, 764 { 765 desc: "missing gomote id", 766 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 767 overrideID: true, 768 gomoteID: "", 769 wantCode: codes.NotFound, 770 }, 771 { 772 desc: "missing public key", 773 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 774 wantCode: codes.InvalidArgument, 775 }, 776 { 777 desc: "gomote does not exist", 778 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 779 overrideID: true, 780 gomoteID: "chucky", 781 publickSSHKey: []byte(devCertCAPublic), 782 wantCode: codes.NotFound, 783 }, 784 { 785 desc: "wrong gomote id", 786 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 787 overrideID: false, 788 publickSSHKey: []byte(devCertCAPublic), 789 wantCode: codes.PermissionDenied, 790 }, 791 } 792 for _, tc := range testCases { 793 t.Run(tc.desc, func(t *testing.T) { 794 client := setupGomoteTest(t, context.Background()) 795 gomoteID := mustCreateInstance(t, client, fakeIAP()) 796 if tc.overrideID { 797 gomoteID = tc.gomoteID 798 } 799 req := &protos.SignSSHKeyRequest{ 800 GomoteId: gomoteID, 801 PublicSshKey: tc.publickSSHKey, 802 } 803 got, err := client.SignSSHKey(tc.ctx, req) 804 if err != nil && status.Code(err) != tc.wantCode { 805 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 806 } 807 if err == nil { 808 t.Fatalf("client.SignSSHKey(ctx, %v) = %v, nil; want error", req, got) 809 } 810 }) 811 } 812 } 813 814 func TestUploadFile(t *testing.T) { 815 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 816 client := setupGomoteTest(t, context.Background()) 817 _ = mustCreateInstance(t, client, fakeIAP()) 818 if _, err := client.UploadFile(ctx, &protos.UploadFileRequest{}); err != nil { 819 t.Fatalf("client.UploadFile(ctx, req) = response, %s; want no error", err) 820 } 821 } 822 823 func TestUploadFileError(t *testing.T) { 824 // This test will create a gomote instance and attempt to call UploadFile. 825 // If overrideID is set to true, the test will use a different gomoteID than 826 // the one created for the test. 827 testCases := []struct { 828 desc string 829 ctx context.Context 830 overrideID bool 831 filename string 832 wantCode codes.Code 833 }{ 834 { 835 desc: "unauthenticated request", 836 ctx: context.Background(), 837 wantCode: codes.Unauthenticated, 838 }, 839 } 840 for _, tc := range testCases { 841 t.Run(tc.desc, func(t *testing.T) { 842 client := setupGomoteTest(t, context.Background()) 843 _ = mustCreateInstance(t, client, fakeIAP()) 844 req := &protos.UploadFileRequest{} 845 got, err := client.UploadFile(tc.ctx, req) 846 if err != nil && status.Code(err) != tc.wantCode { 847 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 848 } 849 if err == nil { 850 t.Fatalf("client.UploadFile(ctx, %v) = %v, nil; want error", req, got) 851 } 852 }) 853 } 854 } 855 856 // TODO(go.dev/issue/48737) add test for files on GCS 857 func TestWriteFileFromURL(t *testing.T) { 858 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 859 client := setupGomoteTest(t, context.Background()) 860 gomoteID := mustCreateInstance(t, client, fakeIAP()) 861 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 862 fmt.Fprintln(w, "Go is an open source programming language") 863 })) 864 defer ts.Close() 865 if _, err := client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{ 866 GomoteId: gomoteID, 867 Url: ts.URL, 868 Filename: "foo", 869 Mode: 0777, 870 }); err != nil { 871 t.Fatalf("client.WriteFileFromURL(ctx, req) = response, %s; want no error", err) 872 } 873 } 874 875 func TestWriteFileFromURLError(t *testing.T) { 876 // This test will create a gomote instance and attempt to call TestWriteFileFromURL. 877 // If overrideID is set to true, the test will use a different gomoteID than 878 // the one created for the test. 879 testCases := []struct { 880 desc string 881 ctx context.Context 882 overrideID bool 883 gomoteID string // Used iff overrideID is true. 884 url string 885 filename string 886 mode uint32 887 wantCode codes.Code 888 }{ 889 { 890 desc: "unauthenticated request", 891 ctx: context.Background(), 892 wantCode: codes.Unauthenticated, 893 }, 894 { 895 desc: "missing gomote id", 896 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 897 overrideID: true, 898 gomoteID: "", 899 wantCode: codes.NotFound, 900 }, 901 { 902 desc: "gomote does not exist", 903 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 904 overrideID: true, 905 gomoteID: "chucky", 906 url: "go.dev/dl/1_14.tar.gz", 907 wantCode: codes.NotFound, 908 }, 909 { 910 desc: "wrong gomote id", 911 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 912 overrideID: false, 913 url: "go.dev/dl/1_14.tar.gz", 914 wantCode: codes.PermissionDenied, 915 }, 916 } 917 for _, tc := range testCases { 918 t.Run(tc.desc, func(t *testing.T) { 919 client := setupGomoteTest(t, context.Background()) 920 gomoteID := mustCreateInstance(t, client, fakeIAP()) 921 if tc.overrideID { 922 gomoteID = tc.gomoteID 923 } 924 req := &protos.WriteFileFromURLRequest{ 925 GomoteId: gomoteID, 926 Url: tc.url, 927 Filename: tc.filename, 928 Mode: 0, 929 } 930 got, err := client.WriteFileFromURL(tc.ctx, req) 931 if err != nil && status.Code(err) != tc.wantCode { 932 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 933 } 934 if err == nil { 935 t.Fatalf("client.WriteFileFromURL(ctx, %v) = %v, nil; want error", req, got) 936 } 937 }) 938 } 939 } 940 941 func TestWriteTGZFromURL(t *testing.T) { 942 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 943 client := setupGomoteTest(t, context.Background()) 944 gomoteID := mustCreateInstance(t, client, fakeIAP()) 945 if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ 946 GomoteId: gomoteID, 947 Directory: "foo", 948 Url: `https://go.dev/dl/go1.17.6.linux-amd64.tar.gz`, 949 }); err != nil { 950 t.Fatalf("client.WriteTGZFromURL(ctx, req) = response, %s; want no error", err) 951 } 952 } 953 954 func TestWriteTGZFromURLGomoteStaging(t *testing.T) { 955 ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()) 956 client := setupGomoteTest(t, context.Background()) 957 gomoteID := mustCreateInstance(t, client, fakeIAP()) 958 if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ 959 GomoteId: gomoteID, 960 Directory: "foo", 961 Url: fmt.Sprintf("https://storage.googleapis.com/%s/go1.17.6.linux-amd64.tar.gz?field=x", testBucketName), 962 }); err != nil { 963 t.Fatalf("client.WriteTGZFromURL(ctx, req) = response, %s; want no error", err) 964 } 965 } 966 967 func TestWriteTGZFromURLError(t *testing.T) { 968 // This test will create a gomote instance and attempt to call TestWriteTGZFromURL. 969 // If overrideID is set to true, the test will use a different gomoteID than 970 // the one created for the test. 971 testCases := []struct { 972 desc string 973 ctx context.Context 974 overrideID bool 975 gomoteID string // Used iff overrideID is true. 976 url string 977 directory string 978 wantCode codes.Code 979 }{ 980 { 981 desc: "unauthenticated request", 982 ctx: context.Background(), 983 wantCode: codes.Unauthenticated, 984 }, 985 { 986 desc: "missing gomote id", 987 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 988 overrideID: true, 989 gomoteID: "", 990 wantCode: codes.InvalidArgument, 991 }, 992 { 993 desc: "missing URL", 994 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()), 995 wantCode: codes.InvalidArgument, 996 }, 997 { 998 desc: "gomote does not exist", 999 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 1000 overrideID: true, 1001 gomoteID: "chucky", 1002 url: "go.dev/dl/1_14.tar.gz", 1003 wantCode: codes.NotFound, 1004 }, 1005 { 1006 desc: "wrong gomote id", 1007 ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")), 1008 overrideID: false, 1009 url: "go.dev/dl/1_14.tar.gz", 1010 wantCode: codes.PermissionDenied, 1011 }, 1012 } 1013 for _, tc := range testCases { 1014 t.Run(tc.desc, func(t *testing.T) { 1015 client := setupGomoteTest(t, context.Background()) 1016 gomoteID := mustCreateInstance(t, client, fakeIAP()) 1017 if tc.overrideID { 1018 gomoteID = tc.gomoteID 1019 } 1020 req := &protos.WriteTGZFromURLRequest{ 1021 GomoteId: gomoteID, 1022 Url: tc.url, 1023 Directory: tc.directory, 1024 } 1025 got, err := client.WriteTGZFromURL(tc.ctx, req) 1026 if err != nil && status.Code(err) != tc.wantCode { 1027 t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode) 1028 } 1029 if err == nil { 1030 t.Fatalf("client.WriteTGZFromURL(ctx, %v) = %v, nil; want error", req, got) 1031 } 1032 }) 1033 } 1034 } 1035 1036 func TestIsPrivilegedUser(t *testing.T) { 1037 in := "accounts.google.com:example@google.com" 1038 if !isPrivilegedUser(in) { 1039 t.Errorf("isPrivilagedUser(%q) = false; want true", in) 1040 } 1041 } 1042 1043 func TestObjectFromURL(t *testing.T) { 1044 url := `https://storage.googleapis.com/example-bucket/cat.jpeg` 1045 bucket := "example-bucket" 1046 wantObject := "cat.jpeg" 1047 object, err := objectFromURL(bucket, url) 1048 if err != nil { 1049 t.Fatalf("urlToBucketObject(%q) = %q, %s; want %q, no error", url, object, err, wantObject) 1050 } 1051 if object != wantObject { 1052 t.Fatalf("urlToBucketObject(%q) = %q; want %q", url, object, wantObject) 1053 } 1054 } 1055 1056 func TestObjectFromURLError(t *testing.T) { 1057 bucket := "example-bucket" 1058 object := "cat.jpeg" 1059 url := fmt.Sprintf("https://bunker.googleapis.com/%s/%s", bucket, object) 1060 got, err := objectFromURL(bucket, url) 1061 if err == nil { 1062 t.Fatalf("urlToBucketObject(url) = %q, nil; want \"\", error", got) 1063 } 1064 } 1065 1066 func TestEmailToUser(t *testing.T) { 1067 testCases := []struct { 1068 desc string 1069 email string 1070 want string 1071 }{ 1072 {"valid email", "accounts.google.com:example@gmail.com", "example"}, 1073 {"valid email", "accounts.google.com:mary@google.com", "mary"}, 1074 {"valid email", "accounts.google.com:george@funky.com", "george"}, 1075 {"single digit local", "accounts.google.com:g@funky.com", "g"}, 1076 {"single digit domain", "accounts.google.com:g@funky.com", "g"}, 1077 {"multiple colon", "accounts.google.com:example@gmail.com:more-info", "example"}, // while not desired, wont lead to a panic 1078 {"multiple at", "accounts.google.com:example@gmail.com:example@gmail.com", "example@gmail.com:example"}, // while not desired, wont lead to a panic 1079 } 1080 for _, tc := range testCases { 1081 t.Run(tc.desc, func(t *testing.T) { 1082 if got, err := emailToUser(tc.email); got != tc.want || err != nil { 1083 t.Errorf("emailToUser(%q) = %q, %s; want %q, no error", tc.email, got, err, tc.want) 1084 } 1085 }) 1086 } 1087 } 1088 1089 func TestEmailToUserError(t *testing.T) { 1090 testCases := []struct { 1091 desc string 1092 email string 1093 }{ 1094 {"no local", "accounts.google.com:@funky.com"}, 1095 {"incorrect authority", "accountsxgoogleycom:george@funky.com"}, 1096 {"hyphens authority", "accounts-google-com:george@funky.com"}, 1097 {"no domain", "accounts.google.com:george@"}, 1098 {"missing colon", "accounts.google.comxgeorge@a.b"}, 1099 } 1100 for _, tc := range testCases { 1101 t.Run(tc.desc, func(t *testing.T) { 1102 if got, err := emailToUser(tc.email); err == nil { 1103 t.Errorf("emailToUser(%q) = %q, no error; want error", tc.email, got) 1104 } 1105 }) 1106 } 1107 } 1108 1109 func fakeAuthContext(ctx context.Context, privileged bool) context.Context { 1110 iap := access.IAPFields{ 1111 Email: "accounts.google.com:example@gmail.com", 1112 ID: "accounts.google.com:randomuuidstuff", 1113 } 1114 if privileged { 1115 iap.Email = "accounts.google.com:test@google.com" 1116 } 1117 return access.ContextWithIAP(ctx, iap) 1118 } 1119 1120 func fakeIAP() access.IAPFields { 1121 return fakeIAPWithUser("example", "randomuuidstuff") 1122 } 1123 1124 func fakeIAPWithUser(user string, id string) access.IAPFields { 1125 return access.IAPFields{ 1126 Email: fmt.Sprintf("accounts.google.com:%s@gmail.com", user), 1127 ID: fmt.Sprintf("accounts.google.com:%s", id), 1128 } 1129 } 1130 1131 func mustCreateInstance(t *testing.T, client protos.GomoteServiceClient, iap access.IAPFields) string { 1132 req := &protos.CreateInstanceRequest{ 1133 BuilderType: "linux-amd64", 1134 } 1135 stream, err := client.CreateInstance(access.FakeContextWithOutgoingIAPAuth(context.Background(), iap), req) 1136 if err != nil { 1137 t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", req, stream, err) 1138 } 1139 var updateComplete bool 1140 var gomoteID string 1141 for { 1142 update, err := stream.Recv() 1143 if err == io.EOF && !updateComplete { 1144 t.Fatal("stream.Recv = stream, io.EOF; want no EOF") 1145 } 1146 if err == io.EOF { 1147 break 1148 } 1149 if err != nil { 1150 t.Fatalf("stream.Recv() = nil, %s; want no error", err) 1151 } 1152 if update.GetStatus() == protos.CreateInstanceResponse_COMPLETE { 1153 gomoteID = update.Instance.GetGomoteId() 1154 updateComplete = true 1155 } 1156 } 1157 return gomoteID 1158 } 1159 1160 const ( 1161 // devCertCAPrivate is a private SSH CA certificate to be used for development. 1162 devCertCAPrivate = `-----BEGIN OPENSSH PRIVATE KEY----- 1163 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 1164 QyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WAAAAJhjy3ePY8t3 1165 jwAAAAtzc2gtZWQyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WA 1166 AAAEALuUJMb/rEaFNa+vn5RejeoBiiViyda7djgEvMnQ8fRJV3YUncNv+hXneJEO3VEuxx 1167 OfjWFhdu6e4JYiVfN7ZYAAAAE3Rlc3R1c2VyQGdvbGFuZy5vcmcBAg== 1168 -----END OPENSSH PRIVATE KEY-----` 1169 1170 // devCertCAPublic is a public SSH CA certificate to be used for development. 1171 devCertCAPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3YUncNv+hXneJEO3VEuxxOfjWFhdu6e4JYiVfN7ZY testuser@golang.org` 1172 ) 1173 1174 type fakeBucketHandler struct{ bucketName string } 1175 1176 func (fbc *fakeBucketHandler) GenerateSignedPostPolicyV4(object string, opts *storage.PostPolicyV4Options) (*storage.PostPolicyV4, error) { 1177 if object == "" || opts == nil { 1178 return nil, errors.New("invalid arguments") 1179 } 1180 return &storage.PostPolicyV4{ 1181 URL: fmt.Sprintf("https://localhost/%s/%s", fbc.bucketName, object), 1182 Fields: map[string]string{ 1183 "x-permission-to-post": "granted", 1184 }, 1185 }, nil 1186 } 1187 1188 func (fbc *fakeBucketHandler) SignedURL(object string, opts *storage.SignedURLOptions) (string, error) { 1189 if object == "" || opts == nil { 1190 return "", errors.New("invalid arguments") 1191 } 1192 return fmt.Sprintf("https://localhost/%s?X-Goog-Algorithm=GOOG4-yyy", object), nil 1193 } 1194 1195 func (fbc *fakeBucketHandler) Object(name string) *storage.ObjectHandle { 1196 return &storage.ObjectHandle{} 1197 }