veyron/services/mgmt/device/impl: implement per-instance updates

A suspended instance can now be updated to the latest version of its
installation.

This cl also include a couple minor unrelated cleanups that I noticed while
working on the update change:

- move the initialization of the instance and installation state to the very end
  of their respective setup methods

- change setupPrincipal to take the blessing extension as an arg, instead of
  letting it extract the title from the envelope and use that internally (it
  seems less obscure that way)

- update the state of instances recovered by the reaper to 'started'

Change-Id: I36baca769dd66749e84f9e6fb583d8be3c209eae
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 4614bab..8abcf82 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -25,14 +25,14 @@
 //         <pkg name>(700d)
 //         <pkg name>.__info(700d)
 //         ...
-//       <version 1 timestamp>(700d)/     - timestamp of when the version was downloaded
+//       <version 1 timestamp>(711d)/     - timestamp of when the version was downloaded
 //         bin(755d)                      - application binary
 //         previous                       - symbolic link to previous version directory
 //         envelope                       - application envelope (JSON-encoded)
 //         packages(755d)/                - installed packages (from envelope+installer)
 //           <pkg name>(755d)/
 //           ...
-//       <version 2 timestamp>(700d)
+//       <version 2 timestamp>(711d)
 //       ...
 //       current                          - symbolic link to the current version
 //       instances(711d)/
@@ -480,10 +480,6 @@
 	if _, err := newVersion(call.Context(), installationDir, envelope, ""); err != nil {
 		return "", err
 	}
-	if err := initializeInstallation(installationDir, active); err != nil {
-		return "", err
-	}
-
 	// TODO(caprita,rjkroege): Should the installation ACLs really be
 	// seeded with the device ACL? Instead, might want to hide the deviceACL
 	// from the app?
@@ -491,7 +487,14 @@
 	if err := i.initializeSubACLs(call.LocalPrincipal(), installationDir, blessings, i.deviceACL.Copy()); err != nil {
 		return "", err
 	}
+	if err := initializeInstallation(installationDir, active); err != nil {
+		return "", err
+	}
 	deferrer = nil
+	// TODO(caprita): Using the title without cleaning out slashes
+	// introduces extra name components that mess up the device manager's
+	// apps object space.  We should fix this either by santizing the title,
+	// or disallowing slashes in titles to begin with.
 	return naming.Join(envelope.Title, installationID), nil
 }
 
@@ -555,7 +558,7 @@
 }
 
 // setupPrincipal sets up the instance's principal, with the right blessings.
-func setupPrincipal(ctx *context.T, instanceDir, versionDir string, call ipc.ServerContext, securityAgent *securityAgentState, info *instanceInfo) error {
+func setupPrincipal(ctx *context.T, instanceDir, blessingExtension string, call ipc.ServerContext, securityAgent *securityAgentState, info *instanceInfo) error {
 	var p security.Principal
 	if securityAgent != nil {
 		// TODO(caprita): Part of the cleanup upon destroying an
@@ -585,15 +588,6 @@
 			return verror.New(ErrOperationFailed, nil)
 		}
 	}
-	// Read the app installation version's envelope to obtain the app title.
-	//
-	// NOTE: we could have gotten this from the suffix as well, but the
-	// format of the object name suffix may change in the future: there's no
-	// guarantee it will always include the title.
-	envelope, err := loadEnvelope(versionDir)
-	if err != nil {
-		return err
-	}
 	dmPrincipal := call.LocalPrincipal()
 	// Take the blessings conferred upon us by the Start-er, extend them
 	// with the app title.
@@ -602,7 +596,7 @@
 		return verror.New(ErrInvalidBlessing, nil)
 	}
 	// TODO(caprita): Revisit UnconstrainedUse.
