internal/gomote: add swarming instance alive

This change adds the instance alive endpoint to the swarming gomote
implementation.

Fixes golang/go#63779

Change-Id: I7e6120df8a7c5b9f87465c7c9f74a74f37b34a07
Reviewed-on: https://go-review.googlesource.com/c/build/+/537901
Auto-Submit: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/gomote/swarming.go b/internal/gomote/swarming.go
index f894eed..7586f32 100644
--- a/internal/gomote/swarming.go
+++ b/internal/gomote/swarming.go
@@ -244,6 +244,27 @@
 	return nil
 }
 
+// InstanceAlive will ensure that the gomote instance is still alive and will extend the timeout. The requester must be authenticated.
+func (ss *SwarmingServer) InstanceAlive(ctx context.Context, req *protos.InstanceAliveRequest) (*protos.InstanceAliveResponse, error) {
+	creds, err := access.IAPFromContext(ctx)
+	if err != nil {
+		log.Printf("InstanceAlive access.IAPFromContext(ctx) = nil, %s", err)
+		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
+	}
+	if req.GetGomoteId() == "" {
+		return nil, status.Errorf(codes.InvalidArgument, "invalid gomote ID")
+	}
+	_, err = ss.session(req.GetGomoteId(), creds.ID)
+	if err != nil {
+		// the helper function returns meaningful GRPC error.
+		return nil, err
+	}
+	if err := ss.buildlets.RenewTimeout(req.GetGomoteId()); err != nil {
+		return nil, status.Errorf(codes.Internal, "unable to renew timeout")
+	}
+	return &protos.InstanceAliveResponse{}, nil
+}
+
 // ListSwarmingBuilders lists all of the swarming builders which run for gotip. The requester must be authenticated.
 func (ss *SwarmingServer) ListSwarmingBuilders(ctx context.Context, req *protos.ListSwarmingBuildersRequest) (*protos.ListSwarmingBuildersResponse, error) {
 	_, err := access.IAPFromContext(ctx)
diff --git a/internal/gomote/swarming_test.go b/internal/gomote/swarming_test.go
index ca705a4..2c4f15b 100644
--- a/internal/gomote/swarming_test.go
+++ b/internal/gomote/swarming_test.go
@@ -418,6 +418,75 @@
 	}
 }
 
+func TestSwarmingInstanceAlive(t *testing.T) {
+	client := setupGomoteSwarmingTest(t, context.Background(), mockSwarmClientSimple())
+	gomoteID := mustCreateSwarmingInstance(t, client, fakeIAP())
+	req := &protos.InstanceAliveRequest{
+		GomoteId: gomoteID,
+	}
+	ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
+	got, err := client.InstanceAlive(ctx, req)
+	if err != nil {
+		t.Fatalf("client.InstanceAlive(ctx, %v) = %v, %s; want no error", req, got, err)
+	}
+}
+
+func TestSwarmingInstanceAliveError(t *testing.T) {
+	// This test will create a gomote instance and attempt to call InstanceAlive.
+	// If overrideID is set to true, the test will use a different gomoteID than
+	// the one created for the test.
+	testCases := []struct {
+		desc       string
+		ctx        context.Context
+		overrideID bool
+		gomoteID   string // Used iff overrideID is true.
+		wantCode   codes.Code
+	}{
+		{
+			desc:     "unauthenticated request",
+			ctx:      context.Background(),
+			wantCode: codes.Unauthenticated,
+		},
+		{
+			desc:       "missing gomote id",
+			ctx:        access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
+			overrideID: true,
+			wantCode:   codes.InvalidArgument,
+		},
+		{
+			desc:       "gomote does not exist",
+			ctx:        access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
+			overrideID: true,
+			gomoteID:   "xyz",
+			wantCode:   codes.NotFound,
+		},
+		{
+			desc:     "gomote is not owned by caller",
+			ctx:      access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("user-x", "email-y")),
+			wantCode: codes.PermissionDenied,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			client := setupGomoteSwarmingTest(t, context.Background(), mockSwarmClientSimple())
+			gomoteID := mustCreateSwarmingInstance(t, client, fakeIAP())
+			if tc.overrideID {
+				gomoteID = tc.gomoteID
+			}
+			req := &protos.InstanceAliveRequest{
+				GomoteId: gomoteID,
+			}
+			got, err := client.InstanceAlive(tc.ctx, req)
+			if err != nil && status.Code(err) != tc.wantCode {
+				t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode)
+			}
+			if err == nil {
+				t.Fatalf("client.InstanceAlive(ctx, %v) = %v, nil; want error", req, got)
+			}
+		})
+	}
+}
+
 func TestSwarmingListInstance(t *testing.T) {
 	client := setupGomoteSwarmingTest(t, context.Background(), mockSwarmClientSimple())
 	ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())