golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gomote/put.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  	"archive/tar"
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"flag"
    13  	"fmt"
    14  	"io"
    15  	"mime/multipart"
    16  	"net/http"
    17  	"net/url"
    18  	"os"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"golang.org/x/build/internal/gomote/protos"
    25  	"golang.org/x/build/tarutil"
    26  	"golang.org/x/sync/errgroup"
    27  )
    28  
    29  // putTar a .tar.gz
    30  func putTar(args []string) error {
    31  	fs := flag.NewFlagSet("put", flag.ContinueOnError)
    32  	fs.Usage = func() {
    33  		fmt.Fprintln(os.Stderr, "puttar usage: gomote puttar [put-opts] [instance] <source>")
    34  		fmt.Fprintln(os.Stderr)
    35  		fmt.Fprintln(os.Stderr, "<source> may be one of:")
    36  		fmt.Fprintln(os.Stderr, "- A path to a local .tar.gz file.")
    37  		fmt.Fprintln(os.Stderr, "- A URL that points at a .tar.gz file.")
    38  		fmt.Fprintln(os.Stderr, "- The '-' character to indicate a .tar.gz file passed via stdin.")
    39  		fmt.Fprintln(os.Stderr, "- Git hash (min 7 characters) for the Go repository (extract a .tar.gz of the repository at that commit w/o history)")
    40  		fmt.Fprintln(os.Stderr)
    41  		fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
    42  		fs.PrintDefaults()
    43  		os.Exit(1)
    44  	}
    45  	var dir string
    46  	fs.StringVar(&dir, "dir", "", "relative directory from buildlet's work dir to extra tarball into")
    47  
    48  	fs.Parse(args)
    49  
    50  	// Parse arguments.
    51  	var putSet []string
    52  	var src string
    53  	switch fs.NArg() {
    54  	case 1:
    55  		// Must be just the source, so we need an active group.
    56  		if activeGroup == nil {
    57  			fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument")
    58  			fs.Usage()
    59  		}
    60  		for _, inst := range activeGroup.Instances {
    61  			putSet = append(putSet, inst)
    62  		}
    63  		src = fs.Arg(0)
    64  	case 2:
    65  		// Instance and source is specified.
    66  		putSet = []string{fs.Arg(0)}
    67  		src = fs.Arg(1)
    68  	case 0:
    69  		fmt.Fprintln(os.Stderr, "error: not enough arguments")
    70  		fs.Usage()
    71  	default:
    72  		fmt.Fprintln(os.Stderr, "error: too many arguments")
    73  		fs.Usage()
    74  	}
    75  
    76  	// Interpret source.
    77  	var putTarFn func(ctx context.Context, inst string) error
    78  	if src == "-" {
    79  		// We might have multiple readers, so slurp up STDIN
    80  		// and store it, then hand out bytes.Readers to everyone.
    81  		var buf bytes.Buffer
    82  		_, err := io.Copy(&buf, os.Stdin)
    83  		if err != nil {
    84  			return fmt.Errorf("reading stdin: %w", err)
    85  		}
    86  		sharedTarBuf := buf.Bytes()
    87  		putTarFn = func(ctx context.Context, inst string) error {
    88  			return doPutTar(ctx, inst, dir, bytes.NewReader(sharedTarBuf))
    89  		}
    90  	} else {
    91  		u, err := url.Parse(src)
    92  		if err != nil {
    93  			// The URL parser should technically accept any of these, so the fact that
    94  			// we failed means its *very* malformed.
    95  			return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash")
    96  		}
    97  		if u.Scheme != "" || u.Host != "" {
    98  			// Probably a real URL.
    99  			putTarFn = func(ctx context.Context, inst string) error {
   100  				return doPutTarURL(ctx, inst, dir, u.String())
   101  			}
   102  		} else {
   103  			// Probably a path. Check if it exists.
   104  			_, err := os.Stat(src)
   105  			if os.IsNotExist(err) {
   106  				// It must be a git hash. Check if this actually matches a git hash.
   107  				if len(src) < 7 || len(src) > 40 || regexp.MustCompile("[^a-f0-9]").MatchString(src) {
   108  					return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash")
   109  				}
   110  				putTarFn = func(ctx context.Context, inst string) error {
   111  					return doPutTarGoRev(ctx, inst, dir, src)
   112  				}
   113  			} else if err != nil {
   114  				return fmt.Errorf("failed to stat %q: %w", src, err)
   115  			} else {
   116  				// It's a path.
   117  				putTarFn = func(ctx context.Context, inst string) error {
   118  					f, err := os.Open(src)
   119  					if err != nil {
   120  						return fmt.Errorf("opening %q: %w", src, err)
   121  					}
   122  					defer f.Close()
   123  					return doPutTar(ctx, inst, dir, f)
   124  				}
   125  			}
   126  		}
   127  	}
   128  	eg, ctx := errgroup.WithContext(context.Background())
   129  	for _, inst := range putSet {
   130  		inst := inst
   131  		eg.Go(func() error {
   132  			return putTarFn(ctx, inst)
   133  		})
   134  	}
   135  	return eg.Wait()
   136  }
   137  
   138  func doPutTarURL(ctx context.Context, name, dir, tarURL string) error {
   139  	client := gomoteServerClient(ctx)
   140  	_, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
   141  		GomoteId:  name,
   142  		Directory: dir,
   143  		Url:       tarURL,
   144  	})
   145  	if err != nil {
   146  		return fmt.Errorf("unable to write tar to instance: %w", err)
   147  	}
   148  	return nil
   149  }
   150  
   151  func doPutTarGoRev(ctx context.Context, name, dir, rev string) error {
   152  	tarURL := "https://go.googlesource.com/go/+archive/" + rev + ".tar.gz"
   153  	if err := doPutTarURL(ctx, name, dir, tarURL); err != nil {
   154  		return err
   155  	}
   156  
   157  	// Put a VERSION file there too, to avoid git usage.
   158  	version := strings.NewReader("devel " + rev)
   159  	var vtar tarutil.FileList
   160  	vtar.AddRegular(&tar.Header{
   161  		Name: "VERSION",
   162  		Mode: 0644,
   163  		Size: int64(version.Len()),
   164  	}, int64(version.Len()), version)
   165  	tgz := vtar.TarGz()
   166  	defer tgz.Close()
   167  
   168  	client := gomoteServerClient(ctx)
   169  	resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
   170  	if err != nil {
   171  		return fmt.Errorf("unable to request credentials for a file upload: %w", err)
   172  	}
   173  	if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil {
   174  		return fmt.Errorf("unable to upload version file to GCS: %w", err)
   175  	}
   176  	if _, err = client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
   177  		GomoteId:  name,
   178  		Directory: dir,
   179  		Url:       fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
   180  	}); err != nil {
   181  		return fmt.Errorf("unable to write tar to instance: %w", err)
   182  	}
   183  	return nil
   184  }
   185  
   186  func doPutTar(ctx context.Context, name, dir string, tgz io.Reader) error {
   187  	client := gomoteServerClient(ctx)
   188  	resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
   189  	if err != nil {
   190  		return fmt.Errorf("unable to request credentials for a file upload: %w", err)
   191  	}
   192  	if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil {
   193  		return fmt.Errorf("unable to upload file to GCS: %w", err)
   194  	}
   195  	if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
   196  		GomoteId:  name,
   197  		Directory: dir,
   198  		Url:       fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
   199  	}); err != nil {
   200  		return fmt.Errorf("unable to write tar to instance: %w", err)
   201  	}
   202  	return nil
   203  }
   204  
   205  // putBootstrap places the bootstrap version of go in the workdir
   206  func putBootstrap(args []string) error {
   207  	fs := flag.NewFlagSet("putbootstrap", flag.ContinueOnError)
   208  	fs.Usage = func() {
   209  		fmt.Fprintln(os.Stderr, "putbootstrap usage: gomote putbootstrap [instance]")
   210  		fmt.Fprintln(os.Stderr)
   211  		fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
   212  		fs.PrintDefaults()
   213  		os.Exit(1)
   214  	}
   215  	fs.Parse(args)
   216  
   217  	var putSet []string
   218  	switch fs.NArg() {
   219  	case 0:
   220  		if activeGroup == nil {
   221  			fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument")
   222  			fs.Usage()
   223  		}
   224  		for _, inst := range activeGroup.Instances {
   225  			putSet = append(putSet, inst)
   226  		}
   227  	case 1:
   228  		putSet = []string{fs.Arg(0)}
   229  	default:
   230  		fmt.Fprintln(os.Stderr, "error: too many arguments")
   231  		fs.Usage()
   232  	}
   233  
   234  	eg, ctx := errgroup.WithContext(context.Background())
   235  	for _, inst := range putSet {
   236  		inst := inst
   237  		eg.Go(func() error {
   238  			// TODO(66635) remove once gomotes can no longer be created via the coordinator.
   239  			if luciDisabled() {
   240  				client := gomoteServerClient(ctx)
   241  				resp, err := client.AddBootstrap(ctx, &protos.AddBootstrapRequest{
   242  					GomoteId: inst,
   243  				})
   244  				if err != nil {
   245  					return fmt.Errorf("unable to add bootstrap version of Go to instance: %w", err)
   246  				}
   247  				if resp.GetBootstrapGoUrl() == "" {
   248  					fmt.Printf("No GoBootstrapURL defined for %q; ignoring. (may be baked into image)\n", inst)
   249  				}
   250  			}
   251  			return nil
   252  		})
   253  	}
   254  	return eg.Wait()
   255  }
   256  
   257  // put single file
   258  func put(args []string) error {
   259  	fs := flag.NewFlagSet("put", flag.ContinueOnError)
   260  	fs.Usage = func() {
   261  		fmt.Fprintln(os.Stderr, "put usage: gomote put [put-opts] [instance] <source or '-' for stdin> [destination]")
   262  		fmt.Fprintln(os.Stderr)
   263  		fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
   264  		fs.PrintDefaults()
   265  		os.Exit(1)
   266  	}
   267  	modeStr := fs.String("mode", "", "Unix file mode (octal); default to source file mode")
   268  	fs.Parse(args)
   269  
   270  	if fs.NArg() == 0 {
   271  		fs.Usage()
   272  	}
   273  
   274  	ctx := context.Background()
   275  	var putSet []string
   276  	var src, dst string
   277  	if err := doPing(ctx, fs.Arg(0)); instanceDoesNotExist(err) {
   278  		// When there's no active group, this is just an error.
   279  		if activeGroup == nil {
   280  			return fmt.Errorf("instance %q: %w", fs.Arg(0), err)
   281  		}
   282  		// When there is an active group, this just means that we're going
   283  		// to use the group instead and assume the rest is a command.
   284  		for _, inst := range activeGroup.Instances {
   285  			putSet = append(putSet, inst)
   286  		}
   287  		src = fs.Arg(0)
   288  		if fs.NArg() == 2 {
   289  			dst = fs.Arg(1)
   290  		} else if fs.NArg() != 1 {
   291  			fmt.Fprintln(os.Stderr, "error: too many arguments")
   292  			fs.Usage()
   293  		}
   294  	} else if err == nil {
   295  		putSet = append(putSet, fs.Arg(0))
   296  		if fs.NArg() == 1 {
   297  			fmt.Fprintln(os.Stderr, "error: missing source")
   298  			fs.Usage()
   299  		}
   300  		src = fs.Arg(1)
   301  		if fs.NArg() == 3 {
   302  			dst = fs.Arg(2)
   303  		} else if fs.NArg() != 2 {
   304  			fmt.Fprintln(os.Stderr, "error: too many arguments")
   305  			fs.Usage()
   306  		}
   307  	} else {
   308  		return fmt.Errorf("checking instance %q: %w", fs.Arg(0), err)
   309  	}
   310  	if dst == "" {
   311  		if src == "-" {
   312  			return errors.New("must specify destination file name when source is standard input")
   313  		}
   314  		dst = filepath.Base(src)
   315  	}
   316  
   317  	var mode os.FileMode = 0666
   318  	if *modeStr != "" {
   319  		modeInt, err := strconv.ParseInt(*modeStr, 8, 64)
   320  		if err != nil {
   321  			return err
   322  		}
   323  		mode = os.FileMode(modeInt)
   324  		if !mode.IsRegular() {
   325  			return fmt.Errorf("bad mode: %v", mode)
   326  		}
   327  	}
   328  
   329  	var putFileFn func(context.Context, string) error
   330  	if src == "-" {
   331  		var buf bytes.Buffer
   332  		_, err := io.Copy(&buf, os.Stdin)
   333  		if err != nil {
   334  			return fmt.Errorf("reading from stdin: %w", err)
   335  		}
   336  		sharedFileBuf := buf.Bytes()
   337  		putFileFn = func(ctx context.Context, inst string) error {
   338  			return doPutFile(ctx, inst, bytes.NewReader(sharedFileBuf), dst, mode)
   339  		}
   340  	} else {
   341  		putFileFn = func(ctx context.Context, inst string) error {
   342  			f, err := os.Open(src)
   343  			if err != nil {
   344  				return err
   345  			}
   346  			defer f.Close()
   347  
   348  			if *modeStr == "" {
   349  				fi, err := f.Stat()
   350  				if err != nil {
   351  					return err
   352  				}
   353  				mode = fi.Mode()
   354  			}
   355  			return doPutFile(ctx, inst, f, dst, mode)
   356  		}
   357  	}
   358  
   359  	eg, ctx := errgroup.WithContext(ctx)
   360  	for _, inst := range putSet {
   361  		inst := inst
   362  		eg.Go(func() error {
   363  			return putFileFn(ctx, inst)
   364  		})
   365  	}
   366  	return eg.Wait()
   367  }
   368  
   369  func doPutFile(ctx context.Context, inst string, r io.Reader, dst string, mode os.FileMode) error {
   370  	client := gomoteServerClient(ctx)
   371  	resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
   372  	if err != nil {
   373  		return fmt.Errorf("unable to request credentials for a file upload: %w", err)
   374  	}
   375  	err = uploadToGCS(ctx, resp.GetFields(), r, dst, resp.GetUrl())
   376  	if err != nil {
   377  		return fmt.Errorf("unable to upload file to GCS: %w", err)
   378  	}
   379  	_, err = client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{
   380  		GomoteId: inst,
   381  		Url:      fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
   382  		Filename: dst,
   383  		Mode:     uint32(mode),
   384  	})
   385  	if err != nil {
   386  		return fmt.Errorf("unable to write the file from URL: %w", err)
   387  	}
   388  	return nil
   389  }
   390  
   391  func uploadToGCS(ctx context.Context, fields map[string]string, file io.Reader, filename, url string) error {
   392  	buf := new(bytes.Buffer)
   393  	mw := multipart.NewWriter(buf)
   394  
   395  	for k, v := range fields {
   396  		if err := mw.WriteField(k, v); err != nil {
   397  			return fmt.Errorf("unable to write field: %w", err)
   398  		}
   399  	}
   400  	_, err := mw.CreateFormFile("file", filename)
   401  	if err != nil {
   402  		return fmt.Errorf("unable to create form file: %w", err)
   403  	}
   404  	// Write our own boundary to avoid buffering entire file into the multipart Writer
   405  	bound := fmt.Sprintf("\r\n--%s--\r\n", mw.Boundary())
   406  	req, err := http.NewRequestWithContext(ctx, "POST", url, io.NopCloser(io.MultiReader(buf, file, strings.NewReader(bound))))
   407  	if err != nil {
   408  		return fmt.Errorf("unable to create request: %w", err)
   409  	}
   410  	req.Header.Set("Content-Type", mw.FormDataContentType())
   411  	res, err := http.DefaultClient.Do(req)
   412  	if err != nil {
   413  		return fmt.Errorf("http request failed: %w", err)
   414  	}
   415  	if res.StatusCode != http.StatusNoContent {
   416  		return fmt.Errorf("http post failed: status code=%d", res.StatusCode)
   417  	}
   418  	return nil
   419  }