-	appBlessings, err := dmPrincipal.Bless(p.PublicKey(), grantedBlessings, envelope.Title, security.UnconstrainedUse())
+	appBlessings, err := dmPrincipal.Bless(p.PublicKey(), grantedBlessings, blessingExtension, security.UnconstrainedUse())
 	if err != nil {
 		vlog.Errorf("Bless() failed: %v", err)
 		return verror.New(ErrOperationFailed, nil)
@@ -732,6 +726,10 @@
 	if mkdirPerm(instanceDir, 0711) != nil {
 		return "", instanceID, verror.New(ErrOperationFailed, call.Context())
 	}
+	rootDir := filepath.Join(instanceDir, "root")
+	if err := mkdir(rootDir); err != nil {
+		return instanceDir, instanceID, verror.New(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)
@@ -746,32 +744,38 @@
 	versionLink := filepath.Join(instanceDir, "version")
 	if err := os.Symlink(versionDir, versionLink); err != nil {
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)
-		return instanceDir, instanceID, verror.New(ErrOperationFailed, call.Context())
 	}
-	rootDir := filepath.Join(instanceDir, "root")
-	if err := mkdir(rootDir); err != nil {
-		return instanceDir, instanceID, verror.New(ErrOperationFailed, call.Context())
-	}
-	packagesDir, packagesLink := filepath.Join(versionDir, "packages"), filepath.Join(rootDir, "packages")
+	packagesDir, packagesLink := filepath.Join(versionLink, "packages"), filepath.Join(rootDir, "packages")
 	if err := os.Symlink(packagesDir, packagesLink); err != nil {
 		vlog.Errorf("Symlink(%v, %v) failed: %v", packagesDir, packagesLink, err)
-		return instanceDir, instanceID, verror.New(ErrOperationFailed, call.Context())
 	}
+
 	instanceInfo := new(instanceInfo)
-	if err := setupPrincipal(call.Context(), instanceDir, versionDir, call, i.securityAgent, instanceInfo); err != nil {
+	// Read the app installation version's envelope to obtain the app title,
+	// which we'll use as a blessing extension.
+	//
+	// NOTE: we could have gotten this from the suffix as well, but the
+	// format of the object name suffix may change in the future: there's no
+	// guarantee it will always include the title.
+	envelope, err := loadEnvelope(versionDir)
+	if err != nil {
+		return instanceDir, instanceID, err
+	}
+	// TODO(caprita): Using the app title as the blessing extension requires
+	// sanitizing to ensure no invalid blessing characters are used.
+	if err := setupPrincipal(call.Context(), instanceDir, envelope.Title, call, i.securityAgent, instanceInfo); err != nil {
 		return instanceDir, instanceID, err
 	}
 	if err := saveInstanceInfo(instanceDir, instanceInfo); err != nil {
 		return instanceDir, instanceID, err
 	}
-	if err := initializeInstance(instanceDir, suspended); err != nil {
-		return instanceDir, instanceID, err
-	}
-
 	blessings, _ := call.RemoteBlessings().ForContext(call)
 	if err := i.initializeSubACLs(call.LocalPrincipal(), instanceDir, blessings, i.deviceACL.Copy()); err != nil {
 		return instanceDir, instanceID, err
 	}
+	if err := initializeInstance(instanceDir, suspended); err != nil {
+		return instanceDir, instanceID, err
+	}
 	return instanceDir, instanceID, nil
 }
 
@@ -1096,9 +1100,41 @@
 	return transitionInstallation(installationDir, active, uninstalled)
 }
 
-func updateInstance(instanceDir string, ctx *context.T) error {
-	// TODO(caprita): Implement.
-	return verror.New(ErrInvalidSuffix, nil)
+func updateInstance(instanceDir string, ctx *context.T) (err error) {
+	// Only suspended instances can be updated.
+	//
+	// TODO(caprita): Consider enabling updates for started instances as
+	// well, and handle the suspend/resume automatically.
+	if err := transitionInstance(instanceDir, suspended, updating); err != nil {
+		return err
+	}
+	defer func() {
+		terr := transitionInstance(instanceDir, updating, suspended)
+		if err == nil {
+			err = terr
+		}
+	}()
+
+	// Check if a newer version of the installation is available.
+	versionLink := filepath.Join(instanceDir, "version")
+	versionDir, err := filepath.EvalSymlinks(versionLink)
+	if err != nil {
+		vlog.Errorf("EvalSymlinks(%v) failed: %v", versionLink, err)
+		return verror.New(ErrOperationFailed, nil)
+	}
+	latestVersionLink := filepath.Join(instanceDir, "installation", "current")
+	latestVersionDir, err := filepath.EvalSymlinks(latestVersionLink)
+	if err != nil {
+		vlog.Errorf("EvalSymlinks(%v) failed: %v", latestVersionLink, err)
+		return verror.New(ErrOperationFailed, nil)
+	}
+	if versionDir == latestVersionDir {
+		return verror.New(ErrUpdateNoOp, ctx)
+	}
+	// Update to the newer version.  Note, this is the only mutation
+	// performed to the instance, and, since it's atomic, the state of the
+	// instance is consistent at all times.
+	return updateLink(latestVersionDir, versionLink)
 }
 
 func updateInstallation(installationDir string, ctx *context.T) error {
diff --git a/services/mgmt/device/impl/app_state.go b/services/mgmt/device/impl/app_state.go
index 0a13676..ee07c86 100644
--- a/services/mgmt/device/impl/app_state.go
+++ b/services/mgmt/device/impl/app_state.go
@@ -57,6 +57,7 @@
 	suspended
 	stopping
 	stopped
+	updating
 )
 
 // String returns the name that will be used to encode the state as a file name
@@ -75,6 +76,8 @@
 		return "stopping"
 	case stopped:
 		return "stopped"
+	case updating:
+		return "updating"
 	default:
 		return "unknown"
 	}
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index 6cda65a..82b6e51 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -752,14 +752,26 @@
 		t.Fatalf("Second endpoint should have been v1EP2: %v, %v", endpoints, v1EP2)
 	}
 
