mirror of https://github.com/hashicorp/consul
command/intention/create
parent
a5ecdc5798
commit
961e9c1eaf
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,6 +51,21 @@ type Intention struct {
|
||||||
ModifyIndex uint64
|
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
|
// IntentionAction is the action that the intention represents. This
|
||||||
// can be "allow" or "deny" to whitelist or blacklist intentions.
|
// can be "allow" or "deny" to whitelist or blacklist intentions.
|
||||||
type IntentionAction string
|
type IntentionAction string
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/hashicorp/consul/command/exec"
|
"github.com/hashicorp/consul/command/exec"
|
||||||
"github.com/hashicorp/consul/command/forceleave"
|
"github.com/hashicorp/consul/command/forceleave"
|
||||||
"github.com/hashicorp/consul/command/info"
|
"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/join"
|
||||||
"github.com/hashicorp/consul/command/keygen"
|
"github.com/hashicorp/consul/command/keygen"
|
||||||
"github.com/hashicorp/consul/command/keyring"
|
"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("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("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("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("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("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 })
|
Register("keyring", func(ui cli.Ui) (cli.Command, error) { return keyring.New(ui), nil })
|
||||||
|
|
|
@ -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.
|
||||||
|
`
|
|
@ -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")
|
||||||
|
}
|
|
@ -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.
|
||||||
|
`
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue