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,
+ }
+}