veyron/services/mgmt/device/impl: Config override for Install

This change builds on the mechanism introduced in go/vcl/1382 to allow the
application Install'er to override flag settings in the app being installed. The
config is provided as an argument to Install, and persisted with the
installation. We may consider adding the same support to Start as well in the
future.

This change only makes the server-side changes. The corresponding cmd-line tool
changes are left to a future cl.

On the testing side, pack more info in the ping reply to obtain the child app
values for a test flag and test env (the test flag is set and then overriden by
the Install config). While at it, we also get rid of tryInstall in favor of
installAppExpectError (which follows the convention for other rpcs in the impl
test).

Change-Id: Ibeaac48ab82a50ab053fe5d53fc5fb902cfeef71
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 1591a29..5ccb84e 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -12,11 +12,12 @@
 //       acls/
 //         data                     - the ACL data for this
 //                                    installation. Controls acces to
-//                                    Start, Uinstall, Update, UpdateTo
+//                                    Start, Uninstall, Update, UpdateTo
 //                                    and Revert.
-//         signature                -  the  signature for the ACLs in data
+//         signature                - the signature for the ACLs in data
 //       <status>                   - one of the values for installationState enum
 //       origin                     - object name for application envelope
+//       config                     - Config provided by the installer
 //       <version 1 timestamp>/     - timestamp of when the version was downloaded
 //         bin                      - application binary
 //         previous                 - symbolic link to previous version directory
@@ -39,6 +40,7 @@
 //           logs/                  - stderr/stdout and log files generated by instance
 //           info                   - metadata for the instance (such as app
 //                                    cycle manager name and process id)
+//           installation           - symbolic link to installation for the instance
 //           version                - symbolic link to installation version for the instance
 //           acls/
 //             data                 - the ACLs for this instance. These
@@ -134,6 +136,7 @@
 	"v.io/core/veyron2/security"
 	"v.io/core/veyron2/services/mgmt/appcycle"
 	"v.io/core/veyron2/services/mgmt/application"
+	"v.io/core/veyron2/services/mgmt/device"
 	"v.io/core/veyron2/services/security/access"
 	"v.io/core/veyron2/verror2"
 	"v.io/core/veyron2/vlog"
@@ -238,6 +241,33 @@
 	return envelope, nil
 }
 
+func saveConfig(dir string, config device.Config) error {
+	jsonConfig, err := json.Marshal(config)
+	if err != nil {
+		vlog.Errorf("Marshal(%v) failed: %v", config, err)
+		return verror2.Make(ErrOperationFailed, nil)
+	}
+	path := filepath.Join(dir, "config")
+	if err := ioutil.WriteFile(path, jsonConfig, 0600); err != nil {
+		vlog.Errorf("WriteFile(%v) failed: %v", path, err)
+		return verror2.Make(ErrOperationFailed, nil)
+	}
+	return nil
+}
+
+func loadConfig(dir string) (device.Config, error) {
+	path := filepath.Join(dir, "config")
+	var config device.Config
+	if configBytes, err := ioutil.ReadFile(path); err != nil {
+		vlog.Errorf("ReadFile(%v) failed: %v", path, err)
+		return nil, verror2.Make(ErrOperationFailed, nil)
+	} else if err := json.Unmarshal(configBytes, &config); err != nil {
+		vlog.Errorf("Unmarshal(%v) failed: %v", configBytes, err)
+		return nil, verror2.Make(ErrOperationFailed, nil)
+	}
+	return config, nil
+}
+
 func saveOrigin(dir, originVON string) error {
 	path := filepath.Join(dir, "origin")
 	if err := ioutil.WriteFile(path, []byte(originVON), 0600); err != nil {
@@ -377,7 +407,7 @@
 	return writeACLs(principal, aclData, aclSig, aclDir, acl)
 }
 
-func (i *appService) Install(call ipc.ServerContext, applicationVON string) (string, error) {
+func (i *appService) Install(call ipc.ServerContext, applicationVON string, config device.Config) (string, error) {
 	if len(i.suffix) > 0 {
 		return "", verror2.Make(ErrInvalidSuffix, call.Context())
 	}
@@ -403,6 +433,9 @@
 	if err := saveOrigin(installationDir, applicationVON); err != nil {
 		return "", err
 	}
+	if err := saveConfig(installationDir, config); err != nil {
+		return "", err
+	}
 	if err := initializeInstallation(installationDir, active); err != nil {
 		return "", err
 	}
@@ -622,6 +655,11 @@
 	if mkdir(instanceDir) != nil {
 		return "", instanceID, verror2.Make(ErrOperationFailed, call.Context())
 	}
+	installationLink := filepath.Join(instanceDir, "installation")
+	if err := os.Symlink(installationDir, installationLink); err != nil {
+		vlog.Errorf("Symlink(%v, %v) failed: %v", installationDir, installationLink, err)
+		return instanceDir, instanceID, verror2.Make(ErrOperationFailed, call.Context())
+	}
 	currLink := filepath.Join(installationDir, "current")
 	versionDir, err := filepath.EvalSymlinks(currLink)
 	if err != nil {
@@ -755,8 +793,10 @@
 	cmd.Args = append(cmd.Args, "--")
 
 	// Args to be passed by helper to the app.
-	cmd.Args = append(cmd.Args, "--log_dir=../logs")
-	cmd.Args = append(cmd.Args, envelope.Args...)
+	appArgs := []string{"--log_dir=../logs"}
+	appArgs = append(appArgs, envelope.Args...)
+
+	cmd.Args = append(cmd.Args, appArgs...)
 	return cmd, nil
 }
 
@@ -770,6 +810,19 @@
 	listener := callbackState.listenFor(mgmt.AppCycleManagerConfigKey)
 	defer listener.cleanup()
 	cfg := vexec.NewConfig()
+	installationLink := filepath.Join(instanceDir, "installation")
+	installationDir, err := filepath.EvalSymlinks(installationLink)
+	if err != nil {
+		vlog.Errorf("EvalSymlinks(%v) failed: %v", installationLink, err)
+		return verror2.Make(ErrOperationFailed, nil)
+	}
+	config, err := loadConfig(installationDir)
+	if err != nil {
+		return err
+	}
+	for k, v := range config {
+		cfg.Set(k, v)
+	}
 	cfg.Set(mgmt.ParentNameConfigKey, listener.name())
 	cfg.Set(mgmt.ProtocolConfigKey, "tcp")
 	cfg.Set(mgmt.AddressConfigKey, "127.0.0.1:0")
diff --git a/services/mgmt/device/impl/device_service.go b/services/mgmt/device/impl/device_service.go
index 02ad0b2..02d1113 100644
--- a/services/mgmt/device/impl/device_service.go
+++ b/services/mgmt/device/impl/device_service.go
@@ -406,7 +406,7 @@
 	return nil
 }
 
-func (*deviceService) Install(ctx ipc.ServerContext, _ string) (string, error) {
+func (*deviceService) Install(ctx ipc.ServerContext, _ string, _ device.Config) (string, error) {
 	return "", verror2.Make(ErrInvalidSuffix, ctx.Context())
 }
 
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index 4d3904c..abe03eb 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -58,13 +58,20 @@
 )
 
 const (
+	// Modules command names.
 	execScriptCmd    = "execScriptCmd"
 	deviceManagerCmd = "deviceManager"
 	appCmd           = "app"
 	installerCmd     = "installer"
 	uninstallerCmd   = "uninstaller"
+
+	testFlagName = "random_test_flag"
+	// VEYRON prefix is necessary to pass the env filtering.
+	testEnvVarName = "VEYRON_RANDOM_ENV_VALUE"
 )
 
+var flagValue = flag.String(testFlagName, "default", "")
+
 func init() {
 	// The installer sets this flag on the installed device manager, so we
 	// need to ensure it's defined.
@@ -222,9 +229,18 @@
 	return string(bytes), nil
 }
 
+type pingArgs struct {
+	HelperEnv, FlagValue, EnvValue string
+}
+
 func ping() {
+	args := &pingArgs{
+		HelperEnv: os.Getenv(suidhelper.SavedArgs),
+		FlagValue: *flagValue,
+		EnvValue:  os.Getenv(testEnvVarName),
+	}
 	client := veyron2.GetClient(globalCtx)
-	if call, err := client.StartCall(globalCtx, "pingserver", "Ping", []interface{}{os.Getenv(suidhelper.SavedArgs)}); err != nil {
+	if call, err := client.StartCall(globalCtx, "pingserver", "Ping", []interface{}{args}); err != nil {
 		vlog.Fatalf("StartCall failed: %v", err)
 	} else if err := call.Finish(); err != nil {
 		vlog.Fatalf("Finish failed: %v", err)
@@ -467,21 +483,21 @@
 	dms.ExpectEOF()
 }
 
-type pingServer chan<- string
+type pingServer chan<- pingArgs
 
 // TODO(caprita): Set the timeout in a more principled manner.
 const pingTimeout = 60 * time.Second
 
-func (p pingServer) Ping(_ ipc.ServerContext, arg string) {
+func (p pingServer) Ping(_ ipc.ServerContext, arg pingArgs) {
 	p <- arg
 }
 
 // setupPingServer creates a server listening for a ping from a child app; it
 // returns a channel on which the app's ping message is returned, and a cleanup
 // function.
-func setupPingServer(t *testing.T) (<-chan string, func()) {
+func setupPingServer(t *testing.T) (<-chan pingArgs, func()) {
 	server, _ := mgmttest.NewServer(globalCtx)
-	pingCh := make(chan string, 1)
+	pingCh := make(chan pingArgs, 1)
 	if err := server.Serve("pingserver", pingServer(pingCh), &openAuthorizer{}); err != nil {
 		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
 	}
@@ -519,22 +535,28 @@
 }
 
 // TODO(rjkroege): Consider validating additional parameters.
-func verifyHelperArgs(t *testing.T, pingCh <-chan string, username string) {
-	var env string
+func verifyPingArgs(t *testing.T, pingCh <-chan pingArgs, username, flagValue, envValue string) {
+	var args pingArgs
 	select {
-	case env = <-pingCh:
+	case args = <-pingCh:
 	case <-time.After(pingTimeout):
-		t.Fatalf(testutil.FormatLogLine(2, "%s: failed to get ping"))
+		t.Fatalf(testutil.FormatLogLine(2, "failed to get ping"))
 	}
-	d := json.NewDecoder(strings.NewReader(env))
+	d := json.NewDecoder(strings.NewReader(args.HelperEnv))
 	var savedArgs suidhelper.ArgsSavedForTest
 
 	if err := d.Decode(&savedArgs); err != nil {
-		t.Fatalf("failed to decode preserved argument %v: %v", env, err)
+		t.Fatalf("failed to decode preserved argument %v: %v", args.HelperEnv, err)
 	}
 
-	if savedArgs.Uname != username {
-		t.Fatalf("got username %v, expected username %v", savedArgs.Uname, username)
+	if got, want := savedArgs.Uname, username; got != want {
+		t.Fatalf(testutil.FormatLogLine(2, "got username %q, expected %q", got, want))
+	}
+	if got, want := args.FlagValue, flagValue; got != want {
+		t.Fatalf(testutil.FormatLogLine(2, "got flag value %q, expected %q", got, want))
+	}
+	if got, want := args.EnvValue, envValue; got != want {
+		t.Fatalf(testutil.FormatLogLine(2, "got env value %q, expected %q", got, want))
 	}
 }
 
@@ -569,10 +591,11 @@
 	resolve(t, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps", "appV1")
+	*envelope = envelopeFromShell(sh, []string{testEnvVarName + "=env-val-envelope"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
 
-	// Install the app.
-	appID := installApp(t, globalCtx)
+	// Install the app.  The config-specified flag value for testFlagName
+	// should override the value specified in the envelope above.
+	appID := installApp(t, globalCtx, device.Config{testFlagName: "flag-val-install"})
 
 	// Start requires the caller to grant a blessing for the app instance.
 	if _, err := startAppImpl(t, globalCtx, appID, ""); err == nil || !verror.Is(err, impl.ErrInvalidBlessing.ID) {
@@ -583,7 +606,7 @@
 	instance1ID := startApp(t, globalCtx, appID)
 
 	// Wait until the app pings us that it's ready.
-	verifyHelperArgs(t, pingCh, userName(t))
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
 
 	v1EP1 := resolve(t, "appV1", 1)[0]
 
@@ -592,7 +615,7 @@
 	resolveExpectNotFound(t, "appV1")
 
 	resumeApp(t, globalCtx, appID, instance1ID)
-	verifyHelperArgs(t, pingCh, userName(t)) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	oldV1EP1 := v1EP1
 	if v1EP1 = resolve(t, "appV1", 1)[0]; v1EP1 == oldV1EP1 {
 		t.Fatalf("Expected a new endpoint for the app after suspend/resume")
@@ -600,7 +623,7 @@
 
 	// Start a second instance.
 	instance2ID := startApp(t, globalCtx, appID)
-	verifyHelperArgs(t, pingCh, userName(t)) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 
 	// There should be two endpoints mounted as "appV1", one for each
 	// instance of the app.
@@ -635,7 +658,7 @@
 	updateAppExpectError(t, appID, impl.ErrAppTitleMismatch.ID)
 
 	// Create a second version of the app and update the app to it.
-	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps", "appV2")
+	*envelope = envelopeFromShell(sh, []string{testEnvVarName + "=env-val-envelope"}, appCmd, "google naps", "appV2")
 
 	updateApp(t, globalCtx, appID)
 
@@ -646,7 +669,7 @@
 
 	// Resume first instance.
 	resumeApp(t, globalCtx, appID, instance1ID)
-	verifyHelperArgs(t, pingCh, userName(t)) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	// Both instances should still be running the first version of the app.
 	// Check that the mounttable contains two endpoints, one of which is
 	// v1EP2.
@@ -671,7 +694,7 @@
 	// Start a third instance.
 	instance3ID := startApp(t, globalCtx, appID)
 	// Wait until the app pings us that it's ready.
-	verifyHelperArgs(t, pingCh, userName(t))
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
 
 	resolve(t, "appV2", 1)
 
@@ -688,7 +711,7 @@
 
 	// Start a fourth instance.  It should be started from version 1.
 	instance4ID := startApp(t, globalCtx, appID)
-	verifyHelperArgs(t, pingCh, userName(t)) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready.
 	resolve(t, "appV1", 1)
 	stopApp(t, globalCtx, appID, instance4ID)
 	resolveExpectNotFound(t, "appV1")
@@ -714,15 +737,6 @@
 	dms.ExpectEOF()
 }
 
-func tryInstall(ctx *context.T) error {
-	appsName := "dm//apps"
-	stub := device.ApplicationClient(appsName)
-	if _, err := stub.Install(ctx, mockApplicationRepoName); err != nil {
-		return fmt.Errorf("Install failed: %v", err)
-	}
-	return nil
-}
-
 func startRealBinaryRepository(t *testing.T) func() {
 	rootDir, err := binaryimpl.SetupRootDir("")
 	if err != nil {
@@ -793,10 +807,8 @@
 	defer otherCancel()
 
 	// Devicemanager should have open ACLs before we claim it and so an
-	// Install from otherRT should succeed.
-	if err := tryInstall(octx); err != nil {
-		t.Errorf("Failed to install: %s", err)
-	}
+	// Install from octx should succeed.
+	installApp(t, octx)
 	// Claim the devicemanager with claimantRT as <defaultblessing>/mydevice
 	if err := deviceStub.Claim(claimantCtx, &granter{p: veyron2.GetPrincipal(claimantCtx), extension: "mydevice"}); err != nil {
 		t.Fatal(err)
@@ -806,11 +818,9 @@
 	// the devicemanager.
 	appID := installApp(t, claimantCtx)
 
-	// otherRT should be unable to install though, since the ACLs have
+	// octx should be unable to install though, since the ACLs have
 	// changed now.
-	if err := tryInstall(octx); err == nil {
-		t.Fatalf("Install should have failed from otherRT")
-	}
+	installAppExpectError(t, octx, verror.NoAccess.ID)
 
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t)
@@ -850,7 +860,7 @@
 		octx, ocancel = mgmttest.NewRuntime(t, globalCtx)
 	)
 	defer ocancel()
-	// By default, selfRT and otherRT will have blessings generated based on
+	// By default, selfCtx and octx will have blessings generated based on
 	// the username/machine name running this process. Since these blessings
 	// will appear in ACLs, give them recognizable names.
 	if err := idp.Bless(veyron2.GetPrincipal(selfCtx), "self"); err != nil {
@@ -901,10 +911,9 @@
 	if etag != expectedETAG {
 		t.Fatalf("getACL expected:%v(%v), got:%v(%v)", expectedACL, expectedETAG, acl, etag)
 	}
-	// Install from otherRT should fail, since it does not match the ACL.
-	if err := tryInstall(octx); err == nil {
-		t.Fatalf("Install should have failed with random identity")
-	}
+	// Install from octx should fail, since it does not match the ACL.
+	installAppExpectError(t, octx, verror.NoAccess.ID)
+
 	newACL := make(access.TaggedACLMap)
 	for _, tag := range access.AllTypicalTags() {
 		newACL.Add("root/other", string(tag))
@@ -915,14 +924,10 @@
 	if err := deviceStub.SetACL(selfCtx, newACL, etag); err != nil {
 		t.Fatal(err)
 	}
-	// Install should now fail with selfRT, which no longer matches the ACLs
-	// but succeed with otherRT, which does.
-	if err := tryInstall(selfCtx); err == nil {
-		t.Errorf("Install should have failed with selfRT since it should no longer match the ACL")
-	}
-	if err := tryInstall(octx); err != nil {
-		t.Error(err)
-	}
+	// Install should now fail with selfCtx, which no longer matches the
+	// ACLs but succeed with octx, which does.
+	installAppExpectError(t, selfCtx, verror.NoAccess.ID)
+	installApp(t, octx)
 }
 
 type simpleRW chan []byte
@@ -1242,9 +1247,9 @@
 		otherCtx, otherCancel = mgmttest.NewRuntime(t, globalCtx)
 	)
 	defer otherCancel()
-	// By default, selfRT and otherRT will have blessings generated based on
-	// the username/machine name running this process. Since these blessings
-	// will appear in test expecations, give them readable names.
+	// By default, selfCtx and otherCtx will have blessings generated based
+	// on the username/machine name running this process. Since these
+	// blessings will appear in test expecations, give them readable names.
 	if err := idp.Bless(veyron2.GetPrincipal(selfCtx), "self"); err != nil {
 		t.Fatal(err)
 	}
@@ -1346,9 +1351,10 @@
 	)
 	defer otherCancel()
 
-	// By default, selfRT and otherRT will have blessings generated based on
-	// the username/machine name running this process. Since these blessings
-	// can appear in debugging output, give them recognizable names.
+	// By default, selfCtx and otherCtx will have blessings generated based
+	// on the username/machine name running this process. Since these
+	// blessings can appear in debugging output, give them recognizable
+	// names.
 	if err := idp.Bless(veyron2.GetPrincipal(selfCtx), "self"); err != nil {
 		t.Fatal(err)
 	}
@@ -1370,20 +1376,16 @@
 
 	// Create the local server that the app uses to tell us which system
 	// name the device manager wished to run it as.
-	server, _ := mgmttest.NewServer(globalCtx)
-	defer server.Stop()
-	pingCh := make(chan string, 1)
-	if err := server.Serve("pingserver", pingServer(pingCh), nil); err != nil {
-		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
-	}
+	pingCh, cleanup := setupPingServer(t)
+	defer cleanup()
 
 	// Create an envelope for a first version of the app.
-	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps", "appV1")
+	*envelope = envelopeFromShell(sh, []string{testEnvVarName + "=env-var"}, appCmd, "google naps", fmt.Sprintf("--%s=flag-val-envelope", testFlagName), "appV1")
 
 	// Install and start the app as root/self.
 	appID := installApp(t, selfCtx)
 
-	// Claim the devicemanager with selfRT as root/self/alice
+	// Claim the devicemanager with selfCtx as root/self/alice
 	if err := deviceStub.Claim(selfCtx, &granter{p: veyron2.GetPrincipal(selfCtx), extension: "alice"}); err != nil {
 		t.Fatal(err)
 	}
@@ -1392,13 +1394,13 @@
 	// have an associated uname for the invoking identity.
 	startAppExpectError(t, selfCtx, appID, verror.NoAccess.ID)
 
-	// Create an association for selfRT
+	// Create an association for selfCtx
 	if err := deviceStub.AssociateAccount(selfCtx, []string{"root/self"}, testUserName); err != nil {
 		t.Fatalf("AssociateAccount failed %v", err)
 	}
 
 	instance1ID := startApp(t, selfCtx, appID)
-	verifyHelperArgs(t, pingCh, testUserName) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 	stopApp(t, selfCtx, appID, instance1ID)
 
 	vlog.VI(2).Infof("other attempting to run an app without access. Should fail.")
@@ -1438,12 +1440,12 @@
 
 	vlog.VI(2).Infof("other attempting to run an app with access. Should succeed.")
 	instance2ID := startApp(t, otherCtx, appID)
-	verifyHelperArgs(t, pingCh, testUserName) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 	suspendApp(t, otherCtx, appID, instance2ID)
 
 	vlog.VI(2).Infof("Verify that Resume with the same systemName works.")
 	resumeApp(t, otherCtx, appID, instance2ID)
-	verifyHelperArgs(t, pingCh, testUserName) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 	suspendApp(t, otherCtx, appID, instance2ID)
 
 	vlog.VI(2).Infof("Verify that other can install and run applications.")
@@ -1451,7 +1453,7 @@
 
 	vlog.VI(2).Infof("other attempting to run an app that other installed. Should succeed.")
 	instance4ID := startApp(t, otherCtx, otherAppID)
-	verifyHelperArgs(t, pingCh, testUserName) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 
 	// Clean up.
 	stopApp(t, otherCtx, otherAppID, instance4ID)
@@ -1469,7 +1471,7 @@
 
 	vlog.VI(2).Infof("Show that Start with different systemName works.")
 	instance3ID := startApp(t, otherCtx, appID)
-	verifyHelperArgs(t, pingCh, anotherTestUserName) // Wait until the app pings us that it's ready.
+	verifyPingArgs(t, pingCh, anotherTestUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready.
 
 	// Clean up.
 	stopApp(t, otherCtx, appID, instance3ID)
diff --git a/services/mgmt/device/impl/util_test.go b/services/mgmt/device/impl/util_test.go
index ae3ddf7..dd072de 100644
--- a/services/mgmt/device/impl/util_test.go
+++ b/services/mgmt/device/impl/util_test.go
@@ -119,20 +119,35 @@
 // The following set of functions are convenience wrappers around various app
 // management methods.
 
+func ocfg(opt []interface{}) device.Config {
+	for _, o := range opt {
+		if c, ok := o.(device.Config); ok {
+			return c
+		}
+	}
+	return device.Config{}
+}
+
 func appStub(nameComponents ...string) device.ApplicationClientMethods {
 	appsName := "dm//apps"
 	appName := naming.Join(append([]string{appsName}, nameComponents...)...)
 	return device.ApplicationClient(appName)
 }
 
-func installApp(t *testing.T, ctx *context.T) string {
-	appID, err := appStub().Install(ctx, mockApplicationRepoName)
+func installApp(t *testing.T, ctx *context.T, opt ...interface{}) string {
+	appID, err := appStub().Install(ctx, mockApplicationRepoName, ocfg(opt))
 	if err != nil {
 		t.Fatalf(testutil.FormatLogLine(2, "Install failed: %v", err))
 	}
 	return appID
 }
 
+func installAppExpectError(t *testing.T, ctx *context.T, expectedError verror.ID, opt ...interface{}) {
+	if _, err := appStub().Install(ctx, mockApplicationRepoName, ocfg(opt)); err == nil || !verror2.Is(err, expectedError) {
+		t.Fatalf(testutil.FormatLogLine(2, "Install expected to fail with %v, got %v instead", expectedError, err))
+	}
+}
+
 type granter struct {
 	ipc.CallOpt
 	p         security.Principal
diff --git a/tools/mgmt/device/devicemanager_mock_test.go b/tools/mgmt/device/devicemanager_mock_test.go
index e6d7081..15b0733 100644
--- a/tools/mgmt/device/devicemanager_mock_test.go
+++ b/tools/mgmt/device/devicemanager_mock_test.go
@@ -84,7 +84,7 @@
 	err   error
 }
 
-func (mni *mockDeviceInvoker) Install(call ipc.ServerContext, appName string) (string, error) {
+func (mni *mockDeviceInvoker) Install(call ipc.ServerContext, appName string, config device.Config) (string, error) {
 	ir := mni.tape.Record(InstallStimulus{"Install", appName})
 	r := ir.(InstallResponse)
 	return r.appId, r.err
diff --git a/tools/mgmt/device/impl.go b/tools/mgmt/device/impl.go
index f724b1c..fced994 100644
--- a/tools/mgmt/device/impl.go
+++ b/tools/mgmt/device/impl.go
@@ -27,7 +27,8 @@
 		return cmd.UsageErrorf("install: incorrect number of arguments, expected %d, got %d", expected, got)
 	}
 	deviceName, appName := args[0], args[1]
-	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName)
+	// TODO(caprita): Add support for config override.
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, nil)
 	if err != nil {
 		return fmt.Errorf("Install failed: %v", err)
 	}