veyron/services/mgmt: Add node mgr test for packages

This change adds a test for packages in the node manager, which requires
running the real binary repository implementation and creating a real
package.

At the same time:
- refactor the root directory initialization for binaryd
- add a library function to create a package from a directory

Change-Id: I055050f44404bdd13d8e69b03e36a7d0aa50c109
diff --git a/services/mgmt/binary/binaryd/main.go b/services/mgmt/binary/binaryd/main.go
index d65984f..677e9e7 100644
--- a/services/mgmt/binary/binaryd/main.go
+++ b/services/mgmt/binary/binaryd/main.go
@@ -2,11 +2,9 @@
 
 import (
 	"flag"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"os"
-	"path/filepath"
 
 	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/rt"
@@ -19,14 +17,11 @@
 	"veyron.io/veyron/veyron/services/mgmt/binary/impl"
 )
 
-const (
-	defaultDepth      = 3
-	defaultRootPrefix = "veyron_binary_repository"
-)
+const defaultDepth = 3
 
 var (
 	name     = flag.String("name", "", "name to mount the binary repository as")
-	root     = flag.String("root", "", "root directory for the binary repository")
+	rootFlag = flag.String("root", "", "root directory for the binary repository")
 	httpAddr = flag.String("http", ":0", "TCP address on which the HTTP server runs")
 )
 
@@ -54,42 +49,16 @@
 func main() {
 	runtime := rt.Init()
 	defer runtime.Cleanup()
-	if *root == "" {
-		var err error
-		if *root, err = ioutil.TempDir("", defaultRootPrefix); err != nil {
-			vlog.Errorf("TempDir() failed: %v\n", err)
-			return
-		}
-		path, perm := filepath.Join(*root, impl.VersionFile), os.FileMode(0600)
-		if err := ioutil.WriteFile(path, []byte(impl.Version), perm); err != nil {
-			vlog.Errorf("WriteFile(%v, %v, %v) failed: %v", path, impl.Version, perm, err)
-			return
-		}
-	} else {
-		_, err := os.Stat(*root)
-		switch {
-		case err == nil:
-		case os.IsNotExist(err):
-			perm := os.FileMode(0700)
-			if err := os.MkdirAll(*root, perm); err != nil {
-				vlog.Errorf("MkdirAll(%v, %v) failed: %v", *root, perm, err)
-				return
-			}
-			path, perm := filepath.Join(*root, impl.VersionFile), os.FileMode(0600)
-			if err := ioutil.WriteFile(path, []byte(impl.Version), perm); err != nil {
-				vlog.Errorf("WriteFile(%v, %v, %v) failed: %v", path, impl.Version, perm, err)
-				return
-			}
-		default:
-			vlog.Errorf("Stat(%v) failed: %v", *root, err)
-			return
-		}
-	}
-	vlog.Infof("Binary repository rooted at %v", *root)
-
-	state, err := impl.NewState(*root, defaultDepth)
+	root, err := impl.SetupRoot(*rootFlag)
 	if err != nil {
-		vlog.Errorf("NewState(%v, %v) failed: %v", *root, defaultDepth, err)
+		vlog.Errorf("SetupRoot(%q) failed: %v", *rootFlag, err)
+		return
+	}
+	vlog.Infof("Binary repository rooted at %v", root)
+
+	state, err := impl.NewState(root, defaultDepth)
+	if err != nil {
+		vlog.Errorf("NewState(%v, %v) failed: %v", root, defaultDepth, err)
 		return
 	}
 
diff --git a/services/mgmt/binary/impl/setup.go b/services/mgmt/binary/impl/setup.go
new file mode 100644
index 0000000..b0309dd
--- /dev/null
+++ b/services/mgmt/binary/impl/setup.go
@@ -0,0 +1,43 @@
+package impl
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"veyron.io/veyron/veyron2/vlog"
+)
+
+const defaultRootPrefix = "veyron_binary_repository"
+
+// SetupRoot sets up the root directory if it doesn't already exist. If an
+// empty string is used as root, create a new temporary directory.
+func SetupRoot(root string) (string, error) {
+	if root == "" {
+		var err error
+		if root, err = ioutil.TempDir("", defaultRootPrefix); err != nil {
+			vlog.Errorf("TempDir() failed: %v\n", err)
+			return "", err
+		}
+	}
+
+	_, err := os.Stat(root)
+	switch {
+	case err == nil:
+	case os.IsNotExist(err):
+		perm := os.FileMode(0700)
+		if err := os.MkdirAll(root, perm); err != nil {
+			vlog.Errorf("MkdirAll(%v, %v) failed: %v", root, perm, err)
+			return "", err
+		}
+		path, perm := filepath.Join(root, VersionFile), os.FileMode(0600)
+		if err := ioutil.WriteFile(path, []byte(Version), perm); err != nil {
+			vlog.Errorf("WriteFile(%v, %v, %v) failed: %v", path, Version, perm, err)
+			return "", err
+		}
+	default:
+		vlog.Errorf("Stat(%v) failed: %v", root, err)
+		return "", err
+	}
+	return root, nil
+}
diff --git a/services/mgmt/lib/binary/impl.go b/services/mgmt/lib/binary/impl.go
index 07bedd9..b68fe7f 100644
--- a/services/mgmt/lib/binary/impl.go
+++ b/services/mgmt/lib/binary/impl.go
@@ -11,6 +11,7 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"time"
 
 	"veyron.io/veyron/veyron2/context"
