1package create
2
3import (
4	"encoding/json"
5	"flag"
6	"fmt"
7	"io"
8	"os"
9
10	"github.com/hashicorp/consul/api"
11	"github.com/hashicorp/consul/command/flags"
12	"github.com/hashicorp/consul/command/intention"
13	"github.com/mitchellh/cli"
14)
15
16func New(ui cli.Ui) *cmd {
17	c := &cmd{UI: ui}
18	c.init()
19	return c
20}
21
22type cmd struct {
23	UI    cli.Ui
24	flags *flag.FlagSet
25	http  *flags.HTTPFlags
26	help  string
27
28	// flags
29	flagAllow   bool
30	flagDeny    bool
31	flagFile    bool
32	flagReplace bool
33	flagMeta    map[string]string
34
35	// testStdin is the input for testing.
36	testStdin io.Reader
37}
38
39func (c *cmd) init() {
40	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
41	c.flags.BoolVar(&c.flagAllow, "allow", false,
42		"Create an intention that allows when matched.")
43	c.flags.BoolVar(&c.flagDeny, "deny", false,
44		"Create an intention that denies when matched.")
45	c.flags.BoolVar(&c.flagFile, "file", false,
46		"Read intention data from one or more files.")
47	c.flags.BoolVar(&c.flagReplace, "replace", false,
48		"Replace matching intentions.")
49	c.flags.Var((*flags.FlagMapValue)(&c.flagMeta), "meta",
50		"Metadata to set on the intention, formatted as key=value. This flag "+
51			"may be specified multiple times to set multiple meta fields.")
52
53	c.http = &flags.HTTPFlags{}
54	flags.Merge(c.flags, c.http.ClientFlags())
55	flags.Merge(c.flags, c.http.ServerFlags())
56	flags.Merge(c.flags, c.http.NamespaceFlags())
57	c.help = flags.Usage(help, c.flags)
58}
59
60func (c *cmd) Run(args []string) int {
61	if err := c.flags.Parse(args); err != nil {
62		return 1
63	}
64
65	// Default to allow
66	if !c.flagAllow && !c.flagDeny {
67		c.flagAllow = true
68	}
69
70	// If both are specified it is an error
71	if c.flagAllow && c.flagDeny {
72		c.UI.Error("Only one of -allow or -deny may be specified.")
73		return 1
74	}
75
76	// Check for arg validation
77	args = c.flags.Args()
78	ixns, err := c.ixnsFromArgs(args)
79	if err != nil {
80		c.UI.Error(fmt.Sprintf("Error: %s", err))
81		return 1
82	}
83
84	// Create and test the HTTP client
85	client, err := c.http.APIClient()
86	if err != nil {
87		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
88		return 1
89	}
90
91	// Go through and create each intention
92	for _, ixn := range ixns {
93		// If replace is set to true, then perform an update operation.
94		if c.flagReplace {
95			oldIxn, _, err := client.Connect().IntentionGetExact(
96				intention.FormatSource(ixn),
97				intention.FormatDestination(ixn),
98				nil,
99			)
100			if err != nil {
101				c.UI.Error(fmt.Sprintf(
102					"Error looking up intention for replacement with source %q "+
103						"and destination %q: %s",
104					intention.FormatSource(ixn),
105					intention.FormatDestination(ixn),
106					err))
107				return 1
108			}
109			if oldIxn != nil {
110				// We set the ID of our intention so we overwrite it
111				ixn.ID = oldIxn.ID
112
113				//nolint:staticcheck
114				if _, err := client.Connect().IntentionUpdate(ixn, nil); err != nil {
115					c.UI.Error(fmt.Sprintf(
116						"Error replacing intention with source %q "+
117							"and destination %q: %s",
118						intention.FormatSource(ixn),
119						intention.FormatDestination(ixn),
120						err))
121					return 1
122				}
123
124				// Continue since we don't want to try to insert a new intention
125				continue
126			}
127		}
128
129		//nolint:staticcheck
130		_, _, err := client.Connect().IntentionCreate(ixn, nil)
131		if err != nil {
132			c.UI.Error(fmt.Sprintf("Error creating intention %q: %s", ixn, err))
133			return 1
134		}
135
136		c.UI.Output(fmt.Sprintf("Created: %s", ixn))
137	}
138
139	return 0
140}
141
142// ixnsFromArgs returns the set of intentions to create based on the arguments
143// given and the flags set. This will call ixnsFromFiles if the -file flag
144// was set.
145func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) {
146	// If we're in file mode, load from files
147	if c.flagFile {
148		return c.ixnsFromFiles(args)
149	}
150
151	// From args we require exactly two
152	if len(args) != 2 {
153		return nil, fmt.Errorf("Must specify two arguments: source and destination")
154	}
155
156	srcName, srcNamespace, err := intention.ParseIntentionTarget(args[0])
157	if err != nil {
158		return nil, fmt.Errorf("Invalid intention source: %v", err)
159	}
160
161	dstName, dstNamespace, err := intention.ParseIntentionTarget(args[1])
162	if err != nil {
163		return nil, fmt.Errorf("Invalid intention destination: %v", err)
164	}
165
166	return []*api.Intention{{
167		SourceNS:        srcNamespace,
168		SourceName:      srcName,
169		DestinationNS:   dstNamespace,
170		DestinationName: dstName,
171		SourceType:      api.IntentionSourceConsul,
172		Action:          c.ixnAction(),
173		Meta:            c.flagMeta,
174	}}, nil
175}
176
177func (c *cmd) ixnsFromFiles(args []string) ([]*api.Intention, error) {
178	var result []*api.Intention
179	for _, path := range args {
180		ixn, err := c.ixnFromFile(path)
181		if err != nil {
182			return nil, err
183		}
184
185		result = append(result, ixn)
186	}
187
188	return result, nil
189}
190
191func (c *cmd) ixnFromFile(path string) (*api.Intention, error) {
192	f, err := os.Open(path)
193	if err != nil {
194		return nil, err
195	}
196	defer f.Close()
197
198	var ixn api.Intention
199	if err := json.NewDecoder(f).Decode(&ixn); err != nil {
200		return nil, err
201	}
202
203	if len(ixn.Permissions) > 0 {
204		return nil, fmt.Errorf("cannot create L7 intention from file %q using this CLI; use 'consul config write' instead", path)
205	}
206
207	return &ixn, nil
208}
209
210// ixnAction returns the api.IntentionAction based on the flag set.
211func (c *cmd) ixnAction() api.IntentionAction {
212	if c.flagAllow {
213		return api.IntentionActionAllow
214	}
215
216	return api.IntentionActionDeny
217}
218
219func (c *cmd) Synopsis() string {
220	return synopsis
221}
222
223func (c *cmd) Help() string {
224	return c.help
225}
226
227const synopsis = "Create intentions for service connections."
228const help = `
229Usage: consul intention create [options] SRC DST
230Usage: consul intention create [options] -file FILE...
231
232  Create one or more intentions. The data can be specified as a single
233  source and destination pair or via a set of files when the "-file" flag
234  is specified.
235
236      $ consul intention create web db
237
238  To consume data from a set of files:
239
240      $ consul intention create -file one.json two.json
241
242  When specifying the "-file" flag, "-" may be used once to read from stdin:
243
244      $ echo "{ ... }" | consul intention create -file -
245
246  An "allow" intention is created by default (allowlist). To create a
247  "deny" intention, the "-deny" flag should be specified.
248
249  If a conflicting intention is found, creation will fail. To replace any
250  conflicting intentions, specify the "-replace" flag. This will replace any
251  conflicting intentions with the intention specified in this command.
252  Metadata and any other fields of the previous intention will not be
253  preserved.
254
255  Additional flags and more advanced use cases are detailed below.
256`
257