1package api 2 3import ( 4 "bytes" 5 "fmt" 6 "io" 7 "time" 8) 9 10// Intention defines an intention for the Connect Service Graph. This defines 11// the allowed or denied behavior of a connection between two services using 12// Connect. 13type Intention struct { 14 // ID is the UUID-based ID for the intention, always generated by Consul. 15 ID string `json:",omitempty"` 16 17 // Description is a human-friendly description of this intention. 18 // It is opaque to Consul and is only stored and transferred in API 19 // requests. 20 Description string `json:",omitempty"` 21 22 // SourceNS, SourceName are the namespace and name, respectively, of 23 // the source service. Either of these may be the wildcard "*", but only 24 // the full value can be a wildcard. Partial wildcards are not allowed. 25 // The source may also be a non-Consul service, as specified by SourceType. 26 // 27 // DestinationNS, DestinationName is the same, but for the destination 28 // service. The same rules apply. The destination is always a Consul 29 // service. 30 SourceNS, SourceName string 31 DestinationNS, DestinationName string 32 33 // SourceType is the type of the value for the source. 34 SourceType IntentionSourceType 35 36 // Action is whether this is an allowlist or denylist intention. 37 Action IntentionAction `json:",omitempty"` 38 39 // Permissions is the list of additional L7 attributes that extend the 40 // intention definition. 41 // 42 // NOTE: This field is not editable unless editing the underlying 43 // service-intentions config entry directly. 44 Permissions []*IntentionPermission `json:",omitempty"` 45 46 // DefaultAddr is not used. 47 // Deprecated: DefaultAddr is not used and may be removed in a future version. 48 DefaultAddr string `json:",omitempty"` 49 // DefaultPort is not used. 50 // Deprecated: DefaultPort is not used and may be removed in a future version. 51 DefaultPort int `json:",omitempty"` 52 53 // Meta is arbitrary metadata associated with the intention. This is 54 // opaque to Consul but is served in API responses. 55 Meta map[string]string `json:",omitempty"` 56 57 // Precedence is the order that the intention will be applied, with 58 // larger numbers being applied first. This is a read-only field, on 59 // any intention update it is updated. 60 Precedence int 61 62 // CreatedAt and UpdatedAt keep track of when this record was created 63 // or modified. 64 CreatedAt, UpdatedAt time.Time 65 66 // Hash of the contents of the intention 67 // 68 // This is needed mainly for replication purposes. When replicating from 69 // one DC to another keeping the content Hash will allow us to detect 70 // content changes more efficiently than checking every single field 71 Hash []byte `json:",omitempty"` 72 73 CreateIndex uint64 74 ModifyIndex uint64 75} 76 77// String returns human-friendly output describing ths intention. 78func (i *Intention) String() string { 79 var detail string 80 switch n := len(i.Permissions); n { 81 case 0: 82 detail = string(i.Action) 83 case 1: 84 detail = "1 permission" 85 default: 86 detail = fmt.Sprintf("%d permissions", len(i.Permissions)) 87 } 88 89 return fmt.Sprintf("%s => %s (%s)", 90 i.SourceString(), 91 i.DestinationString(), 92 detail) 93} 94 95// SourceString returns the namespace/name format for the source, or 96// just "name" if the namespace is the default namespace. 97func (i *Intention) SourceString() string { 98 return i.partString(i.SourceNS, i.SourceName) 99} 100 101// DestinationString returns the namespace/name format for the source, or 102// just "name" if the namespace is the default namespace. 103func (i *Intention) DestinationString() string { 104 return i.partString(i.DestinationNS, i.DestinationName) 105} 106 107func (i *Intention) partString(ns, n string) string { 108 // For now we omit the default namespace from the output. In the future 109 // we might want to look at this and show this in a multi-namespace world. 110 if ns != "" && ns != IntentionDefaultNamespace { 111 n = ns + "/" + n 112 } 113 114 return n 115} 116 117// IntentionDefaultNamespace is the default namespace value. 118const IntentionDefaultNamespace = "default" 119 120// IntentionAction is the action that the intention represents. This 121// can be "allow" or "deny" to allowlist or denylist intentions. 122type IntentionAction string 123 124const ( 125 IntentionActionAllow IntentionAction = "allow" 126 IntentionActionDeny IntentionAction = "deny" 127) 128 129// IntentionSourceType is the type of the source within an intention. 130type IntentionSourceType string 131 132const ( 133 // IntentionSourceConsul is a service within the Consul catalog. 134 IntentionSourceConsul IntentionSourceType = "consul" 135) 136 137// IntentionMatch are the arguments for the intention match API. 138type IntentionMatch struct { 139 By IntentionMatchType 140 Names []string 141} 142 143// IntentionMatchType is the target for a match request. For example, 144// matching by source will look for all intentions that match the given 145// source value. 146type IntentionMatchType string 147 148const ( 149 IntentionMatchSource IntentionMatchType = "source" 150 IntentionMatchDestination IntentionMatchType = "destination" 151) 152 153// IntentionCheck are the arguments for the intention check API. For 154// more documentation see the IntentionCheck function. 155type IntentionCheck struct { 156 // Source and Destination are the source and destination values to 157 // check. The destination is always a Consul service, but the source 158 // may be other values as defined by the SourceType. 159 Source, Destination string 160 161 // SourceType is the type of the value for the source. 162 SourceType IntentionSourceType 163} 164 165// Intentions returns the list of intentions. 166func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) { 167 r := h.c.newRequest("GET", "/v1/connect/intentions") 168 r.setQueryOptions(q) 169 rtt, resp, err := requireOK(h.c.doRequest(r)) 170 if err != nil { 171 return nil, nil, err 172 } 173 defer closeResponseBody(resp) 174 175 qm := &QueryMeta{} 176 parseQueryMeta(resp, qm) 177 qm.RequestTime = rtt 178 179 var out []*Intention 180 if err := decodeBody(resp, &out); err != nil { 181 return nil, nil, err 182 } 183 return out, qm, nil 184} 185 186// IntentionGetExact retrieves a single intention by its unique name instead of 187// its ID. 188func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) { 189 r := h.c.newRequest("GET", "/v1/connect/intentions/exact") 190 r.setQueryOptions(q) 191 r.params.Set("source", source) 192 r.params.Set("destination", destination) 193 rtt, resp, err := h.c.doRequest(r) 194 if err != nil { 195 return nil, nil, err 196 } 197 defer closeResponseBody(resp) 198 199 qm := &QueryMeta{} 200 parseQueryMeta(resp, qm) 201 qm.RequestTime = rtt 202 203 if resp.StatusCode == 404 { 204 return nil, qm, nil 205 } else if resp.StatusCode != 200 { 206 var buf bytes.Buffer 207 io.Copy(&buf, resp.Body) 208 return nil, nil, fmt.Errorf( 209 "Unexpected response %d: %s", resp.StatusCode, buf.String()) 210 } 211 212 var out Intention 213 if err := decodeBody(resp, &out); err != nil { 214 return nil, nil, err 215 } 216 return &out, qm, nil 217} 218 219// IntentionGet retrieves a single intention. 220// 221// Deprecated: use IntentionGetExact instead 222func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) { 223 r := h.c.newRequest("GET", "/v1/connect/intentions/"+id) 224 r.setQueryOptions(q) 225 rtt, resp, err := h.c.doRequest(r) 226 if err != nil { 227 return nil, nil, err 228 } 229 defer closeResponseBody(resp) 230 231 qm := &QueryMeta{} 232 parseQueryMeta(resp, qm) 233 qm.RequestTime = rtt 234 235 if resp.StatusCode == 404 { 236 return nil, qm, nil 237 } else if resp.StatusCode != 200 { 238 var buf bytes.Buffer 239 io.Copy(&buf, resp.Body) 240 return nil, nil, fmt.Errorf( 241 "Unexpected response %d: %s", resp.StatusCode, buf.String()) 242 } 243 244 var out Intention 245 if err := decodeBody(resp, &out); err != nil { 246 return nil, nil, err 247 } 248 return &out, qm, nil 249} 250 251// IntentionDeleteExact deletes a single intention by its unique name instead of its ID. 252func (h *Connect) IntentionDeleteExact(source, destination string, q *WriteOptions) (*WriteMeta, error) { 253 r := h.c.newRequest("DELETE", "/v1/connect/intentions/exact") 254 r.setWriteOptions(q) 255 r.params.Set("source", source) 256 r.params.Set("destination", destination) 257 258 rtt, resp, err := requireOK(h.c.doRequest(r)) 259 if err != nil { 260 return nil, err 261 } 262 defer closeResponseBody(resp) 263 264 qm := &WriteMeta{} 265 qm.RequestTime = rtt 266 267 return qm, nil 268} 269 270// IntentionDelete deletes a single intention. 271// 272// Deprecated: use IntentionDeleteExact instead 273func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) { 274 r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id) 275 r.setWriteOptions(q) 276 rtt, resp, err := requireOK(h.c.doRequest(r)) 277 if err != nil { 278 return nil, err 279 } 280 defer closeResponseBody(resp) 281 282 qm := &WriteMeta{} 283 qm.RequestTime = rtt 284 285 return qm, nil 286} 287 288// IntentionMatch returns the list of intentions that match a given source 289// or destination. The returned intentions are ordered by precedence where 290// result[0] is the highest precedence (if that matches, then that rule overrides 291// all other rules). 292// 293// Matching can be done for multiple names at the same time. The resulting 294// map is keyed by the given names. Casing is preserved. 295func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) { 296 r := h.c.newRequest("GET", "/v1/connect/intentions/match") 297 r.setQueryOptions(q) 298 r.params.Set("by", string(args.By)) 299 for _, name := range args.Names { 300 r.params.Add("name", name) 301 } 302 rtt, resp, err := requireOK(h.c.doRequest(r)) 303 if err != nil { 304 return nil, nil, err 305 } 306 defer closeResponseBody(resp) 307 308 qm := &QueryMeta{} 309 parseQueryMeta(resp, qm) 310 qm.RequestTime = rtt 311 312 var out map[string][]*Intention 313 if err := decodeBody(resp, &out); err != nil { 314 return nil, nil, err 315 } 316 return out, qm, nil 317} 318 319// IntentionCheck returns whether a given source/destination would be allowed 320// or not given the current set of intentions and the configuration of Consul. 321func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) { 322 r := h.c.newRequest("GET", "/v1/connect/intentions/check") 323 r.setQueryOptions(q) 324 r.params.Set("source", args.Source) 325 r.params.Set("destination", args.Destination) 326 if args.SourceType != "" { 327 r.params.Set("source-type", string(args.SourceType)) 328 } 329 rtt, resp, err := requireOK(h.c.doRequest(r)) 330 if err != nil { 331 return false, nil, err 332 } 333 defer closeResponseBody(resp) 334 335 qm := &QueryMeta{} 336 parseQueryMeta(resp, qm) 337 qm.RequestTime = rtt 338 339 var out struct{ Allowed bool } 340 if err := decodeBody(resp, &out); err != nil { 341 return false, nil, err 342 } 343 return out.Allowed, qm, nil 344} 345 346// IntentionUpsert will update an existing intention. The Source & Destination parameters 347// in the structure must be non-empty. The ID must be empty. 348func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { 349 r := c.c.newRequest("PUT", "/v1/connect/intentions/exact") 350 r.setWriteOptions(q) 351 r.params.Set("source", maybePrefixNamespace(ixn.SourceNS, ixn.SourceName)) 352 r.params.Set("destination", maybePrefixNamespace(ixn.DestinationNS, ixn.DestinationName)) 353 r.obj = ixn 354 rtt, resp, err := requireOK(c.c.doRequest(r)) 355 if err != nil { 356 return nil, err 357 } 358 defer closeResponseBody(resp) 359 360 wm := &WriteMeta{} 361 wm.RequestTime = rtt 362 return wm, nil 363} 364 365func maybePrefixNamespace(ns, name string) string { 366 if ns == "" { 367 return name 368 } 369 return ns + "/" + name 370} 371 372// IntentionCreate will create a new intention. The ID in the given 373// structure must be empty and a generate ID will be returned on 374// success. 375// 376// Deprecated: use IntentionUpsert instead 377func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { 378 r := c.c.newRequest("POST", "/v1/connect/intentions") 379 r.setWriteOptions(q) 380 r.obj = ixn 381 rtt, resp, err := requireOK(c.c.doRequest(r)) 382 if err != nil { 383 return "", nil, err 384 } 385 defer closeResponseBody(resp) 386 387 wm := &WriteMeta{} 388 wm.RequestTime = rtt 389 390 var out struct{ ID string } 391 if err := decodeBody(resp, &out); err != nil { 392 return "", nil, err 393 } 394 return out.ID, wm, nil 395} 396 397// IntentionUpdate will update an existing intention. The ID in the given 398// structure must be non-empty. 399// 400// Deprecated: use IntentionUpsert instead 401func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { 402 r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID) 403 r.setWriteOptions(q) 404 r.obj = ixn 405 rtt, resp, err := requireOK(c.c.doRequest(r)) 406 if err != nil { 407 return nil, err 408 } 409 defer closeResponseBody(resp) 410 411 wm := &WriteMeta{} 412 wm.RequestTime = rtt 413 return wm, nil 414} 415