@@ -285,3 +286,16 @@
 	mediaInfo := packages.MediaInfoForFileName(path)
 	return upload(ctx, file, mediaInfo, von)
 }
+
+func UploadFromDir(ctx context.T, von, sourceDir string) error {
+	dir, err := ioutil.TempDir("", "create-package-")
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(dir)
+	zipfile := filepath.Join(dir, "file.zip")
+	if err := packages.CreateZip(zipfile, sourceDir); err != nil {
+		return err
+	}
+	return UploadFromFile(ctx, von, zipfile)
+}
diff --git a/services/mgmt/lib/packages/packages.go b/services/mgmt/lib/packages/packages.go
index 57f9dde..e55374b 100644
--- a/services/mgmt/lib/packages/packages.go
+++ b/services/mgmt/lib/packages/packages.go
@@ -85,6 +85,53 @@
 	return nil
 }
 
+// CreateZip creates a package from the files in the source directory. The
+// created package is a Zip file.
+func CreateZip(zipFile, sourceDir string) error {
+	z, err := os.OpenFile(zipFile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+	if err != nil {
+		return err
+	}
+	defer z.Close()
+	w := zip.NewWriter(z)
+	if err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if sourceDir == path {
+			return nil
+		}
+		fh, err := zip.FileInfoHeader(info)
+		if err != nil {
+			return err
+		}
+		fh.Name, _ = filepath.Rel(sourceDir, path)
+		hdr, err := w.CreateHeader(fh)
+		if err != nil {
+			return err
+		}
+		if !info.IsDir() {
+			content, err := ioutil.ReadFile(path)
+			if err != nil {
+				return err
+			}
+			if _, err = hdr.Write(content); err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+	if err := w.Close(); err != nil {
+		return err
+	}
+	if err := SaveMediaInfo(zipFile, repository.MediaInfo{Type: "application/zip"}); err != nil {
+		return err
+	}
+	return nil
+}
+
 func extractZip(zipFile, installDir string) error {
 	zr, err := zip.OpenReader(zipFile)
 	if err != nil {
diff --git a/services/mgmt/lib/packages/packages_test.go b/services/mgmt/lib/packages/packages_test.go
index 8182b44..f18e9bc 100644
--- a/services/mgmt/lib/packages/packages_test.go
+++ b/services/mgmt/lib/packages/packages_test.go
@@ -2,7 +2,6 @@
 
 import (
 	"archive/tar"
-	"archive/zip"
 	"compress/gzip"
 	"fmt"
 	"io"
@@ -107,44 +106,8 @@
 }
 
 func makeZip(t *testing.T, zipfile, dir string) {
-	z, err := os.OpenFile(zipfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
-	if err != nil {
-		t.Fatalf("os.OpenFile(%q) failed: %v", zipfile, err)
-	}
-	defer z.Close()
-	w := zip.NewWriter(z)
-	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			t.Fatalf("Walk(%q) error: %v", dir, err)
-		}
-		if dir == path {
-			return nil
-		}
-		fh, err := zip.FileInfoHeader(info)
-		if err != nil {
-			t.Fatalf("FileInfoHeader failed: %v", err)
-		}
-		fh.Name, _ = filepath.Rel(dir, path)
-		hdr, err := w.CreateHeader(fh)
-		if err != nil {
-			t.Fatalf("w.CreateHeader failed: %v", err)
-		}
-		if !info.IsDir() {
-			content, err := ioutil.ReadFile(path)
-			if err != nil {
-				t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err)
-			}
-			if _, err = hdr.Write(content); err != nil {
-				t.Fatalf("hdr.Write(%q) failed: %v", content, err)
-			}
-		}
-		return nil
-	})
-	if err := w.Close(); err != nil {
-		t.Fatalf("w.Close() failed: %v", err)
-	}
-	if err := ioutil.WriteFile(zipfile+".__info", []byte(`{"type":"application/zip"}`), os.FileMode(0644)); err != nil {
-		t.Fatalf("ioutil.WriteFile() failed: %v", err)
+	if err := packages.CreateZip(zipfile, dir); err != nil {
+		t.Fatalf("packages.CreateZip failed: %v", err)
 	}
 }
 
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 103b2d4..334814b 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -47,6 +47,8 @@
 	"veyron.io/veyron/veyron/lib/signals"
 	"veyron.io/veyron/veyron/lib/testutil"
 	tsecurity "veyron.io/veyron/veyron/lib/testutil/security"
+	binaryimpl "veyron.io/veyron/veyron/services/mgmt/binary/impl"
+	libbinary "veyron.io/veyron/veyron/services/mgmt/lib/binary"
 	"veyron.io/veyron/veyron/services/mgmt/node/config"
 	"veyron.io/veyron/veyron/services/mgmt/node/impl"
 	suidhelper "veyron.io/veyron/veyron/services/mgmt/suidhelper/impl"
@@ -214,6 +216,17 @@
 	return message, nil
 }
 
+func (appService) Cat(_ ipc.ServerContext, file string) (string, error) {
+	if file == "" || file[0] == filepath.Separator || file[0] == '.' {
+		return "", fmt.Errorf("illegal file name: %q", file)
+	}
+	bytes, err := ioutil.ReadFile(file)
+	if err != nil {
+		return "", err
+	}
+	return string(bytes), nil
+}
+
 func ping() {
 	if call, err := rt.R().Client().StartCall(rt.R().NewContext(), "pingserver", "Ping", []interface{}{os.Getenv(suidhelper.SavedArgs)}); err != nil {
 		vlog.Fatalf("StartCall failed: %v", err)
@@ -222,6 +235,21 @@
 	}
 }
 
+func cat(name, file string) (string, error) {
+	runtime := rt.R()
+	ctx, cancel := runtime.NewContext().WithTimeout(time.Minute)
+	defer cancel()
+	call, err := runtime.Client().StartCall(ctx, name, "Cat", []interface{}{file})
+	if err != nil {
+		return "", err
+	}
+	var content string
+	if ferr := call.Finish(&content, &err); ferr != nil {
+		err = ferr
+	}
+	return content, err
+}
+
 func app(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
 	args = args[1:]
 	if expected, got := 1, len(args); expected != got {
@@ -711,6 +739,42 @@
 	return nil
 }
 
+func startRealBinaryRepository(t *testing.T) func() {
+	root, err := binaryimpl.SetupRoot("")
+	if err != nil {
+		t.Fatalf("binaryimpl.SetupRoot failed: %v", err)
+	}
+	state, err := binaryimpl.NewState(root, 3)
+	if err != nil {
+		t.Fatalf("binaryimpl.NewState failed: %v", err)
+	}
+	server, _ := newServer()
+	name := "realbin"
+	if err := server.ServeDispatcher(name, binaryimpl.NewDispatcher(state, nil)); err != nil {
+		t.Fatalf("server.ServeDispatcher failed: %v", err)
+	}
+
+	tmpdir, err := ioutil.TempDir("", "test-package-")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(tmpdir)
+	if err := ioutil.WriteFile(filepath.Join(tmpdir, "hello.txt"), []byte("Hello World!"), 0600); err != nil {
+		t.Fatalf("ioutil.WriteFile failed: %v", err)
+	}
+	if err := libbinary.UploadFromDir(rt.R().NewContext(), naming.Join(name, "testpkg"), tmpdir); err != nil {
+		t.Fatalf("libbinary.UploadFromDir failed: %v", err)
+	}
+	return func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("server.Stop failed: %v", err)
+		}
+		if err := os.RemoveAll(root); err != nil {
+			t.Fatalf("os.RemoveAll(%q) failed: %v", root, err)
+		}
+	}
+}
+
 // TestNodeManagerClaim claims a nodemanager and tests ACL permissions on its methods.
 func TestNodeManagerClaim(t *testing.T) {
 	sh, deferFn := createShellAndMountTable(t)
@@ -1089,6 +1153,66 @@
 	}
 }
 
