veyron/services/mgmt/node/impl: implement app Uninstall

Introduce app installation status (similar to app instance status) to mark an
installation as active or uninstalled (uninstalled app installations cannot be
used to start new instances, but existing instances are otherwise unaffected).

Change-Id: Ic73cb5d86d2abab4a1adf1876ab617bfa9e7cb99
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index efbfe8e..c81e079 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -9,6 +9,7 @@
 // <config.Root>/
 //   app-<hash 1>/                  - the application dir is named using a hash of the application title
 //     installation-<id 1>/         - installations are labelled with ids
+//       <status>                   - one of the values for installationState enum
 //       <version 1 timestamp>/     - timestamp of when the version was downloaded
 //         bin                      - application binary
 //         previous                 - symbolic link to previous version directory (TODO)
@@ -105,6 +106,43 @@
 	"veyron2/vlog"
 )
 
+// installationState describes the states that an installation can be in at any
+// time.
+type installationState int
+
+const (
+	active installationState = iota
+	uninstalled
+)
+
+// String returns the name that will be used to encode the state as a file name
+// in the installation's dir.
+func (s installationState) String() string {
+	switch s {
+	case active:
+		return "active"
+	case uninstalled:
+		return "uninstalled"
+	default:
+		return "unknown"
+	}
+}
+
+func installationStateIs(installationDir string, state installationState) bool {
+	if _, err := os.Stat(filepath.Join(installationDir, state.String())); err != nil {
+		return false
+	}
+	return true
+}
+
+func transitionInstallation(installationDir string, initial, target installationState) error {
+	return transitionState(installationDir, initial, target)
+}
+
+func initializeInstallation(installationDir string, initial installationState) error {
+	return initializeState(installationDir, initial)
+}
+
 // instanceState describes the states that an instance can be in at any time.
 type instanceState int
 
@@ -138,9 +176,17 @@
 	}
 }
 
