veyron/tools/mgmt/device,veyron/services/mgmt/device/impl: config override
for local-install

This changelist enables updates for apps installed with local-install. There are
two changes that make this work:

1. allow install-local to specify a config override (like install), so we can
specify configuration for the app independently of what will be present in the
app envelope downloaded during update. We use a command-line flag for this (and
change the "install" command to also use the flag instead of a positional arg).

2. allow install-local to specify an origin for the app (to override the default
origin which points back to the server run by the command-line tool). We use a
setting in the config to convey this information.
This is a convenient way to use for now to get things working. Long-term, we may
drop this in favor of other mechanisms (e.g. UpdateTo)

Change-Id: I363737c5013f510b3cdf6e3544e96f8d13c6ae2a
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 2d40769..fd16a44 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -411,6 +411,10 @@
 	if _, err := newVersion(call.Context(), installationDir, envelope, ""); err != nil {
 		return "", err
 	}
+	if newOrigin, ok := config[mgmt.AppOriginConfigKey]; ok {
+		delete(config, mgmt.AppOriginConfigKey)
+		applicationVON = newOrigin
+	}
 	if err := saveOrigin(installationDir, applicationVON); err != nil {
 		return "", err
 	}
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index d146f15..ac9ec54 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -30,6 +30,7 @@
 	"v.io/core/veyron2"
 	"v.io/core/veyron2/context"
 	"v.io/core/veyron2/ipc"
+	"v.io/core/veyron2/mgmt"
 	"v.io/core/veyron2/naming"
 	"v.io/core/veyron2/security"
 	"v.io/core/veyron2/services/mgmt/application"
@@ -42,6 +43,7 @@
 	"v.io/core/veyron2/vlog"
 
 	"v.io/core/veyron/lib/expect"
+	"v.io/core/veyron/lib/flags/consts"
 	"v.io/core/veyron/lib/modules"
 	"v.io/core/veyron/lib/signals"
 	"v.io/core/veyron/lib/testutil"
