cmd/mgmt/device/impl: add publish command for uploading to binaryd/applicationd

Publish is meant as a one-stop-shop to take a local binary and create a binary
service entry and an envelope for it.  It can handle several binaries at a time
for 'batch' publishing.

Change-Id: I4a40fccd8a625dd254682d552cb9ae9b5f833d62
diff --git a/cmd/mgmt/device/doc.go b/cmd/mgmt/device/doc.go
index 0ffed76..f31f925 100644
--- a/cmd/mgmt/device/doc.go
+++ b/cmd/mgmt/device/doc.go
@@ -23,6 +23,7 @@
    update        Update the device manager or application
    debug         Debug the device.
    acl           Tool for setting device manager ACLs
+   publish       Publish the given application(s).
    help          Display help for commands or topics
 Run "device help [command]" for command usage.
 
@@ -313,6 +314,30 @@
    Instead of making the ACLs additive, do a complete replacement based on the
    specified settings.
 
+Device Publish
+
+Publishes the given application(s) to the binary and application servers. The
+binaries should be in $VANADIUM_ROOT/release/go/bin/[<GOOS>_<GOARCH>]. The
+binary is published as <binserv>/<binary name>/<GOOS>-<GOARCH>/<TIMESTAMP>. The
+application envelope is published as <appserv>/<binary name>/0. Optionally, adds
+blessing patterns to the Read and Resolve ACLs.
+
+Usage:
+   device publish [flags] <binary name> ...
+
+The device publish flags are:
+ -appserv=applicationd
+   Name of application service.
+ -binserv=binaryd
+   Name of binary service.
+ -goarch=amd64
+   GOARCH for application.
+ -goos=linux
+   GOOS for application.
+ -readers=dev.v.io
+   If non-empty, comma-separated blessing patterns to add to Read and Resolve
+   ACL.
+
 Device Help
 
 Help with no args displays the usage of the parent command.
