all: add GRPC gomote server

This change:
- Adds a simple GRPC gomote server.
- Updates the documentation for the audiance required for IAP authentication.
- Adds a field for the backend service id in the build enviornment package.
- Creates middleware for the GRPC server use in the existing HTTP servers.

Updates golang/go#47521
Updates golang/go#48742

Change-Id: I2a56e39b96bf1b429f807f79c58aee3f72a45a33
Reviewed-on: https://go-review.googlesource.com/c/build/+/361098
Trust: Carlos Amedee <carlos@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/buildenv/envs.go b/buildenv/envs.go
index d082f9d..bfe2b50 100644
--- a/buildenv/envs.go
+++ b/buildenv/envs.go
@@ -138,6 +138,11 @@
 
 	// AWSRegion is the region where AWS resources are deployed.
 	AWSRegion string
+
+	// iapServiceIDs is a map of service-backends to service IDs for the backend
+	// services used by IAP enabled HTTP paths.
+	// map[backend-service-name]service_id
+	iapServiceIDs map[string]string
 }
 
 // ComputePrefix returns the URI prefix for Compute Engine resources in a project.
@@ -202,6 +207,15 @@
 	return creds, nil
 }
 
+// IAPServiceID returns the service id for the backend service. If a path does not exist for a
+// backend, the service id will be an empty string.
+func (e Environment) IAPServiceID(backendServiceName string) string {
+	if v, ok := e.iapServiceIDs[backendServiceName]; ok {
+		return v
+	}
+	return ""
+}
+
 // ByProjectID returns an Environment for the specified
 // project ID. It is currently limited to the symbolic-datum-552
 // and go-dashboard-dev projects.
@@ -254,6 +268,7 @@
 	COSServiceAccount: "linux-cos-builders@go-dashboard-dev.iam.gserviceaccount.com",
 	AWSSecurityGroup:  "staging-go-builders",
 	AWSRegion:         "us-east-1",
+	iapServiceIDs:     map[string]string{},
 }
 
 // Production defines the environment that the coordinator and build
@@ -285,6 +300,10 @@
 	COSServiceAccount: "linux-cos-builders@symbolic-datum-552.iam.gserviceaccount.com",
 	AWSSecurityGroup:  "go-builders",
 	AWSRegion:         "us-east-2",
+	iapServiceIDs: map[string]string{
+		"coordinator-internal-iap": "5961904996536591018",
+		"relui-internal":           "5124132661507612124",
+	},
 }
 
 var Development = &Environment{
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 2119ce7..2351b03 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -40,6 +40,9 @@
 	builddash "golang.org/x/build/cmd/coordinator/internal/dashboard"
 	"golang.org/x/build/cmd/coordinator/internal/legacydash"
 	"golang.org/x/build/cmd/coordinator/protos"
+	"golang.org/x/build/internal/access"
+	"golang.org/x/build/internal/gomote"
+	gomoteprotos "golang.org/x/build/internal/gomote/protos"
 	"google.golang.org/grpc"
 	grpc4 "grpc.go4.org"
 
@@ -55,6 +58,7 @@
 	"golang.org/x/build/internal/buildstats"
 	"golang.org/x/build/internal/cloud"
 	"golang.org/x/build/internal/coordinator/pool"
+	"golang.org/x/build/internal/coordinator/remote"
 	"golang.org/x/build/internal/https"
 	"golang.org/x/build/internal/secret"
 	"golang.org/x/build/maintner/maintnerd/apipb"
@@ -236,9 +240,6 @@
 	r.buf = nil
 }
 
-// grpcServer is a shared gRPC server. It is global, as it needs to be used in places that aren't factored otherwise.
-var grpcServer = grpc.NewServer()
-
 func main() {
 	https.RegisterFlags(flag.CommandLine)
 	flag.Parse()
@@ -330,12 +331,26 @@
 		log.Printf("Failed to load static resources: %v", err)
 	}
 
