vendor: update vendored upspin.io to 9afe181

This includes the fix to the dir/server's Watch, the removal of the
deprecated subcmd.ServerConfig.Bucket field, and the addition of the
openstack repository to serverutil/frontend.

Change-Id: Id0ccbe115194485c068075e2062b5964894d32e2
Reviewed-on: https://upspin-review.googlesource.com/15680
Reviewed-by: Rob Pike <r@golang.org>
Reviewed-by: David Presotto <presotto@gmail.com>
diff --git a/Gopkg.lock b/Gopkg.lock
index e932b1e..64764c9 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -95,7 +95,7 @@
   branch = "master"
   name = "upspin.io"
   packages = ["access","bind","cache","client","client/clientutil","client/file","cloud/https","cloud/mail","cloud/mail/sendgrid","cloud/storage","config","dir/inprocess","dir/remote","dir/server","dir/server/serverlog","dir/server/tree","dir/unassigned","errors","factotum","flags","key/inprocess","key/remote","key/server","key/sha256key","key/transports","key/unassigned","key/usercache","log","metric","pack","pack/ee","pack/eeintegrity","pack/internal","pack/packutil","pack/plain","path","rpc","rpc/dirserver","rpc/keyserver","rpc/local","rpc/storeserver","serverutil","serverutil/dirserver","serverutil/frontend","serverutil/keyserver","serverutil/perm","serverutil/signup","serverutil/storeserver","serverutil/upspinserver","serverutil/web","shutdown","store/inprocess","store/remote","store/server","store/transports","store/unassigned","subcmd","test/testutil","transports","upspin","upspin/proto","user","valid","version"]
-  revision = "7c0b70de475c76fcb25bacc2b5e01dfa2c3659dd"
+  revision = "9afe1814019d1895ce157e7b8a63b70e6ec61132"
 
 [solve-meta]
   analyzer-name = "dep"
diff --git a/vendor/upspin.io/access/access.go b/vendor/upspin.io/access/access.go
index 48f0e25..e469c50 100644
--- a/vendor/upspin.io/access/access.go
+++ b/vendor/upspin.io/access/access.go
@@ -282,22 +282,22 @@
 	return a, nil
 }
 
-func newAccess(pathName upspin.PathName) (*Access, *path.Parsed, error) {
+func newAccess(pathName upspin.PathName) (*Access, path.Parsed, error) {
 	parsed, err := path.Parse(pathName)
 	if err != nil {
-		return nil, nil, err
+		return nil, parsed, err
 	}
 	_, _, domain, err := user.Parse(parsed.User())
 	// We don't expect an error since it's been parsed, but check anyway.
 	if err != nil {
-		return nil, nil, err
+		return nil, parsed, err
 	}
 	a := &Access{
 		parsed: parsed,
 		owner:  parsed.User(),
 		domain: domain,
 	}
-	return a, &parsed, nil
+	return a, parsed, nil
 }
 
 // For sorting the lists of paths.
@@ -622,15 +622,10 @@
 	}
 }
 
-// canNoGroupLoad reports whether the requesting user can access the file
-// using the specified right according to the rules of the Access
-// file. If it needs to read some group files before the decision can be made,
-// it returns a non-nil slice of their names.
-func (a *Access) canNoGroupLoad(requester upspin.UserName, right Right, pathName upspin.PathName) (bool, []upspin.PathName, error) {
-	parsedRequester, err := path.Parse(upspin.PathName(requester + "/"))
-	if err != nil {
-		return false, nil, err
-	}
+// rightGranted returns whether the requester is granted the
+// right for the path given the rules of the Access file, and if the answer
+// isn't immediately known, the access list to traverse.
+func (a *Access) rightGranted(requester upspin.UserName, right Right, pathName upspin.PathName) (bool, []path.Parsed, error) {
 	isOwner := requester == a.owner
 	// If user is the owner and the request is for read, list, or any access, access is granted.
 	if isOwner {
@@ -648,40 +643,8 @@
 			return isOwner, nil, nil
 		}
 	}
-	list, err := a.getListFor(right)
-	if err != nil {
-		return false, nil, err
-	}
-	// First try the list of regular users we have loaded. Make a note of groups to check.
-	found, groupsToCheck := a.inList(parsedRequester, list, nil)
-	if found {
-		return true, nil, nil
-	}
-	// Now look at the groups we have to check, and build a list of
-	// groups we don't know about in case we can't answer with
-	// what we know so far.
-	var missingGroups []upspin.PathName
-	// This is not a range loop because the groups list may grow.
-	for i := 0; i < len(groupsToCheck); i++ {
-		group := groupsToCheck[i]
-		groupPath := group.Path()
-		mu.RLock()
-		known, ok := groups[groupPath]
-		mu.RUnlock()
-		if !ok {
-			missingGroups = append(missingGroups, groupPath)
-			continue
-		}
-		// The owner of a group is automatically a member of the group
-		if group.User() == requester {
-			return true, nil, nil
-		}
-		found, groupsToCheck = a.inList(parsedRequester, known, groupsToCheck)
-		if found {
-			return true, nil, nil
-		}
-	}
-	return false, missingGroups, nil
+	group, err := a.getListFor(right)
+	return false, group, err
 }
 
 // Can reports whether the requesting user can access the file
