| // 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 { |
| 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 |
| } |