-func transition(instanceDir string, initial, target instanceState) error {
-	initialState := filepath.Join(instanceDir, initial.String())
-	targetState := filepath.Join(instanceDir, target.String())
+func transitionInstance(instanceDir string, initial, target instanceState) error {
+	return transitionState(instanceDir, initial, target)
+}
+
+func initializeInstance(instanceDir string, initial instanceState) error {
+	return initializeState(instanceDir, initial)
+}
+
+func transitionState(dir string, initial, target fmt.Stringer) error {
+	initialState := filepath.Join(dir, initial.String())
+	targetState := filepath.Join(dir, target.String())
 	if err := os.Rename(initialState, targetState); err != nil {
 		if os.IsNotExist(err) {
 			return errInvalidOperation
@@ -151,8 +197,8 @@
 	return nil
 }
 
-func initializeState(instanceDir string, initial instanceState) error {
-	initialStatus := filepath.Join(instanceDir, initial.String())
+func initializeState(dir string, initial fmt.Stringer) error {
+	initialStatus := filepath.Join(dir, initial.String())
 	if err := ioutil.WriteFile(initialStatus, []byte("status"), 0600); err != nil {
 		vlog.Errorf("WriteFile(%v) failed: %v", initialStatus, err)
 		return errOperationFailed
@@ -303,7 +349,7 @@
 		return "", errOperationFailed
 	}
 	deferrer := func() {
-		if err := os.RemoveAll(versionDir); err != nil {
+		if err := os.RemoveAll(installationDir); err != nil {
 			vlog.Errorf("RemoveAll(%v) failed: %v", versionDir, err)
 		}
 	}
@@ -327,6 +373,9 @@
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, link, err)
 		return "", errOperationFailed
 	}
+	if err := initializeInstallation(installationDir, active); err != nil {
+		return "", err
+	}
 	deferrer = nil
 	return naming.Join(envelope.Title, installationID), nil
 }
@@ -383,6 +432,9 @@
 	if err != nil {
 		return "", "", err
 	}
+	if !installationStateIs(installationDir, active) {
+		return "", "", errInvalidOperation
+	}
 	instanceID := generateID()
 	instanceDir := filepath.Join(installationDir, "instances", instanceDirName(instanceID))
 	if mkdir(instanceDir) != nil {
@@ -399,7 +451,7 @@
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)
 		return instanceDir, instanceID, errOperationFailed
 	}
-	if err := initializeState(instanceDir, suspended); err != nil {
+	if err := initializeInstance(instanceDir, suspended); err != nil {
 		return instanceDir, instanceID, err
 	}
 	return instanceDir, instanceID, nil
@@ -494,7 +546,7 @@
 }
 
 func (i *appInvoker) run(instanceDir string) error {
-	if err := transition(instanceDir, suspended, starting); err != nil {
+	if err := transitionInstance(instanceDir, suspended, starting); err != nil {
 		return err
 	}
 	cmd, err := genCmd(instanceDir)
@@ -502,10 +554,10 @@
 		err = i.startCmd(instanceDir, cmd)
 	}
 	if err != nil {
-		transition(instanceDir, starting, suspended)
+		transitionInstance(instanceDir, starting, suspended)
 		return err
 	}
-	return transition(instanceDir, starting, started)
+	return transitionInstance(instanceDir, starting, started)
 }
 
 func (i *appInvoker) Start(ipc.ServerContext) ([]string, error) {
@@ -589,17 +641,17 @@
 	if err != nil {
 		return err
 	}
-	if err := transition(instanceDir, suspended, stopped); err == errOperationFailed || err == nil {
+	if err := transitionInstance(instanceDir, suspended, stopped); err == errOperationFailed || err == nil {
 		return err
 	}
-	if err := transition(instanceDir, started, stopping); err != nil {
+	if err := transitionInstance(instanceDir, started, stopping); err != nil {
 		return err
 	}
 	if err := stop(instanceDir); err != nil {
-		transition(instanceDir, stopping, started)
+		transitionInstance(instanceDir, stopping, started)
 		return err
 	}
-	return transition(instanceDir, stopping, stopped)
+	return transitionInstance(instanceDir, stopping, stopped)
 }
 
 func (i *appInvoker) Suspend(ipc.ServerContext) error {
@@ -607,27 +659,30 @@
 	if err != nil {
 		return err
 	}
-	if err := transition(instanceDir, started, suspending); err != nil {
+	if err := transitionInstance(instanceDir, started, suspending); err != nil {
 		return err
 	}
 	if err := stop(instanceDir); err != nil {
-		transition(instanceDir, suspending, started)
+		transitionInstance(instanceDir, suspending, started)
 		return err
 	}
-	return transition(instanceDir, suspending, suspended)
+	return transitionInstance(instanceDir, suspending, suspended)
 }
 
-func (*appInvoker) Uninstall(ipc.ServerContext) error {
+func (i *appInvoker) Uninstall(ipc.ServerContext) error {
+	installationDir, err := i.installationDir()
+	if err != nil {
+		return err
+	}
+	return transitionInstallation(installationDir, active, uninstalled)
+}
+
+func (*appInvoker) Update(ipc.ServerContext) error {
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
-func (i *appInvoker) Update(ipc.ServerContext) error {
-	// TODO(jsimsa): Implement.
-	return nil
-}
-
-func (i *appInvoker) UpdateTo(_ ipc.ServerContext, von string) error {
+func (*appInvoker) UpdateTo(_ ipc.ServerContext, von string) error {
 	// TODO(jsimsa): Implement.
 	return nil
 }
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 2dd443f..d6856f2 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -452,25 +452,37 @@
 	return appID
 }
 
-func startApp(t *testing.T, appID string) string {
+func startAppImpl(t *testing.T, appID string) (string, error) {
 	appsName := "nm//apps"
 	appName := naming.Join(appsName, appID)
 	stub, err := node.BindApplication(appName)
 	if err != nil {
 		t.Fatalf("BindApplication(%v) failed: %v", appName, err)
 	}
-	var instanceID string
 	if instanceIDs, err := stub.Start(rt.R().NewContext()); err != nil {
-		t.Fatalf("Start failed: %v", err)
+		return "", err
 	} else {
 		if want, got := 1, len(instanceIDs); want != got {
 			t.Fatalf("Expected %v instance ids, got %v instead", want, got)
 		}
-		instanceID = instanceIDs[0]
+		return instanceIDs[0], nil
+	}
+}
+
+func startApp(t *testing.T, appID string) string {
+	instanceID, err := startAppImpl(t, appID)
+	if err != nil {
+		t.Fatalf("Start failed: %v", err)
 	}
 	return instanceID
 }
 
+func startAppShouldFail(t *testing.T, appID string, expectedError verror.ID) {
+	if _, err := startAppImpl(t, appID); err == nil || !verror.Is(err, expectedError) {
+		t.Fatalf("Start(%v) expected to fail with %v, got %v instead", appID, expectedError, err)
+	}
+}
+
 func stopApp(t *testing.T, appID, instanceID string) {
 	appsName := "nm//apps"
 	appName := naming.Join(appsName, appID)
@@ -510,6 +522,18 @@
 	}
 }
 
+func uninstallApp(t *testing.T, appID string) {
+	appsName := "nm//apps"
+	appName := naming.Join(appsName, appID)
+	stub, err := node.BindApplication(appName)
+	if err != nil {
+		t.Fatalf("BindApplication(%v) failed: %v", appName, err)
+	}
+	if err := stub.Uninstall(rt.R().NewContext()); err != nil {
+		t.Fatalf("Uninstall failed: %v", err)
+	}
+}
+
 func verifyAppWorkspace(t *testing.T, root, appID, instanceID string) {
 	// HACK ALERT: for now, we peek inside the node manager's directory
 	// structure (which ought to be opaque) to check for what the app has
@@ -596,6 +620,12 @@
 
 	verifyAppWorkspace(t, root, appID, instanceID)
 
+	// Uninstall the app.
+	uninstallApp(t, appID)
+
+	// Starting new instances should no longer be allowed.
+	startAppShouldFail(t, appID, verror.BadArg)
+
 	// Cleanly shut down the node manager.
 	syscall.Kill(nm.Cmd.Process.Pid, syscall.SIGINT)
 	nm.Expect("nm terminating")