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 ` json:",omitempty" `
// 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 ` json:",omitempty" `
// 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
// SourcePartition and DestinationPartition cannot be wildcards "*" and
// are not compatible with legacy intentions.
SourcePartition string
DestinationPartition 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 ` json:",omitempty" `
// Permissions is the list of additional L7 attributes that extend the
// intention definition.
//
// NOTE: This field is not editable unless editing the underlying
// service-intentions config entry directly.
Permissions [ ] * IntentionPermission ` json:",omitempty" `
// 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 ` json:",omitempty" `
// 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 ` json:",omitempty" `
CreateIndex uint64
ModifyIndex uint64
}
// String returns human-friendly output describing ths intention.
func ( i * Intention ) String ( ) string {
var detail string
switch n := len ( i . Permissions ) ; n {
case 0 :
detail = string ( i . Action )
case 1 :
detail = "1 permission"
default :
detail = fmt . Sprintf ( "%d permissions" , len ( i . Permissions ) )
}
return fmt . Sprintf ( "%s => %s (%s)" ,
i . SourceString ( ) ,
i . DestinationString ( ) ,
detail )
}
// 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 := h . c . doRequest ( r )
if err != nil {
return nil , nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , nil , err
}
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
}
// 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 closeResponseBody ( resp )
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
}
// IntentionGet retrieves a single intention.
//
// Deprecated: use IntentionGetExact instead
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 closeResponseBody ( resp )
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
}
// IntentionDeleteExact deletes a single intention by its unique name instead of its ID.
func ( h * Connect ) IntentionDeleteExact ( source , destination string , q * WriteOptions ) ( * WriteMeta , error ) {
r := h . c . newRequest ( "DELETE" , "/v1/connect/intentions/exact" )
r . setWriteOptions ( q )
r . params . Set ( "source" , source )
r . params . Set ( "destination" , destination )
rtt , resp , err := h . c . doRequest ( r )
if err != nil {
return nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , err
}
qm := & WriteMeta { }
qm . RequestTime = rtt
return qm , nil
}
// IntentionDelete deletes a single intention.
//
// Deprecated: use IntentionDeleteExact instead
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 := h . c . doRequest ( r )
if err != nil {
return nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , err
}
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 := h . c . doRequest ( r )
if err != nil {
return nil , nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , nil , err
}
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 := h . c . doRequest ( r )
if err != nil {
return false , nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return false , nil , err
}
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
}
// IntentionUpsert will update an existing intention. The Source & Destination parameters
// in the structure must be non-empty. The ID must be empty.
func ( c * Connect ) IntentionUpsert ( ixn * Intention , q * WriteOptions ) ( * WriteMeta , error ) {
r := c . c . newRequest ( "PUT" , "/v1/connect/intentions/exact" )
r . setWriteOptions ( q )
r . params . Set ( "source" , maybePrefixNamespaceAndPartition ( ixn . SourcePartition , ixn . SourceNS , ixn . SourceName ) )
r . params . Set ( "destination" , maybePrefixNamespaceAndPartition ( ixn . DestinationPartition , ixn . DestinationNS , ixn . DestinationName ) )
r . obj = ixn
rtt , resp , err := c . c . doRequest ( r )
if err != nil {
return nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , err
}
wm := & WriteMeta { }
wm . RequestTime = rtt
return wm , nil
}
func maybePrefixNamespaceAndPartition ( part , ns , name string ) string {
switch {
case part == "" && ns == "" :
return name
case part == "" && ns != "" :
return ns + "/" + name
case part != "" && ns == "" :
return part + "/" + IntentionDefaultNamespace + "/" + name
default :
return part + "/" + ns + "/" + name
}
}
// IntentionCreate will create a new intention. The ID in the given
// structure must be empty and a generate ID will be returned on
// success.
//
// Deprecated: use IntentionUpsert instead
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 := c . c . doRequest ( r )
if err != nil {
return "" , nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return "" , nil , err
}
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.
//
// Deprecated: use IntentionUpsert instead
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 := c . c . doRequest ( r )
if err != nil {
return nil , err
}
defer closeResponseBody ( resp )
if err := requireOK ( resp ) ; err != nil {
return nil , err
}
wm := & WriteMeta { }
wm . RequestTime = rtt
return wm , nil
}