Holger Hildebrandt | fa07499 | 2020-03-27 15:42:06 +0000 | [diff] [blame] | 1 | package api |
| 2 | |
| 3 | import ( |
| 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. |
| 13 | type Intention struct { |
| 14 | // ID is the UUID-based ID for the intention, always generated by Consul. |
| 15 | ID string |
| 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 |
| 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 a whitelist or blacklist intention. |
| 37 | Action IntentionAction |
| 38 | |
| 39 | // DefaultAddr, DefaultPort of the local listening proxy (if any) to |
| 40 | // make this connection. |
| 41 | DefaultAddr string |
| 42 | DefaultPort int |
| 43 | |
| 44 | // Meta is arbitrary metadata associated with the intention. This is |
| 45 | // opaque to Consul but is served in API responses. |
| 46 | Meta map[string]string |
| 47 | |
| 48 | // Precedence is the order that the intention will be applied, with |
| 49 | // larger numbers being applied first. This is a read-only field, on |
| 50 | // any intention update it is updated. |
| 51 | Precedence int |
| 52 | |
| 53 | // CreatedAt and UpdatedAt keep track of when this record was created |
| 54 | // or modified. |
| 55 | CreatedAt, UpdatedAt time.Time |
| 56 | |
| 57 | // Hash of the contents of the intention |
| 58 | // |
| 59 | // This is needed mainly for replication purposes. When replicating from |
| 60 | // one DC to another keeping the content Hash will allow us to detect |
| 61 | // content changes more efficiently than checking every single field |
| 62 | Hash []byte |
| 63 | |
| 64 | CreateIndex uint64 |
| 65 | ModifyIndex uint64 |
| 66 | } |
| 67 | |
| 68 | // String returns human-friendly output describing ths intention. |
| 69 | func (i *Intention) String() string { |
| 70 | return fmt.Sprintf("%s => %s (%s)", |
| 71 | i.SourceString(), |
| 72 | i.DestinationString(), |
| 73 | i.Action) |
| 74 | } |
| 75 | |
| 76 | // SourceString returns the namespace/name format for the source, or |
| 77 | // just "name" if the namespace is the default namespace. |
| 78 | func (i *Intention) SourceString() string { |
| 79 | return i.partString(i.SourceNS, i.SourceName) |
| 80 | } |
| 81 | |
| 82 | // DestinationString returns the namespace/name format for the source, or |
| 83 | // just "name" if the namespace is the default namespace. |
| 84 | func (i *Intention) DestinationString() string { |
| 85 | return i.partString(i.DestinationNS, i.DestinationName) |
| 86 | } |
| 87 | |
| 88 | func (i *Intention) partString(ns, n string) string { |
| 89 | // For now we omit the default namespace from the output. In the future |
| 90 | // we might want to look at this and show this in a multi-namespace world. |
| 91 | if ns != "" && ns != IntentionDefaultNamespace { |
| 92 | n = ns + "/" + n |
| 93 | } |
| 94 | |
| 95 | return n |
| 96 | } |
| 97 | |
| 98 | // IntentionDefaultNamespace is the default namespace value. |
| 99 | const IntentionDefaultNamespace = "default" |
| 100 | |
| 101 | // IntentionAction is the action that the intention represents. This |
| 102 | // can be "allow" or "deny" to whitelist or blacklist intentions. |
| 103 | type IntentionAction string |
| 104 | |
| 105 | const ( |
| 106 | IntentionActionAllow IntentionAction = "allow" |
| 107 | IntentionActionDeny IntentionAction = "deny" |
| 108 | ) |
| 109 | |
| 110 | // IntentionSourceType is the type of the source within an intention. |
| 111 | type IntentionSourceType string |
| 112 | |
| 113 | const ( |
| 114 | // IntentionSourceConsul is a service within the Consul catalog. |
| 115 | IntentionSourceConsul IntentionSourceType = "consul" |
| 116 | ) |
| 117 | |
| 118 | // IntentionMatch are the arguments for the intention match API. |
| 119 | type IntentionMatch struct { |
| 120 | By IntentionMatchType |
| 121 | Names []string |
| 122 | } |
| 123 | |
| 124 | // IntentionMatchType is the target for a match request. For example, |
| 125 | // matching by source will look for all intentions that match the given |
| 126 | // source value. |
| 127 | type IntentionMatchType string |
| 128 | |
| 129 | const ( |
| 130 | IntentionMatchSource IntentionMatchType = "source" |
| 131 | IntentionMatchDestination IntentionMatchType = "destination" |
| 132 | ) |
| 133 | |
| 134 | // IntentionCheck are the arguments for the intention check API. For |
| 135 | // more documentation see the IntentionCheck function. |
| 136 | type IntentionCheck struct { |
| 137 | // Source and Destination are the source and destination values to |
| 138 | // check. The destination is always a Consul service, but the source |
| 139 | // may be other values as defined by the SourceType. |
| 140 | Source, Destination string |
| 141 | |
| 142 | // SourceType is the type of the value for the source. |
| 143 | SourceType IntentionSourceType |
| 144 | } |
| 145 | |
| 146 | // Intentions returns the list of intentions. |
| 147 | func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) { |
| 148 | r := h.c.newRequest("GET", "/v1/connect/intentions") |
| 149 | r.setQueryOptions(q) |
| 150 | rtt, resp, err := requireOK(h.c.doRequest(r)) |
| 151 | if err != nil { |
| 152 | return nil, nil, err |
| 153 | } |
| 154 | defer resp.Body.Close() |
| 155 | |
| 156 | qm := &QueryMeta{} |
| 157 | parseQueryMeta(resp, qm) |
| 158 | qm.RequestTime = rtt |
| 159 | |
| 160 | var out []*Intention |
| 161 | if err := decodeBody(resp, &out); err != nil { |
| 162 | return nil, nil, err |
| 163 | } |
| 164 | return out, qm, nil |
| 165 | } |
| 166 | |
| 167 | // IntentionGet retrieves a single intention. |
| 168 | func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) { |
| 169 | r := h.c.newRequest("GET", "/v1/connect/intentions/"+id) |
| 170 | r.setQueryOptions(q) |
| 171 | rtt, resp, err := h.c.doRequest(r) |
| 172 | if err != nil { |
| 173 | return nil, nil, err |
| 174 | } |
| 175 | defer resp.Body.Close() |
| 176 | |
| 177 | qm := &QueryMeta{} |
| 178 | parseQueryMeta(resp, qm) |
| 179 | qm.RequestTime = rtt |
| 180 | |
| 181 | if resp.StatusCode == 404 { |
| 182 | return nil, qm, nil |
| 183 | } else if resp.StatusCode != 200 { |
| 184 | var buf bytes.Buffer |
| 185 | io.Copy(&buf, resp.Body) |
| 186 | return nil, nil, fmt.Errorf( |
| 187 | "Unexpected response %d: %s", resp.StatusCode, buf.String()) |
| 188 | } |
| 189 | |
| 190 | var out Intention |
| 191 | if err := decodeBody(resp, &out); err != nil { |
| 192 | return nil, nil, err |
| 193 | } |
| 194 | return &out, qm, nil |
| 195 | } |
| 196 | |
| 197 | // IntentionDelete deletes a single intention. |
| 198 | func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) { |
| 199 | r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id) |
| 200 | r.setWriteOptions(q) |
| 201 | rtt, resp, err := requireOK(h.c.doRequest(r)) |
| 202 | if err != nil { |
| 203 | return nil, err |
| 204 | } |
| 205 | defer resp.Body.Close() |
| 206 | |
| 207 | qm := &WriteMeta{} |
| 208 | qm.RequestTime = rtt |
| 209 | |
| 210 | return qm, nil |
| 211 | } |
| 212 | |
| 213 | // IntentionMatch returns the list of intentions that match a given source |
| 214 | // or destination. The returned intentions are ordered by precedence where |
| 215 | // result[0] is the highest precedence (if that matches, then that rule overrides |
| 216 | // all other rules). |
| 217 | // |
| 218 | // Matching can be done for multiple names at the same time. The resulting |
| 219 | // map is keyed by the given names. Casing is preserved. |
| 220 | func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) { |
| 221 | r := h.c.newRequest("GET", "/v1/connect/intentions/match") |
| 222 | r.setQueryOptions(q) |
| 223 | r.params.Set("by", string(args.By)) |
| 224 | for _, name := range args.Names { |
| 225 | r.params.Add("name", name) |
| 226 | } |
| 227 | rtt, resp, err := requireOK(h.c.doRequest(r)) |
| 228 | if err != nil { |
| 229 | return nil, nil, err |
| 230 | } |
| 231 | defer resp.Body.Close() |
| 232 | |
| 233 | qm := &QueryMeta{} |
| 234 | parseQueryMeta(resp, qm) |
| 235 | qm.RequestTime = rtt |
| 236 | |
| 237 | var out map[string][]*Intention |
| 238 | if err := decodeBody(resp, &out); err != nil { |
| 239 | return nil, nil, err |
| 240 | } |
| 241 | return out, qm, nil |
| 242 | } |
| 243 | |
| 244 | // IntentionCheck returns whether a given source/destination would be allowed |
| 245 | // or not given the current set of intentions and the configuration of Consul. |
| 246 | func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) { |
| 247 | r := h.c.newRequest("GET", "/v1/connect/intentions/check") |
| 248 | r.setQueryOptions(q) |
| 249 | r.params.Set("source", args.Source) |
| 250 | r.params.Set("destination", args.Destination) |
| 251 | if args.SourceType != "" { |
| 252 | r.params.Set("source-type", string(args.SourceType)) |
| 253 | } |
| 254 | rtt, resp, err := requireOK(h.c.doRequest(r)) |
| 255 | if err != nil { |
| 256 | return false, nil, err |
| 257 | } |
| 258 | defer resp.Body.Close() |
| 259 | |
| 260 | qm := &QueryMeta{} |
| 261 | parseQueryMeta(resp, qm) |
| 262 | qm.RequestTime = rtt |
| 263 | |
| 264 | var out struct{ Allowed bool } |
| 265 | if err := decodeBody(resp, &out); err != nil { |
| 266 | return false, nil, err |
| 267 | } |
| 268 | return out.Allowed, qm, nil |
| 269 | } |
| 270 | |
| 271 | // IntentionCreate will create a new intention. The ID in the given |
| 272 | // structure must be empty and a generate ID will be returned on |
| 273 | // success. |
| 274 | func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { |
| 275 | r := c.c.newRequest("POST", "/v1/connect/intentions") |
| 276 | r.setWriteOptions(q) |
| 277 | r.obj = ixn |
| 278 | rtt, resp, err := requireOK(c.c.doRequest(r)) |
| 279 | if err != nil { |
| 280 | return "", nil, err |
| 281 | } |
| 282 | defer resp.Body.Close() |
| 283 | |
| 284 | wm := &WriteMeta{} |
| 285 | wm.RequestTime = rtt |
| 286 | |
| 287 | var out struct{ ID string } |
| 288 | if err := decodeBody(resp, &out); err != nil { |
| 289 | return "", nil, err |
| 290 | } |
| 291 | return out.ID, wm, nil |
| 292 | } |
| 293 | |
| 294 | // IntentionUpdate will update an existing intention. The ID in the given |
| 295 | // structure must be non-empty. |
| 296 | func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { |
| 297 | r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID) |
| 298 | r.setWriteOptions(q) |
| 299 | r.obj = ixn |
| 300 | rtt, resp, err := requireOK(c.c.doRequest(r)) |
| 301 | if err != nil { |
| 302 | return nil, err |
| 303 | } |
| 304 | defer resp.Body.Close() |
| 305 | |
| 306 | wm := &WriteMeta{} |
| 307 | wm.RequestTime = rtt |
| 308 | return wm, nil |
| 309 | } |