cmd/gomote: implements GRPC push command
This change adds the implementation for GRPC push command to the
gomote client.
Updates golang/go#48737
For golang/go#47521
Change-Id: Ibb40dff14b9be0c273fb26a625d5e64b1bca25f0
Reviewed-on: https://go-review.googlesource.com/c/build/+/410819
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Carlos Amedee <carlos@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index 1c8dcf6..65eb3b0 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -155,7 +155,7 @@
registerCommand("ls", "list the contents of a directory on a buildlet", legacyLs)
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("push", "sync your GOROOT directory to the buildlet", legacyPush)
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", legacyPutTar)
@@ -229,6 +229,7 @@
"put": put,
"puttar": putTar,
"putbootstrap": putBootstrap,
+ "push": push,
}
if len(args) == 0 {
usage()
diff --git a/cmd/gomote/push.go b/cmd/gomote/push.go
index 8aa024b..435261a 100644
--- a/cmd/gomote/push.go
+++ b/cmd/gomote/push.go
@@ -23,9 +23,10 @@
"strings"
"golang.org/x/build/buildlet"
+ "golang.org/x/build/internal/gomote/protos"
)
-func push(args []string) error {
+func legacyPush(args []string) error {
fs := flag.NewFlagSet("push", flag.ContinueOnError)
var dryRun bool
fs.BoolVar(&dryRun, "dry-run", false, "print what would be done only")
@@ -292,6 +293,280 @@
return nil
}
+func push(args []string) error {
+ fs := flag.NewFlagSet("push", flag.ContinueOnError)
+ var dryRun bool
+ fs.BoolVar(&dryRun, "dry-run", false, "print what would be done only")
+ fs.Usage = func() {
+ fmt.Fprintln(os.Stderr, "push usage: gomote push <instance>")
+ fs.PrintDefaults()
+ os.Exit(1)
+ }
+ fs.Parse(args)
+
+ goroot := os.Getenv("GOROOT")
+ if goroot == "" {
+ slurp, err := exec.Command("go", "env", "GOROOT").Output()
+ if err != nil {
+ return fmt.Errorf("failed to get GOROOT from go env: %v", err)
+ }
+ goroot = strings.TrimSpace(string(slurp))
+ if goroot == "" {
+ return errors.New("Failed to get $GOROOT from environment or go env")
+ }
+ }
+ goroot = filepath.Clean(goroot)
+
+ if fs.NArg() != 1 {
+ fs.Usage()
+ }
+ name := fs.Arg(0)
+
+ haveGo14 := false
+ remote := map[string]buildlet.DirEntry{} // keys like "src/make.bash"
+
+ ctx := context.Background()
+ client := gomoteServerClient(ctx)
+ resp, err := client.ListDirectory(ctx, &protos.ListDirectoryRequest{
+ GomoteId: name,
+ Directory: ".",
+ Recursive: true,
+ SkipFiles: []string{
+ // Ignore binary output directories:
+ "go/pkg", "go/bin",
+ // We don't care about the digest of
+ // particular source files for Go 1.4. And
+ // exclude /pkg. This leaves go1.4/bin, which
+ // is enough to know whether we have Go 1.4 or
+ // not.
+ "go1.4/src", "go1.4/pkg",
+ },
+ Digest: true,
+ })
+ if err != nil {
+ return fmt.Errorf("error listing buildlet's existing files: %s", statusFromError(err))
+ }
+ for _, entry := range resp.GetEntries() {
+ if strings.HasPrefix(entry, "go1.4/") {
+ haveGo14 = true
+ continue
+ }
+ if strings.HasPrefix(entry, "go/") {
+ remote[name[len("go/"):]] = buildlet.DirEntry{Line: entry}
+ }
+ }
+ if !haveGo14 {
+ log.Printf("installing go1.4")
+ if dryRun {
+ log.Printf("(Dry-run) Would have pushed go1.4")
+ } else {
+ _, err := client.AddBootstrap(ctx, &protos.AddBootstrapRequest{
+ GomoteId: name,
+ })
+ if err != nil {
+ return fmt.Errorf("unable to add bootstrap version of Go to instance: %s", statusFromError(err))
+ }
+ }
+ }
+
+ // Invoke 'git check-ignore' and use it to query whether paths have been gitignored.
+ // If anything goes wrong at any point, fall back to assuming that nothing is gitignored.
+ var isGitIgnored func(string) bool
+ gci := exec.Command("git", "check-ignore", "--stdin", "-n", "-v", "-z")
+ gci.Env = append(os.Environ(), "GIT_FLUSH=1")
+ gciIn, errIn := gci.StdinPipe()
+ defer gciIn.Close() // allow git process to exit
+ gciOut, errOut := gci.StdoutPipe()
+ errStart := gci.Start()
+ if errIn != nil || errOut != nil || errStart != nil {
+ isGitIgnored = func(string) bool { return false }
+ } else {
+ var failed bool
+ br := bufio.NewReader(gciOut)
+ isGitIgnored = func(path string) bool {
+ if failed {
+ return false
+ }
+ fmt.Fprintf(gciIn, "%s\x00", path)
+ // Response is of form "<source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL>"
+ // Read all four and check that the path is correct.
+ // If so, the path is ignored iff the source (reason why ignored) is non-empty.
+ var resp [4][]byte
+ for i := range resp {
+ b, err := br.ReadBytes(0)
+ if err != nil {
+ failed = true
+ return false
+ }
+ resp[i] = b[:len(b)-1] // drop trailing NULL
+ }
+ // Sanity check
+ if string(resp[3]) != path {
+ panic("git check-ignore path did not roundtrip, got " + string(resp[3]) + " sent " + path)
+ }
+ return len(resp[0]) > 0
+ }
+ }
+
+ type fileInfo struct {
+ fi os.FileInfo
+ sha1 string // if regular file
+ }
+ local := map[string]fileInfo{} // keys like "src/make.bash"
+ if err := filepath.Walk(goroot, func(path string, fi os.FileInfo, err error) error {
+ if isEditorBackup(path) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ rel, err := filepath.Rel(goroot, path)
+ if err != nil {
+ return fmt.Errorf("error calculating relative path from %q to %q", goroot, path)
+ }
+ rel = filepath.ToSlash(rel)
+ if rel == "." {
+ return nil
+ }
+ if fi.IsDir() {
+ switch rel {
+ case ".git", "pkg", "bin":
+ return filepath.SkipDir
+ }
+ }
+ inf := fileInfo{fi: fi}
+ if isGitIgnored(path) {
+ if fi.Mode().IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ if fi.Mode().IsRegular() {
+ inf.sha1, err = fileSHA1(path)
+ if err != nil {
+ return err
+ }
+ }
+ local[rel] = inf
+ return nil
+ }); err != nil {
+ return fmt.Errorf("error enumerating local GOROOT files: %v", err)
+ }
+
+ var toDel []string
+ for rel := range remote {
+ if rel == "VERSION" {
+ // Don't delete this. It's harmless, and
+ // necessary. Clients can overwrite it if they
+ // want. But if there's no VERSION file there,
+ // make.bash/bat assumes there's a git repo in
+ // place, but there's not only not a git repo
+ // there with gomote, but there's no git tool
+ // available either.
+ continue
+ }
+ // Also don't delete the auto-generated files from cmd/dist.
+ // Otherwise gomote users can't gomote push + gomote run make.bash
+ // and then iteratively:
+ // -- hack locally
+ // -- gomote push
+ // -- gomote run go test -v ...
+ // Because the go test would fail remotely without
+ // these files if they were deleted by gomote push.
+ if isGoToolDistGenerated(rel) {
+ continue
+ }
+ if isGitIgnored(rel) {
+ // Don't delete remote gitignored files; this breaks built toolchains.
+ continue
+ }
+ rel = strings.TrimRight(rel, "/")
+ if rel == "" {
+ continue
+ }
+ if _, ok := local[rel]; !ok {
+ toDel = append(toDel, rel)
+ }
+ }
+ if len(toDel) > 0 {
+ withGo := make([]string, len(toDel)) // with the "go/" prefix
+ for i, v := range toDel {
+ withGo[i] = "go/" + v
+ }
+ sort.Strings(withGo)
+ if dryRun {
+ log.Printf("(Dry-run) Would have deleted remote files: %q", withGo)
+ } else {
+ log.Printf("Deleting remote files: %q", withGo)
+ if _, err := client.RemoveFiles(ctx, &protos.RemoveFilesRequest{
+ GomoteId: name,
+ Paths: withGo,
+ }); err != nil {
+ return fmt.Errorf("failed to delete remote unwanted files: %s", statusFromError(err))
+ }
+ }
+ }
+ var toSend []string
+ notHave := 0
+ const maxNotHavePrint = 5
+ for rel, inf := range local {
+ if isGoToolDistGenerated(rel) || rel == "VERSION.cache" {
+ continue
+ }
+ if !inf.fi.Mode().IsRegular() {
+ if !inf.fi.IsDir() {
+ log.Printf("Ignoring local non-regular, non-directory file %s: %v", rel, inf.fi.Mode())
+ }
+ continue
+ }
+ rem, ok := remote[rel]
+ if !ok {
+ if notHave++; notHave <= maxNotHavePrint {
+ log.Printf("Remote doesn't have %q", rel)
+ }
+ toSend = append(toSend, rel)
+ continue
+ }
+ if rem.Digest() != inf.sha1 {
+ log.Printf("Remote's %s digest is %q; want %q", rel, rem.Digest(), inf.sha1)
+ toSend = append(toSend, rel)
+ }
+ }
+ if notHave > maxNotHavePrint {
+ log.Printf("Remote doesn't have %d files (only showed %d).", notHave, maxNotHavePrint)
+ }
+ if _, hasVersion := remote["VERSION"]; !hasVersion {
+ log.Printf("Remote lacks a VERSION file; sending a fake one")
+ toSend = append(toSend, "VERSION")
+ }
+ if len(toSend) > 0 {
+ sort.Strings(toSend)
+ tgz, err := generateDeltaTgz(goroot, toSend)
+ if err != nil {
+ return err
+ }
+ log.Printf("Uploading %d new/changed files; %d byte .tar.gz", len(toSend), tgz.Len())
+ if dryRun {
+ log.Printf("(Dry-run mode; not doing anything.")
+ return nil
+ }
+ resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{})
+ if err != nil {
+ return fmt.Errorf("unable to request credentials for a file upload: %s", statusFromError(err))
+ }
+ if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil {
+ return fmt.Errorf("unable to upload file to GCS: %s", err)
+ }
+ if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
+ GomoteId: name,
+ Url: fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()),
+ }); err != nil {
+ return fmt.Errorf("failed writing tarball to buildlet: %s", statusFromError(err))
+ }
+ }
+ return nil
+}
+
func isGoToolDistGenerated(path string) bool {
switch path {
case "src/cmd/cgo/zdefaultcc.go",