72a515f5ec
Highlights: - add new endpoint to query for intentions by exact match - using this endpoint from the CLI instead of the dump+filter approach - enforcing that OSS can only read/write intentions with a SourceNS or DestinationNS field of "default". - preexisting OSS intentions with now-invalid namespace fields will delete those intentions on initial election or for wildcard namespaces an attempt will be made to downgrade them to "default" unless one exists. - also allow the '-namespace' CLI arg on all of the intention subcommands - update lots of docs
345 lines
10 KiB
Go
345 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// Intention defines an intention for the Connect Service Graph. This defines
|
|
// the allowed or denied behavior of a connection between two services using
|
|
// Connect.
|
|
type Intention struct {
|
|
// ID is the UUID-based ID for the intention, always generated by Consul.
|
|
ID string
|
|
|
|
// Description is a human-friendly description of this intention.
|
|
// It is opaque to Consul and is only stored and transferred in API
|
|
// requests.
|
|
Description string
|
|
|
|
// SourceNS, SourceName are the namespace and name, respectively, of
|
|
// the source service. Either of these may be the wildcard "*", but only
|
|
// the full value can be a wildcard. Partial wildcards are not allowed.
|
|
// The source may also be a non-Consul service, as specified by SourceType.
|
|
//
|
|
// DestinationNS, DestinationName is the same, but for the destination
|
|
// service. The same rules apply. The destination is always a Consul
|
|
// service.
|
|
SourceNS, SourceName string
|
|
DestinationNS, DestinationName string
|
|
|
|
// SourceType is the type of the value for the source.
|
|
SourceType IntentionSourceType
|
|
|
|
// Action is whether this is an allowlist or denylist intention.
|
|
Action IntentionAction
|
|
|
|
// DefaultAddr is not used.
|
|
// Deprecated: DefaultAddr is not used and may be removed in a future version.
|
|
DefaultAddr string `json:",omitempty"`
|
|
// DefaultPort is not used.
|
|
// Deprecated: DefaultPort is not used and may be removed in a future version.
|
|
DefaultPort int `json:",omitempty"`
|
|
|
|
// Meta is arbitrary metadata associated with the intention. This is
|
|
// opaque to Consul but is served in API responses.
|
|
Meta map[string]string
|
|
|
|
// Precedence is the order that the intention will be applied, with
|
|
// larger numbers being applied first. This is a read-only field, on
|
|
// any intention update it is updated.
|
|
Precedence int
|
|
|
|
// CreatedAt and UpdatedAt keep track of when this record was created
|
|
// or modified.
|
|
CreatedAt, UpdatedAt time.Time
|
|
|
|
// Hash of the contents of the intention
|
|
//
|
|
// This is needed mainly for replication purposes. When replicating from
|
|
// one DC to another keeping the content Hash will allow us to detect
|
|
// content changes more efficiently than checking every single field
|
|
Hash []byte
|
|
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
// String returns human-friendly output describing ths intention.
|
|
func (i *Intention) String() string {
|
|
return fmt.Sprintf("%s => %s (%s)",
|
|
i.SourceString(),
|
|
i.DestinationString(),
|
|
i.Action)
|
|
}
|
|
|
|
// SourceString returns the namespace/name format for the source, or
|
|
// just "name" if the namespace is the default namespace.
|
|
func (i *Intention) SourceString() string {
|
|
return i.partString(i.SourceNS, i.SourceName)
|
|
}
|
|
|
|
// DestinationString returns the namespace/name format for the source, or
|
|
// just "name" if the namespace is the default namespace.
|
|
func (i *Intention) DestinationString() string {
|
|
return i.partString(i.DestinationNS, i.DestinationName)
|
|
}
|
|
|
|
func (i *Intention) partString(ns, n string) string {
|
|
// For now we omit the default namespace from the output. In the future
|
|
// we might want to look at this and show this in a multi-namespace world.
|
|
if ns != "" && ns != IntentionDefaultNamespace {
|
|
n = ns + "/" + n
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// IntentionDefaultNamespace is the default namespace value.
|
|
const IntentionDefaultNamespace = "default"
|
|
|
|
// IntentionAction is the action that the intention represents. This
|
|
// can be "allow" or "deny" to allowlist or denylist intentions.
|
|
type IntentionAction string
|
|
|
|
const (
|
|
IntentionActionAllow IntentionAction = "allow"
|
|
IntentionActionDeny IntentionAction = "deny"
|
|
)
|
|
|
|
// IntentionSourceType is the type of the source within an intention.
|
|
type IntentionSourceType string
|
|
|
|
const (
|
|
// IntentionSourceConsul is a service within the Consul catalog.
|
|
IntentionSourceConsul IntentionSourceType = "consul"
|
|
)
|
|
|
|
// IntentionMatch are the arguments for the intention match API.
|
|
type IntentionMatch struct {
|
|
By IntentionMatchType
|
|
Names []string
|
|
}
|
|
|
|
// IntentionMatchType is the target for a match request. For example,
|
|
// matching by source will look for all intentions that match the given
|
|
// source value.
|
|
type IntentionMatchType string
|
|
|
|
const (
|
|
IntentionMatchSource IntentionMatchType = "source"
|
|
IntentionMatchDestination IntentionMatchType = "destination"
|
|
)
|
|
|
|
// IntentionCheck are the arguments for the intention check API. For
|
|
// more documentation see the IntentionCheck function.
|
|
type IntentionCheck struct {
|
|
// Source and Destination are the source and destination values to
|
|
// check. The destination is always a Consul service, but the source
|
|
// may be other values as defined by the SourceType.
|
|
Source, Destination string
|
|
|
|
// SourceType is the type of the value for the source.
|
|
SourceType IntentionSourceType
|
|
}
|
|
|
|
// Intentions returns the list of intentions.
|
|
func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) {
|
|
r := h.c.newRequest("GET", "/v1/connect/intentions")
|
|
r.setQueryOptions(q)
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
var out []*Intention
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return out, qm, nil
|
|
}
|
|
|
|
// IntentionGet retrieves a single intention.
|
|
func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) {
|
|
r := h.c.newRequest("GET", "/v1/connect/intentions/"+id)
|
|
r.setQueryOptions(q)
|
|
rtt, resp, err := h.c.doRequest(r)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if resp.StatusCode == 404 {
|
|
return nil, qm, nil
|
|
} else if resp.StatusCode != 200 {
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, resp.Body)
|
|
return nil, nil, fmt.Errorf(
|
|
"Unexpected response %d: %s", resp.StatusCode, buf.String())
|
|
}
|
|
|
|
var out Intention
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &out, qm, nil
|
|
}
|
|
|
|
// IntentionDelete deletes a single intention.
|
|
func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) {
|
|
r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id)
|
|
r.setWriteOptions(q)
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &WriteMeta{}
|
|
qm.RequestTime = rtt
|
|
|
|
return qm, nil
|
|
}
|
|
|
|
// IntentionMatch returns the list of intentions that match a given source
|
|
// or destination. The returned intentions are ordered by precedence where
|
|
// result[0] is the highest precedence (if that matches, then that rule overrides
|
|
// all other rules).
|
|
//
|
|
// Matching can be done for multiple names at the same time. The resulting
|
|
// map is keyed by the given names. Casing is preserved.
|
|
func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) {
|
|
r := h.c.newRequest("GET", "/v1/connect/intentions/match")
|
|
r.setQueryOptions(q)
|
|
r.params.Set("by", string(args.By))
|
|
for _, name := range args.Names {
|
|
r.params.Add("name", name)
|
|
}
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
var out map[string][]*Intention
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return out, qm, nil
|
|
}
|
|
|
|
// IntentionCheck returns whether a given source/destination would be allowed
|
|
// or not given the current set of intentions and the configuration of Consul.
|
|
func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) {
|
|
r := h.c.newRequest("GET", "/v1/connect/intentions/check")
|
|
r.setQueryOptions(q)
|
|
r.params.Set("source", args.Source)
|
|
r.params.Set("destination", args.Destination)
|
|
if args.SourceType != "" {
|
|
r.params.Set("source-type", string(args.SourceType))
|
|
}
|
|
rtt, resp, err := requireOK(h.c.doRequest(r))
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
var out struct{ Allowed bool }
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return false, nil, err
|
|
}
|
|
return out.Allowed, qm, nil
|
|
}
|
|
|
|
// IntentionGetExact retrieves a single intention by its unique name instead of
|
|
// its ID.
|
|
func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) {
|
|
r := h.c.newRequest("GET", "/v1/connect/intentions/exact")
|
|
r.setQueryOptions(q)
|
|
r.params.Set("source", source)
|
|
r.params.Set("destination", destination)
|
|
rtt, resp, err := h.c.doRequest(r)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
qm := &QueryMeta{}
|
|
parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if resp.StatusCode == 404 {
|
|
return nil, qm, nil
|
|
} else if resp.StatusCode != 200 {
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, resp.Body)
|
|
return nil, nil, fmt.Errorf(
|
|
"Unexpected response %d: %s", resp.StatusCode, buf.String())
|
|
}
|
|
|
|
var out Intention
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &out, qm, nil
|
|
}
|
|
|
|
// IntentionCreate will create a new intention. The ID in the given
|
|
// structure must be empty and a generate ID will be returned on
|
|
// success.
|
|
func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) {
|
|
r := c.c.newRequest("POST", "/v1/connect/intentions")
|
|
r.setWriteOptions(q)
|
|
r.obj = ixn
|
|
rtt, resp, err := requireOK(c.c.doRequest(r))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
wm := &WriteMeta{}
|
|
wm.RequestTime = rtt
|
|
|
|
var out struct{ ID string }
|
|
if err := decodeBody(resp, &out); err != nil {
|
|
return "", nil, err
|
|
}
|
|
return out.ID, wm, nil
|
|
}
|
|
|
|
// IntentionUpdate will update an existing intention. The ID in the given
|
|
// structure must be non-empty.
|
|
func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
|
|
r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID)
|
|
r.setWriteOptions(q)
|
|
r.obj = ixn
|
|
rtt, resp, err := requireOK(c.c.doRequest(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
wm := &WriteMeta{}
|
|
wm.RequestTime = rtt
|
|
return wm, nil
|
|
}
|