зеркало из https://github.com/golang/build.git
184 строки
6.1 KiB
Go
184 строки
6.1 KiB
Go
// 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.
|
|
|
|
// bootstapswarm will bootstrap the swarming bot depending
|
|
// on the environment that it is run on.
|
|
//
|
|
// On GCE: bootstrapswarm will retrieve authentication credentials
|
|
// from the GCE metadata service and use those credentials to download
|
|
// the swarming bot. It will then start the swarming bot in a directory
|
|
// within the user's home directory.
|
|
//
|
|
// Requirements:
|
|
// - Python3 installed and in the calling user's PATH.
|
|
//
|
|
// Not on GCE: bootstrapswarm will read the token file and retrieve the
|
|
// the luci machine token. It will use that token to authenticate and
|
|
// download the swarming bot. It will then start the swarming bot in a
|
|
// directory within the user's home directory.
|
|
//
|
|
// Requirements:
|
|
// - Python3 installed and in the calling user's PATH.
|
|
// - luci_machine_tokend running as root in a cron job.
|
|
// https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/tokenserver
|
|
// Further instructions can be found at https://github.com/golang/go/wiki/DashboardBuilders
|
|
// The default locations for the token files should be used if possible:
|
|
// Most OS: /var/lib/luci_machine_tokend/token.json
|
|
// Windows: C:\luci_machine_tokend\token.json
|
|
// - bootstrapswarm should not be run as a privileged user.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"cloud.google.com/go/compute/metadata"
|
|
)
|
|
|
|
var (
|
|
tokenFilePath = flag.String("token-file-path", defaultTokenLocation(), "Path to the token file (used when not on GCE)")
|
|
hostname = flag.String("hostname", os.Getenv("HOSTNAME"), "Hostname of machine to bootstrap (required)")
|
|
)
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, "Usage: bootstrapswarm")
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
if *hostname == "" {
|
|
flag.Usage()
|
|
os.Exit(2)
|
|
}
|
|
ctx := context.Background()
|
|
if err := bootstrap(ctx, *hostname, *tokenFilePath); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var httpClient = http.DefaultClient
|
|
|
|
func bootstrap(ctx context.Context, hostname, tokenPath string) error {
|
|
httpHeaders := map[string]string{"X-Luci-Swarming-Bot-ID": hostname}
|
|
if metadata.OnGCE() {
|
|
log.Println("Bootstrapping the swarming bot with GCE authentication")
|
|
log.Println("retrieving the GCE VM token")
|
|
token, err := retrieveGCEVMToken(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to retrieve GCE Machine Token: %w", err)
|
|
}
|
|
httpHeaders["X-Luci-Gce-Vm-Token"] = token
|
|
} else {
|
|
log.Println("Bootstrapping the swarming bot with certificate authentication")
|
|
log.Println("retrieving the luci-machine-token from the token file")
|
|
tokBytes, err := os.ReadFile(tokenPath)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read file %q: %w", tokenPath, err)
|
|
}
|
|
type token struct {
|
|
LuciMachineToken string `json:"luci_machine_token"`
|
|
}
|
|
var tok token
|
|
if err := json.Unmarshal(tokBytes, &tok); err != nil {
|
|
return fmt.Errorf("unable to unmarshal token %s: %w", tokenPath, err)
|
|
}
|
|
if tok.LuciMachineToken == "" {
|
|
return fmt.Errorf("unable to retrieve machine token from token file %s", tokenPath)
|
|
}
|
|
httpHeaders["X-Luci-Machine-Token"] = tok.LuciMachineToken
|
|
}
|
|
log.Println("Downloading the swarming bot")
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, `https://chromium-swarm.appspot.com/bot_code`, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("http.NewRequest: %w", err)
|
|
}
|
|
for k, v := range httpHeaders {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("client.Do: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("status code %d", resp.StatusCode)
|
|
}
|
|
botBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("io.ReadAll: %w", err)
|
|
}
|
|
botPath, err := writeToWorkDirectory(botBytes, "swarming_bot.zip")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to save swarming bot to disk: %w", err)
|
|
}
|
|
log.Printf("Starting the swarming bot %s", botPath)
|
|
cmd := exec.CommandContext(ctx, "python3", botPath, "start_bot")
|
|
// swarming client checks the SWARMING_BOT_ID environment variable for hostname overrides.
|
|
cmd.Env = append(os.Environ(), fmt.Sprintf("SWARMING_BOT_ID=%s", hostname))
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("command execution %s: %s", cmd, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeToWorkDirectory writes a file to the swarming working directory and returns the path
|
|
// to where the file was written.
|
|
func writeToWorkDirectory(b []byte, filename string) (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("os.UserHomeDir: %w", err)
|
|
}
|
|
workDir := filepath.Join(homeDir, ".swarming")
|
|
if err := os.Mkdir(workDir, 0755); err != nil && !os.IsExist(err) {
|
|
return "", fmt.Errorf("os.Mkdir(%s): %w", workDir, err)
|
|
}
|
|
path := filepath.Join(workDir, filename)
|
|
if err = os.WriteFile(path, b, 0644); err != nil {
|
|
return "", fmt.Errorf("os.WriteFile(%s): %w", path, err)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// retrieveGCEVMToken retrieves a GCE VM token from the GCP metadata service.
|
|
func retrieveGCEVMToken(ctx context.Context) (string, error) {
|
|
const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://chromium-swarm.appspot.com&format=full`
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("http.NewRequest: %w", err)
|
|
}
|
|
req.Header.Set("Metadata-Flavor", "Google")
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("client.Do: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("status code %d", resp.StatusCode)
|
|
}
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("io.ReadAll: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func defaultTokenLocation() string {
|
|
out := "/var/lib/luci_machine_tokend/token.json"
|
|
if runtime.GOOS == "windows" {
|
|
return `C:\luci_machine_tokend\token.json`
|
|
}
|
|
return out
|
|
}
|