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