mirror of https://github.com/status-im/consul.git
command/intention/create
This commit is contained in:
parent
a5ecdc5798
commit
961e9c1eaf
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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