Merge "veyron/services/mgmt/node/impl: implement app Uninstall"
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")