golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/bootstrapswarm/bootstrapswarm.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  // bootstapswarm will bootstrap the swarming bot depending
     6  // on the environment that it is run on.
     7  //
     8  // On GCE: bootstrapswarm will retrieve authentication credentials
     9  // from the GCE metadata service and use those credentials to download
    10  // the swarming bot. It will then start the swarming bot in a directory
    11  // within the user's home directory.
    12  //
    13  // Requirements:
    14  // - Python3 installed and in the calling user's PATH.
    15  //
    16  // Not on GCE: bootstrapswarm will read the token file and retrieve the
    17  // the luci machine token. It will use that token to authenticate and
    18  // download the swarming bot. It will then start the swarming bot in a
    19  // directory within the user's home directory.
    20  //
    21  // Requirements:
    22  //   - Python3 installed and in the calling user's PATH.
    23  //   - luci_machine_tokend running as root in a cron job.
    24  //     See https://chromium.googlesource.com/infra/luci/luci-go/+/main/tokenserver.
    25  //     Further instructions can be found at https://go.dev/wiki/DashboardBuilders.
    26  //     The default locations for the token files should be used if possible:
    27  //     Most OS: /var/lib/luci_machine_tokend/token.json
    28  //     Windows: C:\luci_machine_tokend\token.json
    29  //     A custom default location can be set via the environment variable LUCI_MACHINE_TOKEN.
    30  //   - bootstrapswarm should not be run as a privileged user.
    31  package main
    32  
    33  import (
    34  	"context"
    35  	"encoding/json"
    36  	"flag"
    37  	"fmt"
    38  	"io"
    39  	"log"
    40  	"net/http"
    41  	"os"
    42  	"os/exec"
    43  	"path/filepath"
    44  	"runtime"
    45  	"strings"
    46  
    47  	"cloud.google.com/go/compute/metadata"
    48  )
    49  
    50  var (
    51  	hostname = flag.String("hostname", os.Getenv("HOSTNAME"), "Hostname of machine to bootstrap")
    52  	swarming = flag.String("swarming", "chromium-swarm.appspot.com", "Swarming server to connect to")
    53  )
    54  
    55  func main() {
    56  	flag.Usage = func() {
    57  		fmt.Fprintln(os.Stderr, "Usage: bootstrapswarm")
    58  		flag.PrintDefaults()
    59  	}
    60  	flag.Parse()
    61  	if *hostname == "" {
    62  		flag.Usage()
    63  		os.Exit(2)
    64  	}
    65  	ctx := context.Background()
    66  	if err := bootstrap(ctx, *hostname); err != nil {
    67  		log.Fatal(err)
    68  	}
    69  }
    70  
    71  var httpClient = http.DefaultClient
    72  
    73  func bootstrap(ctx context.Context, hostname string) error {
    74  	httpHeaders := map[string]string{}
    75  	if metadata.OnGCE() {
    76  		log.Println("Bootstrapping the swarming bot with GCE authentication")
    77  		log.Println("retrieving the GCE VM token")
    78  		token, err := retrieveGCEVMToken(ctx)
    79  		if err != nil {
    80  			return fmt.Errorf("unable to retrieve GCE Machine Token: %w", err)
    81  		}
    82  		httpHeaders["X-Luci-Gce-Vm-Token"] = token
    83  
    84  		// Override the hostname flag with the GCE hostname. This is a hard
    85  		// requirement for LUCI, so there's no point in trying anything else.
    86  		fullHost, err := metadata.Hostname()
    87  		if err != nil {
    88  			return fmt.Errorf("retrieving hostname: %w", err)
    89  		}
    90  		hostname = strings.Split(fullHost, ".")[0]
    91  	} else {
    92  		log.Println("Bootstrapping the swarming bot with certificate authentication")
    93  		tokenPath, desc := tokenFile()
    94  		log.Printf("retrieving the luci-machine-token from the token file %s (%s)\n", tokenPath, desc)
    95  		tokBytes, err := os.ReadFile(tokenPath)
    96  		if err != nil {
    97  			return fmt.Errorf("unable to read file %q: %w", tokenPath, err)
    98  		}
    99  		type token struct {
   100  			LuciMachineToken string `json:"luci_machine_token"`
   101  		}
   102  		var tok token
   103  		if err := json.Unmarshal(tokBytes, &tok); err != nil {
   104  			return fmt.Errorf("unable to unmarshal token %s: %w", tokenPath, err)
   105  		}
   106  		if tok.LuciMachineToken == "" {
   107  			return fmt.Errorf("unable to retrieve machine token from token file %s", tokenPath)
   108  		}
   109  		httpHeaders["X-Luci-Machine-Token"] = tok.LuciMachineToken
   110  	}
   111  	httpHeaders["X-Luci-Swarming-Bot-ID"] = hostname
   112  	log.Println("Downloading the swarming bot")
   113  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+*swarming+"/bot_code", nil)
   114  	if err != nil {
   115  		return fmt.Errorf("http.NewRequest: %w", err)
   116  	}
   117  	for k, v := range httpHeaders {
   118  		req.Header.Set(k, v)
   119  	}
   120  	resp, err := httpClient.Do(req)
   121  	if err != nil {
   122  		return fmt.Errorf("client.Do: %w", err)
   123  	}
   124  	defer resp.Body.Close()
   125  	if resp.StatusCode != 200 {
   126  		return fmt.Errorf("status code %d", resp.StatusCode)
   127  	}
   128  	botBytes, err := io.ReadAll(resp.Body)
   129  	if err != nil {
   130  		return fmt.Errorf("io.ReadAll: %w", err)
   131  	}
   132  	botPath, err := writeToWorkDirectory(botBytes, "swarming_bot.zip")
   133  	if err != nil {
   134  		return fmt.Errorf("unable to save swarming bot to disk: %w", err)
   135  	}
   136  	log.Printf("Starting the swarming bot %s", botPath)
   137  	cmd := exec.CommandContext(ctx, "python3", botPath, "start_bot")
   138  	// swarming client checks the SWARMING_BOT_ID environment variable for hostname overrides.
   139  	cmd.Env = append(os.Environ(), fmt.Sprintf("SWARMING_BOT_ID=%s", hostname))
   140  	cmd.Stdout = os.Stdout
   141  	cmd.Stderr = os.Stderr
   142  	if err := cmd.Run(); err != nil {
   143  		return fmt.Errorf("command execution %s: %s", cmd, err)
   144  	}
   145  	return nil
   146  }
   147  
   148  // writeToWorkDirectory writes a file to the swarming working directory and returns the path
   149  // to where the file was written.
   150  func writeToWorkDirectory(b []byte, filename string) (string, error) {
   151  	homeDir, err := os.UserHomeDir()
   152  	if err != nil {
   153  		return "", fmt.Errorf("os.UserHomeDir: %w", err)
   154  	}
   155  	workDir := filepath.Join(homeDir, ".swarming")
   156  	if err := os.Mkdir(workDir, 0755); err != nil && !os.IsExist(err) {
   157  		return "", fmt.Errorf("os.Mkdir(%s): %w", workDir, err)
   158  	}
   159  	path := filepath.Join(workDir, filename)
   160  	if err = os.WriteFile(path, b, 0644); err != nil {
   161  		return "", fmt.Errorf("os.WriteFile(%s): %w", path, err)
   162  	}
   163  	return path, nil
   164  }
   165  
   166  // retrieveGCEVMToken retrieves a GCE VM token from the GCP metadata service.
   167  func retrieveGCEVMToken(ctx context.Context) (string, error) {
   168  	url := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://" + *swarming + "&format=full"
   169  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   170  	if err != nil {
   171  		return "", fmt.Errorf("http.NewRequest: %w", err)
   172  	}
   173  	req.Header.Set("Metadata-Flavor", "Google")
   174  	resp, err := httpClient.Do(req)
   175  	if err != nil {
   176  		return "", fmt.Errorf("client.Do: %w", err)
   177  	}
   178  	defer resp.Body.Close()
   179  	if resp.StatusCode != 200 {
   180  		return "", fmt.Errorf("status code %d", resp.StatusCode)
   181  	}
   182  	b, err := io.ReadAll(resp.Body)
   183  	if err != nil {
   184  		return "", fmt.Errorf("io.ReadAll: %w", err)
   185  	}
   186  	return string(b), nil
   187  }
   188  
   189  // tokenFile reports the path of the LUCI machine token file (used when not on GCE),
   190  // and a description of where that value came from.
   191  func tokenFile() (path, desc string) {
   192  	if v := os.Getenv("LUCI_MACHINE_TOKEN"); v != "" {
   193  		return v, "via LUCI_MACHINE_TOKEN env var"
   194  	}
   195  	if runtime.GOOS == "windows" {
   196  		return `C:\luci_machine_tokend\token.json`, "default path for GOOS == windows"
   197  	}
   198  	return "/var/lib/luci_machine_tokend/token.json", "default path for GOOS != windows"
   199  }