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