veyron/services/mgmt/node/impl: Add Glob implementation

This change adds a basic Glob implementation for the app invoker. It scans
the config directory to find all the app installations and instances,
and then matches the glob pattern against the result.

Change-Id: I8bb2828eff7429fc005edbfc03787f353200f6ed
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index 5922123..35a8268 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -729,14 +729,117 @@
 	return updateLink(prevVersionDir, currLink)
 }
 
+type treeNode struct {
+	children map[string]*treeNode
+}
+
+func newTreeNode() *treeNode {
+	return &treeNode{make(map[string]*treeNode)}
+}
+
+func (n *treeNode) find(names []string, create bool) *treeNode {
+	for {
+		if len(names) == 0 {
+			return n
+		}
+		if next, ok := n.children[names[0]]; ok {
+			n = next
+			names = names[1:]
+			continue
+		}
+		if create {
+			nn := newTreeNode()
+			n.children[names[0]] = nn
+			n = nn
+			names = names[1:]
+			continue
+		}
+		return nil
+	}
+}
+
+// scanConfigDir scans the config directory to build tree representation of all
+// the valid object names.
+func (i *appInvoker) scanConfigDir() *treeNode {
+	tree := newTreeNode()
+
+	// appIDMap[appID]title
+	appIDMap := make(map[string]string)
+
+	// Find all envelopes, extract appID and installID.
+	envGlob := []string{i.config.Root, "app-*", "installation-*", "*", "envelope"}
+	envelopes, err := filepath.Glob(filepath.Join(envGlob...))
+	if err != nil {
+		vlog.Errorf("unexpected error: %v", err)
+		return nil
+	}
+	for _, path := range envelopes {
+		env, err := loadEnvelope(filepath.Dir(path))
+		if err != nil {
+			continue
+		}
+		relpath, _ := filepath.Rel(i.config.Root, path)
+		elems := strings.Split(relpath, string(filepath.Separator))
+		if len(elems) != len(envGlob)-1 {
+			vlog.Errorf("unexpected number of path components: %q (%q)", elems, path)
+			continue
+		}
+		appID := strings.TrimPrefix(elems[0], "app-")
+		installID := strings.TrimPrefix(elems[1], "installation-")
+		appIDMap[appID] = env.Title
+		tree.find([]string{env.Title, installID}, true)
+	}
+
+	// Find all instances.
+	infoGlob := []string{i.config.Root, "app-*", "installation-*", "instances", "instance-*", "info"}
+	instances, err := filepath.Glob(filepath.Join(infoGlob...))
+	if err != nil {
+		vlog.Errorf("unexpected error: %v", err)
+		return nil
+	}
+	for _, path := range instances {
+		if _, err := loadInstanceInfo(filepath.Dir(path)); err != nil {
+			continue
+		}
+		relpath, _ := filepath.Rel(i.config.Root, path)
+		elems := strings.Split(relpath, string(filepath.Separator))
+		if len(elems) != len(infoGlob)-1 {
+			vlog.Errorf("unexpected number of path components: %q (%q)", elems, path)
+			continue
+		}
+		appID := strings.TrimPrefix(elems[0], "app-")
+		installID := strings.TrimPrefix(elems[1], "installation-")
+		instanceID := strings.TrimPrefix(elems[3], "instance-")
+		if title, ok := appIDMap[appID]; ok {
+			tree.find([]string{title, installID, instanceID}, true)
+		}
+	}
+	return tree
+}
+
 func (i *appInvoker) Glob(ctx ipc.ServerContext, pattern string, stream mounttable.GlobbableServiceGlobStream) error {
-	// TODO(rthellend): Finish implementing Glob
 	g, err := glob.Parse(pattern)
 	if err != nil {
 		return err
 	}
-	if g.Len() == 0 {
-		return stream.SendStream().Send(types.MountEntry{Name: ""})
+	n := i.scanConfigDir().find(i.suffix, false)
+	if n == nil {
+		return errInvalidSuffix
 	}
+	i.globStep("", g, n, stream)
 	return nil
 }
+
+func (i *appInvoker) globStep(prefix string, g *glob.Glob, n *treeNode, stream mounttable.GlobbableServiceGlobStream) {
+	if g.Len() == 0 {
+		stream.SendStream().Send(types.MountEntry{Name: prefix})
+	}
+	if g.Finished() {
+		return
+	}
+	for name, child := range n.children {
+		if ok, _, left := g.MatchInitialSegment(name); ok {
+			i.globStep(naming.Join(prefix, name), left, child, stream)
+		}
+	}
+}
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 1ca4db6..6d66f0f 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -11,6 +11,7 @@
 	"os"
 	goexec "os/exec"
 	"os/user"
+	"path"
 	"path/filepath"
 	"reflect"
 	"sort"
@@ -920,8 +921,12 @@
 */
 
 func TestNodeManagerGlob(t *testing.T) {
-	// Set up mount table.
+	// Set up mount table, application, and binary repositories.
 	defer setupLocalNamespace(t)()
+	envelope, cleanup := startApplicationRepository()
+	defer cleanup()
+	defer startBinaryRepository()()
+
 	root, cleanup := setupRootDir(t)
 	defer cleanup()
 
@@ -935,33 +940,70 @@
 	defer nm.Cleanup()
 	readPID(t, nm)
 
-	c, err := mounttable.BindGlobbable("nm")
-	if err != nil {
-		t.Fatalf("BindGlobbable failed: %v", err)
+	// Create a script wrapping the test target that implements suidhelper.
+	generateSuidHelperScript(t, root)
+
+	// Create the local server that the app uses to let us know it's ready.
+	server, _ := newServer()
+	defer server.Stop()
+	pingCh := make(chan string, 1)
+	if err := server.Serve("pingserver", ipc.LeafDispatcher(pingServerDisp(pingCh), nil)); err != nil {
+		t.Fatalf("Serve(%q, <dispatcher>) failed: %v", "pingserver", err)
 	}
 
-	stream, err := c.Glob(rt.R().NewContext(), "...")
-	if err != nil {
-		t.Errorf("Glob failed: %v", err)
+	// Create the envelope for the first version of the app.
+	app := blackbox.HelperCommand(t, "app", "appV1")
+	defer setupChildCommand(app)()
+	appTitle := "google naps"
+	*envelope = *envelopeFromCmd(appTitle, app.Cmd)
+
+	// Install the app.
+	appID := installApp(t)
+	installID := path.Base(appID)
+
+	// Start an instance of the app.
+	instance1ID := startApp(t, appID)
+	<-pingCh // Wait until the app pings us that it's ready.
+
+	testcases := []struct {
+		name, pattern string
+		expected      []string
+	}{
+		{"nm", "...", []string{
+			"",
+			"apps",
+			"apps/google naps",
+			"apps/google naps/" + installID,
+			"apps/google naps/" + installID + "/" + instance1ID,
+			"nm",
+		}},
+		{"nm/apps", "*", []string{"google naps"}},
+		{"nm/apps/google naps", "*", []string{installID}},
 	}
-	results := []string{}
-	iterator := stream.RecvStream()
-	for iterator.Advance() {
-		results = append(results, iterator.Value().Name)
-	}
-	sort.Strings(results)
-	expected := []string{
-		"",
-		"apps",
-		"nm",
-	}
-	if !reflect.DeepEqual(results, expected) {
-		t.Errorf("unexpected result. Got %v, want %v", results, expected)
-	}
-	if err := iterator.Err(); err != nil {
-		t.Errorf("unexpected stream error: %v", err)
-	}
-	if err := stream.Finish(); err != nil {
-		t.Errorf("Finish failed: %v", err)
+	for _, tc := range testcases {
+		c, err := mounttable.BindGlobbable(tc.name)
+		if err != nil {
+			t.Fatalf("BindGlobbable failed: %v", err)
+		}
+
+		stream, err := c.Glob(rt.R().NewContext(), tc.pattern)
+		if err != nil {
+			t.Errorf("Glob failed: %v", err)
+		}
+		results := []string{}
+		iterator := stream.RecvStream()
+		for iterator.Advance() {
+			results = append(results, iterator.Value().Name)
+		}
+		sort.Strings(results)
+		if !reflect.DeepEqual(results, tc.expected) {
+			t.Errorf("unexpected result. Got %q, want %q", results, tc.expected)
+		}
+		if err := iterator.Err(); err != nil {
+			t.Errorf("unexpected stream error: %v", err)
+		}
+		if err := stream.Finish(); err != nil {
+			t.Errorf("Finish failed: %v", err)
+		}
 	}
 }