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

     1  // Copyright 2017 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  // The debugnewvm command creates and destroys a VM-based buildlet
     6  // with lots of logging for debugging. Nothing depends on this.
     7  package main
     8  
     9  import (
    10  	"context"
    11  	"flag"
    12  	"fmt"
    13  	"log"
    14  	"net/http"
    15  	"os"
    16  	"path"
    17  	"regexp"
    18  	"strings"
    19  	"time"
    20  
    21  	"cloud.google.com/go/compute/metadata"
    22  	"golang.org/x/build/buildenv"
    23  	"golang.org/x/build/buildlet"
    24  	"golang.org/x/build/dashboard"
    25  	"golang.org/x/build/internal/buildgo"
    26  	"golang.org/x/build/internal/cloud"
    27  	"golang.org/x/build/internal/secret"
    28  	"golang.org/x/oauth2"
    29  	"golang.org/x/oauth2/google"
    30  	compute "google.golang.org/api/compute/v1"
    31  )
    32  
    33  var (
    34  	hostType      = flag.String("host", "", "host type to create")
    35  	zone          = flag.String("zone", "", "if non-empty, force a certain GCP zone")
    36  	overrideImage = flag.String("override-image", "", "if non-empty, an alternate GCE VM image or container image to use, depending on the host type")
    37  	serial        = flag.Bool("serial", true, "watch serial. Supported for GCE VMs")
    38  	pauseAfterUp  = flag.Duration("pause-after-up", 0, "pause for this duration before buildlet is destroyed")
    39  	sleepSec      = flag.Int("sleep-test-secs", 0, "number of seconds to sleep when buildlet comes up, to test time source; OpenBSD only for now")
    40  
    41  	runBuild = flag.String("run-build", "", "optional builder name to run all.bash or make.bash for")
    42  	makeOnly = flag.Bool("make-only", false, "if a --run-build builder name is given, this controls whether make.bash or all.bash is run")
    43  	buildRev = flag.String("rev", "master", "if --run-build is specified, the git hash or branch name to build")
    44  
    45  	useIAPTunnel = flag.Bool("use-iap-tunnel", true, "use an IAP tunnel to connect to GCE builders")
    46  
    47  	awsKeyID     = flag.String("aws-key-id", "", "if the builder runs on aws then key id is required. If executed on GCE, it will be retrieved from secrets.")
    48  	awsAccessKey = flag.String("aws-access-key", "", "if the builder runs on aws then the access key is required. If executed on GCE, it will be retrieved from secrets.")
    49  	awsRegion    = flag.String("aws-region", "", "if non-empty and the requested builder is an EC2 instance, force an EC2 region.")
    50  )
    51  
    52  var (
    53  	computeSvc *compute.Service
    54  	env        *buildenv.Environment
    55  )
    56  
    57  func main() {
    58  	buildenv.RegisterFlags()
    59  	flag.Parse()
    60  
    61  	var bconf *dashboard.BuildConfig
    62  	if *runBuild != "" {
    63  		var ok bool
    64  		bconf, ok = dashboard.Builders[*runBuild]
    65  		if !ok {
    66  			log.Fatalf("unknown builder %q", *runBuild)
    67  		}
    68  		if *hostType == "" {
    69  			*hostType = bconf.HostType
    70  		}
    71  	}
    72  
    73  	if *hostType == "" {
    74  		log.Fatalf("missing --host (or --run-build)")
    75  	}
    76  	if *sleepSec != 0 && !strings.Contains(*hostType, "openbsd") {
    77  		log.Fatalf("The --sleep-test-secs is currently only supported for openbsd hosts.")
    78  	}
    79  
    80  	hconf, ok := dashboard.Hosts[*hostType]
    81  	if !ok {
    82  		log.Fatalf("unknown host type %q", *hostType)
    83  	}
    84  	if !hconf.IsVM() && !hconf.IsContainer() {
    85  		log.Fatalf("host type %q is type %q; want a VM or container host type", *hostType, hconf.PoolName())
    86  	}
    87  	if hconf.IsEC2 && (*awsKeyID == "" || *awsAccessKey == "") {
    88  		if !metadata.OnGCE() {
    89  			log.Fatal("missing -aws-key-id and -aws-access-key params are required for builders on AWS")
    90  		}
    91  		var err error
    92  		*awsKeyID, *awsAccessKey, err = awsCredentialsFromSecrets()
    93  		if err != nil {
    94  			log.Fatalf("unable to retrieve AWS credentials: %s", err)
    95  		}
    96  	}
    97  	if img := *overrideImage; img != "" {
    98  		if hconf.IsContainer() {
    99  			hconf.ContainerImage = img
   100  		} else {
   101  			hconf.VMImage = img
   102  		}
   103  	}
   104  	vmImageSummary := fmt.Sprintf("%q", hconf.VMImage)
   105  	if hconf.IsContainer() {
   106  		containerHost := hconf.ContainerVMImage()
   107  		if containerHost == "" {
   108  			containerHost = "default container host"
   109  		}
   110  		vmImageSummary = fmt.Sprintf("%s, running container %q", containerHost, hconf.ContainerImage)
   111  	}
   112  
   113  	env = buildenv.FromFlags()
   114  	ctx := context.Background()
   115  	name := fmt.Sprintf("debug-temp-%d-%s", time.Now().Unix(), os.Getenv("USER"))
   116  
   117  	log.Printf("Creating %s (with VM image %s)", name, vmImageSummary)
   118  	var bc buildlet.Client
   119  	if hconf.IsEC2 {
   120  		region := env.AWSRegion
   121  		if *awsRegion != "" {
   122  			region = *awsRegion
   123  		}
   124  		awsC, err := cloud.NewAWSClient(region, *awsKeyID, *awsAccessKey)
   125  		if err != nil {
   126  			log.Fatalf("unable to create aws cloud client: %s", err)
   127  		}
   128  		ec2C := buildlet.NewEC2Client(awsC)
   129  		if err != nil {
   130  			log.Fatalf("unable to create ec2 client: %v", err)
   131  		}
   132  		bc, err = ec2Buildlet(context.Background(), ec2C, hconf, env, name, *hostType, *zone)
   133  		if err != nil {
   134  			log.Fatalf("Start EC2 VM: %v", err)
   135  		}
   136  	} else {
   137  		buildenv.CheckUserCredentials()
   138  		creds, err := env.Credentials(ctx)
   139  		if err != nil {
   140  			log.Fatal(err)
   141  		}
   142  		computeSvc, _ = compute.New(oauth2.NewClient(ctx, creds.TokenSource))
   143  		bc, err = gceBuildlet(creds, env, name, *hostType, *zone)
   144  		if err != nil {
   145  			log.Fatalf("Start GCE VM: %v", err)
   146  		}
   147  	}
   148  	dir, err := bc.WorkDir(ctx)
   149  	log.Printf("WorkDir: %v, %v", dir, err)
   150  
   151  	if *sleepSec > 0 {
   152  		bc.Exec(ctx, "sysctl", buildlet.ExecOpts{
   153  			Output:      os.Stdout,
   154  			SystemLevel: true,
   155  			Args:        []string{"kern.timecounter.hardware"},
   156  		})
   157  		bc.Exec(ctx, "bash", buildlet.ExecOpts{
   158  			Output:      os.Stdout,
   159  			SystemLevel: true,
   160  			Args:        []string{"-c", "rdate -p -v time.nist.gov; sleep " + fmt.Sprint(*sleepSec) + "; rdate -p -v time.nist.gov"},
   161  		})
   162  	}
   163  
   164  	var buildFailed bool
   165  	if *runBuild != "" {
   166  		// Push GOROOT_BOOTSTRAP, if needed.
   167  		if u := bconf.GoBootstrapURL(env); u != "" {
   168  			log.Printf("Pushing 'go1.4' Go bootstrap dir from %s...", u)
   169  			const bootstrapDir = "go1.4" // might be newer; name is the default
   170  			if err := bc.PutTarFromURL(ctx, u, bootstrapDir); err != nil {
   171  				bc.Close()
   172  				log.Fatalf("Putting Go bootstrap: %v", err)
   173  			}
   174  		}
   175  
   176  		// Push Go code
   177  		log.Printf("Pushing 'go' dir...")
   178  		goTarGz := "https://go.googlesource.com/go/+archive/" + *buildRev + ".tar.gz"
   179  		if err := bc.PutTarFromURL(ctx, goTarGz, "go"); err != nil {
   180  			bc.Close()
   181  			log.Fatalf("Putting go code: %v", err)
   182  		}
   183  
   184  		// Push a synthetic VERSION file to prevent git usage:
   185  		if err := bc.PutTar(ctx, buildgo.VersionTgz(*buildRev), "go"); err != nil {
   186  			bc.Close()
   187  			log.Fatalf("Putting VERSION file: %v", err)
   188  		}
   189  
   190  		script := bconf.AllScript()
   191  		if *makeOnly {
   192  			script = bconf.MakeScript()
   193  		}
   194  		t0 := time.Now()
   195  		log.Printf("Running %s ...", script)
   196  		remoteErr, err := bc.Exec(ctx, path.Join("go", script), buildlet.ExecOpts{
   197  			Output:   os.Stdout,
   198  			ExtraEnv: bconf.Env(),
   199  			Debug:    true,
   200  			Args:     bconf.AllScriptArgs(),
   201  		})
   202  		if err != nil {
   203  			log.Fatalf("error trying to run %s: %v", script, err)
   204  		}
   205  		if remoteErr != nil {
   206  			log.Printf("remote failure running %s: %v", script, remoteErr)
   207  			buildFailed = true
   208  		} else {
   209  			log.Printf("ran %s in %v", script, time.Since(t0).Round(time.Second))
   210  		}
   211  	}
   212  
   213  	if *pauseAfterUp != 0 {
   214  		log.Printf("Sleeping for %v before shutting down...", *pauseAfterUp)
   215  		time.Sleep(*pauseAfterUp)
   216  	}
   217  	if err := bc.Close(); err != nil {
   218  		log.Fatalf("Close: %v", err)
   219  	}
   220  	log.Printf("done.")
   221  	time.Sleep(2 * time.Second) // wait for serial logging to catch up
   222  
   223  	if buildFailed {
   224  		os.Exit(1)
   225  	}
   226  }
   227  
   228  // watchSerial streams the named VM's serial port to log.Printf. It's roughly:
   229  //
   230  //	gcloud compute connect-to-serial-port --zone=xxx $NAME
   231  //
   232  // but in Go and works. For some reason, gcloud doesn't work as a
   233  // child process and has weird errors.
   234  // TODO(golang.org/issue/39485) - investigate if this is possible for EC2 instances
   235  func watchSerial(zone, name string) {
   236  	start := int64(0)
   237  	indent := strings.Repeat(" ", len("2017/07/25 06:37:14 SERIAL: "))
   238  	for {
   239  		sout, err := computeSvc.Instances.GetSerialPortOutput(env.ProjectName, zone, name).Start(start).Do()
   240  		if err != nil {
   241  			log.Printf("serial output error: %v", err)
   242  			return
   243  		}
   244  		moved := sout.Next != start
   245  		start = sout.Next
   246  		contents := strings.Replace(strings.TrimSpace(sout.Contents), "\r\n", "\r\n"+indent, -1)
   247  		if contents != "" {
   248  			log.Printf("SERIAL: %s", contents)
   249  		}
   250  		if !moved {
   251  			time.Sleep(1 * time.Second)
   252  		}
   253  	}
   254  }
   255  
   256  // awsCredentialsFromSecrets retrieves AWS credentials from the secret management service.
   257  // This function returns the key ID and the access key.
   258  func awsCredentialsFromSecrets() (string, string, error) {
   259  	c, err := secret.NewClient()
   260  	if err != nil {
   261  		return "", "", fmt.Errorf("unable to create secret client: %w", err)
   262  	}
   263  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   264  	defer cancel()
   265  	keyID, err := c.Retrieve(ctx, secret.NameAWSKeyID)
   266  	if err != nil {
   267  		return "", "", fmt.Errorf("unable to retrieve key ID: %w", err)
   268  	}
   269  	accessKey, err := c.Retrieve(ctx, secret.NameAWSAccessKey)
   270  	if err != nil {
   271  		return "", "", fmt.Errorf("unable to retrieve access key: %w", err)
   272  	}
   273  	return keyID, accessKey, nil
   274  }
   275  
   276  func gceBuildlet(creds *google.Credentials, env *buildenv.Environment, name, hostType, zone string) (buildlet.Client, error) {
   277  	return buildlet.StartNewVM(creds, env, name, hostType, buildlet.VMOpts{
   278  		Zone:                zone,
   279  		OnInstanceRequested: func() { log.Printf("instance requested") },
   280  		OnInstanceCreated: func() {
   281  			log.Printf("instance created")
   282  		},
   283  		OnGotInstanceInfo: func(inst *compute.Instance) {
   284  			zone := inst.Zone
   285  			m := regexp.MustCompile(`/projects/([^/]+)/zones/([^/]+)`).FindStringSubmatch(inst.Zone)
   286  			if m != nil {
   287  				zone = m[2]
   288  			}
   289  			log.Printf("got instance info; running in %v (%v)", inst.Zone, zone)
   290  			if *serial {
   291  				go watchSerial(zone, name)
   292  			}
   293  		},
   294  		OnBeginBuildletProbe: func(buildletURL string) {
   295  			log.Printf("About to hit %s to see if buildlet is up yet...", buildletURL)
   296  		},
   297  		OnEndBuildletProbe: func(res *http.Response, err error) {
   298  			if err != nil {
   299  				log.Printf("client buildlet probe error: %v", err)
   300  				return
   301  			}
   302  			log.Printf("buildlet probe: %s", res.Status)
   303  		},
   304  		UseIAPTunnel: *useIAPTunnel,
   305  	})
   306  }
   307  
   308  func ec2Buildlet(ctx context.Context, ec2Client *buildlet.EC2Client, hconf *dashboard.HostConfig, env *buildenv.Environment, name, hostType, zone string) (buildlet.Client, error) {
   309  	kp, err := buildlet.NewKeyPair()
   310  	if err != nil {
   311  		log.Fatalf("key pair failed: %v", err)
   312  	}
   313  	return ec2Client.StartNewVM(ctx, env, hconf, name, hostType, &buildlet.VMOpts{
   314  		TLS:                 kp,
   315  		Zone:                zone,
   316  		OnInstanceRequested: func() { log.Printf("instance requested") },
   317  		OnInstanceCreated: func() {
   318  			log.Printf("instance created")
   319  		},
   320  		OnGotEC2InstanceInfo: func(inst *cloud.Instance) {
   321  			log.Printf("got instance info: running in %v", inst.Zone)
   322  		},
   323  		OnBeginBuildletProbe: func(buildletURL string) {
   324  			log.Printf("About to hit %s to see if buildlet is up yet...", buildletURL)
   325  		},
   326  		OnEndBuildletProbe: func(res *http.Response, err error) {
   327  			if err != nil {
   328  				log.Printf("client buildlet probe error: %v", err)
   329  				return
   330  			}
   331  			log.Printf("buildlet probe: %s", res.Status)
   332  		},
   333  	})
   334  }