blob: 946bbb71ba8650c98287a7ebf400e2cdb9f5761b [file] [log] [blame]
package impl
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/ipc"
"v.io/v23/naming"
"v.io/v23/security"
"v.io/v23/services/mgmt/application"
"v.io/v23/services/mgmt/binary"
"v.io/v23/services/mgmt/device"
"v.io/v23/services/mgmt/repository"
"v.io/v23/services/security/access"
"v.io/v23/uniqueid"
"v.io/v23/vlog"
pkglib "v.io/core/veyron/services/mgmt/lib/packages"
"v.io/x/lib/cmdline"
)
var cmdInstallLocal = &cmdline.Command{
Run: runInstallLocal,
Name: "install-local",
Short: "Install the given application from the local system.",
Long: "Install the given application, specified using a local path.",
ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]",
ArgsLong: `
<device> is the veyron object name of the device manager's app service.
<title> is the app title.
This is followed by an arbitrary number of environment variable settings, the
local path for the binary to install, and arbitrary flag settings and args.
Optionally, this can be followed by 'PACKAGES' and a list of local files and
directories to be installed as packages for the app`}
func init() {
cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
cmdInstallLocal.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"local file path1\"},\"pkg2\":{\"File\":\"local file path 2\"}}'")
}
type openAuthorizer struct{}
func (openAuthorizer) Authorize(security.Context) error { return nil }
type mapDispatcher map[string]interface{}
func (d mapDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
o, ok := d[suffix]
if !ok {
return nil, nil, fmt.Errorf("suffix %s not found", suffix)
}
// TODO(caprita): Do not open authorizer even for a short-lived server.
return o, &openAuthorizer{}, nil
}
type mapServer struct {
name string
dispatcher mapDispatcher
}
func (ms *mapServer) serve(name string, object interface{}) (string, error) {
if _, ok := ms.dispatcher[name]; ok {
return "", fmt.Errorf("can't have more than one object with name %v", name)
}
ms.dispatcher[name] = object
return naming.Join(ms.name, name), nil
}
func createServer(ctx *context.T, stderr io.Writer) (*mapServer, func(), error) {
server, err := v23.NewServer(ctx)
if err != nil {
return nil, nil, err
}
spec := v23.GetListenSpec(ctx)
endpoints, err := server.Listen(spec)
if err != nil {
return nil, nil, err
}
var name string
if spec.Proxy != "" {
id, err := uniqueid.Random()
if err != nil {
return nil, nil, err
}
name = id.String()
}
dispatcher := make(mapDispatcher)
if err := server.ServeDispatcher(name, dispatcher); err != nil {
return nil, nil, err
}
vlog.VI(1).Infof("Server listening on %v (%v)", endpoints, name)
cleanup := func() {
if err := server.Stop(); err != nil {
fmt.Fprintf(stderr, "server.Stop failed: %v", err)
}
}
if name != "" {
// Send a name rooted in our namespace root rather than the
// relative name (in case the device manager uses a different
// namespace root).
//
// TODO(caprita): Avoid relying on a mounttable altogether, and
// instead pull out the proxied address and just send that.
nsRoots := v23.GetNamespace(ctx).Roots()
if len(nsRoots) > 0 {
name = naming.Join(nsRoots[0], name)
}
} else if len(endpoints) > 0 {
name = endpoints[0].Name()
} else {
return nil, nil, fmt.Errorf("no endpoints")
}
return &mapServer{name: name, dispatcher: dispatcher}, cleanup, nil
}
var errNotImplemented = fmt.Errorf("method not implemented")
type binaryInvoker string
func (binaryInvoker) Create(ipc.ServerContext, int32, repository.MediaInfo) error {
return errNotImplemented
}
func (binaryInvoker) Delete(ipc.ServerContext) error {
return errNotImplemented
}
func (i binaryInvoker) Download(ctx repository.BinaryDownloadContext, _ int32) error {
fileName := string(i)
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
bufferLength := 4096
buffer := make([]byte, bufferLength)
sender := ctx.SendStream()
for {
n, err := file.Read(buffer)
switch err {
case io.EOF:
return nil
case nil:
if err := sender.Send(buffer[:n]); err != nil {
return err
}
default:
return err
}
}
}
func (binaryInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
return "", 0, errNotImplemented
}
func (i binaryInvoker) Stat(ctx ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) {
fileName := string(i)
h := md5.New()
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return []binary.PartInfo{}, repository.MediaInfo{}, err
}
h.Write(bytes)
part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
return []binary.PartInfo{part}, pkglib.MediaInfoForFileName(fileName), nil
}
func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error {
return errNotImplemented
}
func (binaryInvoker) GetACL(ctx ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
return nil, "", errNotImplemented
}
func (binaryInvoker) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
return errNotImplemented
}
type envelopeInvoker application.Envelope
func (i envelopeInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
return application.Envelope(i), nil
}
func (envelopeInvoker) GetACL(ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
return nil, "", errNotImplemented
}
func (envelopeInvoker) SetACL(ipc.ServerContext, access.TaggedACLMap, string) error {
return errNotImplemented
}
func servePackage(p string, ms *mapServer, tmpZipDir string) (string, string, error) {
info, err := os.Stat(p)
if os.IsNotExist(err) {
return "", "", fmt.Errorf("%v not found: %v", p, err)
} else if err != nil {
return "", "", fmt.Errorf("Stat(%v) failed: %v", p, err)
}
pkgName := naming.Join("packages", info.Name())
fileName := p
// Directory packages first get zip'ped.
if info.IsDir() {
fileName = filepath.Join(tmpZipDir, info.Name()+".zip")
if err := pkglib.CreateZip(fileName, p); err != nil {
return "", "", err
}
}
name, err := ms.serve(pkgName, repository.BinaryServer(binaryInvoker(fileName)))
return info.Name(), name, err
}
// runInstallLocal creates a new envelope on the fly from the provided
// arguments, and then points the device manager back to itself for downloading
// the app envelope and binary.
//
// It sets up an app and binary server that only lives for the duration of the
// command, and listens on the profile's listen spec. The caller should set the
// --veyron.proxy if the machine running the command is not accessible from the
// device manager.
//
// TODO(caprita/ashankar): We should use bi-directional streams to get this
// working over the same connection that the command makes to the device
// manager.
func runInstallLocal(cmd *cmdline.Command, args []string) error {
if expectedMin, got := 2, len(args); got < expectedMin {
return cmd.UsageErrorf("install-local: incorrect number of arguments, expected at least %d, got %d", expectedMin, got)
}
deviceName, title := args[0], args[1]
args = args[2:]
envelope := application.Envelope{Title: title}
// Extract the environment settings, binary, and arguments.
firstNonEnv := len(args)
for i, arg := range args {
if strings.Index(arg, "=") <= 0 {
firstNonEnv = i
break
}
}
envelope.Env = args[:firstNonEnv]
args = args[firstNonEnv:]
if len(args) == 0 {
return cmd.UsageErrorf("install-local: missing binary")
}
binary := args[0]
args = args[1:]
firstNonArg, firstPackage := len(args), len(args)
for i, arg := range args {
if arg == "PACKAGES" {
firstNonArg = i
firstPackage = i + 1
break
}
}
envelope.Args = args[:firstNonArg]
pkgs := args[firstPackage:]
if _, err := os.Stat(binary); err != nil {
return fmt.Errorf("binary %v not found: %v", binary, err)
}
server, cancel, err := createServer(gctx, cmd.Stderr())
if err != nil {
return fmt.Errorf("failed to create server: %v", err)
}
defer cancel()
envelope.Binary.File, err = server.serve("binary", repository.BinaryServer(binaryInvoker(binary)))
if err != nil {
return err
}
// For each package dir/file specified in the arguments list, set up an
// object in the binary service to serve that package, and add the
// object name to the envelope's Packages map.
tmpZipDir, err := ioutil.TempDir("", "packages")
if err != nil {
return fmt.Errorf("failed to create a temp dir for zip packages: %v", err)
}
defer os.RemoveAll(tmpZipDir)
for _, p := range pkgs {
if envelope.Packages == nil {
envelope.Packages = make(application.Packages)
}
pname, oname, err := servePackage(p, server, tmpZipDir)
if err != nil {
return err
}
vlog.VI(1).Infof("package %v serving as %v", pname, oname)
envelope.Packages[pname] = application.SignedFile{File: oname}
}
packagesRewritten := application.Packages{}
for pname, pspec := range packagesOverride {
_, oname, err := servePackage(pspec.File, server, tmpZipDir)
if err != nil {
return err
}
vlog.VI(1).Infof("package %v serving as %v", pname, oname)
pspec.File = oname
packagesRewritten[pname] = pspec
}
appName, err := server.serve("application", repository.ApplicationServer(envelopeInvoker(envelope)))
if err != nil {
return err
}
vlog.VI(1).Infof("application serving envelope as %v", appName)
appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride), packagesRewritten)
// Reset the value for any future invocations of "install" or
// "install-local" (we run more than one command per process in unit
// tests).
configOverride = configFlag{}
packagesOverride = packagesFlag{}
if err != nil {
return fmt.Errorf("Install failed: %v", err)
}
fmt.Fprintf(cmd.Stdout(), "Successfully installed: %q\n", naming.Join(deviceName, appID))
return nil
}