-	dashV1 := legacydash.Handler(gce.GoDSClient(), maintnerClient, string(masterKey()))
+	var opts []grpc.ServerOption
+	if env := buildenv.FromFlags(); env == buildenv.Production {
+		var coordinatorBackend, serviceID = "coordinator-internal-iap", ""
+		if serviceID = env.IAPServiceID(coordinatorBackend); serviceID == "" {
+			log.Fatalf("unable to retrieve Service ID for backend service=%q", coordinatorBackend)
+		}
+		opts = append(opts, grpc.UnaryInterceptor(access.RequireIAPAuthUnaryInterceptor(access.IAPAudienceGCE(env.ProjectNumber, serviceID))))
+		opts = append(opts, grpc.StreamInterceptor(access.RequireIAPAuthStreamInterceptor(access.IAPAudienceGCE(env.ProjectNumber, serviceID))))
+	}
+	// grpcServer is a shared gRPC server. It is global, as it needs to be used in places that aren't factored otherwise.
+	grpcServer := grpc.NewServer(opts...)
+
+	dashV1 := legacydash.Handler(gce.GoDSClient(), maintnerClient, string(masterKey()), grpcServer)
 	dashV2 := &builddash.Handler{Datastore: gce.GoDSClient(), Maintner: maintnerClient}
 	gs := &gRPCServer{dashboardURL: "https://build.golang.org"}
+	gomoteServer := gomote.New(remote.NewSessionPool(context.Background()))
 	protos.RegisterCoordinatorServer(grpcServer, gs)
-	http.HandleFunc("/", handleStatus)       // Serve a status page at farmer.golang.org.
-	http.Handle("build.golang.org/", dashV1) // Serve a build dashboard at build.golang.org.
+	gomoteprotos.RegisterGomoteServiceServer(grpcServer, gomoteServer)
+	http.HandleFunc("/", grpcHandlerFunc(grpcServer, handleStatus)) // Serve a status page at farmer.golang.org.
+	http.Handle("build.golang.org/", dashV1)                        // Serve a build dashboard at build.golang.org.
 	http.Handle("build-staging.golang.org/", dashV1)
 	http.HandleFunc("/builders", handleBuilders)
 	http.HandleFunc("/temporarylogs", handleLogs)
diff --git a/cmd/coordinator/internal/legacydash/dash.go b/cmd/coordinator/internal/legacydash/dash.go
index 51180dd..c4e9a01 100644
--- a/cmd/coordinator/internal/legacydash/dash.go
+++ b/cmd/coordinator/internal/legacydash/dash.go
@@ -18,11 +18,13 @@
 	"embed"
 	"net/http"
 	"sort"
+	"strings"
 
 	"cloud.google.com/go/datastore"
 	"github.com/NYTimes/gziphandler"
 	"golang.org/x/build/maintner/maintnerd/apipb"
 	"golang.org/x/build/repos"
+	"google.golang.org/grpc"
 )
 
 var (
@@ -44,12 +46,13 @@
 // fakeResults controls whether to make up fake random results. If true, datastore is not used.
 const fakeResults = false
 
-// Handler sets a datastore client, maintner client, and builder master key
-// at the package scope, and returns an HTTP mux for the legacy dashboard.
-func Handler(dc *datastore.Client, mc apipb.MaintnerServiceClient, key string) http.Handler {
+// Handler sets a datastore client, maintner client, builder master key and
+// GRPC server at the package scope, and returns an HTTP mux for the legacy dashboard.
+func Handler(dc *datastore.Client, mc apipb.MaintnerServiceClient, key string, grpcServer *grpc.Server) http.Handler {
 	datastoreClient = dc
 	maintnerClient = mc
 	masterKey = key
+	grpcServer = grpcServer
 
 	mux := http.NewServeMux()
 
@@ -58,7 +61,7 @@
 	mux.Handle("/result", hstsGzip(AuthHandler(resultHandler)))              // called by coordinator after build
 
 	// public handlers
-	mux.Handle("/", hstsGzip(http.HandlerFunc(uiHandler)))
+	mux.Handle("/", GRPCHandler(grpcServer, hstsGzip(http.HandlerFunc(uiHandler)))) // enables GRPC server for build.golang.org
 	mux.Handle("/log/", hstsGzip(http.HandlerFunc(logHandler)))
 
 	// static handler
@@ -71,6 +74,18 @@
 //go:embed static
 var static embed.FS
 
+// GRPCHandler creates handler which intercepts requests intended for a GRPC server and directs the calls to the server.
+// All other requests are directed toward the passed in handler.
+func GRPCHandler(gs *grpc.Server, h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
+			gs.ServeHTTP(w, r)
+			return
+		}
+		h.ServeHTTP(w, r)
+	})
+}
+
 // hstsGzip is short for hstsHandler(GzipHandler(h)).
 func hstsGzip(h http.Handler) http.Handler {
 	return hstsHandler(gziphandler.GzipHandler(h))
diff --git a/cmd/coordinator/status.go b/cmd/coordinator/status.go
index cac5f00..5ccd69a 100644
--- a/cmd/coordinator/status.go
+++ b/cmd/coordinator/status.go
@@ -39,6 +39,7 @@
 	"golang.org/x/build/internal/secret"
 	"golang.org/x/build/kubernetes/api"
 	"golang.org/x/oauth2"
+	"google.golang.org/grpc"
 )
 
 // status