diff --git a/cmd/mgmt/device/impl/publish.go b/cmd/mgmt/device/impl/publish.go
new file mode 100644
index 0000000..35621e4
--- /dev/null
+++ b/cmd/mgmt/device/impl/publish.go
@@ -0,0 +1,161 @@
+package impl
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/security/access"
+	"v.io/v23/services/security/access/object"
+	"v.io/v23/verror"
+
+	"v.io/x/lib/cmdline"
+	appdimpl "v.io/x/ref/services/mgmt/application/impl"
+	"v.io/x/ref/services/mgmt/lib/binary"
+	irepos "v.io/x/ref/services/mgmt/repository"
+)
+
+// TODO(caprita): Add unit test.
+
+// TODO(caprita): Extend to include env, args, packages.
+
+var cmdPublish = &cmdline.Command{
+	Run:   runPublish,
+	Name:  "publish",
+	Short: "Publish the given application(s).",
+	Long: `
+Publishes the given application(s) to the binary and application servers.
+The binaries should be in $VANADIUM_ROOT/release/go/bin/[<GOOS>_<GOARCH>].
+The binary is published as <binserv>/<binary name>/<GOOS>-<GOARCH>/<TIMESTAMP>.
+The application envelope is published as <appserv>/<binary name>/0.
+Optionally, adds blessing patterns to the Read and Resolve ACLs.`,
+	ArgsName: "<binary name> ...",
+}
+
+var binaryService, applicationService, goos, goarch, readBlessings string
+
+func init() {
+	cmdPublish.Flags.StringVar(&binaryService, "binserv", "binaryd", "Name of binary service.")
+	cmdPublish.Flags.StringVar(&applicationService, "appserv", "applicationd", "Name of application service.")
+	cmdPublish.Flags.StringVar(&goos, "goos", runtime.GOOS, "GOOS for application.")
+	cmdPublish.Flags.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH for application.")
+	cmdPublish.Flags.StringVar(&readBlessings, "readers", "dev.v.io", "If non-empty, comma-separated blessing patterns to add to Read and Resolve ACL.")
+}
+
+func setACLs(cmd *cmdline.Command, von string) error {
+	if readBlessings == "" {
+		return nil
+	}
+	acl, etag, err := object.ObjectClient(von).GetACL(gctx)
+	if err != nil {
+		// TODO(caprita): This is a workaround until we sort out the
+		// default ACLs for applicationd (see issue #1317).  At that
+		// time, uncomment the line below.
+		//
+		//   return err
+		acl = make(access.TaggedACLMap)
+	}
+	for _, blessing := range strings.Split(readBlessings, ",") {
+		for _, tag := range []access.Tag{access.Read, access.Resolve} {
+			acl.Add(security.BlessingPattern(blessing), string(tag))
+		}
+	}
+	if err := object.ObjectClient(von).SetACL(gctx, acl, etag); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Added patterns %q to Read,Resolve ACL for %q\n", readBlessings, von)
+	return nil
+}
+
+func publishOne(cmd *cmdline.Command, binPath, binaryName string) error {
+	// Step 1, upload the binary to the binary service.
+
+	// TODO(caprita): Instead of the current timestamp, use each binary's
+	// BuildTimestamp from the buildinfo.
+	timestamp := time.Now().UTC().Format(time.RFC3339)
+	binaryVON := naming.Join(binaryService, binaryName, fmt.Sprintf("%s-%s", goos, goarch), timestamp)
+	binaryFile := filepath.Join(binPath, binaryName)
+	// TODO(caprita): Take signature of binary and put it in the envelope.
+	if _, err := binary.UploadFromFile(gctx, binaryVON, binaryFile); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Binary %q uploaded from file %s\n", binaryVON, binaryFile)
+
+	// Step 2, set the acls for the uploaded binary.
+
+	if err := setACLs(cmd, binaryVON); err != nil {
+		return err
+	}
+
+	// Step 3, download existing envelope (or create a new one), update, and
+	// upload to application service.
+
+	// TODO(caprita): use the profile detection machinery and/or let user
+	// specify profiles by hand.
+	profiles := []string{fmt.Sprintf("%s-%s", goos, goarch)}
+	// TODO(caprita): use a label e.g. "prod" instead of "0".
+	appVON := naming.Join(applicationService, binaryName, "0")
+	appClient := irepos.ApplicationClient(appVON)
+	// NOTE: If profiles contains more than one entry, this will return only
+	// the first match.  But presumably that's ok, since we're going to set
+	// the envelopes for all the profiles to the same envelope anyway below.
+	envelope, err := appClient.Match(gctx, profiles)
+	if verror.Is(err, appdimpl.ErrNotFound.ID) {
+		// There was nothing published yet, create a new envelope.
+		envelope = application.Envelope{Title: binaryName}
+	} else if err != nil {
+		return err
+	}
+	envelope.Binary.File = binaryVON
+	if err := irepos.ApplicationClient(appVON).Put(gctx, profiles, envelope); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Published %q\n", appVON)
+
+	// Step 4, set the acls for the uploaded envelope.
+
+	if err := setACLs(cmd, appVON); err != nil {
+		return err
+	}
+	return nil
+}
+
+func runPublish(cmd *cmdline.Command, args []string) error {
+	if expectedMin, got := 1, len(args); got < expectedMin {
+		return cmd.UsageErrorf("publish: incorrect number of arguments, expected at least %d, got %d", expectedMin, got)
+	}
+	binaries := args
+	vroot := os.Getenv("VANADIUM_ROOT")
+	if vroot == "" {
+		return cmd.UsageErrorf("publish: $VANADIUM_ROOT environment variable should be set")
+	}
+	binPath := filepath.Join(vroot, "release/go/bin")
+	if goos != runtime.GOOS || goarch != runtime.GOARCH {
+		binPath = filepath.Join(binPath, fmt.Sprintf("%s_%s", goos, goarch))
+	}
+	if fi, err := os.Stat(binPath); err != nil {
+		return cmd.UsageErrorf("publish: failed to stat %v: %v", binPath, err)
+	} else if !fi.IsDir() {
+		return cmd.UsageErrorf("publish: %v is not a directory", binPath)
+	}
+	if binaryService == "" {
+		return cmd.UsageErrorf("publish: --binserv must point to a binary service name")
+	}
+	if applicationService == "" {
+		return cmd.UsageErrorf("publish: --appserv must point to an application service name")
+	}
+	var lastErr error
+	for _, b := range binaries {
+		if err := publishOne(cmd, binPath, b); err != nil {
+			fmt.Fprintf(cmd.Stderr(), "Failed to publish %q: %v\n", b, err)
+			lastErr = err
+		}
+	}
+	return lastErr
+}
diff --git a/cmd/mgmt/device/impl/root.go b/cmd/mgmt/device/impl/root.go
index a05baaa..e555286 100644
--- a/cmd/mgmt/device/impl/root.go
+++ b/cmd/mgmt/device/impl/root.go
@@ -19,6 +19,6 @@
 		Long: `
 The device tool facilitates interaction with the veyron device manager.
 `,
-		Children: []*cmdline.Command{cmdInstall, cmdInstallLocal, cmdUninstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdRevert, cmdUpdate, cmdDebug, aclRoot()},
+		Children: []*cmdline.Command{cmdInstall, cmdInstallLocal, cmdUninstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdRevert, cmdUpdate, cmdDebug, aclRoot(), cmdPublish},
 	}
 }