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

     1  // Copyright 2015 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  package main
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"log"
    14  	"net/http"
    15  	"os"
    16  	"path/filepath"
    17  	"sort"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"golang.org/x/build/internal/gomote/protos"
    23  	"golang.org/x/sync/errgroup"
    24  )
    25  
    26  type builderType struct {
    27  	Name      string
    28  	IsReverse bool
    29  	ExpectNum int
    30  }
    31  
    32  func builders() (bt []builderType) {
    33  	type builderInfo struct {
    34  		HostType string
    35  	}
    36  	type hostInfo struct {
    37  		IsReverse      bool
    38  		ExpectNum      int
    39  		ContainerImage string
    40  		VMImage        string
    41  	}
    42  	// resj is the response JSON from the builders.
    43  	var resj struct {
    44  		Builders map[string]builderInfo
    45  		Hosts    map[string]hostInfo
    46  	}
    47  	res, err := http.Get("https://farmer.golang.org/builders?mode=json")
    48  	if err != nil {
    49  		log.Fatal(err)
    50  	}
    51  	defer res.Body.Close()
    52  	if res.StatusCode != 200 {
    53  		log.Fatalf("fetching builder types: %s", res.Status)
    54  	}
    55  	if err := json.NewDecoder(res.Body).Decode(&resj); err != nil {
    56  		log.Fatalf("decoding builder types: %v", err)
    57  	}
    58  	for b, bi := range resj.Builders {
    59  		if strings.HasPrefix(b, "misc-compile") {
    60  			continue
    61  		}
    62  		hi, ok := resj.Hosts[bi.HostType]
    63  		if !ok {
    64  			continue
    65  		}
    66  		if !hi.IsReverse && hi.ContainerImage == "" && hi.VMImage == "" {
    67  			continue
    68  		}
    69  		bt = append(bt, builderType{
    70  			Name:      b,
    71  			IsReverse: hi.IsReverse,
    72  			ExpectNum: hi.ExpectNum,
    73  		})
    74  	}
    75  	sort.Slice(bt, func(i, j int) bool {
    76  		return bt[i].Name < bt[j].Name
    77  	})
    78  	return
    79  }
    80  
    81  func swarmingBuilders() ([]string, error) {
    82  	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
    83  	client := gomoteServerClient(ctx)
    84  	resp, err := client.ListSwarmingBuilders(ctx, &protos.ListSwarmingBuildersRequest{})
    85  	if err != nil {
    86  		return nil, fmt.Errorf("unable to retrieve swarming builders: %s", err)
    87  	}
    88  	return resp.Builders, nil
    89  }
    90  
    91  func create(args []string) error {
    92  	fs := flag.NewFlagSet("create", flag.ContinueOnError)
    93  
    94  	fs.Usage = func() {
    95  		fmt.Fprintln(os.Stderr, "create usage: gomote create [create-opts] <type>")
    96  		fmt.Fprintln(os.Stderr)
    97  		fmt.Fprintln(os.Stderr, "If there's a valid group specified, new instances are")
    98  		fmt.Fprintln(os.Stderr, "automatically added to the group. If the group in")
    99  		fmt.Fprintln(os.Stderr, "$GOMOTE_GROUP doesn't exist, and there's no other group")
   100  		fmt.Fprintln(os.Stderr, "specified, it will be created and new instances will be")
   101  		fmt.Fprintln(os.Stderr, "added to that group.")
   102  		fs.PrintDefaults()
   103  		fmt.Fprintln(os.Stderr, "\nValid types:")
   104  		if luciDisabled() {
   105  			for _, bt := range builders() {
   106  				var warn string
   107  				if bt.IsReverse {
   108  					if bt.ExpectNum > 0 {
   109  						warn = fmt.Sprintf("   [limited capacity: %d machines]", bt.ExpectNum)
   110  					} else {
   111  						warn = "   [limited capacity]"
   112  					}
   113  				}
   114  				fmt.Fprintf(os.Stderr, "  * %s%s\n", bt.Name, warn)
   115  			}
   116  			os.Exit(1)
   117  		} else {
   118  			swarmingBuilders, err := swarmingBuilders()
   119  			if err != nil {
   120  				fmt.Fprintf(os.Stderr, " %s\n", err)
   121  			} else {
   122  				for _, builder := range swarmingBuilders {
   123  					fmt.Fprintf(os.Stderr, "  * %s\n", builder)
   124  				}
   125  			}
   126  			os.Exit(1)
   127  		}
   128  	}
   129  	var status bool
   130  	fs.BoolVar(&status, "status", true, "print regular status updates while waiting")
   131  	var count int
   132  	fs.IntVar(&count, "count", 1, "number of instances to create")
   133  	var setup bool
   134  	fs.BoolVar(&setup, "setup", false, "set up the instance by pushing GOROOT and building the Go toolchain")
   135  	var newGroup string
   136  	fs.StringVar(&newGroup, "new-group", "", "also create a new group and add the new instances to it")
   137  	var useGolangbuild bool
   138  	fs.BoolVar(&useGolangbuild, "use-golangbuild", true, "disable the installation of build dependencies installed by golangbuild")
   139  
   140  	fs.Parse(args)
   141  	if fs.NArg() != 1 {
   142  		fs.Usage()
   143  	}
   144  	builderType := fs.Arg(0)
   145  
   146  	var groupMu sync.Mutex
   147  	group := activeGroup
   148  	var err error
   149  	if newGroup != "" {
   150  		group, err = doCreateGroup(newGroup)
   151  		if err != nil {
   152  			return err
   153  		}
   154  	}
   155  	if group == nil && os.Getenv("GOMOTE_GROUP") != "" {
   156  		group, err = doCreateGroup(os.Getenv("GOMOTE_GROUP"))
   157  		if err != nil {
   158  			return err
   159  		}
   160  	}
   161  
   162  	var tmpOutDir string
   163  	var tmpOutDirOnce sync.Once
   164  	eg, ctx := errgroup.WithContext(context.Background())
   165  	client := gomoteServerClient(ctx)
   166  	for i := 0; i < count; i++ {
   167  		i := i
   168  		eg.Go(func() error {
   169  			start := time.Now()
   170  			var exp []string
   171  			if !useGolangbuild {
   172  				exp = append(exp, "disable-golang-build")
   173  			}
   174  			stream, err := client.CreateInstance(ctx, &protos.CreateInstanceRequest{BuilderType: builderType, ExperimentOption: exp})
   175  			if err != nil {
   176  				return fmt.Errorf("failed to create buildlet: %w", err)
   177  			}
   178  			var inst string
   179  		updateLoop:
   180  			for {
   181  				update, err := stream.Recv()
   182  				switch {
   183  				case err == io.EOF:
   184  					break updateLoop
   185  				case err != nil:
   186  					return fmt.Errorf("failed to create buildlet (%d): %w", i+1, err)
   187  				case update.GetStatus() != protos.CreateInstanceResponse_COMPLETE && status:
   188  					fmt.Fprintf(os.Stderr, "# still creating %s (%d) after %v; %d requests ahead of you\n", builderType, i+1, time.Since(start).Round(time.Second), update.GetWaitersAhead())
   189  				case update.GetStatus() == protos.CreateInstanceResponse_COMPLETE:
   190  					inst = update.GetInstance().GetGomoteId()
   191  				}
   192  			}
   193  			fmt.Println(inst)
   194  			if group != nil {
   195  				groupMu.Lock()
   196  				group.Instances = append(group.Instances, inst)
   197  				groupMu.Unlock()
   198  			}
   199  			if !setup {
   200  				return nil
   201  			}
   202  
   203  			// -setup is set, so push GOROOT and run make.bash.
   204  
   205  			tmpOutDirOnce.Do(func() {
   206  				tmpOutDir, err = os.MkdirTemp("", "gomote")
   207  			})
   208  			if err != nil {
   209  				return fmt.Errorf("failed to create a temporary directory for setup output: %w", err)
   210  			}
   211  
   212  			// Push GOROOT.
   213  			detailedProgress := count == 1
   214  			goroot, err := getGOROOT()
   215  			if err != nil {
   216  				return err
   217  			}
   218  			if !detailedProgress {
   219  				fmt.Fprintf(os.Stderr, "# Pushing GOROOT %q to %q...\n", goroot, inst)
   220  			}
   221  			if err := doPush(ctx, inst, goroot, false, detailedProgress); err != nil {
   222  				return err
   223  			}
   224  
   225  			// Run make.bash or make.bat.
   226  			cmd := "go/src/make.bash"
   227  			if strings.Contains(builderType, "windows") {
   228  				cmd = "go/src/make.bat"
   229  			}
   230  
   231  			// Create a file to write output to so it doesn't get lost.
   232  			outf, err := os.Create(filepath.Join(tmpOutDir, fmt.Sprintf("%s.stdout", inst)))
   233  			if err != nil {
   234  				return err
   235  			}
   236  			defer func() {
   237  				outf.Close()
   238  				fmt.Fprintf(os.Stderr, "# Wrote results from %q to %q.\n", inst, outf.Name())
   239  			}()
   240  			fmt.Fprintf(os.Stderr, "# Streaming results from %q to %q...\n", inst, outf.Name())
   241  
   242  			// If this is the only command running, print to stdout too, for convenience and
   243  			// backwards compatibility.
   244  			outputs := []io.Writer{outf}
   245  			if detailedProgress {
   246  				outputs = append(outputs, os.Stdout)
   247  			} else {
   248  				fmt.Fprintf(os.Stderr, "# Running %q on %q...\n", cmd, inst)
   249  			}
   250  			return doRun(ctx, inst, cmd, []string{}, runWriters(outputs...))
   251  		})
   252  	}
   253  	if err := eg.Wait(); err != nil {
   254  		return err
   255  	}
   256  	if group != nil {
   257  		return storeGroup(group)
   258  	}
   259  	return nil
   260  }