@@ -706,96 +669,125 @@
 // reported only if the requester does not match any names that
 // can be found in the Access file or other Group files.
 func (a *Access) Can(requester upspin.UserName, right Right, pathName upspin.PathName, load func(upspin.PathName) ([]byte, error)) (bool, error) {
+
+	parsedRequester, err := path.Parse(upspin.PathName(requester + "/"))
+	if err != nil {
+		return false, err
+	}
+
+	requesterUserName := parsedRequester.User()
+
+	_, _, domain, err := user.Parse(requesterUserName)
+	// We don't expect an error since it's been parsed, but check anyway.
+	if err != nil {
+		return false, err
+	}
+
+	granted, group, err := a.rightGranted(requester, right, pathName)
+	if granted || err != nil {
+		return granted, err
+	}
+
+	// The groups graph is traversed depth-first, always preferring to check
+	// loaded groups first.
+
+	var groupsToCheck iter
+	var missing []path.Parsed
 	var groupErr error
-	var failedGroups []upspin.PathName
-	for {
-		granted, missing, err := a.canNoGroupLoad(requester, right, pathName)
-		if err != nil {
-			return false, err
-		}
-		if missing == nil {
-			return granted, nil
+
+	for len(group) > 0 {
+		// The loop searches lists to find whether the requester is represented
+		// in the group graph.
+
+		granted = inGroup(requesterUserName, domain, group, &groupsToCheck)
+		if granted {
+			return true, nil
 		}
 
-		loaded := false
-	missingLoop:
-		for _, group := range missing {
-			// Don't load this group if it failed
-			// in a previous iteration.
-			for _, g := range failedGroups {
-				if g == group {
-					continue missingLoop
-				}
-			}
+		// Until a non-empty group is found, iterate through groupsToCheck,
+		// checking groups already loaded and deferring the rest.
+		group = nil
 
-			// Load and parse the group.
-			data, err := load(group)
-			if err == nil {
-				err = AddGroup(group, data)
-				if err == nil {
-					loaded = true
-					continue
-				}
-			}
+		for len(group) == 0 && !groupsToCheck.done() {
+			parsed := groupsToCheck.next()
 
-			// Remember failures.
-			failedGroups = append(failedGroups, group)
-			if groupErr != nil {
+			var found bool
+			mu.RLock()
+			group, found = groups[parsed.Path()]
+			mu.RUnlock()
+
+			if !found {
+				// Defer check.
+				missing = append(missing, parsed)
+			}
+		}
+
+		// If necessary and possible, load another group.
+		for len(group) == 0 && len(missing) > 0 {
+			var parsed path.Parsed
+			parsed, missing = missing[len(missing)-1], missing[:len(missing)-1]
+
+			group, err = loadAndAdd(parsed, load)
+			// TODO issue #489, change to groupErr == nil, so we actually
+			// return an error. Leaving like this for now, to mimic the
+			// previous behavior, so the tests in ../dir/server and ../test
+			// pass.
+			if err != nil && groupErr != nil {
+				// Remember first load or parse error.
 				groupErr = err
 			}
 		}
-		// We have tried and failed to retrieve all of missing. Give up.
-		if !loaded {
-			break
-		}
 	}
 	return false, groupErr
 }
 
-// expandGroups expands a list of groups to the user names they represent.
-// If the Access file does not know the members of a group that it
-// needs to resolve the answer, it returns a list of the group files it needs to have read for it.
-// The caller should fetch these and report them with the AddGroup method, then retry.
-// TODO: use this in Can.
-func (a *Access) expandGroups(toExpand []upspin.PathName) ([]upspin.UserName, []upspin.PathName) {
-	var missingGroups []upspin.PathName
-	var userNames []upspin.UserName
-Outer:
-	for i := 0; i < len(toExpand); i++ { // not range since list may grow
-		group := toExpand[i]
-		mu.RLock()
-		usersFromGroup, found := groups[group]
-		mu.RUnlock()
-		if found {
-			for _, p := range usersFromGroup {
-				if p.IsRoot() {
-					userNames = append(userNames, p.User())
-				} else {
-					// This means there are nested Groups.
-					// Add it to the list to expand if not already there.
-					newGroupToExpand := p.Path()
-					for _, te := range toExpand {
-						if te == newGroupToExpand {
-							continue Outer
-						}
-					}
-					toExpand = append(toExpand, newGroupToExpand)
-				}
+// inGroup reports whether the requester is present in the group, either
+// directly, by wildcard, by being the owner of a nested group, or virtually by
+// finding the allUsersParsed id in the list. Any nested groups encountered
+// before ascertaining an answer get included in the set of groupsToCheck.
+func inGroup(requesterUserName upspin.UserName, domain string, group []path.Parsed, groupsToCheck *iter) bool {
+	for _, member := range group {
+		memberUserName := member.User()
+		if member.IsRoot() {
+			// A user id
+			// Simple test for AllUsers, granting universal access.
+			if member == allUsersParsed {
+				return true
+			}
+
+			if memberUserName == requesterUserName {
+				return true
+			}
+			// Wildcard: The path name *@domain.com matches anyone in domain.
+			if strings.HasPrefix(string(memberUserName), "*@") && string(memberUserName[2:]) == domain {
+				return true
 			}
 		} else {
-			// Add to missingGroups if not already there.
-			for _, mg := range missingGroups {
-				if string(group) == string(mg) {
-					continue Outer
-				}
+			// A nested group
+			if memberUserName == requesterUserName {
+				// The owner of a group is automatically a member of the group.
+				// No need to see that the group can even be loaded.
+				return true
 			}
-			missingGroups = append(missingGroups, group)
+			groupsToCheck.add(member)
 		}
 	}
-	if len(missingGroups) > 0 {
-		return userNames, missingGroups
+	return false
+}
+
+// loadAndAdd returns the group having loaded the file and calling AddGroup on the result.
+func loadAndAdd(parsed path.Parsed, load func(upspin.PathName) ([]byte, error)) (group []path.Parsed, err error) {
+	var data []byte
+	data, err = load(parsed.Path())
+	if err == nil {
+		err = AddGroup(parsed.Path(), data)
+		if err == nil {
+			mu.RLock()
+			group = groups[parsed.Path()]
+			mu.RUnlock()
+		}
 	}
-	return userNames, nil
+	return
 }
 
 func (a *Access) getListFor(right Right) ([]path.Parsed, error) {
@@ -809,44 +801,6 @@
 	}
 }
 
-// usersNoGroupLoad returns the user names granted a given right according to the rules
-// of the Access file. It also interprets the rule that the owner can always
-// Read and List. The list is unsorted and may contain duplicates.
-//
-// If the Access file does not know the members of a group that it
-// needs to resolve the answer, it returns a list of the group files it needs to
-// have read for it. The caller should fetch these and report them
-// with the AddGroup method, then retry.
-func (a *Access) usersNoGroupLoad(right Right) ([]upspin.UserName, []upspin.PathName, error) {
-	list, err := a.getListFor(right)
-	if err != nil {
-		return nil, nil, err
-	}
-	userNames := make([]upspin.UserName, 0, len(list)+1)
-	switch right {
-	case Read, List:
-		userNames = append(userNames, a.owner)
-	}
-	var groups []upspin.PathName
-	for _, user := range list {
-		if user.IsRoot() {
-			// It's a user
-			userNames = append(userNames, user.User())
-		} else {
-			// It's a group. Need to unroll groups.
-			groups = append(groups, user.Path())
-		}
-	}
-	if len(groups) > 0 {
-		users, missingGroups := a.expandGroups(groups)
-		userNames = append(userNames, users...)
-		if missingGroups != nil {
-			return userNames, missingGroups, err
-		}
-	}
-	return userNames, nil, nil
-}
-
 // For sorting the lists of UserNames.
 type sliceOfUserName []upspin.UserName
 
@@ -877,46 +831,72 @@
 	return out
 }
 
-// Users returns the user names granted a given right according to the rules
-// of the Access file. It also interprets the rule that the owner can always
-// Read and List.  Users loads group files as needed by calling the provided
-// function to read each file's contents.
+// Users returns the user names granted a given right according to the rules of
+// the Access file. It also interprets the rule that the owner can always Read
+// and List. Users loads group files as needed by calling the provided function
+// to read each file's contents.
 func (a *Access) Users(right Right, load func(upspin.PathName) ([]byte, error)) ([]upspin.UserName, error) {
-	var userNames []upspin.UserName
+	group, err := a.getListFor(right)
+	if err != nil {
+		return nil, err
+	}
+
+	userNameSet := make(map[upspin.UserName]struct{})
+	var groupsToCheck iter
+
+	switch right {
+	case Read, List:
+		userNameSet[a.owner] = struct{}{}
+	}
+
+	// Loop over all the group lists reachable by traversing the graph rooted
+	// with the access right given. Every group list can include parsed user
+	// ids and nested groups. User ids and groups are uniquely tracked. The
+	// traversal is done when no more new groups are found.
 	for {
-		users, neededGroups, err := a.usersNoGroupLoad(right)
-		if err != nil {
-			return nil, err
+		for _, parsed := range group {
+			// Be it a user or a nested group owner, the group member user is granted the right.
+			userNameSet[parsed.User()] = struct{}{}
+
+			// A nested group bears traversal too.
+			if !parsed.IsRoot() {
+				groupsToCheck.add(parsed)
+			}
 		}
-		if neededGroups == nil {
-			userNames = users
+
+		// Loop done when the transitive closure of group membership has been
+		// exhausted, that is, when all groups encountered have been expanded.
+		if groupsToCheck.done() {
 			break
 		}
-		for _, group := range neededGroups {
-			groupData, err := load(group)
-			if err != nil {
-				return nil, err
-			}
-			err = AddGroup(group, groupData)
+
+		parsed := groupsToCheck.next()
+
+		var found bool
+		mu.RLock()
+		group, found = groups[parsed.Path()]
+		mu.RUnlock()
+
+		if !found {
+			group, err = loadAndAdd(parsed, load)
 			if err != nil {
 				return nil, err
 			}
 		}
 	}
 
-	if len(userNames) == 0 {
+	if len(userNameSet) == 0 {
 		return nil, nil
 	}
-	sort.Sort(sliceOfUserName(userNames))
-	// Remove duplicates.
-	for i := 0; i < len(userNames)-1; {
-		if userNames[i] == userNames[i+1] {
-			userNames = append(userNames[:i], userNames[i+1:]...)
-			continue
-		}
-		i++
+
+	// Build a slice and then sort it.
+	userNames := make([]upspin.UserName, 0, len(userNameSet))
+	for k := range userNameSet {
+		userNames = append(userNames, k)
 	}
 
+	sort.Sort(sliceOfUserName(userNames))
+
 	return userNames, nil
 }
 
@@ -956,44 +936,40 @@
 	return access, nil
 }
 
-// inList reports whether the requester is present in the list, either directly or by wildcard.
-// If we encounter a new group in the list, we add it the list of groups to check and will
-// process it in another call from Can.
-func (a *Access) inList(requester path.Parsed, list []path.Parsed, groupsToCheck []path.Parsed) (bool, []path.Parsed) {
-	_, _, domain, err := user.Parse(requester.User())
-	// We don't expect an error since it's been parsed, but check anyway.
-	if err != nil {
-		return false, nil
-	}
-Outer:
-	for _, allowed := range list {
-		// Simple test for AllUsers, granting universal access.
-		if allowed == allUsersParsed {
-			return true, nil
-		}
-		if allowed.IsRoot() {
-			if allowed.User() == requester.User() {
-				return true, nil
-			}
-			// Wildcard: The path name *@domain.com matches anyone in domain.
-			if strings.HasPrefix(string(allowed.User()), "*@") && string(allowed.User()[2:]) == domain {
-				return true, nil
-			}
-		} else {
-			// It's a group. Make sure we don't put the same one in twice. TODO: Could be n^2.
-			for _, group := range groupsToCheck {
-				if allowed.Equal(group) {
-					// Already there, so don't add it again.
-					continue Outer
-				}
-			}
-			groupsToCheck = append(groupsToCheck, allowed)
-		}
-	}
-	return false, groupsToCheck
-}
-
 // IsReadableByAll reports whether the Access file has read:all or read:all@upspin.io
 func (a *Access) IsReadableByAll() bool {
 	return a.worldReadable
 }
+
+// iter implements an iterator over path.Parsed items.
+// The iterator allows items to be added during iteration. Duplicate items
+// may be added but duplicates are not returned by method next.
+type iter struct {
+	set    map[path.Parsed]struct{}
+	posted []path.Parsed
+}
+
+// add will add the path.Parsed item to iterator if it hadn't already been added,
+// irrespective of whether the item has already been iterated over.
+func (i *iter) add(p path.Parsed) {
+	if i.set == nil {
+		i.set = make(map[path.Parsed]struct{})
+	}
+	if _, found := i.set[p]; !found {
+		i.set[p] = struct{}{}
+		i.posted = append(i.posted, p)
+	}
+}
+
+// done reports when iteration is complete.
+func (i *iter) done() bool {
+	return len(i.posted) == 0
+}
+
+// next returns another iteration item.
+// Caller should test against being done first.
+func (i *iter) next() path.Parsed {
+	var p path.Parsed
+	p, i.posted = i.posted[len(i.posted)-1], i.posted[:len(i.posted)-1]
+	return p
+}
diff --git a/vendor/upspin.io/cloud/https/https.go b/vendor/upspin.io/cloud/https/https.go
index c488e3b..9909f1a 100644
--- a/vendor/upspin.io/cloud/https/https.go
+++ b/vendor/upspin.io/cloud/https/https.go
@@ -20,6 +20,7 @@
 	"upspin.io/errors"
 	"upspin.io/flags"
 	"upspin.io/log"
+	"upspin.io/serverutil"
 	"upspin.io/shutdown"
 )
 
@@ -161,7 +162,7 @@
 		if err != nil {
 			log.Fatalf("https: couldn't parse address: %v", err)
 		}
-		if host != "localhost" && host != "127.0.0.1" && host != "::1" {
+		if !serverutil.IsLoopback(host) {
 			log.Error.Printf("https: WARNING: serving insecure HTTP on non-loopback address %q", addr)
 		}
 	case hasLetsEncryptCache && !hasAutocertCache && !hasCert:
diff --git a/vendor/upspin.io/cloud/mail/logger.go b/vendor/upspin.io/cloud/mail/logger.go
new file mode 100644
index 0000000..d05773e
--- /dev/null
+++ b/vendor/upspin.io/cloud/mail/logger.go
@@ -0,0 +1,22 @@
+// Copyright 2016 The Upspin Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package mail
+
+import "upspin.io/log"
+
+// Logger returns a Mail implementation that logs any sent messages to the
+// given Logger.
+func Logger(l log.Logger) Mail {
+	return &logger{l}
+}
+
+type logger struct {
+	logger log.Logger
+}
+
+func (l logger) Send(to, from, subject, text, html string) error {
+	l.logger.Printf("mail to=%q from=%q subject=%q:\n%s", to, from, subject, text)
+	return nil
+}
diff --git a/vendor/upspin.io/dir/server/tree/tree.go b/vendor/upspin.io/dir/server/tree/tree.go
index 32bc032..11ac9f1 100644
--- a/vendor/upspin.io/dir/server/tree/tree.go
+++ b/vendor/upspin.io/dir/server/tree/tree.go
@@ -142,7 +142,7 @@
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
-	node, _, err := t.loadPath(p)
+	node, err := t.loadPath(p)
 	if err == upspin.ErrFollowLink {
 		return &node.entry, node.dirty, err
 	}
@@ -195,28 +195,30 @@
 	de.Sequence = t.sequence
 	// If putting a/b/c/d, ensure a/b/c is loaded.
 	parentPath := p.Drop(1)
-	parent, watchers, err := t.loadPath(parentPath)
+	parent, watchers, childWatchers, err := t.loadPathWatchers(p)
+
 	if err == upspin.ErrFollowLink { // encountered a link along the path.
-		return parent, watchers, err
+		return parent, nil, err
 	}
 	if err != nil {
-		return nil, watchers, err
+		return nil, nil, err
 	}
 	if parent.entry.IsLink() {
-		return parent, watchers, upspin.ErrFollowLink
+		return parent, nil, upspin.ErrFollowLink
 	}
 	// Now add this dirEntry as a new node
 	node := &node{
-		entry: *de,
+		entry:    *de,
+		watchers: childWatchers,
 	}
 	// If any parent watchers were watching this node, move them to this
 	// node.
 	moveDownWatchers(node, parent)
 	err = t.addKid(node, p, parent, parentPath)
 	if err != nil {
-		return nil, watchers, err
+		return nil, nil, err
 	}
-	return node, watchers, nil
+	return node, append(watchers, childWatchers...), nil
 }
 
 // PutDir puts a DirEntry representing an existing directory (with existing
@@ -233,7 +235,7 @@
 	}
 
 	// The destination must not exist nor cross a link.
-	if node, _, err := t.loadPath(dstDir); errors.Is(errors.NotExist, err) {
+	if node, err := t.loadPath(dstDir); errors.Is(errors.NotExist, err) {
 		// Destination does not exist; OK.
 	} else if err == upspin.ErrFollowLink {
 		return nil, errors.E(dstDir.Path(), errors.Errorf("destination path crosses link: %s", node.entry.Name))
@@ -361,39 +363,76 @@
 	n.entry.Sequence = t.sequence
 }
 
-// loadPath ensures the tree contains all nodes up to p and returns p's node.
-// If any node is not already in memory, it is loaded from the store server.
-// If while loading the path a link is discovered, the link is returned and if
-// it's not the last element of the path, ErrFollowLink is returned. If the node
-// does not exist, loadPath returns a NotExist error and the closest existing
-// ancestor of p, if any. Along with node, loadPath also returns all watchers on
-// p's node and in all of p's node's ancestors.
-// t.mu must be held.
-func (t *Tree) loadPath(p path.Parsed) (*node, []*watcher, error) {
+// loadPathWatchers is a variant of loadPath that also returns the
+// watchers for the path, in two parts:
+// The first part is for the watchers of the parent directory of the path;
+// the second is for the watchers of the node itself, if it it exists.
+func (t *Tree) loadPathWatchers(fullPath path.Parsed) (*node, []*watcher, []*watcher, error) {
+	p := fullPath.Drop(1)
 	err := t.loadRoot()
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, nil, err
 	}
 	node := t.root
-	// Keep track of all of p's ancestors watchers.
+	// Keep track of all of p's ancestors' watchers.
 	removeDeadWatchers(node)
 	watchers := append([]*watcher(nil), node.watchers...)
 	for i := 0; i < p.NElem(); i++ {
 		child, err := t.loadNode(node, p.Elem(i))
 		if errors.Is(errors.NotExist, err) {
-			return node, watchers, err
+			return node, watchers, nil, err
 		}
 		node = child
 		if err != nil {
-			return node, watchers, err // err could be upspin.ErrFollowLink.
+			return node, watchers, nil, err // err could be upspin.ErrFollowLink.
 		}
 		removeDeadWatchers(node)
 		watchers = append(watchers, node.watchers...)
 	}
 	if node.entry.Name != p.Path() {
-		return node, watchers, errors.E(errors.NotExist, p.Path())
+		return node, watchers, nil, errors.E(errors.NotExist, p.Path())
 	}
-	return node, watchers, nil
+
+	// The actual item might already exist. If so, grab that node's watchers too.
+	if !fullPath.IsRoot() {
+		child, err := t.loadNode(node, fullPath.Elem(fullPath.NElem()-1))
+		if err == nil {
+			// The file already exists. Add any watchers.
+			// watchers = append(watchers, child.watchers...)
+			return node, watchers, child.watchers, nil
+		}
+	}
+
+	return node, watchers, nil, nil
+}
+
+// loadPath ensures the tree contains all nodes up to p and returns p's node.
+// If any node is not already in memory, it is loaded from the store server.
+// If while loading the path a link is discovered, the link is returned and if
+// it's not the last element of the path, ErrFollowLink is returned. If the node
+// does not exist, loadPath returns a NotExist error and the closest existing
+// ancestor of p, if any.
+// t.mu must be held.
+func (t *Tree) loadPath(p path.Parsed) (*node, error) {
+	err := t.loadRoot()
+	if err != nil {
+		return nil, err
+	}
+	node := t.root
+	for i := 0; i < p.NElem(); i++ {
+		child, err := t.loadNode(node, p.Elem(i))
+		if errors.Is(errors.NotExist, err) {
+			return node, err
+		}
+		node = child
+		if err != nil {
+			return node, err // err could be upspin.ErrFollowLink.
+		}
+	}
+	if node.entry.Name != p.Path() {
+		return node, errors.E(errors.NotExist, p.Path())
+	}
+	return node, nil
 }
 
 // loadDir loads the contents of a directory's node if it's not already loaded.
@@ -527,7 +566,7 @@
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
-	node, _, err := t.loadPath(prefix)
+	node, err := t.loadPath(prefix)
 	if err == upspin.ErrFollowLink {
 		return []*upspin.DirEntry{node.entry.Copy()}, node.dirty, err
 	}
@@ -594,27 +633,27 @@
 // t.mu must be held.
 func (t *Tree) delete(p path.Parsed) (*node, []*watcher, error) {
 	parentPath := p.Drop(1)
-	parent, watchers, err := t.loadPath(parentPath)
+	parent, watchers, childWatchers, err := t.loadPathWatchers(p)
 	if err == upspin.ErrFollowLink {
-		return parent, watchers, err
+		return parent, nil, err
 	}
 	if err != nil {
-		return nil, watchers, err
+		return nil, nil, err
 	}
 	// Load the node of interest, which is the NElem-th element in its
 	// parent's path.
 	elem := p.Elem(parentPath.NElem())
 	node, err := t.loadNode(parent, elem)
 	if err == upspin.ErrFollowLink {
-		return node, watchers, err
+		return node, nil, err
 	}
 	if err != nil {
 		// Can't load parent.
-		return nil, watchers, err
+		return nil, nil, err
 	}
 	if len(node.kids) > 0 {
 		// Node is a non-empty directory.
-		return nil, watchers, errors.E(errors.NotEmpty, p.Path())
+		return nil, nil, errors.E(errors.NotEmpty, p.Path())
 	}
 
 	t.sequence++                     // We know it will succeed now.
@@ -635,9 +674,9 @@
 	if err != nil {
 		// In practice this can't happen, since the entire path is
 		// already loaded.
-		return nil, watchers, err
+		return nil, nil, err
 	}
-	return node, watchers, nil
+	return node, append(watchers, childWatchers...), nil
 }
 
 // deleteRoot deletes the root, if it's empty.
@@ -739,7 +778,6 @@
 // used by the tree. Further uses of the tree will have unpredictable
 // results.
 func (t *Tree) Close() error {
-
 	// Notify watchers and any other goroutine that the tree is closing
 	// immediately.
 	t.mu.Lock()
diff --git a/vendor/upspin.io/dir/server/tree/watch.go b/vendor/upspin.io/dir/server/tree/watch.go
index 535f464..16424ad 100644
--- a/vendor/upspin.io/dir/server/tree/watch.go
+++ b/vendor/upspin.io/dir/server/tree/watch.go
@@ -176,7 +176,7 @@
 // addWatcher adds a watcher to the node at a given path location.
 // t.mu must be held.
 func (t *Tree) addWatcher(p path.Parsed, w *watcher) error {
-	n, _, err := t.loadPath(p)
+	n, err := t.loadPath(p)
 	if err != nil && !errors.Is(errors.NotExist, err) {
 		return err
 	}
@@ -197,7 +197,7 @@
 func (w *watcher) sendCurrentAndWatch(clone, orig *Tree, p path.Parsed, offset int64) {
 	defer clone.Close()
 
-	n, _, err := clone.loadPath(p)
+	n, err := clone.loadPath(p)
 	if err != nil && !errors.Is(errors.NotExist, err) {
 		w.sendError(err)
 		w.close()
@@ -424,7 +424,7 @@
 		// next ith watcher to the curr watcher. Otherwise just shrink
 		// the slice.
 		// Note: The node is newly-put, so it does not have watchers yet
-		// and hence there's not need to look for duplicate watchers
+		// and hence there's no need to look for duplicate watchers
 		// here.
 		node.watchers = append(node.watchers, w)
 		if i > curr {
diff --git a/vendor/upspin.io/rpc/certpool.go b/vendor/upspin.io/rpc/certpool.go
index b9cd695..37d5c88 100644
--- a/vendor/upspin.io/rpc/certpool.go
+++ b/vendor/upspin.io/rpc/certpool.go
@@ -19,7 +19,8 @@
 	m map[string]*x509.CertPool // [dir]pool
 }
 
-func certPoolFromConfig(cfg upspin.Config) (*x509.CertPool, error) {
+// CertPoolFromConfig returns the TLS certificate pool for this config.
+func CertPoolFromConfig(cfg upspin.Config) (*x509.CertPool, error) {
 	dir := cfg.Value("tlscerts")
 	if dir == "" {
 		return nil, nil
diff --git a/vendor/upspin.io/rpc/client.go b/vendor/upspin.io/rpc/client.go
index d6e4b26..118280f 100644
--- a/vendor/upspin.io/rpc/client.go
+++ b/vendor/upspin.io/rpc/client.go
@@ -11,7 +11,6 @@
 	"fmt"
 	"io"
 	"io/ioutil"
-	"net"
 	"net/http"
 	"strings"
 	"sync"
@@ -20,6 +19,7 @@
 	"upspin.io/bind"
 	"upspin.io/errors"
 	"upspin.io/rpc/local"
+	"upspin.io/serverutil"
 	"upspin.io/upspin"
 
 	pb "github.com/golang/protobuf/proto"
@@ -102,12 +102,12 @@
 	switch security {
 	case NoSecurity:
 		// Only allow insecure connections to the loop back network.
-		if !isLocal(string(netAddr)) {
+		if !serverutil.IsLoopback(string(netAddr)) {
 			return nil, errors.E(op, errors.IO, errors.Errorf("insecure dial to non-loopback destination %q", netAddr))
 		}
 		c.baseURL = "http://" + string(netAddr)
 	case Secure:
-		certPool, err := certPoolFromConfig(cfg)
+		certPool, err := CertPoolFromConfig(cfg)
 		if err != nil {
 			return nil, errors.E(op, errors.Invalid, err)
 		}
@@ -360,29 +360,6 @@
 	}
 }
 
-func isLocal(addr string) bool {
-	// Check for local IPC.
-	if local.IsLocal(addr) {
-		return true
-	}
-
-	// Check for loopback network.
-	host, _, err := net.SplitHostPort(addr)
-	if err != nil {
-		return false
-	}
-	ips, err := net.LookupIP(host)
-	if err != nil {
-		return false
-	}
-	for _, ip := range ips {
-		if !ip.IsLoopback() {
-			return false
-		}
-	}
-	return true
-}
-
 func (c *httpClient) isProxy() bool {
 	return c.proxyFor.Transport != upspin.Unassigned
 }
diff --git a/vendor/upspin.io/serverutil/addr.go b/vendor/upspin.io/serverutil/addr.go
new file mode 100644
index 0000000..9c16a8c
--- /dev/null
+++ b/vendor/upspin.io/serverutil/addr.go
@@ -0,0 +1,37 @@
+// Copyright 2016 The Upspin Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package serverutil
+
+import (
+	"net"
+
+	"upspin.io/rpc/local"
+)
+
+// IsLoopback returns true if the name only resolves to loopback addresses.
+func IsLoopback(addr string) bool {
+	host, _, err := net.SplitHostPort(addr)
+	if err != nil {
+		host = addr
+	}
+	if host == "localhost" || host == "127.0.0.1" || host == "::1" {
+		return true
+	}
+	// Check for local IPC.
+	if local.IsLocal(host) {
+		return true
+	}
+	// Check for loopback network.
+	ips, err := net.LookupIP(host)
+	if err != nil {
+		return false
+	}
+	for _, ip := range ips {
+		if !ip.IsLoopback() {
+			return false
+		}
+	}
+	return true
+}
diff --git a/vendor/upspin.io/serverutil/frontend/frontend.go b/vendor/upspin.io/serverutil/frontend/frontend.go
index 40ec5f1..c7a0216 100644
--- a/vendor/upspin.io/serverutil/frontend/frontend.go
+++ b/vendor/upspin.io/serverutil/frontend/frontend.go
@@ -81,13 +81,14 @@
 var sourceRepo = map[string]string{
 	"upspin.io": "https://upspin.googlesource.com/upspin",
 
-	"android.upspin.io": "https://upspin.googlesource.com/android",
-	"augie.upspin.io":   "https://upspin.googlesource.com/augie",
-	"aws.upspin.io":     "https://upspin.googlesource.com/aws",
-	"b2.upspin.io":      "https://upspin.googlesource.com/b2",
-	"dropbox.upspin.io": "https://upspin.googlesource.com/dropbox",
-	"exp.upspin.io":     "https://upspin.googlesource.com/exp",
-	"gcp.upspin.io":     "https://upspin.googlesource.com/gcp",
+	"android.upspin.io":   "https://upspin.googlesource.com/android",
+	"augie.upspin.io":     "https://upspin.googlesource.com/augie",
+	"aws.upspin.io":       "https://upspin.googlesource.com/aws",
+	"b2.upspin.io":        "https://upspin.googlesource.com/b2",
+	"dropbox.upspin.io":   "https://upspin.googlesource.com/dropbox",
+	"exp.upspin.io":       "https://upspin.googlesource.com/exp",
+	"gcp.upspin.io":       "https://upspin.googlesource.com/gcp",
+	"openstack.upspin.io": "https://upspin.googlesource.com/openstack",
 }
 
 func defaultDocPath() string {
diff --git a/vendor/upspin.io/serverutil/glob.go b/vendor/upspin.io/serverutil/glob.go
index 7c50cca..26ce9a2 100644
--- a/vendor/upspin.io/serverutil/glob.go
+++ b/vendor/upspin.io/serverutil/glob.go
@@ -38,6 +38,12 @@
 		if de == nil {
 			return nil, err
 		}
+		// If the pattern we look up is just a plain file, and it's a link,
+		// just return it. In effect this is equivalent to passing false as the
+		// final argument to Client.Lookup.
+		if err == upspin.ErrFollowLink && de.Name == p.Path() {
+			err = nil
+		}
 		return []*upspin.DirEntry{de}, err
 	}
 
diff --git a/vendor/upspin.io/serverutil/keyserver/main.go b/vendor/upspin.io/serverutil/keyserver/main.go
index 9af1589..341924d 100644
--- a/vendor/upspin.io/serverutil/keyserver/main.go
+++ b/vendor/upspin.io/serverutil/keyserver/main.go
@@ -12,6 +12,7 @@
 	"net/http"
 	"strings"
 
+	"upspin.io/cloud/mail"
 	"upspin.io/cloud/mail/sendgrid"
 	"upspin.io/cloud/storage"
 	"upspin.io/config"
@@ -77,28 +78,30 @@
 		http.Handle("/log", logHandler{logger: logger})
 	}
 
-	if *mailConfigFile != "" {
-		signupURL := "https://" + flags.NetAddr + "/signup"
-		f := cfg.Factotum()
-		if f == nil {
-			log.Fatal("keyserver: supplied config must include keys when -mail_config set")
+	signupURL := "https://" + flags.NetAddr + "/signup"
+	f := cfg.Factotum()
+	if f == nil {
+		log.Fatal("keyserver: supplied config must include keys")
+	}
+	project := ""
+	flag.Visit(func(f *flag.Flag) {
+		if f.Name != "project" {
+			return
 		}
-		project := ""
-		flag.Visit(func(f *flag.Flag) {
-			if f.Name != "project" {
-				return
-			}
-			project = f.Value.String()
-		})
+		project = f.Value.String()
+	})
+	var m mail.Mail
+	if *mailConfigFile != "" {
 		apiKey, err := parseMailConfig(*mailConfigFile)
 		if err != nil {
 			log.Fatalf("keyserver: %v", err)
 		}
-		m := sendgrid.New(apiKey)
-		http.Handle("/signup", signup.NewHandler(signupURL, f, key, m, project))
+		m = sendgrid.New(apiKey)
 	} else {
-		log.Println("keyserver: -mail_config not set, /signup deactivated")
+		log.Info.Printf("keyserver: -mail_config not supplied; logging mail messages instead")
+		m = mail.Logger(log.Info)
 	}
+	http.Handle("/signup", signup.NewHandler(signupURL, f, key, m, project))
 }
 
 func parseMailConfig(name string) (apiKey string, err error) {
diff --git a/vendor/upspin.io/serverutil/signup/signup.go b/vendor/upspin.io/serverutil/signup/signup.go
index 4e143b6..9200f38 100644
--- a/vendor/upspin.io/serverutil/signup/signup.go
+++ b/vendor/upspin.io/serverutil/signup/signup.go
@@ -9,6 +9,7 @@
 import (
 	"bytes"
 	"crypto/sha256"
+	"crypto/tls"
 	"encoding/binary"
 	"encoding/json"
 	"fmt"
@@ -25,6 +26,7 @@
 	"upspin.io/errors"
 	"upspin.io/factotum"
 	"upspin.io/log"
+	"upspin.io/rpc"
 	"upspin.io/serverutil"
 	"upspin.io/upspin"
 	"upspin.io/user"
@@ -360,13 +362,28 @@
 	return upspin.UserName(name + "+snapshot@" + domain), nil
 }
 
-// MakeRequest sends a signup request to the given URL for the given Config.
-func MakeRequest(signupURL string, cfg upspin.Config) error {
+var signupURLScheme = "https" // Tests may override this.
+
+// MakeRequest sends a signup request for the given Config to the Config's
+// KeyServer Endpoint using the Config's TLS certs (if any).
+func MakeRequest(cfg upspin.Config) error {
 	query, err := makeQueryString(cfg)
 	if err != nil {
 		return err
 	}
-	r, err := http.Post(signupURL+"?"+query, "text/plain", nil)
+	certPool, err := rpc.CertPoolFromConfig(cfg)
+	if err != nil {
+		return err
+	}
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				RootCAs: certPool,
+			},
+		},
+	}
+	signupURL := fmt.Sprintf("%s://%s/signup", signupURLScheme, cfg.KeyEndpoint().NetAddr)
+	r, err := client.Post(signupURL+"?"+query, "text/plain", nil)
 	if err != nil {
 		return err
 	}
diff --git a/vendor/upspin.io/serverutil/upspinserver/main.go b/vendor/upspin.io/serverutil/upspinserver/main.go
index 5bbd3a8..8120adb 100644
--- a/vendor/upspin.io/serverutil/upspinserver/main.go
+++ b/vendor/upspin.io/serverutil/upspinserver/main.go
@@ -9,7 +9,6 @@
 package upspinserver // import "upspin.io/serverutil/upspinserver"
 
 import (
-	"encoding/base64"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -302,37 +301,5 @@
 		return nil, err
 	}
 
-	return cfg, updateServerConfig(cfg)
-}
-
-// If Bucket is set, look for the old style serviceaccount.json file, stuff it
-// into the StoreConfig field, clear the Bucket field, and rewrite the config.
-// TODO(adg): remove this at the same time as removing the Bucket field.
-func updateServerConfig(cfg *subcmd.ServerConfig) error {
-	if cfg.Bucket != "" {
-		cfgFile := filepath.Join(*cfgPath, subcmd.ServerConfigFile)
-		serviceFile := filepath.Join(*cfgPath, "serviceaccount.json")
-		b, err := ioutil.ReadFile(serviceFile)
-		if err != nil {
-			return fmt.Errorf("Bucket set in %v, but: %v", cfgFile, err)
-		}
-		privateKeyData := base64.StdEncoding.EncodeToString(b)
-		cfg.StoreConfig = []string{
-			"backend=GCS",
-			"defaultACL=publicRead",
-			"gcpBucketName=" + cfg.Bucket,
-			"privateKeyData=" + privateKeyData,
-		}
-		cfg.Bucket = ""
-		b, err = json.Marshal(cfg)
-		if err != nil {
-			return fmt.Errorf("encoding config %v: %v", cfgFile, err)
-		}
-		err = ioutil.WriteFile(cfgFile, b, 0700)
-		if err != nil {
-			return fmt.Errorf("rewriting config: %v", err)
-		}
-		os.Remove(serviceFile)
-	}
-	return nil
+	return cfg, nil
 }
diff --git a/vendor/upspin.io/subcmd/io.go b/vendor/upspin.io/subcmd/io.go
index f7581b1..8c2bf3b 100644
--- a/vendor/upspin.io/subcmd/io.go
+++ b/vendor/upspin.io/subcmd/io.go
@@ -212,7 +212,7 @@
 	}
 	// If it has no metacharacters, look it up to be sure it exists.
 	if !HasGlobChar(string(pat)) {
-		entry, err := s.Client.Lookup(pat, true)
+		entry, err := s.Client.Lookup(pat, false)
 		if err != nil {
 			s.Exit(err)
 		}
diff --git a/vendor/upspin.io/subcmd/server.go b/vendor/upspin.io/subcmd/server.go
index 46148cb..4624e53 100644
--- a/vendor/upspin.io/subcmd/server.go
+++ b/vendor/upspin.io/subcmd/server.go
@@ -25,14 +25,6 @@
 
 	// StoreConfig specifies the configuration options for the StoreServer.
 	StoreConfig []string
-
-	// Bucket specifies the Google Cloud Storage bucket that the
-	// upspinserver should use to store data.
-	// If empty, local disk is used instead.
-	// Deprecated: StoreConfig should be used instead.
-	Bucket string
-
-	// TODO(adg): remove the Bucket field.
 }
 
 // ServerConfigFile specifies the file name of the JSON-encoded ServerConfig.
diff --git a/vendor/upspin.io/upspin/upspin.go b/vendor/upspin.io/upspin/upspin.go
index 5ea42d6..c6808c0 100644
--- a/vendor/upspin.io/upspin/upspin.go
+++ b/vendor/upspin.io/upspin/upspin.go
@@ -426,6 +426,11 @@
 	// DirEntries as outlined in the description for ErrFollowLink,
 	// updating the pattern as appropriate. Note that any returned
 	// links may only partially match the original argument pattern.
+	//
+	// If the pattern evaluates to one or more name that identifies
+	// a link, the DirEntry for the link is returned, not the target.
+	// This is analogous to passing false as the second argument
+	// to Client.Lookup.
 	Glob(pattern string) ([]*DirEntry, error)
 
 	// Delete deletes the DirEntry for a name from the directory service.
diff --git a/vendor/upspin.io/user/user.go b/vendor/upspin.io/user/user.go
index f0bd7a3..c6f54f5 100644
--- a/vendor/upspin.io/user/user.go
+++ b/vendor/upspin.io/user/user.go
@@ -26,20 +26,20 @@
 //
 // The rules are:
 //
-// <name> := <user name>@<domain name>
+// 	<name> := <user name>@<domain name>
 //
-// <domain name> :=
+// 	<domain name> :=
 //
-// - each . separated token < 64 characters
-// - character set for tokens [a-z0-9\-]
-// - final token at least two characters
-// - whole name < 254 characters
-// - characters are case insensitive
-// - final period is OK, but we remove it
+// 	- each . separated token < 64 characters
+// 	- character set for tokens [a-z0-9\-]
+// 	- final token at least two characters
+// 	- whole name < 254 characters
+// 	- characters are case insensitive
+// 	- final period is OK, but we remove it
 //
 // We ignore the rules of punycode, which is defined in https://tools.ietf.org/html/rfc3490 .
 //
-// <user name> :=
+// 	<user name> :=
 //
 // Names are validated and canonicalized by the UsernameCasePreserved profile
 // of the RFC 7613, "Preparation, Enforcement, and Comparison of Internationalized Strings",