golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/rundockerbuildlet/rundockerbuildlet.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 rundockerbuildlet command loops forever and creates and cleans
     6  // up Docker containers running reverse buildlets. It keeps a fixed
     7  // number of them running at a time. See x/build/env/linux-arm64/packet/README
     8  // for one example user.
     9  package main
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	"encoding/json"
    15  	"flag"
    16  	"fmt"
    17  	"log"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/aws/aws-sdk-go/aws/ec2metadata"
    25  	"github.com/aws/aws-sdk-go/aws/session"
    26  	"golang.org/x/build/internal/cloud"
    27  )
    28  
    29  var (
    30  	image      = flag.String("image", "golang/builder", "docker image to run; required.")
    31  	numInst    = flag.Int("n", 1, "number of containers to keep running at once")
    32  	basename   = flag.String("basename", "builder", "prefix before the builder number to use for the container names and host names")
    33  	memory     = flag.String("memory", "3g", "memory limit flag for docker run")
    34  	keyFile    = flag.String("key", "/etc/gobuild.key", "go build key file")
    35  	builderEnv = flag.String("env", "", "optional GO_BUILDER_ENV environment variable value to set in the guests")
    36  	cpu        = flag.Int("cpu", 0, "if non-zero, how many CPUs to assign from the host and pass to docker run --cpuset-cpus")
    37  	pull       = flag.Bool("pull", false, "whether to pull the --image before each container starting")
    38  )
    39  
    40  var (
    41  	buildKey    []byte
    42  	isReverse   = true
    43  	isSingleRun = false
    44  	// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
    45  	ec2UD *cloud.EC2UserData
    46  	// ec2MetaClient is an EC2 metadata client.
    47  	ec2MetaClient *ec2metadata.EC2Metadata
    48  )
    49  
    50  func main() {
    51  	flag.Parse()
    52  
    53  	if onEC2() {
    54  		initEC2Meta()
    55  		*memory = ""
    56  		*image = ec2UD.BuildletImageURL
    57  		*builderEnv = ec2UD.BuildletHostType
    58  		*pull = true
    59  		*numInst = 1
    60  		isReverse = false
    61  		isSingleRun = true
    62  	}
    63  
    64  	if isReverse {
    65  		buildKey = getBuildKey()
    66  	}
    67  
    68  	if *image == "" {
    69  		log.Fatalf("docker --image is required")
    70  	}
    71  
    72  	log.Printf("Started. Will keep %d copies of %s running.", *numInst, *image)
    73  	for {
    74  		if err := checkFix(); err != nil {
    75  			log.Print(err)
    76  		}
    77  		if isSingleRun {
    78  			log.Printf("Configured to run a single instance. Exiting")
    79  			os.Exit(0)
    80  		}
    81  		time.Sleep(time.Second) // TODO: docker wait on the running containers?
    82  	}
    83  }
    84  
    85  func ec2MdClient() *ec2metadata.EC2Metadata {
    86  	if ec2MetaClient != nil {
    87  		return ec2MetaClient
    88  	}
    89  	ses, err := session.NewSession()
    90  	if err != nil {
    91  		return nil
    92  	}
    93  	ec2MetaClient = ec2metadata.New(ses)
    94  	return ec2MetaClient
    95  }
    96  
    97  func onEC2() bool {
    98  	if ec2MdClient() == nil {
    99  		return false
   100  	}
   101  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   102  	defer cancel()
   103  	return ec2MdClient().AvailableWithContext(ctx)
   104  }
   105  
   106  func getBuildKey() []byte {
   107  	key, err := os.ReadFile(*keyFile)
   108  	if err != nil {
   109  		log.Fatalf("error reading build key from --key=%s: %v", *keyFile, err)
   110  	}
   111  	return bytes.TrimSpace(key)
   112  }
   113  
   114  func checkFix() error {
   115  	running := map[string]bool{}
   116  
   117  	out, err := exec.Command("docker", "ps", "-a", "--format", "{{.ID}} {{.Names}} {{.Status}}").Output()
   118  	if err != nil {
   119  		return fmt.Errorf("error running docker ps: %v", err)
   120  	}
   121  	// Out is like:
   122  	// b1dc9ec2e646 packet14 Up 23 minutes
   123  	// eeb458938447 packet11 Exited (0) About a minute ago
   124  	// ...
   125  	lines := strings.Split(string(out), "\n")
   126  	for _, line := range lines {
   127  		f := strings.SplitN(line, " ", 3)
   128  		if len(f) < 3 {
   129  			continue
   130  		}
   131  		container, name, status := f[0], f[1], f[2]
   132  		prefix := *basename
   133  		if !strings.HasPrefix(name, prefix) {
   134  			continue
   135  		}
   136  		if strings.HasPrefix(status, "Exited") {
   137  			removeContainer(container)
   138  		}
   139  		running[name] = strings.HasPrefix(status, "Up")
   140  	}
   141  
   142  	for num := 1; num <= *numInst; num++ {
   143  		var name string
   144  		if onEC2() {
   145  			name = ec2UD.BuildletName
   146  		} else {
   147  			name = fmt.Sprintf("%s%02d", *basename, num)
   148  		}
   149  		if running[name] {
   150  			continue
   151  		}
   152  
   153  		// Just in case we have a container that exists but is not "running"
   154  		// check if it exists and remove it before creating a new one.
   155  		out, err = exec.Command("docker", "ps", "-a", "--filter", "name="+name, "--format", "{{.CreatedAt}}").Output()
   156  		if err == nil && len(bytes.TrimSpace(out)) > 0 {
   157  			// The format for the output is the create time and date:
   158  			// 2017-07-24 17:07:39 +0000 UTC
   159  			// To avoid a race with a container that is "Created" but not yet running
   160  			// check how long ago the container was created.
   161  			// If it's longer than minute, remove it.
   162  			created, err := time.Parse("2006-01-02 15:04:05 -0700 MST", strings.TrimSpace(string(out)))
   163  			if err != nil {
   164  				log.Printf("converting output %q for container %s to time failed: %v", out, name, err)
   165  				continue
   166  			}
   167  			dur := time.Since(created)
   168  			if dur.Minutes() > 0 {
   169  				removeContainer(name)
   170  			}
   171  
   172  			log.Printf("Container %s is already being created, duration %s", name, dur.String())
   173  			continue
   174  		}
   175  
   176  		if *pull {
   177  			log.Printf("Pulling %s ...", *image)
   178  			out, err := exec.Command("docker", "pull", *image).CombinedOutput()
   179  			if err != nil {
   180  				log.Printf("docker pull %s failed: %v, %s", *image, err, out)
   181  			}
   182  		}
   183  
   184  		log.Printf("Creating %s ...", name)
   185  		keyFile := fmt.Sprintf("/tmp/buildkey%02d/gobuildkey", num)
   186  		if err := os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil {
   187  			return err
   188  		}
   189  		if err := os.WriteFile(keyFile, buildKey, 0600); err != nil {
   190  			return err
   191  		}
   192  		cmd := exec.Command("docker", "run",
   193  			"-d",
   194  			"--name="+name,
   195  			"-e", "HOSTNAME="+name,
   196  			"--security-opt=seccomp=unconfined", // Issue 35547
   197  			"--tmpfs=/workdir:rw,exec")
   198  		if *memory != "" {
   199  			cmd.Args = append(cmd.Args, "--memory="+*memory)
   200  		}
   201  		if isReverse {
   202  			cmd.Args = append(cmd.Args, "-v", filepath.Dir(keyFile)+":/buildkey/")
   203  		} else {
   204  			cmd.Args = append(cmd.Args, "-p", "443:443")
   205  		}
   206  		if *cpu > 0 {
   207  			cmd.Args = append(cmd.Args, fmt.Sprintf("--cpuset-cpus=%d-%d", *cpu*(num-1), *cpu*num-1))
   208  		}
   209  		if *builderEnv != "" {
   210  			cmd.Args = append(cmd.Args, "-e", "GO_BUILDER_ENV="+*builderEnv)
   211  		}
   212  		cmd.Args = append(cmd.Args,
   213  			"-e", "GO_BUILD_KEY_PATH=/buildkey/gobuildkey",
   214  			"-e", "GO_BUILD_KEY_DELETE_AFTER_READ=true",
   215  		)
   216  		cmd.Args = append(cmd.Args, *image)
   217  		out, err := cmd.CombinedOutput()
   218  		if err != nil {
   219  			log.Printf("Error creating %s: %v, %s", name, err, out)
   220  			continue
   221  		}
   222  		log.Printf("Created %v", name)
   223  	}
   224  	return nil
   225  }
   226  
   227  func initEC2Meta() {
   228  	if !onEC2() {
   229  		log.Fatal("attempt to initialize metadata on non-EC2 instance")
   230  	}
   231  	if ec2UD != nil {
   232  		return
   233  	}
   234  	if ec2MdClient() == nil {
   235  		log.Fatalf("unable to retrieve EC2 metadata client")
   236  	}
   237  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   238  	defer cancel()
   239  	ec2MetaJson, err := ec2MdClient().GetUserDataWithContext(ctx)
   240  	if err != nil {
   241  		log.Fatalf("unable to retrieve EC2 user data: %v", err)
   242  	}
   243  	ec2UD = &cloud.EC2UserData{}
   244  	err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
   245  	if err != nil {
   246  		log.Fatalf("unable to unmarshal user data json: %v", err)
   247  	}
   248  }
   249  
   250  func removeContainer(container string) {
   251  	if out, err := exec.Command("docker", "rm", "-f", container).CombinedOutput(); err != nil {
   252  		log.Printf("error running docker rm -f %s: %v, %s", container, err, out)
   253  		return
   254  	}
   255  	log.Printf("Removed container %s", container)
   256  }