@@ -628,13 +629,19 @@
 
 func uptime() time.Duration { return time.Since(processStartTime).Round(time.Second) }
 
-func handleStatus(w http.ResponseWriter, r *http.Request) {
-	// Support gRPC handlers. handleStatus is our toplevel ("/") handler, so reroute to the gRPC server for
-	// matching requests.
-	if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
-		grpcServer.ServeHTTP(w, r)
-		return
+// grpcHandlerFunc creates handler which intercepts requests intended for a GRPC server and directs the calls to the server.
+// All other requests are directed toward the passed in handler.
+func grpcHandlerFunc(gs *grpc.Server, h http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
+			gs.ServeHTTP(w, r)
+			return
+		}
+		h(w, r)
 	}
+}
+
+func handleStatus(w http.ResponseWriter, r *http.Request) {
 	if r.URL.Path != "/" {
 		http.NotFound(w, r)
 		return
diff --git a/internal/access/access.go b/internal/access/access.go
index 17c2c77..c5a31f0 100644
--- a/internal/access/access.go
+++ b/internal/access/access.go
@@ -55,6 +55,7 @@
 // It ensures that the caller has successfully authenticated via IAP. If the caller
 // has authenticated, the headers created by IAP will be added to the request scope
 // context passed down to the server implementation.
+// https://cloud.google.com/iap/docs/signed-headers-howto
 func iapAuthFunc(audience string, validatorFn validator) grpcauth.AuthFunc {
 	return func(ctx context.Context) (context.Context, error) {
 		md, ok := metadata.FromIncomingContext(ctx)
@@ -117,11 +118,17 @@
 type validator func(ctx context.Context, token, audiance string) (*idtoken.Payload, error)
 
 // IAPAudienceGCE returns the jwt audience for GCE and GKE services.
+// The project number is the numerical GCP project number the service is deployed in.
+// The service ID is the identifier for the backend service used to route IAP requests.
+// https://cloud.google.com/iap/docs/signed-headers-howto
 func IAPAudienceGCE(projectNumber int64, serviceID string) string {
 	return fmt.Sprintf("/projects/%d/global/backendServices/%s", projectNumber, serviceID)
 }
 
 // IAPAudienceAppEngine returns the JWT audience for App Engine services.
+// The project number is the numerical GCP project number the service is deployed in.
+// The project ID is the textual identifier for the GCP project that the App Engine instance is deployed in.
+// https://cloud.google.com/iap/docs/signed-headers-howto
 func IAPAudienceAppEngine(projectNumber int64, projectID string) string {
 	return fmt.Sprintf("/projects/%d/apps/%s", projectNumber, projectID)
 }
diff --git a/internal/gomote/doc.go b/internal/gomote/doc.go
new file mode 100644
index 0000000..d8f29a2
--- /dev/null
+++ b/internal/gomote/doc.go
@@ -0,0 +1,8 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// package gomote contains all of the necessary components to implement
+// and use the gomote funcitonality. Gomotes are instances which are dedicated
+// to an individual user or service.
+package gomote
diff --git a/internal/gomote/gomote.go b/internal/gomote/gomote.go
new file mode 100644
index 0000000..51332e1
--- /dev/null
+++ b/internal/gomote/gomote.go
@@ -0,0 +1,25 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gomote
+
+import (
+	"golang.org/x/build/internal/coordinator/remote"
+	"golang.org/x/build/internal/gomote/protos"
+)
+
+// Server is a gomote server implementation.
+type Server struct {
+	// embed the unimplemented server.
+	protos.UnimplementedGomoteServiceServer
+
+	buildlets *remote.SessionPool
+}
+
+// New creates a gomote server.
+func New(rsp *remote.SessionPool) *Server {
+	return &Server{
+		buildlets: rsp,
+	}
+}