@@ -584,8 +586,17 @@
 	*envelope = envelopeFromShell(sh, []string{testEnvVarName + "=env-val-envelope"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
-	// should override the value specified in the envelope above.
-	appID := installApp(t, ctx, device.Config{testFlagName: "flag-val-install"})
+	// should override the value specified in the envelope above, and the
+	// config-specified value for origin should override the value in the
+	// Install rpc argument.
+	mtName, ok := sh.GetVar(consts.NamespaceRootPrefix)
+	if !ok {
+		t.Fatalf("failed to get namespace root var from shell")
+	}
+	// This rooted name should be equivalent to the relative name "ar", but
+	// we want to test that the config override for origin works.
+	rootedAppRepoName := naming.Join(mtName, "ar")
+	appID := installApp(t, ctx, device.Config{testFlagName: "flag-val-install", mgmt.AppOriginConfigKey: rootedAppRepoName})
 	installationDebug := debug(t, ctx, appID)
 	// We spot-check a couple pieces of information we expect in the debug
 	// output.
@@ -593,7 +604,7 @@
 	// logic that assumes too much about the format?  This may be one
 	// argument in favor of making the output of Debug a struct instead of
 	// free-form string.
-	if !strings.Contains(installationDebug, "Origin: ar") {
+	if !strings.Contains(installationDebug, fmt.Sprintf("Origin: %v", rootedAppRepoName)) {
 		t.Fatalf("debug response doesn't contain expected info: %v", installationDebug)
 	}
 	if !strings.Contains(installationDebug, "Config: map[random_test_flag:flag-val-install]") {
diff --git a/tools/mgmt/device/doc.go b/tools/mgmt/device/doc.go
index b760e71..c8f4780 100644
--- a/tools/mgmt/device/doc.go
+++ b/tools/mgmt/device/doc.go
@@ -75,21 +75,23 @@
 Install the given application.
 
 Usage:
-   device install <device> <application> [<config override>]
+   device install [flags] <device> <application>
 
 <device> is the veyron object name of the device manager's app service.
 
 <application> is the veyron object name of the application.
 
-<config override> is an optional JSON-encoded device.Config object, of the form:
-   '{"flag1":"value1","flag2":"value2"}'.
+The device install flags are:
+ -config={}
+   JSON-encoded device.Config object, of the form:
+   '{"flag1":"value1","flag2":"value2"}'
 
 Device Install-Local
 
 Install the given application, specified using a local path.
 
 Usage:
-   device install-local <device> <title> [ENV=VAL ...] binary [--flag=val ...]
+   device install-local [flags] <device> <title> [ENV=VAL ...] binary [--flag=val ...]
 
 <device> is the veyron object name of the device manager's app service.
 
@@ -98,6 +100,11 @@
 This is followed by an arbitrary number of environment variable settings, the
 local path for the binary to install, and arbitrary flag settings.
 
+The device install-local flags are:
+ -config={}
+   JSON-encoded device.Config object, of the form:
+   '{"flag1":"value1","flag2":"value2"}'
+
 Device Start
 
 Start an instance of the given application.
diff --git a/tools/mgmt/device/impl/impl.go b/tools/mgmt/device/impl/impl.go
index 8762b8a..2600b4e 100644
--- a/tools/mgmt/device/impl/impl.go
+++ b/tools/mgmt/device/impl/impl.go
@@ -17,29 +17,43 @@
 	Name:     "install",
 	Short:    "Install the given application.",
 	Long:     "Install the given application.",
-	ArgsName: "<device> <application> [<config override>]",
+	ArgsName: "<device> <application>",
 	ArgsLong: `
 <device> is the veyron object name of the device manager's app service.
 
 <application> is the veyron object name of the application.
+`,
+}
 
-<config override> is an optional JSON-encoded device.Config object, of the form:
-   '{"flag1":"value1","flag2":"value2"}'.`,
+type configFlag device.Config
+
+func (c *configFlag) String() string {
+	jsonConfig, _ := json.Marshal(c)
+	return string(jsonConfig)
+}
+func (c *configFlag) Set(s string) error {
+	if err := json.Unmarshal([]byte(s), c); err != nil {
+		return fmt.Errorf("Unmarshal(%v) failed: %v", s, err)
+	}
+	return nil
+}
+
+var configOverride configFlag = configFlag{}
+
+func init() {
+	cmdInstall.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
 }
 
 func runInstall(cmd *cmdline.Command, args []string) error {
-	if expectedMin, expectedMax, got := 2, 3, len(args); expectedMin > got || expectedMax < got {
-		return cmd.UsageErrorf("install: incorrect number of arguments, expected between %d and %d, got %d", expectedMin, expectedMax, got)
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("install: incorrect number of arguments, expected %d, got %d", expected, got)
 	}
 	deviceName, appName := args[0], args[1]
-	var cfg device.Config
-	if len(args) > 2 {
-		jsonConfig := args[2]
-		if err := json.Unmarshal([]byte(jsonConfig), &cfg); err != nil {
-			return fmt.Errorf("Unmarshal(%v) failed: %v", jsonConfig, err)
-		}
-	}
-	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, cfg)
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride))
+	// 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{}
 	if err != nil {
 		return fmt.Errorf("Install failed: %v", err)
 	}
diff --git a/tools/mgmt/device/impl/impl_test.go b/tools/mgmt/device/impl/impl_test.go
index 435d185..09e0a83 100644
--- a/tools/mgmt/device/impl/impl_test.go
+++ b/tools/mgmt/device/impl/impl_test.go
@@ -190,35 +190,35 @@
 		expectedTape interface{}
 	}{
 		{
-			[]string{"install", "blech"},
+			[]string{"blech"},
 			nil,
 			true,
 			nil,
 			nil,
 		},
 		{
-			[]string{"install", "blech1", "blech2", "blech3", "blech4"},
+			[]string{"blech1", "blech2", "blech3", "blech4"},
 			nil,
 			true,
 			nil,
 			nil,
 		},
 		{
-			[]string{"install", deviceName, appNameNoFetch, "not-valid-json"},
+			[]string{deviceName, appNameNoFetch, "not-valid-json"},
 			nil,
 			true,
 			nil,
 			nil,
 		},
 		{
-			[]string{"install", deviceName, appNameNoFetch},
+			[]string{deviceName, appNameNoFetch},
 			nil,
 			false,
 			InstallResponse{appId, nil},
 			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, 0},
 		},
 		{
-			[]string{"install", deviceName, appNameNoFetch},
+			[]string{deviceName, appNameNoFetch},
 			cfg,
 			false,
 			InstallResponse{appId, nil},
@@ -231,8 +231,9 @@
 			if err != nil {
 				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err)
 			}
-			c.args = append(c.args, string(jsonConfig))
+			c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...)
 		}
+		c.args = append([]string{"install"}, c.args...)
 		err := cmd.Execute(c.args)
 		if c.shouldErr {
 			if err == nil {
diff --git a/tools/mgmt/device/impl/local_install.go b/tools/mgmt/device/impl/local_install.go
index a8b8fb9..1598559 100644
--- a/tools/mgmt/device/impl/local_install.go
+++ b/tools/mgmt/device/impl/local_install.go
@@ -23,9 +23,6 @@
 	"v.io/lib/cmdline"
 )
 
-// TODO(caprita): Add a way to provide an origin for the app, so we can do
-// updates after it's been installed.
-
 var cmdInstallLocal = &cmdline.Command{
 	Run:      runInstallLocal,
 	Name:     "install-local",
@@ -41,6 +38,10 @@
 local path for the binary to install, and arbitrary flag settings.`,
 }
 
+func init() {
+	cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
+}
+
 type openAuthorizer struct{}
 
 func (openAuthorizer) Authorize(security.Context) error { return nil }
@@ -226,7 +227,11 @@
 
 	objects["application"] = repository.ApplicationServer(envelopeInvoker(envelope))
 	appName := naming.Join(name, "application")
-	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, nil)
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride))
+	// 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{}
 	if err != nil {
 		return fmt.Errorf("Install failed: %v", err)
 	}
