blob: 14933484d1c3c8bb74c1c709a1d38ecd18766d05 [file] [log] [blame]
// Copyright 2023 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.
//go:build go1.20 && (linux || darwin)
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"strings"
"time"
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/storage"
"golang.org/x/build/buildenv"
"golang.org/x/build/internal/access"
"golang.org/x/build/internal/coordinator/pool"
"golang.org/x/build/internal/coordinator/remote"
"golang.org/x/build/internal/gomote"
gomotepb "golang.org/x/build/internal/gomote/protos"
"golang.org/x/build/internal/https"
"golang.org/x/build/internal/secret"
"golang.org/x/build/internal/swarmclient"
"google.golang.org/api/option"
"google.golang.org/grpc"
)
var (
sshAddr = flag.String("ssh_addr", ":2222", "Address the gomote SSH server should listen on")
buildEnvName = flag.String("env", "", "The build environment configuration to use. Not required if running in dev mode locally or prod mode on GCE.")
mode = flag.String("mode", "", "Valid modes are 'dev', 'prod', or '' for auto-detect. dev means localhost development, not be confused with staging on go-dashboard-dev, which is still the 'prod' mode.")
)
func main() {
https.RegisterFlags(flag.CommandLine)
flag.Parse()
if err := secret.InitFlagSupport(context.Background()); err != nil {
log.Fatalln(err)
}
log.Println("starting gomote server")
sp := remote.NewSessionPool(context.Background())
sshCA := mustRetrieveSSHCertificateAuthority()
var gomoteBucket string
var opts []grpc.ServerOption
if *buildEnvName == "" && *mode != "dev" && metadata.OnGCE() {
projectID, err := metadata.ProjectID()
if err != nil {
log.Fatalf("metadata.ProjectID() = %v", err)
}
env := buildenv.ByProjectID(projectID)
gomoteBucket = env.GomoteTransferBucket
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.IAPSkipAudienceValidation)))
opts = append(opts, grpc.StreamInterceptor(access.RequireIAPAuthStreamInterceptor(access.IAPSkipAudienceValidation)))
}
grpcServer := grpc.NewServer(opts...)
gomoteServer, err := gomote.NewSwarming(sp, sshCA, gomoteBucket, mustStorageClient(), mustLUCIConfigClient())
if err != nil {
log.Fatalf("unable to create gomote server: %s", err)
}
gomotepb.RegisterGomoteServiceServer(grpcServer, gomoteServer)
mux := http.NewServeMux()
mux.HandleFunc("/reverse", pool.HandleReverse)
mux.HandleFunc("/", grpcHandlerFunc(grpcServer, handleStatus)) // Serve a status page.
configureSSHServer := func() (*remote.SSHServer, error) {
// Always generate a gomote SSH key pair. If the server is restarted then the existing
// instances will be destroyed and a new pair of keys can be used.
priKey, pubKey, err := remote.SSHKeyPair()
if err != nil {
return nil, fmt.Errorf("unable to generate development SSH key pair: %s", err)
}
return remote.NewSSHServer(*sshAddr, priKey, pubKey, sshCA, sp)
}
sshServ, err := configureSSHServer()
if err != nil {
log.Printf("unable to configure SSH server: %s", err)
} else {
go func() {
log.Printf("running SSH server on %s", *sshAddr)
err := sshServ.ListenAndServe()
log.Printf("SSH server ended with error: %v", err)
}()
defer func() {
err := sshServ.Close()
if err != nil {
log.Printf("unable to close SSH server: %s", err)
}
}()
}
log.Fatalln(https.ListenAndServe(context.Background(), mux))
}
func mustLUCIConfigClient() *swarmclient.ConfigClient {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
c, err := swarmclient.NewConfigClient(ctx)
if err != nil {
log.Fatalf("unable to create LUCI config client: %s", err)
}
return c
}
func mustRetrieveSSHCertificateAuthority() (privateKey []byte) {
privateKey, _, err := remote.SSHKeyPair()
if err != nil {
log.Fatalf("unable to create SSH CA cert: %s", err)
}
return privateKey
}
func mustStorageClient() *storage.Client {
if metadata.OnGCE() {
sc, err := pool.StorageClient(context.Background())
if err != nil {
log.Fatalf("unable to create authenticated storage client: %v", err)
}
return sc
}
sc, err := storage.NewClient(context.Background(), option.WithoutAuthentication())
if err != nil {
log.Fatalf("unable to create unauthenticated storage client: %s", err)
}
return sc
}
func fromSecret(ctx context.Context, sc *secret.Client, secretName string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return sc.Retrieve(ctx, secretName)
}
// 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
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "gomote status page placeholder")
}