golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gomoteserver/gomoteserver.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux || darwin
     6  
     7  package main
     8  
     9  import (
    10  	"context"
    11  	"flag"
    12  	"log"
    13  	"net/http"
    14  	"strings"
    15  
    16  	"cloud.google.com/go/compute/metadata"
    17  	"cloud.google.com/go/storage"
    18  	"go.chromium.org/luci/auth"
    19  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    20  	"go.chromium.org/luci/grpc/prpc"
    21  	"go.chromium.org/luci/hardcoded/chromeinfra"
    22  	"go.chromium.org/luci/swarming/client/swarming"
    23  	"golang.org/x/build/buildenv"
    24  	"golang.org/x/build/internal/access"
    25  	"golang.org/x/build/internal/coordinator/pool"
    26  	"golang.org/x/build/internal/coordinator/remote"
    27  	"golang.org/x/build/internal/gomote"
    28  	gomotepb "golang.org/x/build/internal/gomote/protos"
    29  	"golang.org/x/build/internal/gomoteserver/ui"
    30  	"golang.org/x/build/internal/https"
    31  	"golang.org/x/build/internal/rendezvous"
    32  	"golang.org/x/build/internal/secret"
    33  	"golang.org/x/build/revdial/v2"
    34  	"google.golang.org/api/option"
    35  	"google.golang.org/grpc"
    36  )
    37  
    38  var (
    39  	sshAddr      = flag.String("ssh_addr", ":2222", "Address the gomote SSH server should listen on")
    40  	buildEnvName = flag.String("env", "", "The build environment configuration to use. Not required if running in dev mode locally or prod mode on GCE.")
    41  	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.")
    42  )
    43  
    44  var Version string // set by linker -X
    45  
    46  const (
    47  	gomoteHost    = "gomote.golang.org"
    48  	gomoteSSHHost = "gomotessh.golang.org"
    49  )
    50  
    51  func main() {
    52  	https.RegisterFlags(flag.CommandLine)
    53  	if err := secret.InitFlagSupport(context.Background()); err != nil {
    54  		log.Fatalln(err)
    55  	}
    56  	hostKey := secret.Flag("private-host-key", "Gomote SSH Server host private key")
    57  	pubKey := secret.Flag("public-host-key", "Gomote SSH Server host public key")
    58  	flag.Parse()
    59  
    60  	log.Println("starting gomote server")
    61  	ctx, cancel := context.WithCancel(context.Background())
    62  	defer cancel()
    63  
    64  	sp := remote.NewSessionPool(context.Background())
    65  	sshCA := mustRetrieveSSHCertificateAuthority()
    66  
    67  	var gomoteBucket string
    68  	var opts []grpc.ServerOption
    69  	if *buildEnvName == "" && *mode != "dev" && metadata.OnGCE() {
    70  		projectID, err := metadata.ProjectID()
    71  		if err != nil {
    72  			log.Fatalf("metadata.ProjectID() = %v", err)
    73  		}
    74  		luciEnv := buildenv.ByProjectID("golang-ci-luci")
    75  		env := buildenv.ByProjectID(projectID)
    76  		gomoteBucket = luciEnv.GomoteTransferBucket
    77  		var coordinatorBackend, serviceID = "coordinator-internal-iap", ""
    78  		if serviceID = env.IAPServiceID(coordinatorBackend); serviceID == "" {
    79  			log.Fatalf("unable to retrieve Service ID for backend service=%q", coordinatorBackend)
    80  		}
    81  		opts = append(opts, grpc.UnaryInterceptor(access.RequireIAPAuthUnaryInterceptor(access.IAPSkipAudienceValidation)))
    82  		opts = append(opts, grpc.StreamInterceptor(access.RequireIAPAuthStreamInterceptor(access.IAPSkipAudienceValidation)))
    83  	}
    84  	grpcServer := grpc.NewServer(opts...)
    85  	rdv := rendezvous.New(ctx)
    86  	gomoteServer, err := gomote.NewSwarming(sp, sshCA, gomoteBucket, mustStorageClient(), rdv, mustSwarmingClient(ctx), mustBuildersClient(ctx))
    87  	if err != nil {
    88  		log.Fatalf("unable to create gomote server: %s", err)
    89  	}
    90  	gomotepb.RegisterGomoteServiceServer(grpcServer, gomoteServer)
    91  
    92  	mux := http.NewServeMux()
    93  	mux.HandleFunc("/reverse", rdv.HandleReverse)
    94  	mux.Handle("/revdial", revdial.ConnHandler())
    95  	mux.HandleFunc("/style.css", ui.Redirect(ui.HandleStyleCSS, gomoteSSHHost, gomoteHost))
    96  	mux.HandleFunc("/", ui.Redirect(grpcHandlerFunc(grpcServer, ui.HandleStatusFunc(sp, Version)), gomoteSSHHost, gomoteHost)) // Serve a status page.
    97  
    98  	sshServ, err := remote.NewSSHServer(*sshAddr, []byte(*hostKey), []byte(*pubKey), sshCA, sp, remote.EnableLUCIOption())
    99  	if err != nil {
   100  		log.Printf("unable to configure SSH server: %s", err)
   101  	} else {
   102  		go func() {
   103  			log.Printf("running SSH server on %s", *sshAddr)
   104  			err := sshServ.ListenAndServe()
   105  			log.Printf("SSH server ended with error: %v", err)
   106  		}()
   107  		defer func() {
   108  			err := sshServ.Close()
   109  			if err != nil {
   110  				log.Printf("unable to close SSH server: %s", err)
   111  			}
   112  		}()
   113  	}
   114  	log.Fatalln(https.ListenAndServe(context.Background(), mux))
   115  }
   116  
   117  func mustRetrieveSSHCertificateAuthority() (privateKey []byte) {
   118  	privateKey, _, err := remote.SSHKeyPair()
   119  	if err != nil {
   120  		log.Fatalf("unable to create SSH CA cert: %s", err)
   121  	}
   122  	return privateKey
   123  }
   124  
   125  func mustStorageClient() *storage.Client {
   126  	if metadata.OnGCE() {
   127  		sc, err := pool.StorageClient(context.Background())
   128  		if err != nil {
   129  			log.Fatalf("unable to create authenticated storage client: %v", err)
   130  		}
   131  		return sc
   132  	}
   133  	sc, err := storage.NewClient(context.Background(), option.WithoutAuthentication())
   134  	if err != nil {
   135  		log.Fatalf("unable to create unauthenticated storage client: %s", err)
   136  	}
   137  	return sc
   138  }
   139  
   140  // grpcHandlerFunc creates handler which intercepts requests intended for a GRPC server and directs the calls to the server.
   141  // All other requests are directed toward the passed in handler.
   142  func grpcHandlerFunc(gs *grpc.Server, h http.HandlerFunc) http.HandlerFunc {
   143  	return func(w http.ResponseWriter, r *http.Request) {
   144  		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
   145  			gs.ServeHTTP(w, r)
   146  			return
   147  		}
   148  		h(w, r)
   149  	}
   150  }
   151  
   152  func mustSwarmingClient(ctx context.Context) swarming.Client {
   153  	c, err := swarming.NewClient(ctx, swarming.ClientOptions{
   154  		ServiceURL: "https://chromium-swarm.appspot.com",
   155  		UserAgent:  "go-gomoteserver",
   156  		Auth:       auth.Options{Method: auth.GCEMetadataMethod},
   157  	})
   158  	if err != nil {
   159  		log.Fatalf("unable to create swarming client: %s", err)
   160  	}
   161  	return c
   162  }
   163  
   164  func mustBuildersClient(ctx context.Context) buildbucketpb.BuildersClient {
   165  	httpC, err := auth.NewAuthenticator(ctx, auth.SilentLogin, auth.Options{Method: auth.GCEMetadataMethod}).Client()
   166  	if err != nil {
   167  		log.Fatalf("unable to create buildbucket authenticator: %s", err)
   168  	}
   169  	prpcC := prpc.Client{C: httpC, Host: chromeinfra.BuildbucketHost}
   170  	return buildbucketpb.NewBuildersClient(&prpcC)
   171  }