blob: ede3526f7430b7fcde0beab67ab9febc9666dec7 [file] [log] [blame]
// Copyright 2015 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.
package main
import (
"archive/tar"
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/build/internal/gomote/protos"
"golang.org/x/build/tarutil"
"golang.org/x/sync/errgroup"
)
// putTar a .tar.gz
func putTar(args []string) error {
fs := flag.NewFlagSet("put", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "puttar usage: gomote puttar [put-opts] [instance] <source>")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "<source> may be one of:")
fmt.Fprintln(os.Stderr, "- A path to a local .tar.gz file.")
fmt.Fprintln(os.Stderr, "- A URL that points at a .tar.gz file.")
fmt.Fprintln(os.Stderr, "- The '-' character to indicate a .tar.gz file passed via stdin.")
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)")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
fs.PrintDefaults()
os.Exit(1)
}
var dir string
fs.StringVar(&dir, "dir", "", "relative directory from buildlet's work dir to extra tarball into")
fs.Parse(args)
// Parse arguments.
var putSet []string
var src string
switch fs.NArg() {
case 1:
// Must be just the source, so we need an active group.
if activeGroup == nil {
fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument")
fs.Usage()
}
for _, inst := range activeGroup.Instances {
putSet = append(putSet, inst)
}
src = fs.Arg(0)
case 2:
// Instance and source is specified.
putSet = []string{fs.Arg(0)}
src = fs.Arg(1)
case 0:
fmt.Fprintln(os.Stderr, "error: not enough arguments")
fs.Usage()
default:
fmt.Fprintln(os.Stderr, "error: too many arguments")
fs.Usage()
}
// Interpret source.
var putTarFn func(ctx context.Context, inst string) error
if src == "-" {
// We might have multiple readers, so slurp up STDIN
// and store it, then hand out bytes.Readers to everyone.
var buf bytes.Buffer
_, err := io.Copy(&buf, os.Stdin)
if err != nil {
return fmt.Errorf("reading stdin: %w", err)
}
sharedTarBuf := buf.Bytes()
putTarFn = func(ctx context.Context, inst string) error {
return doPutTar(ctx, inst, dir, bytes.NewReader(sharedTarBuf))
}
} else {
u, err := url.Parse(src)
if err != nil {
// The URL parser should technically accept any of these, so the fact that
// we failed means its *very* malformed.
return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash")
}
if u.Scheme != "" || u.Host != "" {
// Probably a real URL.
putTarFn = func(ctx context.Context, inst string) error {
return doPutTarURL(ctx, inst, dir, u.String())
}
} else {
// Probably a path. Check if it exists.
_, err := os.Stat(src)
if os.IsNotExist(err) {
// It must be a git hash. Check if this actually matches a git hash.
if len(src) < 7 || len(src) > 40 || regexp.MustCompile("[^a-f0-9]").MatchString(src) {
return fmt.Errorf("malformed source: not a path, a URL, -, or a git hash")
}
putTarFn = func(ctx context.Context, inst string) error {
return doPutTarGoRev(ctx, inst, dir, src)
}
} else if err != nil {
return fmt.Errorf("failed to stat %q: %w", src, err)
} else {
// It's a path.
putTarFn = func(ctx context.Context, inst string) error {
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("opening %q: %w", src, err)
}
defer f.Close()
return doPutTar(ctx, inst, dir, f)
}
}
}
}
eg, ctx := errgroup.WithContext(context.Background())
for _, inst := range putSet {
inst := inst
eg.Go(func() error {
return putTarFn(ctx, inst)
})
}
return eg.Wait()
}
func doPutTarURL(ctx context.Context, name, dir, tarURL string) error {
client := gomoteServerClient(ctx)
_, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
GomoteId: name,
Directory: dir,
Url: tarURL,
})
if err != nil {
return fmt.Errorf("unable to write tar to instance: %w", err)
}
return nil
}
func doPutTarGoRev(ctx context.Context, name, dir, rev string) error {
tarURL := "https://go.googlesource.com/go/+archive/" + rev + ".tar.gz"
if err := doPutTarURL(ctx, name, dir, tarURL); err != nil {
return err
}
// Put a VERSION file there too, to avoid git usage.
version := strings.NewReader("devel " + rev)
var vtar tarutil.FileList
vtar.AddRegular(&tar.Header{
Name: "VERSION",
Mode: 0644,
Size: int64(version.Len()),
}, int64(version.Len()), version)
tgz := vtar.TarGz()
defer tgz.Close()
client := gomoteServerClient(ctx)
resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
if err != nil {
return fmt.Errorf("unable to request credentials for a file upload: %w", err)
}
if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil {
return fmt.Errorf("unable to upload version file to GCS: %w", err)
}
if _, err = client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
GomoteId: name,
Directory: dir,
Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
}); err != nil {
return fmt.Errorf("unable to write tar to instance: %w", err)
}
return nil
}
func doPutTar(ctx context.Context, name, dir string, tgz io.Reader) error {
client := gomoteServerClient(ctx)
resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
if err != nil {
return fmt.Errorf("unable to request credentials for a file upload: %w", err)
}
if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil {
return fmt.Errorf("unable to upload file to GCS: %w", err)
}
if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
GomoteId: name,
Directory: dir,
Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
}); err != nil {
return fmt.Errorf("unable to write tar to instance: %w", err)
}
return nil
}
// putBootstrap places the bootstrap version of go in the workdir
func putBootstrap(args []string) error {
fs := flag.NewFlagSet("putbootstrap", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "putbootstrap usage: gomote putbootstrap [instance]")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
fs.PrintDefaults()
os.Exit(1)
}
fs.Parse(args)
var putSet []string
switch fs.NArg() {
case 0:
if activeGroup == nil {
fmt.Fprintln(os.Stderr, "no active group found; need an active group with only 1 argument")
fs.Usage()
}
for _, inst := range activeGroup.Instances {
putSet = append(putSet, inst)
}
case 1:
putSet = []string{fs.Arg(0)}
default:
fmt.Fprintln(os.Stderr, "error: too many arguments")
fs.Usage()
}
eg, ctx := errgroup.WithContext(context.Background())
for _, inst := range putSet {
inst := inst
eg.Go(func() error {
// TODO(66635) remove once gomotes can no longer be created via the coordinator.
if luciDisabled() {
client := gomoteServerClient(ctx)
resp, err := client.AddBootstrap(ctx, &protos.AddBootstrapRequest{
GomoteId: inst,
})
if err != nil {
return fmt.Errorf("unable to add bootstrap version of Go to instance: %w", err)
}
if resp.GetBootstrapGoUrl() == "" {
fmt.Printf("No GoBootstrapURL defined for %q; ignoring. (may be baked into image)\n", inst)
}
}
return nil
})
}
return eg.Wait()
}
// put single file
func put(args []string) error {
fs := flag.NewFlagSet("put", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "put usage: gomote put [put-opts] [instance] <source or '-' for stdin> [destination]")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Instance name is optional if a group is specified.")
fs.PrintDefaults()
os.Exit(1)
}
modeStr := fs.String("mode", "", "Unix file mode (octal); default to source file mode")
fs.Parse(args)
if fs.NArg() == 0 {
fs.Usage()
}
ctx := context.Background()
var putSet []string
var src, dst string
if err := doPing(ctx, fs.Arg(0)); instanceDoesNotExist(err) {
// When there's no active group, this is just an error.
if activeGroup == nil {
return fmt.Errorf("instance %q: %w", fs.Arg(0), err)
}
// When there is an active group, this just means that we're going
// to use the group instead and assume the rest is a command.
for _, inst := range activeGroup.Instances {
putSet = append(putSet, inst)
}
src = fs.Arg(0)
if fs.NArg() == 2 {
dst = fs.Arg(1)
} else if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "error: too many arguments")
fs.Usage()
}
} else if err == nil {
putSet = append(putSet, fs.Arg(0))
if fs.NArg() == 1 {
fmt.Fprintln(os.Stderr, "error: missing source")
fs.Usage()
}
src = fs.Arg(1)
if fs.NArg() == 3 {
dst = fs.Arg(2)
} else if fs.NArg() != 2 {
fmt.Fprintln(os.Stderr, "error: too many arguments")
fs.Usage()
}
} else {
return fmt.Errorf("checking instance %q: %w", fs.Arg(0), err)
}
if dst == "" {
if src == "-" {
return errors.New("must specify destination file name when source is standard input")
}
dst = filepath.Base(src)
}
var mode os.FileMode = 0666
if *modeStr != "" {
modeInt, err := strconv.ParseInt(*modeStr, 8, 64)
if err != nil {
return err
}
mode = os.FileMode(modeInt)
if !mode.IsRegular() {
return fmt.Errorf("bad mode: %v", mode)
}
}
var putFileFn func(context.Context, string) error
if src == "-" {
var buf bytes.Buffer
_, err := io.Copy(&buf, os.Stdin)
if err != nil {
return fmt.Errorf("reading from stdin: %w", err)
}
sharedFileBuf := buf.Bytes()
putFileFn = func(ctx context.Context, inst string) error {
return doPutFile(ctx, inst, bytes.NewReader(sharedFileBuf), dst, mode)
}
} else {
putFileFn = func(ctx context.Context, inst string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if *modeStr == "" {
fi, err := f.Stat()
if err != nil {
return err
}
mode = fi.Mode()
}
return doPutFile(ctx, inst, f, dst, mode)
}
}
eg, ctx := errgroup.WithContext(ctx)
for _, inst := range putSet {
inst := inst
eg.Go(func() error {
return putFileFn(ctx, inst)
})
}
return eg.Wait()
}
func doPutFile(ctx context.Context, inst string, r io.Reader, dst string, mode os.FileMode) error {
client := gomoteServerClient(ctx)
resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
if err != nil {
return fmt.Errorf("unable to request credentials for a file upload: %w", err)
}
err = uploadToGCS(ctx, resp.GetFields(), r, dst, resp.GetUrl())
if err != nil {
return fmt.Errorf("unable to upload file to GCS: %w", err)
}
_, err = client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{
GomoteId: inst,
Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
Filename: dst,
Mode: uint32(mode),
})
if err != nil {
return fmt.Errorf("unable to write the file from URL: %w", err)
}
return nil
}
func uploadToGCS(ctx context.Context, fields map[string]string, file io.Reader, filename, url string) error {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
for k, v := range fields {
if err := mw.WriteField(k, v); err != nil {
return fmt.Errorf("unable to write field: %w", err)
}
}
_, err := mw.CreateFormFile("file", filename)
if err != nil {
return fmt.Errorf("unable to create form file: %w", err)
}
// Write our own boundary to avoid buffering entire file into the multipart Writer
bound := fmt.Sprintf("\r\n--%s--\r\n", mw.Boundary())
req, err := http.NewRequestWithContext(ctx, "POST", url, io.NopCloser(io.MultiReader(buf, file, strings.NewReader(bound))))
if err != nil {
return fmt.Errorf("unable to create request: %w", err)
}
req.Header.Set("Content-Type", mw.FormDataContentType())
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("http request failed: %w", err)
}
if res.StatusCode != http.StatusNoContent {
return fmt.Errorf("http post failed: status code=%d", res.StatusCode)
}
return nil
}