diff --git a/api/connect_intention.go b/api/connect_intention.go index b7af3163c..95ed335a8 100644 --- a/api/connect_intention.go +++ b/api/connect_intention.go @@ -72,13 +72,18 @@ func (i *Intention) DestinationString() string { } func (i *Intention) partString(ns, n string) string { - if ns != "" { + // For now we omit the default namespace from the output. In the future + // we might want to look at this and show this in a multi-namespace world. + if ns != "" && ns != IntentionDefaultNamespace { n = ns + "/" + n } return n } +// IntentionDefaultNamespace is the default namespace value. +const IntentionDefaultNamespace = "default" + // IntentionAction is the action that the intention represents. This // can be "allow" or "deny" to whitelist or blacklist intentions. type IntentionAction string diff --git a/command/commands_oss.go b/command/commands_oss.go index 79636c598..9eba97b09 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/command/info" "github.com/hashicorp/consul/command/intention" ixncreate "github.com/hashicorp/consul/command/intention/create" + ixnget "github.com/hashicorp/consul/command/intention/get" "github.com/hashicorp/consul/command/join" "github.com/hashicorp/consul/command/keygen" "github.com/hashicorp/consul/command/keyring" @@ -69,7 +70,8 @@ func init() { Register("force-leave", func(ui cli.Ui) (cli.Command, error) { return forceleave.New(ui), nil }) Register("info", func(ui cli.Ui) (cli.Command, error) { return info.New(ui), nil }) Register("intention", func(ui cli.Ui) (cli.Command, error) { return intention.New(), nil }) - Register("intention create", func(ui cli.Ui) (cli.Command, error) { return ixncreate.New(), nil }) + Register("intention create", func(ui cli.Ui) (cli.Command, error) { return ixncreate.New(ui), nil }) + Register("intention get", func(ui cli.Ui) (cli.Command, error) { return ixnget.New(ui), nil }) Register("join", func(ui cli.Ui) (cli.Command, error) { return join.New(ui), nil }) Register("keygen", func(ui cli.Ui) (cli.Command, error) { return keygen.New(ui), nil }) Register("keyring", func(ui cli.Ui) (cli.Command, error) { return keyring.New(ui), nil }) diff --git a/command/intention/finder/finder.go b/command/intention/finder/finder.go index c8db7ba5c..f4c6109a0 100644 --- a/command/intention/finder/finder.go +++ b/command/intention/finder/finder.go @@ -1,6 +1,7 @@ package finder import ( + "strings" "sync" "github.com/hashicorp/consul/api" @@ -22,6 +23,9 @@ type Finder struct { // Find finds the intention that matches the given src and dst. This will // return nil when the result is not found. func (f *Finder) Find(src, dst string) (*api.Intention, error) { + src = StripDefaultNS(src) + dst = StripDefaultNS(dst) + f.lock.Lock() defer f.lock.Unlock() @@ -44,3 +48,15 @@ func (f *Finder) Find(src, dst string) (*api.Intention, error) { return nil, nil } + +// StripDefaultNS strips the default namespace from an argument. For now, +// the API and lookups strip this value from string output so we strip it. +func StripDefaultNS(v string) string { + if idx := strings.IndexByte(v, '/'); idx > 0 { + if v[:idx] == api.IntentionDefaultNamespace { + return v[:idx+1] + } + } + + return v +} diff --git a/command/intention/get/get.go b/command/intention/get/get.go new file mode 100644 index 000000000..0c0eba77e --- /dev/null +++ b/command/intention/get/get.go @@ -0,0 +1,133 @@ +package create + +import ( + "flag" + "fmt" + "io" + "sort" + "time" + + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/intention/finder" + "github.com/mitchellh/cli" + "github.com/ryanuber/columnize" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + // testStdin is the input for testing. + testStdin io.Reader +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + // Create and test the HTTP client + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + // Get the intention ID to load + var id string + args = c.flags.Args() + switch len(args) { + case 1: + id = args[0] + + case 2: + f := &finder.Finder{Client: client} + ixn, err := f.Find(args[0], args[1]) + if err != nil { + c.UI.Error(fmt.Sprintf("Error looking up intention: %s", err)) + return 1 + } + if ixn == nil { + c.UI.Error(fmt.Sprintf( + "Intention with source %q and destination %q not found.", + args[0], args[1])) + return 1 + } + + id = ixn.ID + + default: + c.UI.Error(fmt.Sprintf("Error: get requires exactly 1 or 2 arguments")) + return 1 + } + + // Read the intention + ixn, _, err := client.Connect().IntentionGet(id, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading the intention: %s", err)) + return 1 + } + + // Format the tabular data + data := []string{ + fmt.Sprintf("Source:|%s", ixn.SourceString()), + fmt.Sprintf("Destination:|%s", ixn.DestinationString()), + fmt.Sprintf("Action:|%s", ixn.Action), + fmt.Sprintf("ID:|%s", ixn.ID), + } + if v := ixn.Description; v != "" { + data = append(data, fmt.Sprintf("Description:|%s", v)) + } + if len(ixn.Meta) > 0 { + var keys []string + for k := range ixn.Meta { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + data = append(data, fmt.Sprintf("Meta[%s]:|%s", k, ixn.Meta[k])) + } + } + data = append(data, + fmt.Sprintf("Created At:|%s", ixn.CreatedAt.Local().Format(time.RFC850)), + ) + + c.UI.Output(columnize.SimpleFormat(data)) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return c.help +} + +const synopsis = "Show information about an intention." +const help = ` +Usage: consul intention get [options] SRC DST +Usage: consul intention get [options] ID + + Read and show the details about an intention. The intention can be looked + up via an exact source/destination match or via the unique intention ID. + + $ consul intention get web db + +`