cmd/gomote: implements GRPC put command
This change adds the implementation for GRPC put command to the gomote client.
Updates golang/go#48737
For golang/go#47521
Change-Id: Ib2376444321ef9d0a754b60bcd3783f66a932f3d
Reviewed-on: https://go-review.googlesource.com/c/build/+/406015
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index 1651e1e..0553db1 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -156,7 +156,7 @@
registerCommand("list", "list active buildlets", legacyList)
registerCommand("ping", "test whether a buildlet is alive and reachable ", ping)
registerCommand("push", "sync your GOROOT directory to the buildlet", push)
- registerCommand("put", "put files on a buildlet", put)
+ registerCommand("put", "put files on a buildlet", legacyPut)
registerCommand("put14", "put Go 1.4 in place", put14)
registerCommand("puttar", "extract a tar.gz to a buildlet", putTar)
registerCommand("rdp", "RDP (Remote Desktop Protocol) to a Windows buildlet", rdp)
@@ -225,6 +225,7 @@
"ssh": ssh,
"rm": rm,
"gettar": getTar,
+ "put": put,
}
if len(args) == 0 {
usage()
diff --git a/cmd/gomote/put.go b/cmd/gomote/put.go
index 39bcece..3315e6f 100644
--- a/cmd/gomote/put.go
+++ b/cmd/gomote/put.go
@@ -6,16 +6,20 @@
import (
"archive/tar"
+ "bytes"
"context"
"errors"
"flag"
"fmt"
"io"
+ "mime/multipart"
+ "net/http"
"os"
"path/filepath"
"strconv"
"strings"
+ "golang.org/x/build/internal/gomote/protos"
"golang.org/x/build/tarutil"
)
@@ -115,8 +119,8 @@
return bc.PutTarFromURL(ctx, u, "go1.4")
}
-// put single file
-func put(args []string) error {
+// legacyPut single file
+func legacyPut(args []string) error {
fs := flag.NewFlagSet("put", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "put usage: gomote put [put-opts] <buildlet-name> <source or '-' for stdin> [destination]")
@@ -176,3 +180,107 @@
ctx := context.Background()
return bc.Put(ctx, r, dest, mode)
}
+
+// 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] <buildlet-name> <source or '-' for stdin> [destination]")
+ fs.PrintDefaults()
+ os.Exit(1)
+ }
+ modeStr := fs.String("mode", "", "Unix file mode (octal); default to source file mode")
+ fs.Parse(args)
+ if n := fs.NArg(); n < 2 || n > 3 {
+ fs.Usage()
+ }
+
+ var r io.Reader = os.Stdin
+ var mode os.FileMode = 0666
+
+ src := fs.Arg(1)
+ if src != "-" {
+ f, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ r = f
+
+ if *modeStr == "" {
+ fi, err := f.Stat()
+ if err != nil {
+ return err
+ }
+ mode = fi.Mode()
+ }
+ }
+ 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)
+ }
+ }
+ dest := fs.Arg(2)
+ if dest == "" {
+ if src == "-" {
+ return errors.New("must specify destination file name when source is standard input")
+ }
+ dest = filepath.Base(src)
+ }
+ ctx := context.Background()
+ client := gomoteServerClient(ctx)
+ resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
+ if err != nil {
+ return fmt.Errorf("unable to request credentials for a file upload: %s", statusFromError(err))
+ }
+ err = uploadToGCS(ctx, resp.GetFields(), r, dest, resp.GetUrl())
+ if err != nil {
+ return fmt.Errorf("unable to upload file to GCS: %s", err)
+ }
+ name := fs.Arg(0)
+ _, err = client.WriteFileFromURL(ctx, &protos.WriteFileFromURLRequest{
+ GomoteId: name,
+ Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
+ Filename: dest,
+ Mode: uint32(mode),
+ })
+ if err != nil {
+ return fmt.Errorf("unable to write the file from URL: %s", statusFromError(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: %s", err)
+ }
+ }
+ _, err := mw.CreateFormFile("file", filename)
+ if err != nil {
+ return fmt.Errorf("unable to create form file: %s", 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: %s", err)
+ }
+ req.Header.Set("Content-Type", mw.FormDataContentType())
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("http request failed: %s", err)
+ }
+ if res.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("http post failed: status code=%d", res.StatusCode)
+ }
+ return nil
+}