diff --git a/tools/mgmt/device/impl/local_install_test.go b/tools/mgmt/device/impl/local_install_test.go
index 7abb65f..9a65931 100644
--- a/tools/mgmt/device/impl/local_install_test.go
+++ b/tools/mgmt/device/impl/local_install_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"os"
 	"reflect"
@@ -10,6 +11,7 @@
 
 	"v.io/core/veyron2/naming"
 	"v.io/core/veyron2/services/mgmt/application"
+	"v.io/core/veyron2/services/mgmt/device"
 
 	"v.io/core/veyron/tools/mgmt/device/impl"
 )
@@ -35,18 +37,19 @@
 		stderrSubstr string
 	}{
 		{
-			[]string{"install-local", deviceName}, "incorrect number of arguments",
+			[]string{deviceName}, "incorrect number of arguments",
 		},
 		{
-			[]string{"install-local", deviceName, appTitle}, "missing binary",
+			[]string{deviceName, appTitle}, "missing binary",
 		},
 		{
-			[]string{"install-local", deviceName, appTitle, "a=b"}, "missing binary",
+			[]string{deviceName, appTitle, "a=b"}, "missing binary",
 		},
 		{
-			[]string{"install-local", deviceName, appTitle, "foo"}, "binary foo not found",
+			[]string{deviceName, appTitle, "foo"}, "binary foo not found",
 		},
 	} {
+		c.args = append([]string{"install-local"}, c.args...)
 		if err := cmd.Execute(c.args); err == nil {
 			t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i)
 		} else {
@@ -69,20 +72,37 @@
 		t.Fatalf("Failed to stat %v: %v", binary, err)
 	}
 	binarySize := fi.Size()
+	cfg := device.Config{"someflag": "somevalue"}
 	for i, c := range []struct {
 		args         []string
+		config       device.Config
 		expectedTape interface{}
 	}{
 		{
-			[]string{"install-local", deviceName, appTitle, binary},
+			[]string{deviceName, appTitle, binary},
+			nil,
 			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch}, binarySize},
 		},
 		{
-			[]string{"install-local", deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
+			[]string{deviceName, appTitle, binary},
+			cfg,
+			InstallStimulus{"Install", appNameAfterFetch, cfg, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch}, binarySize},
+		},
+		{
+			[]string{deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
+			nil,
 			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Env: []string{"ENV1=V1", "ENV2=V2"}, Args: []string{"FLAG1=V1", "FLAG2=V2"}}, binarySize},
 		},
 	} {
 		tape.SetResponses([]interface{}{InstallResponse{appId, nil}})
+		if c.config != nil {
+			jsonConfig, err := json.Marshal(c.config)
+			if err != nil {
+				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err)
+			}
+			c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...)
+		}
+		c.args = append([]string{"install-local"}, c.args...)
 		if err := cmd.Execute(c.args); err != nil {
 			t.Fatalf("test case %d: %v", i, err)
 		}