+func TestNodeManagerPackages(t *testing.T) {
+	sh, deferFn := createShellAndMountTable(t)
+	defer deferFn()
+
+	// Set up mock application and binary repositories.
+	envelope, cleanup := startMockRepos(t)
+	defer cleanup()
+
+	defer startRealBinaryRepository(t)()
+
+	root, cleanup := setupRootDir(t)
+	defer cleanup()
+
+	crDir, crEnv := credentialsForChild("nodemanager")
+	defer os.RemoveAll(crDir)
+
+	// Create a script wrapping the test target that implements suidhelper.
+	helperPath := generateSuidHelperScript(t, root)
+
+	// Set up the node manager.  Since we won't do node manager updates,
+	// don't worry about its application envelope and current link.
+	_, nms := runShellCommand(t, sh, crEnv, nodeManagerCmd, "nm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	pid := readPID(t, nms)
+	defer syscall.Kill(pid, syscall.SIGINT)
+
+	// Create the local server that the app uses to let us know it's ready.
+	pingCh, cleanup := setupPingServer(t)
+	defer cleanup()
+
+	// Create the envelope for the first version of the app.
+	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps", "appV1")
+	(*envelope).Packages = map[string]string{
+		"test": "realbin/testpkg",
+	}
+
+	// Install the app.
+	appID := installApp(t)
+
+	// Start an instance of the app.
+	startApp(t, appID)
+
+	// Wait until the app pings us that it's ready.
+	select {
+	case <-pingCh:
+	case <-time.After(pingTimeout):
+		t.Fatalf("failed to get ping")
+	}
+
+	// Ask the app to cat a file from the package.
+	file := filepath.Join("packages", "test", "hello.txt")
+	name := "appV1"
+	content, err := cat(name, file)
+	if err != nil {
+		t.Errorf("cat(%q, %q) failed: %v", name, file, err)
+	}
+	if expected := "Hello World!"; content != expected {
+		t.Errorf("unexpected content: expected %q, got %q", expected, content)
+	}
+}
+
 func listAndVerifyAssociations(t *testing.T, stub node.NodeClientMethods, run veyron2.Runtime, expected []node.Association) {
 	assocs, err := stub.ListAssociations(run.NewContext())
 	if err != nil {