b9e4544ec3
Found using staticcheck. binary.Write does not accept int types without a size. The error from binary.Write was ignored, so we never saw this error. Casting the data to uint64 produces a correct hash. Also deprecate the Default{Addr,Port} fields, and prevent them from being encoded. These fields will always be empty and are not used. Removing these would break backwards compatibility, so they are left in place for now. Co-authored-by: Hans Hasselberg <me@hans.io>
312 lines
9.1 KiB
Go
312 lines
9.1 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
|
|
}
|
|
|
|
// 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
|
|
}
|