Implement syncgroup manipulation.

Functional commands: create, list, inspect, join.
Nonfunctional commands: leave, destroy, eject.

The nonfunctional are such because they haven't been
implemented in the syncbase library. I'm including
them in this CL so that (if the interface doesn't
change) they'll work properly as soon as the library
functions are implemented.

Change-Id: I53298f304bb531ab3c7f288649abf8cf02950a83
diff --git a/cmd/sb/commands/acl.go b/cmd/sb/commands/acl.go
index d566fc9..6afdc5f 100644
--- a/cmd/sb/commands/acl.go
+++ b/cmd/sb/commands/acl.go
@@ -21,18 +21,20 @@
 var cmdAcl = &cmdline.Command{
 	Name:     "acl",
 	Short:    "Read or mutate the ACLs",
-	Long:     `Read or mutate the ACLs`,
+	Long:     `Read or mutate the ACLs.`,
 	Children: []*cmdline.Command{cmdAclGet, cmdAclAdd, cmdAclRm, cmdAclDump},
 }
 
 var (
 	flagTarget     string
 	flagCollection string
+	flagSyncgroup  string
 )
 
 func init() {
-	cmdAcl.Flags.StringVar(&flagTarget, "target", "db", "The access controller type to act on (service, db, collection).")
-	cmdAcl.Flags.StringVar(&flagCollection, "collection", "", "The collection to act on. (note: requires -target=collection)")
+	cmdAcl.Flags.StringVar(&flagTarget, "target", "db", "The access controller type to act on (service, db, database, cx, collection, sg, syncgroup).")
+	cmdAcl.Flags.StringVar(&flagCollection, "cx", "", "The collection to act on. (note: requires -target=collection or cx)")
+	cmdAcl.Flags.StringVar(&flagSyncgroup, "sg", "", "The syncgroup to act on. (note: requires -target=syncgroup or sg)")
 }
 
 func updatePerms(ctx *context.T, db syncbase.Database, env *cmdline.Env, callback func(access.Permissions) (access.Permissions, error)) error {
@@ -52,15 +54,14 @@
 
 		if perms, err = callback(perms); err != nil {
 			return err
+		} else if perms == nil {
+			return nil
 		}
 
-		if perms != nil {
-			if err := ac.SetPermissions(ctx, perms, version); err != nil {
-				return fmt.Errorf("error setting permissions: %q", err)
-			}
+		if err := ac.SetPermissions(ctx, perms, version); err != nil {
+			return fmt.Errorf("error setting permissions: %q", err)
 		}
-		return nil
-	case "collection":
+	case "cx", "collection":
 		bdb, err := db.BeginBatch(ctx, wire.BatchOptions{})
 		if err != nil {
 			return err
@@ -75,15 +76,29 @@
 
 		if perms, err = callback(perms); err != nil {
 			return err
+		} else if perms == nil {
+			return nil
 		}
 
-		if perms != nil {
-			if err = collection.SetPermissions(ctx, perms); err != nil {
-				return err
-			}
-
-			return bdb.Commit(ctx)
+		if err = collection.SetPermissions(ctx, perms); err != nil {
+			return err
 		}
+
+		return bdb.Commit(ctx)
+	case "sg", "syncgroup":
+		sg := db.Syncgroup(ctx, flagSyncgroup)
+		spec, version, err := sg.GetSpec(ctx)
+		if err != nil {
+			return err
+		}
+
+		if spec.Perms, err = callback(spec.Perms); err != nil {
+			return err
+		} else if spec.Perms == nil {
+			return nil
+		}
+
+		sg.SetSpec(ctx, spec, version)
 		return nil
 	default:
 		return env.UsageErrorf("target %s not recognized", flagTarget)
@@ -95,7 +110,7 @@
 var cmdAclGet = &cmdline.Command{
 	Name:     "get",
 	Short:    "Read a blessing's permissions",
-	Long:     "Read a blessing's permissions",
+	Long:     "Read a blessing's permissions.",
 	ArgsName: "<blessing>",
 	ArgsLong: `
 <blessing> is the blessing to check permissions for.
@@ -143,7 +158,7 @@
 var cmdAclAdd = &cmdline.Command{
 	Name:     "add",
 	Short:    "Add blessing to groups",
-	Long:     "Add blessing to groups",
+	Long:     "Add blessing to groups.",
 	ArgsName: "(<blessing> [!]<tag>(,[!]<tag>)* )+",
 	ArgsLong: `
 The args are sequential pairs of the form "<blessing> <taglist>"
@@ -180,7 +195,7 @@
 
 var cmdAclRm = &cmdline.Command{
 	Name:     "rm",
-	Short:    "Remove specific permissions from the acl.",
+	Short:    "Remove specific permissions from the acl",
 	Long:     "Remove specific permissions from the acl.",
 	ArgsName: "(<blessing> <tag>(,<tag>)* )+",
 	ArgsLong: `
@@ -214,7 +229,7 @@
 var cmdAclDump = &cmdline.Command{
 	Name:   "dump",
 	Short:  "Pretty-print the whole acl",
-	Long:   "Pretty-print the whole acl",
+	Long:   "Pretty-print the whole acl.",
 	Runner: SbRunner(runAclDump),
 }
 
diff --git a/cmd/sb/commands/commands.go b/cmd/sb/commands/commands.go
index 38bf78e..5900016 100644
--- a/cmd/sb/commands/commands.go
+++ b/cmd/sb/commands/commands.go
@@ -15,10 +15,17 @@
 	"v.io/x/ref/lib/v23cmd"
 )
 
+// Commands holds all the commands runnable at the top level (as $ sb foo)
+// and in the shell, as:
+// $ sb sh
+// ? foo
+// with the exception of shell builtins like help and quit. Adding a command
+// to Commands will make it accessible by either mechanism.
 var Commands = []*cmdline.Command{
 	cmdDump,
 	cmdMakeDemo,
 	cmdSelect,
+	cmdSg,
 	cmdAcl,
 }
 
diff --git a/cmd/sb/commands/syncgroup.go b/cmd/sb/commands/syncgroup.go
new file mode 100644
index 0000000..dc97868
--- /dev/null
+++ b/cmd/sb/commands/syncgroup.go
@@ -0,0 +1,256 @@
+// Copyright 2016 The Vanadium 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 commands
+
+import (
+	"bytes"
+	"fmt"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/syncbase"
+	"v.io/v23/syncbase/util"
+	"v.io/x/lib/cmdline"
+)
+
+var cmdSg = &cmdline.Command{
+	Name:     "sg",
+	Short:    "Manipulate syncgroups",
+	Long:     "Manipulate syncgroups.",
+	Children: []*cmdline.Command{cmdSgCreate, cmdSgList, cmdSgInspect, cmdSgJoin, cmdSgLeave, cmdSgDestroy, cmdSgEject},
+}
+
+var (
+	flagSgTarget      string
+	flagSgDescription string
+	flagSgPerms       string
+	flagSgListVerbose bool
+)
+
+func init() {
+	cmdSg.Flags.StringVar(&flagSgTarget, "target", "", "Syncgroup to act on.")
+	cmdSg.Flags.StringVar(&flagSgTarget, "t", "", "Syncgroup to act on.")
+	cmdSgCreate.Flags.StringVar(&flagSgDescription, "description", "", "Description for the new syncgroup.")
+	cmdSgCreate.Flags.StringVar(&flagSgPerms, "perms", "", `
+Permissions for the new syncgroup.
+The permissions format for syncgroups is:
+{<tag>:{"In":[<blessing_pattern>,...],"Out":[<blessing_pattern>,...]},...}
+where the <tag>s Admin and Read must exist. The specified permissions must
+include at least one Admin.
+`)
+	cmdSgList.Flags.BoolVar(&flagSgListVerbose, "verbose", false, "Show entire id of syncgroups and blessings")
+	cmdSgList.Flags.BoolVar(&flagSgListVerbose, "v", false, "Show entire id of syncgroups and blessings")
+}
+
+var cmdSgCreate = &cmdline.Command{
+	Name:     "create",
+	Short:    "Create a new syncgroup with the given spec",
+	Long:     "Create a new syncgroup with the given spec.",
+	ArgsName: "<collection_id>[ <collection_id>]*",
+	ArgsLong: `
+Takes a space separated list of collection ids where each collection is covered
+by this syncgroup.
+
+The -perms flag can be used to specify a specific set of permissions.
+Alternatively, leave the perms flag absent for default permissions (principal has
+Admin and Read) and use the acl command to adjust these as desired.
+`,
+	Runner: SbRunner(runSgCreate),
+}
+
+func runSgCreate(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	var spec wire.SyncgroupSpec
+	spec.Description = flagSgDescription
+
+	// Get collection ids for specified collections.
+	spec.Collections = make([]wire.Id, len(args))
+	for i, name := range args {
+		if id, err := wire.ParseId(name); err != nil {
+			return env.UsageErrorf("create: error parsing id %s: %q", name, err)
+		} else {
+			spec.Collections[i] = id
+		}
+	}
+
+	// TODO(zsterling): Allow users to override default blessing.
+	if bp, err := getDefaultBlessing(ctx); err != nil {
+		return err
+	} else if flagSgPerms == "" {
+		// Default perms.
+		spec.Perms = map[string]access.AccessList{
+			"Admin": access.AccessList{[]security.BlessingPattern{bp}, []string{}},
+			"Read":  access.AccessList{[]security.BlessingPattern{bp}, []string{}},
+		}
+	} else {
+		// Manually set perms by flag.
+		if perms, err := access.ReadPermissions(bytes.NewBufferString(flagSgPerms)); err != nil {
+			return env.UsageErrorf("create: bad permissions: %q", err)
+		} else {
+			spec.Perms = perms
+		}
+	}
+
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	if err := sg.Create(ctx, spec, wire.SyncgroupMemberInfo{}); err != nil {
+		return fmt.Errorf("create: error creating syncgroup: %q", err)
+	}
+
+	return nil
+}
+
+func getDefaultBlessing(ctx *context.T) (security.BlessingPattern, error) {
+	principal := v23.GetPrincipal(ctx)
+	blessings := security.DefaultBlessingNames(principal)
+	_, user, err := util.AppAndUserPatternFromBlessings(blessings...)
+	return user, err
+}
+
+var cmdSgList = &cmdline.Command{
+	Name:   "list",
+	Short:  "List all syncgroups on the database",
+	Long:   "List all syncgroups on the database.",
+	Runner: SbRunner(runSgList),
+}
+
+func runSgList(ctx *context.T, db syncbase.Database, _ *cmdline.Env, _ []string) error {
+	ids, err := db.ListSyncgroups(ctx)
+	if err != nil {
+		return err
+	}
+
+	collections := make(map[wire.Id][]wire.Id)
+	for _, id := range ids {
+		sg := db.SyncgroupForId(id)
+		var spec wire.SyncgroupSpec
+		spec, _, err = sg.GetSpec(ctx)
+		if err != nil {
+			return err
+		}
+		collections[id] = spec.Collections
+	}
+
+	for i, id := range ids {
+		if flagSgListVerbose {
+			fmt.Println(id)
+		} else {
+			fmt.Println(id.Name)
+		}
+		for _, coll := range collections[id] {
+			if flagSgListVerbose {
+				fmt.Println("\t", coll)
+			} else {
+				fmt.Println("\t", coll.Name)
+			}
+		}
+		if i < len(ids)-1 {
+			fmt.Println()
+		}
+	}
+	return nil
+}
+
+var cmdSgInspect = &cmdline.Command{
+	Name:  "inspect",
+	Short: "Show spec of a syncgroup",
+	Long: `
+Show spec of the syncgroup specified by the -target flag.
+`,
+	Runner: SbRunner(runSgInspect),
+}
+
+func runSgInspect(ctx *context.T, db syncbase.Database, _ *cmdline.Env, _ []string) error {
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	spec, _, err := sg.GetSpec(ctx)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("Description:\t\t", spec.Description)
+	fmt.Println("PublishSyncbaseName:\t", spec.PublishSyncbaseName)
+	fmt.Println("Perms:\t\t\t", spec.Perms)
+	fmt.Println("Collections:\t\t", spec.Collections)
+	fmt.Println("MountTables:\t\t", spec.MountTables)
+	fmt.Println("IsPrivate:\t\t", spec.IsPrivate)
+
+	return nil
+}
+
+var cmdSgJoin = &cmdline.Command{
+	Name:     "join",
+	Short:    "Join specified syncgroup",
+	Long:     "Join specified syncgroup.",
+	ArgsName: "[<admin>]",
+	ArgsLong: `
+<admin> is the name of a syncbase instance which is an Admin on the syncgroup.
+If <admin> is not specified join will look for syncgroups on the local neighborhood.
+`,
+	Runner: SbRunner(runSgJoin),
+}
+
+func runSgJoin(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	var adminName string
+	switch len(args) {
+	case 0:
+		adminName = ""
+	case 1:
+		adminName = args[0]
+	default:
+		return env.UsageErrorf("join: too many args")
+	}
+
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	// TODO(zsterling): Pass in expected blessings.
+	_, err := sg.Join(ctx, adminName, nil, wire.SyncgroupMemberInfo{})
+	return err
+}
+
+var cmdSgLeave = &cmdline.Command{
+	Name:   "leave",
+	Short:  "Leave specified syncgroup",
+	Long:   "Leave specified syncgroup.",
+	Runner: SbRunner(runSgLeave),
+}
+
+func runSgLeave(ctx *context.T, db syncbase.Database, _ *cmdline.Env, _ []string) error {
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	return sg.Leave(ctx)
+}
+
+var cmdSgDestroy = &cmdline.Command{
+	Name:   "destroy",
+	Short:  "Destroy specified syncgroup",
+	Long:   "Destroy specified syncgroup.",
+	Runner: SbRunner(runSgDestroy),
+}
+
+func runSgDestroy(ctx *context.T, db syncbase.Database, _ *cmdline.Env, _ []string) error {
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	return sg.Destroy(ctx)
+}
+
+var cmdSgEject = &cmdline.Command{
+	Name:     "eject",
+	Short:    "Eject user from syncgroup",
+	Long:     "Eject user from syncgroup.",
+	ArgsName: "<user>",
+	// TODO(zsterling): Ensure user should really be a blessing when the api is
+	// implemented.
+	ArgsLong: `
+<user> should be the blessing of the user to eject.
+`,
+	Runner: SbRunner(runSgEject),
+}
+
+func runSgEject(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	if got := len(args); got != 1 {
+		return env.UsageErrorf("eject: expected 1 argument, got %d", got)
+	}
+
+	sg := db.Syncgroup(ctx, flagSgTarget)
+	return sg.Eject(ctx, args[0])
+}
diff --git a/cmd/sb/doc.go b/cmd/sb/doc.go
index 1a5ee03..182e1b0 100644
--- a/cmd/sb/doc.go
+++ b/cmd/sb/doc.go
@@ -17,6 +17,7 @@
    dump        Print a dump of the database
    make-demo   Populate the db with dummy data
    select      Display particular rows, or parts of rows
+   sg          Manipulate syncgroups
    acl         Read or mutate the ACLs
    help        Display help for commands or topics
 
diff --git a/cmd/sb/shell.go b/cmd/sb/shell.go
index 869a4b4..61522b7 100644
--- a/cmd/sb/shell.go
+++ b/cmd/sb/shell.go
@@ -5,6 +5,7 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"io"
 	"os"
@@ -80,6 +81,7 @@
 	fields []string, isTerminal bool) error {
 	commands.SetCtx(ctx)
 	commands.SetDB(db)
+	resetFlags(cmdSb, false)
 	if err := cmdline.ParseAndRun(cmdSb, env, fields); err != nil {
 		if isTerminal {
 			fmt.Fprintln(env.Stderr, "Error:", err)
@@ -91,6 +93,19 @@
 	return nil
 }
 
+func resetFlags(cmd *cmdline.Command, resetThisLevel bool) {
+	if resetThisLevel {
+		if cmd.ParsedFlags != nil {
+			cmd.ParsedFlags.Visit(func(f *flag.Flag) {
+				cmd.ParsedFlags.Set(f.Name, f.DefValue)
+			})
+		}
+	}
+	for _, nextCmd := range cmd.Children {
+		resetFlags(nextCmd, true)
+	}
+}
+
 func help(args []string) error {
 	switch len(args) {
 	case 0: