command/intention/create

This commit is contained in:
Mitchell Hashimoto 2018-05-11 19:47:26 -07:00
parent a5ecdc5798
commit 961e9c1eaf
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
6 changed files with 462 additions and 0 deletions

View File

@ -1,6 +1,7 @@
package api
import (
"fmt"
"time"
)
@ -50,6 +51,21 @@ type Intention struct {
ModifyIndex uint64
}
// String returns human-friendly output describing ths intention.
func (i *Intention) String() string {
source := i.SourceName
if i.SourceNS != "" {
source = i.SourceNS + "/" + source
}
dest := i.DestinationName
if i.DestinationNS != "" {
dest = i.DestinationNS + "/" + dest
}
return fmt.Sprintf("%s => %s (%s)", source, dest, i.Action)
}
// IntentionAction is the action that the intention represents. This
// can be "allow" or "deny" to whitelist or blacklist intentions.
type IntentionAction string

View File

@ -12,6 +12,8 @@ import (
"github.com/hashicorp/consul/command/exec"
"github.com/hashicorp/consul/command/forceleave"
"github.com/hashicorp/consul/command/info"
"github.com/hashicorp/consul/command/intention"
ixncreate "github.com/hashicorp/consul/command/intention/create"
"github.com/hashicorp/consul/command/join"
"github.com/hashicorp/consul/command/keygen"
"github.com/hashicorp/consul/command/keyring"
@ -66,6 +68,8 @@ func init() {
Register("exec", func(ui cli.Ui) (cli.Command, error) { return exec.New(ui, MakeShutdownCh()), nil })
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("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 })

View File

@ -0,0 +1,193 @@
package create
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
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
// flags
flagAllow bool
flagDeny bool
flagFile bool
flagReplace bool
flagMeta map[string]string
// testStdin is the input for testing.
testStdin io.Reader
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.flagAllow, "allow", false,
"Create an intention that allows when matched.")
c.flags.BoolVar(&c.flagDeny, "deny", false,
"Create an intention that denies when matched.")
c.flags.BoolVar(&c.flagFile, "file", false,
"Read intention data from one or more files.")
c.flags.BoolVar(&c.flagReplace, "replace", false,
"Replace matching intentions.")
c.flags.Var((*flags.FlagMapValue)(&c.flagMeta), "meta",
"Metadata to set on the intention, formatted as key=value. This flag "+
"may be specified multiple times to set multiple meta fields.")
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
}
// Default to allow
if !c.flagAllow && !c.flagDeny {
c.flagAllow = true
}
// If both are specified it is an error
if c.flagAllow && c.flagDeny {
c.UI.Error("Only one of -allow or -deny may be specified.")
return 1
}
// Check for arg validation
args = c.flags.Args()
ixns, err := c.ixnsFromArgs(args)
if err != nil {
c.UI.Error(fmt.Sprintf("Error: %s", err))
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
}
// Go through and create each intention
for _, ixn := range ixns {
_, _, err := client.Connect().IntentionCreate(ixn, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating intention %q: %s", ixn, err))
return 1
}
c.UI.Output(fmt.Sprintf("Created: %s", ixn))
}
return 0
}
// ixnsFromArgs returns the set of intentions to create based on the arguments
// given and the flags set. This will call ixnsFromFiles if the -file flag
// was set.
func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) {
// If we're in file mode, load from files
if c.flagFile {
return c.ixnsFromFiles(args)
}
// From args we require exactly two
if len(args) != 2 {
return nil, fmt.Errorf("Must specify two arguments: source and destination")
}
return []*api.Intention{&api.Intention{
SourceName: args[0],
DestinationName: args[1],
SourceType: api.IntentionSourceConsul,
Action: c.ixnAction(),
Meta: c.flagMeta,
}}, nil
}
func (c *cmd) ixnsFromFiles(args []string) ([]*api.Intention, error) {
var result []*api.Intention
for _, path := range args {
f, err := os.Open(path)
if err != nil {
return nil, err
}
var ixn api.Intention
err = json.NewDecoder(f).Decode(&ixn)
f.Close()
if err != nil {
return nil, err
}
result = append(result, &ixn)
}
return result, nil
}
// ixnAction returns the api.IntentionAction based on the flag set.
func (c *cmd) ixnAction() api.IntentionAction {
if c.flagAllow {
return api.IntentionActionAllow
}
return api.IntentionActionDeny
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return c.help
}
const synopsis = "Create intentions for service connections."
const help = `
Usage: consul intention create [options] SRC DST
Usage: consul intention create [options] -file FILE...
Create one or more intentions. The data can be specified as a single
source and destination pair or via a set of files when the "-file" flag
is specified.
$ consul intention create web db
To consume data from a set of files:
$ consul intention create -file one.json two.json
When specifying the "-file" flag, "-" may be used once to read from stdin:
$ echo "{ ... }" | consul intention create -file -
An "allow" intention is created by default (whitelist). To create a
"deny" intention, the "-deny" flag should be specified.
If a conflicting intention is found, creation will fail. To replace any
conflicting intentions, specify the "-replace" flag. This will replace any
conflicting intentions with the intention specified in this command.
Metadata and any other fields of the previous intention will not be
preserved.
Additional flags and more advanced use cases are detailed below.
`

View File

@ -0,0 +1,188 @@
package create
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(nil).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestCommand_Validation(t *testing.T) {
t.Parallel()
ui := cli.NewMockUi()
c := New(ui)
cases := map[string]struct {
args []string
output string
}{
"-allow and -deny": {
[]string{"-allow", "-deny", "foo", "bar"},
"one of -allow",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
require := require.New(t)
c.init()
// Ensure our buffer is always clear
if ui.ErrorWriter != nil {
ui.ErrorWriter.Reset()
}
if ui.OutputWriter != nil {
ui.OutputWriter.Reset()
}
require.Equal(1, c.Run(tc.args))
output := ui.ErrorWriter.String()
require.Contains(output, tc.output)
})
}
}
func TestCommand(t *testing.T) {
t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"foo", "bar",
}
require.Equal(0, c.Run(args), ui.ErrorWriter.String())
ixns, _, err := client.Connect().Intentions(nil)
require.NoError(err)
require.Len(ixns, 1)
require.Equal("foo", ixns[0].SourceName)
require.Equal("bar", ixns[0].DestinationName)
require.Equal(api.IntentionActionAllow, ixns[0].Action)
}
func TestCommand_deny(t *testing.T) {
t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-deny",
"foo", "bar",
}
require.Equal(0, c.Run(args), ui.ErrorWriter.String())
ixns, _, err := client.Connect().Intentions(nil)
require.NoError(err)
require.Len(ixns, 1)
require.Equal("foo", ixns[0].SourceName)
require.Equal("bar", ixns[0].DestinationName)
require.Equal(api.IntentionActionDeny, ixns[0].Action)
}
func TestCommand_meta(t *testing.T) {
t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-meta", "hello=world",
"foo", "bar",
}
require.Equal(0, c.Run(args), ui.ErrorWriter.String())
ixns, _, err := client.Connect().Intentions(nil)
require.NoError(err)
require.Len(ixns, 1)
require.Equal("foo", ixns[0].SourceName)
require.Equal("bar", ixns[0].DestinationName)
require.Equal(map[string]string{"hello": "world"}, ixns[0].Meta)
}
func TestCommand_File(t *testing.T) {
t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
contents := `{ "SourceName": "foo", "DestinationName": "bar", "Action": "allow" }`
f := testutil.TempFile(t, "intention-create-command-file")
defer os.Remove(f.Name())
if _, err := f.WriteString(contents); err != nil {
t.Fatalf("err: %#v", err)
}
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-file",
f.Name(),
}
require.Equal(0, c.Run(args), ui.ErrorWriter.String())
ixns, _, err := client.Connect().Intentions(nil)
require.NoError(err)
require.Len(ixns, 1)
require.Equal("foo", ixns[0].SourceName)
require.Equal("bar", ixns[0].DestinationName)
require.Equal(api.IntentionActionAllow, ixns[0].Action)
}
func TestCommand_FileNoExist(t *testing.T) {
t.Parallel()
require := require.New(t)
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-file",
"shouldnotexist.txt",
}
require.Equal(1, c.Run(args), ui.ErrorWriter.String())
require.Contains(ui.ErrorWriter.String(), "no such file")
}

View File

@ -0,0 +1,48 @@
package intention
import (
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New() *cmd {
return &cmd{}
}
type cmd struct{}
func (c *cmd) Run(args []string) int {
return cli.RunResultHelp
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Interact with Connect service intentions"
const help = `
Usage: consul intention <subcommand> [options] [args]
This command has subcommands for interacting with intentions. Intentions
are the permissions for what services are allowed to communicate via
Connect. Here are some simple examples, and more detailed examples are
available in the subcommands or the documentation.
Create an intention to allow "web" to talk to "db":
$ consul intention create web db
Test whether a "web" is allowed to connect to "db":
$ consul intention check web db
Find all intentions for communicating to the "db" service:
$ consul intention match db
For more examples, ask for subcommand help or view the documentation.
`

View File

@ -0,0 +1,13 @@
package intention
import (
"strings"
"testing"
)
func TestCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New().Help(), '\t') {
t.Fatal("help has tabs")
}
}