veyron/tools/mgmt/nodex: add commands for manipulating ACLs
Remaining functionality to manipulate ACLs on the node manager with
the nodex command.
Change-Id: Ib41d2a23ddad3748d422a9f50174016479436635
diff --git a/tools/mgmt/nodex/acl_impl.go b/tools/mgmt/nodex/acl_impl.go
index b4efc6a..384d0a7 100644
--- a/tools/mgmt/nodex/acl_impl.go
+++ b/tools/mgmt/nodex/acl_impl.go
@@ -7,7 +7,10 @@
"sort"
"veyron.io/veyron/veyron2/rt"
+ "veyron.io/veyron/veyron2/security"
"veyron.io/veyron/veyron2/services/mgmt/node"
+ "veyron.io/veyron/veyron2/services/security/access"
+ "veyron.io/veyron/veyron2/verror"
"veyron.io/veyron/veyron/lib/cmdline"
)
@@ -50,30 +53,143 @@
// TODO(rjkroege): Update for custom labels.
output := make([]formattedACLEntry, 0)
for k, _ := range objACL.In {
- output = append(output, formattedACLEntry{string(k), "in", objACL.In[k].String()})
+ output = append(output, formattedACLEntry{string(k), "", objACL.In[k].String()})
}
for k, _ := range objACL.NotIn {
- output = append(output, formattedACLEntry{string(k), "nin", objACL.NotIn[k].String()})
+ output = append(output, formattedACLEntry{string(k), "!", objACL.NotIn[k].String()})
}
sort.Sort(byBlessing(output))
for _, e := range output {
- fmt.Fprintf(cmd.Stdout(), "%s %s %s\n", e.blessing, e.inout, e.label)
+ fmt.Fprintf(cmd.Stdout(), "%s %s%s\n", e.blessing, e.inout, e.label)
}
return nil
}
-// TODO(rjkroege): Implement the remaining sub-commands.
-// nodex acl set <target> ([!]<label> <blessing>)...
+var cmdSet = &cmdline.Command{
+ Run: runSet,
+ Name: "set",
+ Short: "Set ACLs for the given target.",
+ Long: "Set ACLs for the given target",
+ ArgsName: "<node manager name> (<blessing> [!]<label>)...",
+ ArgsLong: `
+<node manager name> can be a Vanadium name for a node manager,
+application installation or instance.
+
+<blessing> is a blessing pattern.
+
+<label> is a character sequence defining a set of rights: some subset
+of the defined standard Vanadium labels of XRWADM where X is resolve,
+R is read, W for write, A for admin, D for debug and M is for
+monitoring. By default, the combination of <blessing>, <label>
+replaces whatever entry is present in the ACL's In field for the
+<blessing> but it can instead be added to the NotIn field by prefacing
+<label> with a '!' character. Use the <label> of 0 to clear the label.
+
+For example: root/self !0 will clear the NotIn field for blessingroot/self.`,
+}
+
+type inAdditionTuple struct {
+ blessing security.BlessingPattern
+ ls *security.LabelSet
+}
+
+type notInAdditionTuple struct {
+ blessing string
+ ls *security.LabelSet
+}
+
+func runSet(cmd *cmdline.Command, args []string) error {
+ if got := len(args); !((got%2) == 1 && got >= 3) {
+ return cmd.UsageErrorf("set: incorrect number of arguments %d, must be 1 + 2n", got)
+ }
+
+ vanaName := args[0]
+ pairs := args[1:]
+
+ // Parse each pair and aggregate what should happen to all of them
+ notInDeletions := make([]string, 0)
+ inDeletions := make([]security.BlessingPattern, 0)
+ inAdditions := make([]inAdditionTuple, 0)
+ notInAdditions := make([]notInAdditionTuple, 0)
+
+ for i := 0; i < len(pairs); i += 2 {
+ blessing, label := pairs[i], pairs[i+1]
+ if label == "" || label == "!" {
+ return cmd.UsageErrorf("failed to parse LabelSet pair %s, %s", blessing, label)
+ }
+
+ switch {
+ case label == "!0":
+ notInDeletions = append(notInDeletions, blessing)
+ case label == "0":
+ inDeletions = append(inDeletions, security.BlessingPattern(blessing))
+ case label[0] == '!':
+ // e.g. !RW
+ ls := new(security.LabelSet)
+ if err := ls.FromString(label[1:]); err != nil {
+ return cmd.UsageErrorf("failed to parse LabelSet %s: %v", label, err)
+ }
+ notInAdditions = append(notInAdditions, notInAdditionTuple{blessing, ls})
+ default:
+ // e.g. X
+ ls := new(security.LabelSet)
+ if err := ls.FromString(label); err != nil {
+ return fmt.Errorf("failed to parse LabelSet %s: %v", label, err)
+ }
+ inAdditions = append(inAdditions, inAdditionTuple{security.BlessingPattern(blessing), ls})
+ }
+ }
+
+ // Set the ACLs on the specified name.
+ for {
+ objACL, etag, err := node.ApplicationClient(vanaName).GetACL(rt.R().NewContext())
+ if err != nil {
+ return cmd.UsageErrorf("GetACL(%s) failed: %v", vanaName, err)
+ }
+
+ // Insert into objACL
+ for _, b := range notInDeletions {
+ if _, contains := objACL.NotIn[b]; !contains {
+ fmt.Fprintf(cmd.Stderr(), "WARNING: ignoring attempt to remove non-existing NotIn ACL for %s\n", b)
+ }
+ delete(objACL.NotIn, b)
+ }
+
+ for _, b := range inDeletions {
+ if _, contains := objACL.In[b]; !contains {
+ fmt.Fprintf(cmd.Stderr(), "WARNING: ignoring attempt to remove non-existing In ACL for %s\n", b)
+ }
+ delete(objACL.In, b)
+ }
+
+ for _, b := range inAdditions {
+ objACL.In[b.blessing] = *b.ls
+ }
+
+ for _, b := range notInAdditions {
+ objACL.NotIn[b.blessing] = *b.ls
+ }
+
+ switch err := node.ApplicationClient(vanaName).SetACL(rt.R().NewContext(), objACL, etag); {
+ case err != nil && !verror.Is(err, access.ErrBadEtag):
+ return cmd.UsageErrorf("SetACL(%s) failed: %v", vanaName, err)
+ case err == nil:
+ return nil
+ }
+ fmt.Fprintf(cmd.Stderr(), "WARNING: trying again because of asynchronous change\n")
+ }
+ return nil
+}
func aclRoot() *cmdline.Command {
return &cmdline.Command{
Name: "acl",
- Short: "Tool for creating associations between Vanadium blessings and a system account",
+ Short: "Tool for setting node manager ACLs",
Long: `
The acl tool manages ACLs on the node manger, installations and instances.
`,
- Children: []*cmdline.Command{cmdGet},
+ Children: []*cmdline.Command{cmdGet, cmdSet},
}
}
diff --git a/tools/mgmt/nodex/acl_test.go b/tools/mgmt/nodex/acl_test.go
index ccd6d6b..9f4d581 100644
--- a/tools/mgmt/nodex/acl_test.go
+++ b/tools/mgmt/nodex/acl_test.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "fmt"
"reflect"
"strings"
"testing"
@@ -9,6 +10,8 @@
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/rt"
"veyron.io/veyron/veyron2/security"
+ "veyron.io/veyron/veyron2/services/security/access"
+ "veyron.io/veyron/veyron2/verror"
)
func TestACLGetCommand(t *testing.T) {
@@ -45,7 +48,7 @@
if err := cmd.Execute([]string{"acl", "get", nodeName}); err != nil {
t.Fatalf("%v, ouput: %v, error: %v", err)
}
- if expected, got := "root/bob/... nin W\nroot/other in R\nroot/self/... in XRWADM", strings.TrimSpace(stdout.String()); got != expected {
+ if expected, got := "root/bob/... !W\nroot/other R\nroot/self/... XRWADM", strings.TrimSpace(stdout.String()); got != expected {
t.Fatalf("Unexpected output from get. Got %q, expected %q", got, expected)
}
if got, expected := tape.Play(), []interface{}{"GetACL"}; !reflect.DeepEqual(expected, got) {
@@ -54,3 +57,256 @@
tape.Rewind()
stdout.Reset()
}
+
+func TestACLSetCommand(t *testing.T) {
+ runtime := rt.Init()
+ tape := NewTape()
+ server, endpoint, err := startServer(t, runtime, tape)
+ if err != nil {
+ return
+ }
+ defer stopServer(t, server)
+
+ // Setup the command-line.
+ cmd := root()
+ var stdout, stderr bytes.Buffer
+ cmd.Init(nil, &stdout, &stderr)
+ nodeName := naming.JoinAddressName(endpoint.String(), "")
+
+ // Some tests to validate parse.
+ if err := cmd.Execute([]string{"acl", "set", nodeName}); err == nil {
+ t.Fatalf("failed to correctly detect insufficient parameters")
+ }
+ if expected, got := "ERROR: set: incorrect number of arguments 1, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+ }
+
+ stderr.Reset()
+ stdout.Reset()
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "foo"}); err == nil {
+ t.Fatalf("failed to correctly detect insufficient parameters")
+ }
+ if expected, got := "ERROR: set: incorrect number of arguments 2, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+ }
+
+ stderr.Reset()
+ stdout.Reset()
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "foo", "bar", "ohno"}); err == nil {
+ t.Fatalf("failed to correctly detect insufficient parameters")
+ }
+ if expected, got := "ERROR: set: incorrect number of arguments 4, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+ }
+
+ stderr.Reset()
+ stdout.Reset()
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "foo", "!"}); err == nil {
+ t.Fatalf("failed to detect invalid parameter")
+ }
+ if expected, got := "ERROR: failed to parse LabelSet pair foo, !", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+ }
+
+ // Correct operation in the absence of errors.
+ stderr.Reset()
+ stdout.Reset()
+ tape.SetResponses([]interface{}{GetACLResponse{
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/other": security.LabelSet(security.ReadLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/bob": security.LabelSet(security.WriteLabel),
+ },
+ },
+ etag: "anEtagForToday",
+ err: nil,
+ },
+ verror.Make(access.ErrBadEtag, fmt.Sprintf("etag mismatch in:%s vers:%s", "anEtagForToday", "anEtagForTomorrow")),
+ GetACLResponse{
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/other": security.LabelSet(security.ReadLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/bob": security.LabelSet(security.WriteLabel),
+ "root/alice/cat": security.LabelSet(security.AdminLabel),
+ },
+ },
+ etag: "anEtagForTomorrow",
+ err: nil,
+ },
+ nil,
+ })
+
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err != nil {
+ t.Fatalf("SetACL failed: %v", err)
+ }
+
+ if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+ if expected, got := "WARNING: trying again because of asynchronous change", strings.TrimSpace(stderr.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+
+ expected := []interface{}{
+ "GetACL",
+ SetACLStimulus{
+ fun: "SetACL",
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/vana/bad": security.LabelSet(security.ResolveLabel | security.ReadLabel),
+ },
+ },
+ etag: "anEtagForToday",
+ },
+ "GetACL",
+ SetACLStimulus{
+ fun: "SetACL",
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/alice/cat": security.LabelSet(security.AdminLabel),
+ "root/vana/bad": security.LabelSet(security.ResolveLabel | security.ReadLabel),
+ },
+ },
+ etag: "anEtagForTomorrow",
+ },
+ }
+
+ if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+ t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+ }
+ tape.Rewind()
+ stdout.Reset()
+ stderr.Reset()
+
+ // GetACL fails.
+ tape.SetResponses([]interface{}{GetACLResponse{
+ acl: security.ACL{In: nil, NotIn: nil},
+ etag: "anEtagForToday",
+ err: verror.BadArgf("oops!"),
+ },
+ })
+
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err == nil {
+ t.Fatalf("GetACL RPC inside acl set command failed but error wrongly not detected")
+ }
+ if expected, got := "ERROR: GetACL("+nodeName+") failed: oops!", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, prefix %q", got, expected)
+ }
+ if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+ expected = []interface{}{
+ "GetACL",
+ }
+
+ if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+ t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+ }
+ tape.Rewind()
+ stdout.Reset()
+ stderr.Reset()
+
+ // SetACL fails with not a bad etag failure.
+ tape.SetResponses([]interface{}{GetACLResponse{
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/other": security.LabelSet(security.ReadLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/bob": security.LabelSet(security.WriteLabel),
+ },
+ },
+ etag: "anEtagForToday",
+ err: nil,
+ },
+ verror.BadArgf("oops!"),
+ })
+
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err == nil {
+ t.Fatalf("SetACL should have failed: %v", err)
+ }
+ if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+ if expected, got := "ERROR: SetACL("+nodeName+") failed: oops!", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+ t.Fatalf("Unexpected output from list. Got %q, prefix %q", got, expected)
+ }
+
+ expected = []interface{}{
+ "GetACL",
+ SetACLStimulus{
+ fun: "SetACL",
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{
+ "root/self/...": security.AllLabels,
+ "root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+ },
+ NotIn: map[string]security.LabelSet{
+ "root/vana/bad": security.LabelSet(security.ResolveLabel | security.ReadLabel),
+ },
+ },
+ etag: "anEtagForToday",
+ },
+ }
+
+ if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+ t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+ }
+ tape.Rewind()
+ stdout.Reset()
+ stderr.Reset()
+
+ // Trying to delete non-existent items.
+ stderr.Reset()
+ stdout.Reset()
+ tape.SetResponses([]interface{}{GetACLResponse{
+ acl: security.ACL{
+ In: map[security.BlessingPattern]security.LabelSet{},
+ NotIn: map[string]security.LabelSet{},
+ },
+ etag: "anEtagForToday",
+ err: nil,
+ },
+ nil,
+ })
+
+ if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/notin/missing", "!0", "root/vana/in/missing", "0"}); err != nil {
+ t.Fatalf("SetACL failed: %v", err)
+ }
+ if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+ if expected, got := "WARNING: ignoring attempt to remove non-existing NotIn ACL for root/vana/notin/missing\nWARNING: ignoring attempt to remove non-existing In ACL for root/vana/in/missing", strings.TrimSpace(stderr.String()); got != expected {
+ t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+ }
+
+ expected = []interface{}{
+ "GetACL",
+ SetACLStimulus{
+ fun: "SetACL",
+ acl: security.ACL{},
+ etag: "anEtagForToday",
+ },
+ }
+ if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+ t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+ }
+ tape.Rewind()
+ stdout.Reset()
+ stderr.Reset()
+}
diff --git a/tools/mgmt/nodex/doc.go b/tools/mgmt/nodex/doc.go
index 7f2fa78..6b3ff6d 100644
--- a/tools/mgmt/nodex/doc.go
+++ b/tools/mgmt/nodex/doc.go
@@ -13,7 +13,10 @@
associate Tool for creating associations between Vanadium blessings and a
system account
claim Claim the node.
- instance Subtool for managing application instances
+ stop Stop the given application instance.
+ suspend Suspend the given application instance.
+ resume Resume the given application instance.
+ acl Tool for setting node manager ACLs
help Display help for commands or topics
Run "nodex help [command]" for command usage.
@@ -124,66 +127,75 @@
<grant extension> is used to extend the default blessing of the current
principal when blessing the app instance.
-Nodex Instance
-
-The instance tool permits controlling application instances.
-
-Usage:
- nodex instance <command>
-
-The nodex instance commands are:
- stop Stop the given application instance.
- suspend Suspend the given application instance.
- resume Resume the given application instance.
- refresh Refresh the given application instance.
- suspend Restart the given application instance.
-
-Nodex Instance Stop
+Nodex Stop
Stop the given application instance.
Usage:
- nodex instance stop <app instance> [<deadline>]
+ nodex stop <app instance>
<app instance> is the veyron object name of the application instance to stop.
-<deadline> is the optional deadline to shut down by. If not provided, defaults
-to 5 seconds.
-Nodex Instance Suspend
+Nodex Suspend
Suspend the given application instance.
Usage:
- nodex instance suspend <app instance>
+ nodex suspend <app instance>
-<app instance> is the veyron object name of the application instance to stop.
+<app instance> is the veyron object name of the application instance to suspend.
-Nodex Instance Resume
+Nodex Resume
Resume the given application instance.
Usage:
- nodex instance resume <app instance>
+ nodex resume <app instance>
-<app instance> is the veyron object name of the application instance to stop.
+<app instance> is the veyron object name of the application instance to resume.
-Nodex Instance Refresh
+Nodex Acl
-Refresh the given application instance.
+The acl tool manages ACLs on the node manger, installations and instances.
Usage:
- nodex instance refresh <app instance>
+ nodex acl <command>
-<app instance> is the veyron object name of the application instance to stop.
+The nodex acl commands are:
+ get Get ACLs for the given target.
+ set Set ACLs for the given target.
-Nodex Instance Suspend
+Nodex Acl Get
-Restart the given application instance.
+Get ACLs for the given target.
Usage:
- nodex instance suspend <app instance>
+ nodex acl get <node manager name>
-<app instance> is the veyron object name of the application instance to stop.
+<node manager name> can be a Vanadium name for a node manager, application
+installation or instance.
+
+Nodex Acl Set
+
+Set ACLs for the given target
+
+Usage:
+ nodex acl set <node manager name> (<blessing> [!]<label>)...
+
+<node manager name> can be a Vanadium name for a node manager, application
+installation or instance.
+
+<blessing> is a blessing pattern.
+
+<label> is a character sequence defining a set of rights: some subset of the
+defined standard Vanadium labels of XRWADM where X is resolve, R is read, W for
+write, A for admin, D for debug and M is for monitoring. By default, the
+combination of <blessing>, <label> replaces whatever entry is present in the
+ACL's In field for the <blessing> but it can instead be added to the NotIn field
+by prefacing <label> with a '!' character. Use the <label> of 0 to clear the
+label.
+
+For example: root/self !0 will clear the NotIn field for blessingroot/self.
Nodex Help
diff --git a/tools/mgmt/nodex/impl_test.go b/tools/mgmt/nodex/impl_test.go
index 3eb47c7..ce92e31 100644
--- a/tools/mgmt/nodex/impl_test.go
+++ b/tools/mgmt/nodex/impl_test.go
@@ -3,20 +3,21 @@
import (
"bytes"
"fmt"
+ "log"
"reflect"
"strings"
"testing"
- "veyron.io/veyron/veyron2/security"
- "veyron.io/veyron/veyron2/services/mgmt/binary"
- "veyron.io/veyron/veyron2/services/mgmt/node"
-
- "veyron.io/veyron/veyron/profiles"
"veyron.io/veyron/veyron2"
"veyron.io/veyron/veyron2/ipc"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/rt"
+ "veyron.io/veyron/veyron2/security"
+ "veyron.io/veyron/veyron2/services/mgmt/binary"
+ "veyron.io/veyron/veyron2/services/mgmt/node"
"veyron.io/veyron/veyron2/vlog"
+
+ "veyron.io/veyron/veyron/profiles"
)
type mockNodeInvoker struct {
@@ -53,7 +54,7 @@
case error:
return r
}
- i.t.Fatalf("AssociateAccount (mock) response %v is of bad type", ri)
+ log.Fatalf("AssociateAccount (mock) response %v is of bad type", ri)
return nil
}
@@ -101,7 +102,24 @@
err error
}
-func (mni *mockNodeInvoker) SetACL(ipc.ServerContext, security.ACL, string) error { return nil }
+type SetACLStimulus struct {
+ fun string
+ acl security.ACL
+ etag string
+}
+
+func (mni *mockNodeInvoker) SetACL(_ ipc.ServerContext, acl security.ACL, etag string) error {
+ ri := mni.tape.Record(SetACLStimulus{"SetACL", acl, etag})
+ switch r := ri.(type) {
+ case nil:
+ return nil
+ case error:
+ return r
+ }
+ log.Fatalf("AssociateAccount (mock) response %v is of bad type\n", ri)
+ return nil
+}
+
func (mni *mockNodeInvoker) GetACL(ipc.ServerContext) (security.ACL, string, error) {
ir := mni.tape.Record("GetACL")
r := ir.(GetACLResponse)
diff --git a/tools/mgmt/nodex/main.go b/tools/mgmt/nodex/main.go
index a99a159..b3c5b7b 100644
--- a/tools/mgmt/nodex/main.go
+++ b/tools/mgmt/nodex/main.go
@@ -4,6 +4,8 @@
// 2) go:generate performs $NAME expansion, so the bash cmd can't contain '$'.
// 3) We generate into a *.tmp file first, otherwise go run will pick up the
// initially empty *.go file, and fail.
+// TODO(rjkroege): The below fails because _test.go files are present. Fix this.
+// An alternative in Sam/Acme: Edit /\/\*/+1, /\*\//-1 <./nodex help -style'='godoc ...
//
//go:generate bash -c "{ echo -e '// This file was auto-generated via go generate.\n// DO NOT UPDATE MANUALLY\n\n/*' && veyron go run *.go help -style=godoc ... && echo -e '*/\npackage main'; } > ./doc.go.tmp && mv ./doc.go.tmp ./doc.go"