-	// Stop first instance.
-	stopApp(t, ctx, appID, instance1ID)
-	verifyAppWorkspace(t, root, appID, instance1ID)
-
-	// Only second instance is still running.
+	// Trying to update first instance while it's running should fail.
+	updateInstanceExpectError(t, ctx, appID, instance1ID, impl.ErrInvalidOperation.ID)
+	// Suspend first instance and try again.
+	suspendApp(t, ctx, appID, instance1ID)
+	// Only the second instance should still be running and mounted.
 	if want, got := v1EP2, resolve(t, ctx, "appV1", 1)[0]; want != got {
 		t.Fatalf("Resolve(%v): want: %v, got %v", "appV1", want, got)
 	}
+	// Update succeeds now.
+	updateInstance(t, ctx, appID, instance1ID)
+	// Resume the first instance and verify it's running v2 now.
+	resumeApp(t, ctx, appID, instance1ID)
+	verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
+	resolve(t, ctx, "appV1", 1)
+	resolve(t, ctx, "appV2", 1)
+
+	// Stop first instance.
+	stopApp(t, ctx, appID, instance1ID)
+	verifyAppWorkspace(t, root, appID, instance1ID)
+	resolveExpectNotFound(t, ctx, "appV2")
 
 	// Start a third instance.
 	instance3ID := startApp(t, ctx, appID)
@@ -1235,6 +1247,8 @@
 	}
 }
 
+// TODO(caprita): We need better test coverage for how updating/reverting apps
+// affects the package configured for the app.
 func TestDeviceManagerPackages(t *testing.T) {
 	ctx, shutdown := initForTest()
 	defer shutdown()
diff --git a/services/mgmt/device/impl/instance_reaping.go b/services/mgmt/device/impl/instance_reaping.go
index 045193a..1de27ad 100644
--- a/services/mgmt/device/impl/instance_reaping.go
+++ b/services/mgmt/device/impl/instance_reaping.go
@@ -129,6 +129,14 @@
 	if instanceStateIs(instancePath, stopped) || instanceStateIs(instancePath, suspended) {
 		return
 	}
+	// If the app was updating, it means it was already suspended, so just
+	// update its state back to suspended.
+	if instanceStateIs(instancePath, updating) {
+		if err := initializeInstance(instancePath, suspended); err != nil {
+			vlog.Errorf("initializeInstance(%s,%s) failed: %v", instancePath, suspended, err)
+		}
+		return
+	}
 
 	vlog.VI(2).Infof("perInstance firing up on %s", instancePath)
 	nctx, _ := context.WithTimeout(ctx, appCycleTimeout*time.Second)
@@ -157,7 +165,7 @@
 		// No process is actually running for this instance.
 		vlog.VI(2).Infof("perinstance stats fetching failed: %v", err)
 		if err := initializeInstance(instancePath, suspended); err != nil {
-			vlog.Errorf("initializeInstance  failed: %v", err)
+			vlog.Errorf("initializeInstance(%s,%s) failed: %v", instancePath, suspended, err)
 		}
 		ptuple.err = err
 		c <- ptuple
@@ -178,6 +186,12 @@
 		info.Pid = pid
 		ptuple.err = saveInstanceInfo(instancePath, info)
 	}
+	// The instance was found to be running, so update its state accordingly
+	// (in case the device restarted while the instance was in one of the
+	// transitional states like starting, suspending, etc).
+	if err := initializeInstance(instancePath, started); err != nil {
+		vlog.Errorf("initializeInstance(%s,%s) failed: %v", instancePath, started, err)
+	}
 
 	vlog.VI(2).Infof("perInstance go routine for %v ending", instancePath)
 	c <- ptuple
diff --git a/services/mgmt/device/impl/util_test.go b/services/mgmt/device/impl/util_test.go
index fdb1b64..c5fcb77 100644
--- a/services/mgmt/device/impl/util_test.go
+++ b/services/mgmt/device/impl/util_test.go
@@ -258,6 +258,18 @@
 	}
 }
 
+func updateInstance(t *testing.T, ctx *context.T, appID, instanceID string) {
+	if err := appStub(appID, instanceID).Update(ctx); err != nil {
+		t.Fatalf(testutil.FormatLogLine(2, "Update(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err))
+	}
+}
+
+func updateInstanceExpectError(t *testing.T, ctx *context.T, appID, instanceID string, expectedError verror.ID) {
+	if err := appStub(appID, instanceID).Update(ctx); err == nil || !verror.Is(err, expectedError) {
+		t.Fatalf(testutil.FormatLogLine(2, "Update(%v/%v) expected to fail with %v, got %v [%v]", appID, instanceID, expectedError, verror.ErrorID(err), err))
+	}
+}
+
 func updateApp(t *testing.T, ctx *context.T, appID string) {
 	if err := appStub(appID).Update(ctx); err != nil {
 		t.Fatalf(testutil.FormatLogLine(2, "Update(%v) failed: %v [%v]", appID, verror.ErrorID(err), err))