consul/agent/structs/structs.go

3177 lines
94 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"bytes"
"crypto/md5"
"crypto/sha256"
"encoding/json"
"fmt"
"math/rand"
"os"
"reflect"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/serf/coordinate"
"github.com/mitchellh/hashstructure"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/types"
)
type MessageType uint8
// RaftIndex is used to track the index used while creating
// or modifying a given struct type.
type RaftIndex struct {
CreateIndex uint64 `bexpr:"-"`
ModifyIndex uint64 `bexpr:"-"`
}
// These are serialized between Consul servers and stored in Consul snapshots,
// so entries must only ever be added.
const (
RegisterRequestType MessageType = 0
DeregisterRequestType = 1
KVSRequestType = 2
SessionRequestType = 3
DeprecatedACLRequestType = 4 // Removed with the legacy ACL system
TombstoneRequestType = 5
CoordinateBatchUpdateType = 6
PreparedQueryRequestType = 7
TxnRequestType = 8
AutopilotRequestType = 9
AreaRequestType = 10
ACLBootstrapRequestType = 11
IntentionRequestType = 12
ConnectCARequestType = 13
ConnectCAProviderStateType = 14
ConnectCAConfigType = 15 // FSM snapshots only.
IndexRequestType = 16 // FSM snapshots only.
ACLTokenSetRequestType = 17
ACLTokenDeleteRequestType = 18
ACLPolicySetRequestType = 19
ACLPolicyDeleteRequestType = 20
ConnectCALeafRequestType = 21
ConfigEntryRequestType = 22
ACLRoleSetRequestType = 23
ACLRoleDeleteRequestType = 24
ACLBindingRuleSetRequestType = 25
ACLBindingRuleDeleteRequestType = 26
ACLAuthMethodSetRequestType = 27
ACLAuthMethodDeleteRequestType = 28
ChunkingStateType = 29
FederationStateRequestType = 30
SystemMetadataRequestType = 31
ServiceVirtualIPRequestType = 32
FreeVirtualIPRequestType = 33
KindServiceNamesType = 34
PeeringWriteType = 35
PeeringDeleteType = 36
PeeringTerminateByIDType = 37
PeeringTrustBundleWriteType = 38
PeeringTrustBundleDeleteType = 39
PeeringSecretsWriteType = 40
RaftLogVerifierCheckpoint = 41 // Only used for log verifier, no-op on FSM.
ResourceOperationType = 42
UpdateVirtualIPRequestType = 43
)
const (
// LocalPeerKeyword is a reserved keyword used for indexing in the state store for objects in the local peer.
LocalPeerKeyword = "~"
// DefaultPeerKeyword is the PeerName to use to refer to the local
// cluster's own data, rather than replicated peered data.
//
// This may internally be converted into LocalPeerKeyword, but external
// uses should not use that symbol directly in most cases.
DefaultPeerKeyword = ""
// TODOPeerKeyword is the peer keyword to use if you aren't sure if the
// usage SHOULD be peering-aware yet.
//
// TODO(peering): remove this in the future
TODOPeerKeyword = ""
)
// if a new request type is added above it must be
// added to the map below
// requestTypeStrings is used for snapshot enhance
// any new request types added must be placed here
var requestTypeStrings = map[MessageType]string{
RegisterRequestType: "Register",
DeregisterRequestType: "Deregister",
KVSRequestType: "KVS",
SessionRequestType: "Session",
DeprecatedACLRequestType: "ACL", // DEPRECATED (ACL-Legacy-Compat)
TombstoneRequestType: "Tombstone",
CoordinateBatchUpdateType: "CoordinateBatchUpdate",
PreparedQueryRequestType: "PreparedQuery",
TxnRequestType: "Txn",
AutopilotRequestType: "Autopilot",
AreaRequestType: "Area",
ACLBootstrapRequestType: "ACLBootstrap",
IntentionRequestType: "Intention",
ConnectCARequestType: "ConnectCA",
ConnectCAProviderStateType: "ConnectCAProviderState",
ConnectCAConfigType: "ConnectCAConfig", // FSM snapshots only.
IndexRequestType: "Index", // FSM snapshots only.
ACLTokenSetRequestType: "ACLToken",
ACLTokenDeleteRequestType: "ACLTokenDelete",
ACLPolicySetRequestType: "ACLPolicy",
ACLPolicyDeleteRequestType: "ACLPolicyDelete",
ConnectCALeafRequestType: "ConnectCALeaf",
ConfigEntryRequestType: "ConfigEntry",
ACLRoleSetRequestType: "ACLRole",
ACLRoleDeleteRequestType: "ACLRoleDelete",
ACLBindingRuleSetRequestType: "ACLBindingRule",
ACLBindingRuleDeleteRequestType: "ACLBindingRuleDelete",
ACLAuthMethodSetRequestType: "ACLAuthMethod",
ACLAuthMethodDeleteRequestType: "ACLAuthMethodDelete",
ChunkingStateType: "ChunkingState",
FederationStateRequestType: "FederationState",
SystemMetadataRequestType: "SystemMetadata",
ServiceVirtualIPRequestType: "ServiceVirtualIP",
FreeVirtualIPRequestType: "FreeVirtualIP",
KindServiceNamesType: "KindServiceName",
PeeringWriteType: "Peering",
PeeringDeleteType: "PeeringDelete",
PeeringTrustBundleWriteType: "PeeringTrustBundle",
PeeringTrustBundleDeleteType: "PeeringTrustBundleDelete",
PeeringSecretsWriteType: "PeeringSecret",
RaftLogVerifierCheckpoint: "RaftLogVerifierCheckpoint",
ResourceOperationType: "Resource",
UpdateVirtualIPRequestType: "UpdateManualVirtualIPRequestType",
}
const (
// IgnoreUnknownTypeFlag is set along with a MessageType
// to indicate that the message type can be safely ignored
// if it is not recognized. This is for future proofing, so
// that new commands can be added in a way that won't cause
// old servers to crash when the FSM attempts to process them.
IgnoreUnknownTypeFlag MessageType = 128
// NodeMaint is the special key set by a node in maintenance mode.
NodeMaint = "_node_maintenance"
// ServiceMaintPrefix is the prefix for a service in maintenance mode.
ServiceMaintPrefix = "_service_maintenance:"
// The meta key prefix reserved for Consul's internal use
MetaKeyReservedPrefix = "consul-"
// metaMaxKeyPairs is maximum number of metadata key pairs allowed to be registered
metaMaxKeyPairs = 64
// metaKeyMaxLength is the maximum allowed length of a metadata key
metaKeyMaxLength = 128
// metaValueMaxLength is the maximum allowed length of a metadata value
metaValueMaxLength = 512
// MetaSegmentKey is the node metadata key used to store the node's network segment
MetaSegmentKey = "consul-network-segment"
// MetaWANFederationKey is the mesh gateway metadata key that indicates a
// mesh gateway is usable for wan federation.
MetaWANFederationKey = "consul-wan-federation"
// MetaExternalSource is the metadata key used when a resource is managed by a source outside Consul like nomad/k8s
MetaExternalSource = "external-source"
// TaggedAddressVirtualIP is the key used to store tagged virtual IPs generated by Consul.
TaggedAddressVirtualIP = "consul-virtual"
// MaxLockDelay provides a maximum LockDelay value for
// a session. Any value above this will not be respected.
MaxLockDelay = 60 * time.Second
// lockDelayMinThreshold is used in JSON decoding to convert a
// numeric lockdelay value from nanoseconds to seconds if it is
// below thisthreshold. Users often send a value like 5, which
// they assumeis seconds, but because Go uses nanosecond granularity,
// ends up being very small. If we see a value below this threshold,
// we multiply by time.Second
lockDelayMinThreshold = 1000
// JitterFraction is a the limit to the amount of jitter we apply
// to a user specified MaxQueryTime. We divide the specified time by
// the fraction. So 16 == 6.25% limit of jitter. This same fraction
// is applied to the RPCHoldTimeout
JitterFraction = 16
// WildcardSpecifier is the string which should be used for specifying a wildcard
// The exact semantics of the wildcard is left up to the code where its used.
WildcardSpecifier = "*"
// MetaConsulVersion is the node metadata key used to store the node's consul version
MetaConsulVersion = "consul-version"
)
var allowedConsulMetaKeysForMeshGateway = map[string]struct{}{MetaWANFederationKey: {}}
// CEDowngrade indicates if we are in downgrading from ent to ce
var CEDowngrade = os.Getenv("CONSUL_ENTERPRISE_DOWNGRADE_TO_CE") == "true"
var (
NodeMaintCheckID = NewCheckID(NodeMaint, nil)
)
const (
TaggedAddressWAN = "wan"
TaggedAddressWANIPv4 = "wan_ipv4"
TaggedAddressWANIPv6 = "wan_ipv6"
TaggedAddressLAN = "lan"
TaggedAddressLANIPv4 = "lan_ipv4"
TaggedAddressLANIPv6 = "lan_ipv6"
)
// metaKeyFormat checks if a metadata key string is valid
var metaKeyFormat = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString
func ValidStatus(s string) bool {
return s == api.HealthPassing || s == api.HealthWarning || s == api.HealthCritical
}
// RPCInfo is used to describe common information about query
type RPCInfo interface {
RequestDatacenter() string
IsRead() bool
AllowStaleRead() bool
TokenSecret() string
SetTokenSecret(string)
HasTimedOut(since time.Time, rpcHoldTimeout, maxQueryTime, defaultQueryTime time.Duration) (bool, error)
}
// QueryOptions is used to specify various flags for read queries
type QueryOptions struct {
// Token is the ACL token ID. If not provided, the 'anonymous'
// token is assumed for backwards compatibility.
Token string `mapstructure:"x-consul-token,omitempty"`
// If set, wait until query exceeds given index. Must be provided
// with MaxQueryTime.
MinQueryIndex uint64 `mapstructure:"min-query-index,omitempty"`
// Provided with MinQueryIndex to wait for change.
MaxQueryTime time.Duration `mapstructure:"max-query-time,omitempty"`
// If set, any follower can service the request. Results
// may be arbitrarily stale.
AllowStale bool `mapstructure:"allow-stale,omitempty"`
// If set, the leader must verify leadership prior to
// servicing the request. Prevents a stale read.
RequireConsistent bool `mapstructure:"require-consistent,omitempty"`
// If set, the local agent may respond with an arbitrarily stale locally
// cached response. The semantics differ from AllowStale since the agent may
// be entirely partitioned from the servers and still considered "healthy" by
// operators. Stale responses from Servers are also arbitrarily stale, but can
// provide additional bounds on the last contact time from the leader. It's
// expected that servers that are partitioned are noticed and replaced in a
// timely way by operators while the same may not be true for client agents.
UseCache bool `mapstructure:"use-cache,omitempty"`
// If set and AllowStale is true, will try first a stale
// read, and then will perform a consistent read if stale
// read is older than value.
MaxStaleDuration time.Duration `mapstructure:"max-stale-duration,omitempty"`
// MaxAge limits how old a cached value will be returned if UseCache is true.
// If there is a cached response that is older than the MaxAge, it is treated
// as a cache miss and a new fetch invoked. If the fetch fails, the error is
// returned. Clients that wish to allow for stale results on error can set
// StaleIfError to a longer duration to change this behavior. It is ignored
// if the endpoint supports background refresh caching. See
// https://www.consul.io/api/index.html#agent-caching for more details.
MaxAge time.Duration `mapstructure:"max-age,omitempty"`
// MustRevalidate forces the agent to fetch a fresh version of a cached
// resource or at least validate that the cached version is still fresh. It is
// implied by either max-age=0 or must-revalidate Cache-Control headers. It
// only makes sense when UseCache is true. We store it since MaxAge = 0 is the
// default unset value.
MustRevalidate bool `mapstructure:"must-revalidate,omitempty"`
// StaleIfError specifies how stale the client will accept a cached response
// if the servers are unavailable to fetch a fresh one. Only makes sense when
// UseCache is true and MaxAge is set to a lower, non-zero value. It is
// ignored if the endpoint supports background refresh caching. See
// https://www.consul.io/api/index.html#agent-caching for more details.
StaleIfError time.Duration `mapstructure:"stale-if-error,omitempty"`
// Filter specifies the go-bexpr filter expression to be used for
// filtering the data prior to returning a response
Filter string `mapstructure:"filter,omitempty"`
// AllowNotModifiedResponse indicates that if the MinIndex matches the
// QueryMeta.Index, the response can be left empty and QueryMeta.NotModified
// will be set to true to indicate the result of the query has not changed.
AllowNotModifiedResponse bool `mapstructure:"allow-not-modified-response,omitempty"`
}
// IsRead is always true for QueryOption.
func (q QueryOptions) IsRead() bool {
return true
}
// ConsistencyLevel display the consistency required by a request
func (q QueryOptions) ConsistencyLevel() string {
if q.RequireConsistent {
return "consistent"
} else if q.AllowStale {
return "stale"
} else {
return "leader"
}
}
func (q QueryOptions) AllowStaleRead() bool {
return q.AllowStale
}
func (q QueryOptions) TokenSecret() string {
return q.Token
}
func (q *QueryOptions) SetTokenSecret(s string) {
q.Token = s
}
// BlockingTimeout implements pool.BlockableQuery
func (q QueryOptions) BlockingTimeout(maxQueryTime, defaultQueryTime time.Duration) time.Duration {
// Match logic in Server.blockingQuery.
if q.MinQueryIndex > 0 {
if q.MaxQueryTime > maxQueryTime {
q.MaxQueryTime = maxQueryTime
} else if q.MaxQueryTime <= 0 {
q.MaxQueryTime = defaultQueryTime
}
// Timeout after maximum jitter has elapsed.
q.MaxQueryTime += q.MaxQueryTime / JitterFraction
return q.MaxQueryTime
}
return 0
}
func (q QueryOptions) HasTimedOut(start time.Time, rpcHoldTimeout, maxQueryTime, defaultQueryTime time.Duration) (bool, error) {
// In addition to BlockingTimeout, allow for an additional rpcHoldTimeout buffer
// in case we need to wait for a leader election.
return time.Since(start) > rpcHoldTimeout+q.BlockingTimeout(maxQueryTime, defaultQueryTime), nil
}
type WriteRequest struct {
// Token is the ACL token ID. If not provided, the 'anonymous'
// token is assumed for backwards compatibility.
Token string
}
// WriteRequest only applies to writes, always false
func (w WriteRequest) IsRead() bool {
return false
}
func (w WriteRequest) AllowStaleRead() bool {
return false
}
func (w WriteRequest) TokenSecret() string {
return w.Token
}
func (w *WriteRequest) SetTokenSecret(s string) {
w.Token = s
}
func (w WriteRequest) HasTimedOut(start time.Time, rpcHoldTimeout, _, _ time.Duration) (bool, error) {
return time.Since(start) > rpcHoldTimeout, nil
}
type QueryBackend int
const (
QueryBackendBlocking QueryBackend = iota
QueryBackendStreaming
)
func (q QueryBackend) GoString() string { return q.String() }
func (q QueryBackend) String() string {
switch q {
case QueryBackendBlocking:
return "blocking-query"
case QueryBackendStreaming:
return "streaming"
default:
return ""
}
}
func QueryBackendFromString(s string) QueryBackend {
switch s {
case "blocking-query":
return QueryBackendBlocking
case "streaming":
return QueryBackendStreaming
default:
return QueryBackendBlocking
}
}
// QueryMeta allows a query response to include potentially
// useful metadata about a query
type QueryMeta struct {
// Index in the raft log of the latest item returned by the query. If the
// query did not return any results the Index will be a value that will
// change when a new item is added.
Index uint64
// If AllowStale is used, this is time elapsed since
// last contact between the follower and leader. This
// can be used to gauge staleness.
LastContact time.Duration
// Used to indicate if there is a known leader node
KnownLeader bool
// Consistencylevel returns the consistency used to serve the query
// Having `discovery_max_stale` on the agent can affect whether
// the request was served by a leader.
ConsistencyLevel string
// NotModified is true when the Index of the query is the same value as the
// requested MinIndex. It indicates that the entity has not been modified.
// When NotModified is true, the response will not contain the result of
// the query.
NotModified bool
// Backend used to handle this query, either blocking-query or streaming.
Backend QueryBackend
// ResultsFilteredByACLs is true when some of the query's results were
// filtered out by enforcing ACLs. It may be false because nothing was
// removed, or because the endpoint does not yet support this flag.
ResultsFilteredByACLs bool
}
// RegisterRequest is used for the Catalog.Register endpoint
// to register a node as providing a service. If no service
// is provided, the node is registered.
type RegisterRequest struct {
Datacenter string
ID types.NodeID
Node string
Address string
TaggedAddresses map[string]string
NodeMeta map[string]string
Service *NodeService
Check *HealthCheck
Checks HealthChecks
Locality *Locality
// SkipNodeUpdate can be used when a register request is intended for
// updating a service and/or checks, but doesn't want to overwrite any
// node information if the node is already registered. If the node
// doesn't exist, it will still be created, but if the node exists, any
// node portion of this update will not apply.
SkipNodeUpdate bool
PeerName string
// EnterpriseMeta is the embedded enterprise metadata
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
WriteRequest
RaftIndex `bexpr:"-"`
}
func (r *RegisterRequest) RequestDatacenter() string {
return r.Datacenter
}
// ChangesNode returns true if the given register request changes the given
// node, which can be nil. This only looks for changes to the node record itself,
// not any of the health checks.
func (r *RegisterRequest) ChangesNode(node *Node) bool {
// This means it's creating the node.
if node == nil {
return true
}
// If we've been asked to skip the node update, then say there are no
// changes.
if r.SkipNodeUpdate {
return false
}
// Check if any of the node-level fields are being changed.
if r.ID != node.ID ||
!strings.EqualFold(r.Node, node.Node) ||
r.PartitionOrDefault() != node.PartitionOrDefault() ||
r.PeerName != node.PeerName ||
r.Address != node.Address ||
r.Datacenter != node.Datacenter ||
!reflect.DeepEqual(r.TaggedAddresses, node.TaggedAddresses) ||
!reflect.DeepEqual(r.NodeMeta, node.Meta) ||
!reflect.DeepEqual(r.Locality, node.Locality) {
return true
}
return false
}
// DeregisterRequest is used for the Catalog.Deregister endpoint to
// deregister a service, check, or node (only one should be provided).
// If ServiceID or CheckID are not provided, the entire node is deregistered.
// If a ServiceID is provided, any associated Checks with that service
// are also deregistered.
type DeregisterRequest struct {
Datacenter string
Node string
ServiceID string
CheckID types.CheckID
PeerName string
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
WriteRequest
}
func (r *DeregisterRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *DeregisterRequest) UnmarshalJSON(data []byte) error {
type Alias DeregisterRequest
aux := &struct {
Address string // obsolete field - but we want to explicitly allow it
*Alias
}{
Alias: (*Alias)(r),
}
if err := lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
return nil
}
// QuerySource is used to pass along information about the source node
// in queries so that we can adjust the response based on its network
// coordinates.
type QuerySource struct {
Datacenter string
Segment string
Node string
NodePartition string `json:",omitempty"`
// DisableNode indicates that the Node and NodePartition fields should not be used
// for determining the flow of the RPC. This is needed for agentless + wanfed to
// utilize streaming RPCs.
DisableNode bool `json:",omitempty"`
Ip string
}
func (s QuerySource) NodeEnterpriseMeta() *acl.EnterpriseMeta {
return NodeEnterpriseMetaInPartition(s.NodePartition)
}
func (s QuerySource) NodePartitionOrDefault() string {
return acl.PartitionOrDefault(s.NodePartition)
}
type DatacentersRequest struct {
QueryOptions
}
func (r *DatacentersRequest) CacheInfo() cache.RequestInfo {
return cache.RequestInfo{
Token: "",
Datacenter: "",
MinIndex: 0,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
Key: "catalog-datacenters", // must not be empty for cache to work
}
}
// DCSpecificRequest is used to query about a specific DC
type DCSpecificRequest struct {
Datacenter string
NodeMetaFilters map[string]string
Source QuerySource
PeerName string
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
QueryOptions
}
func (r *DCSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *DCSpecificRequest) CacheInfo() cache.RequestInfo {
info := cache.RequestInfo{
Token: r.Token,
Datacenter: r.Datacenter,
PeerName: r.PeerName,
MinIndex: r.MinQueryIndex,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
}
// To calculate the cache key we only hash the node meta filters and the bexpr filter.
// The datacenter is handled by the cache framework. The other fields are
// not, but should not be used in any cache types.
v, err := hashstructure.Hash([]interface{}{
r.NodeMetaFilters,
r.Filter,
r.EnterpriseMeta,
}, nil)
if err == nil {
// If there is an error, we don't set the key. A blank key forces
// no cache for this request so the request is forwarded directly
// to the server.
info.Key = strconv.FormatUint(v, 10)
}
return info
}
func (r *DCSpecificRequest) CacheMinIndex() uint64 {
return r.QueryOptions.MinQueryIndex
}
type OperatorUsageRequest struct {
DCSpecificRequest
Global bool
}
type ServiceDumpRequest struct {
Datacenter string
ServiceKind ServiceKind
UseServiceKind bool
NodesOnly bool
Source QuerySource
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
PeerName string
QueryOptions
}
func (r *ServiceDumpRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *ServiceDumpRequest) CacheInfo() cache.RequestInfo {
info := cache.RequestInfo{
Token: r.Token,
Datacenter: r.Datacenter,
PeerName: r.PeerName,
MinIndex: r.MinQueryIndex,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
}
// When we are not using the service kind we want to normalize the ServiceKind
keyKind := ServiceKindTypical
if r.UseServiceKind {
keyKind = r.ServiceKind
}
// To calculate the cache key we only hash the node meta filters and the bexpr filter.
// The datacenter is handled by the cache framework. The other fields are
// not, but should not be used in any cache types.
v, err := hashstructure.Hash([]interface{}{
keyKind,
r.UseServiceKind,
r.NodesOnly,
r.Filter,
r.EnterpriseMeta,
}, nil)
if err == nil {
// If there is an error, we don't set the key. A blank key forces
// no cache for this request so the request is forwarded directly
// to the server.
info.Key = strconv.FormatUint(v, 10)
}
return info
}
func (r *ServiceDumpRequest) CacheMinIndex() uint64 {
return r.QueryOptions.MinQueryIndex
}
// PartitionSpecificRequest is used to query about a specific partition.
type PartitionSpecificRequest struct {
Datacenter string
acl.EnterpriseMeta
QueryOptions
}
func (r *PartitionSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *PartitionSpecificRequest) CacheInfo() cache.RequestInfo {
return cache.RequestInfo{
Token: r.Token,
Datacenter: r.Datacenter,
MinIndex: r.MinQueryIndex,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
Key: r.EnterpriseMeta.PartitionOrDefault(),
}
}
// ServiceSpecificRequest is used to query about a specific service
type ServiceSpecificRequest struct {
Datacenter string
// The name of the peer that the requested service was imported from.
PeerName string
NodeMetaFilters map[string]string
ServiceName string
ServiceKind ServiceKind
// DEPRECATED (singular-service-tag) - remove this when backwards RPC compat
// with 1.2.x is not required.
ServiceTag string
ServiceTags []string
ServiceAddress string
TagFilter bool // Controls tag filtering
HealthFilterType HealthFilterType
Source QuerySource
// Connect if true will only search for Connect-compatible services.
Connect bool
// Ingress if true will only search for Ingress gateways for the given service.
Ingress bool
// MergeCentralConfig when set to true returns a service definition merged with
// the proxy-defaults/global and service-defaults/:service config entries.
// This can be used to ensure a full service definition is returned in the response
// especially when the service might not be written into the catalog that way.
MergeCentralConfig bool
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
QueryOptions
}
func (r *ServiceSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *ServiceSpecificRequest) CacheInfo() cache.RequestInfo {
info := cache.RequestInfo{
Token: r.Token,
Datacenter: r.Datacenter,
MinIndex: r.MinQueryIndex,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
}
// To calculate the cache key we hash over all the fields that affect the
// output other than Datacenter and Token which are dealt with in the cache
// framework already. Note the order here is important for the outcome - if we
// ever care about cache-invalidation on updates e.g. because we persist
// cached results, we need to be careful we maintain the same order of fields
// here. We could alternatively use `hash:set` struct tag on an anonymous
// struct to make it more robust if it becomes significant.
sort.Strings(r.ServiceTags)
v, err := hashstructure.Hash([]interface{}{
r.NodeMetaFilters,
strings.ToLower(r.ServiceName),
// DEPRECATED (singular-service-tag) - remove this when upgrade RPC compat
// with 1.2.x is not required. We still need this in because <1.3 agents
// might still send RPCs with singular tag set. In fact the only place we
// use this method is in agent cache so if the agent is new enough to have
// this code this should never be set, but it's safer to include it until we
// completely remove this field just in case it's erroneously used anywhere
// (e.g. until this change DNS still used it).
r.ServiceTag,
r.ServiceTags,
r.ServiceAddress,
r.TagFilter,
r.Connect,
r.Filter,
r.EnterpriseMeta,
r.PeerName,
r.Ingress,
r.ServiceKind,
r.MergeCentralConfig,
r.HealthFilterType,
}, nil)
if err == nil {
// If there is an error, we don't set the key. A blank key forces
// no cache for this request so the request is forwarded directly
// to the server.
info.Key = strconv.FormatUint(v, 10)
}
return info
}
func (r *ServiceSpecificRequest) CacheMinIndex() uint64 {
return r.QueryOptions.MinQueryIndex
}
// NodeSpecificRequest is used to request the information about a single node
type NodeSpecificRequest struct {
Datacenter string
Node string
PeerName string
// MergeCentralConfig when set to true returns a service definition merged with
// the proxy-defaults/global and service-defaults/:service config entries.
// This can be used to ensure a full service definition is returned in the response
// especially when the service might not be written into the catalog that way.
MergeCentralConfig bool
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
QueryOptions
}
func (r *NodeSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
func (r *NodeSpecificRequest) CacheInfo() cache.RequestInfo {
info := cache.RequestInfo{
Token: r.Token,
Datacenter: r.Datacenter,
MinIndex: r.MinQueryIndex,
Timeout: r.MaxQueryTime,
MaxAge: r.MaxAge,
MustRevalidate: r.MustRevalidate,
}
v, err := hashstructure.Hash([]interface{}{
r.Node,
r.Filter,
r.EnterpriseMeta,
r.MergeCentralConfig,
}, nil)
if err == nil {
// If there is an error, we don't set the key. A blank key forces
// no cache for this request so the request is forwarded directly
// to the server.
info.Key = strconv.FormatUint(v, 10)
}
return info
}
// ChecksInStateRequest is used to query for checks in a state
type ChecksInStateRequest struct {
Datacenter string
NodeMetaFilters map[string]string
State string
Source QuerySource
PeerName string
acl.EnterpriseMeta `mapstructure:",squash"`
QueryOptions
}
func (r *ChecksInStateRequest) RequestDatacenter() string {
return r.Datacenter
}
// Used to return information about a node
type Node struct {
ID types.NodeID
Node string
Address string
Datacenter string
Partition string `json:",omitempty"`
PeerName string `json:",omitempty"`
TaggedAddresses map[string]string
Meta map[string]string
Locality *Locality `json:",omitempty" bexpr:"-"`
RaftIndex `bexpr:"-"`
}
func (n *Node) PeerOrEmpty() string {
return n.PeerName
}
func (n *Node) GetEnterpriseMeta() *acl.EnterpriseMeta {
return NodeEnterpriseMetaInPartition(n.Partition)
}
func (n *Node) PartitionOrDefault() string {
return acl.PartitionOrDefault(n.Partition)
}
func (n *Node) BestAddress(wan bool) string {
if wan {
if addr, ok := n.TaggedAddresses[TaggedAddressWAN]; ok {
return addr
}
}
return n.Address
}
func (n *Node) ToRegisterRequest() RegisterRequest {
return RegisterRequest{
ID: n.ID,
Node: n.Node,
Datacenter: n.Datacenter,
Address: n.Address,
TaggedAddresses: n.TaggedAddresses,
NodeMeta: n.Meta,
RaftIndex: n.RaftIndex,
EnterpriseMeta: *n.GetEnterpriseMeta(),
PeerName: n.PeerName,
}
}
type Nodes []*Node
// IsSame return whether nodes are similar without taking into account
// RaftIndex fields.
func (n *Node) IsSame(other *Node) bool {
return n.ID == other.ID &&
strings.EqualFold(n.Node, other.Node) &&
n.PartitionOrDefault() == other.PartitionOrDefault() &&
strings.EqualFold(n.PeerName, other.PeerName) &&
n.Address == other.Address &&
n.Datacenter == other.Datacenter &&
reflect.DeepEqual(n.TaggedAddresses, other.TaggedAddresses) &&
reflect.DeepEqual(n.Meta, other.Meta)
}
// ValidateNodeMetadata validates a set of key/value pairs from the agent
// config for use on a Node.
func ValidateNodeMetadata(meta map[string]string, allowConsulPrefix bool) error {
return validateMetadata(meta, allowConsulPrefix, nil)
}
// ValidateServiceMetadata validates a set of key/value pairs from the agent config for use on a Service.
// ValidateMeta validates a set of key/value pairs from the agent config
func ValidateServiceMetadata(kind ServiceKind, meta map[string]string, allowConsulPrefix bool) error {
switch kind {
case ServiceKindMeshGateway:
return validateMetadata(meta, allowConsulPrefix, allowedConsulMetaKeysForMeshGateway)
default:
return validateMetadata(meta, allowConsulPrefix, nil)
}
}
// ValidateMetaTags validates arbitrary key/value pairs from the agent_endpoints
func ValidateMetaTags(metaTags map[string]string) error {
return validateMetadata(metaTags, false, nil)
}
func validateMetadata(meta map[string]string, allowConsulPrefix bool, allowedConsulKeys map[string]struct{}) error {
if len(meta) > metaMaxKeyPairs {
return fmt.Errorf("Node metadata cannot contain more than %d key/value pairs", metaMaxKeyPairs)
}
for key, value := range meta {
if err := validateMetaPair(key, value, allowConsulPrefix, allowedConsulKeys); err != nil {
return fmt.Errorf("Couldn't load metadata pair ('%s', '%s'): %s", key, value, err)
}
}
return nil
}
// ValidateWeights checks the definition of DNS weight is valid
func ValidateWeights(weights *Weights) error {
if weights == nil {
return nil
}
if weights.Passing < 1 {
return fmt.Errorf("Passing must be greater than 0")
}
if weights.Warning < 0 {
return fmt.Errorf("Warning must be greater or equal than 0")
}
if weights.Passing > 65535 || weights.Warning > 65535 {
return fmt.Errorf("DNS Weight must be between 0 and 65535")
}
return nil
}
// validateMetaPair checks that the given key/value pair is in a valid format
func validateMetaPair(key, value string, allowConsulPrefix bool, allowedConsulKeys map[string]struct{}) error {
if key == "" {
return fmt.Errorf("Key cannot be blank")
}
if !metaKeyFormat(key) {
return fmt.Errorf("Key contains invalid characters")
}
if len(key) > metaKeyMaxLength {
return fmt.Errorf("Key is too long (limit: %d characters)", metaKeyMaxLength)
}
if strings.HasPrefix(key, MetaKeyReservedPrefix) {
if _, ok := allowedConsulKeys[key]; !allowConsulPrefix && !ok {
return fmt.Errorf("Key prefix '%s' is reserved for internal use", MetaKeyReservedPrefix)
}
}
if len(value) > metaValueMaxLength {
return fmt.Errorf("Value is too long (limit: %d characters)", metaValueMaxLength)
}
return nil
}
// SatisfiesMetaFilters returns true if the metadata map contains the given filters
func SatisfiesMetaFilters(meta map[string]string, filters map[string]string) bool {
for key, value := range filters {
if v, ok := meta[key]; !ok || v != value {
return false
}
}
return true
}
// Used to return information about a provided services.
// Maps service name to available tags
type Services map[string][]string
// ServiceNode represents a node that is part of a service. ID, Address,
// TaggedAddresses, and NodeMeta are node-related fields that are always empty
// in the state store and are filled in on the way out by parseServiceNodes().
// This is also why PartialClone() skips them, because we know they are blank
// already so it would be a waste of time to copy them.
// This is somewhat complicated when the address is really a unix domain socket; technically that
// will override the address field, but in practice the two use cases should not overlap.
type ServiceNode struct {
ID types.NodeID
Node string
Address string
Datacenter string
TaggedAddresses map[string]string
NodeMeta map[string]string
ServiceKind ServiceKind
ServiceID string
ServiceName string
ServiceTags []string
ServiceAddress string
ServiceTaggedAddresses map[string]ServiceAddress `json:",omitempty"`
ServiceWeights Weights
ServiceMeta map[string]string
ServicePort int
ServiceSocketPath string
ServiceEnableTagOverride bool
ServiceProxy ConnectProxyConfig
ServiceConnect ServiceConnect
ServiceLocality *Locality `bexpr:"-"`
// If not empty, PeerName represents the peer that this ServiceNode was imported from.
PeerName string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash" bexpr:"-"`
RaftIndex `bexpr:"-"`
}
func (s *ServiceNode) FillAuthzContext(ctx *acl.AuthorizerContext) {
if ctx == nil {
return
}
ctx.Peer = s.PeerName
s.EnterpriseMeta.FillAuthzContext(ctx)
}
func (s *ServiceNode) PeerOrEmpty() string {
return s.PeerName
}
// PartialClone() returns a clone of the given service node, minus the node-
// related fields that get filled in later, Address and TaggedAddresses.
func (s *ServiceNode) PartialClone() *ServiceNode {
tags := make([]string, len(s.ServiceTags))
copy(tags, s.ServiceTags)
nsmeta := make(map[string]string)
for k, v := range s.ServiceMeta {
nsmeta[k] = v
}
var svcTaggedAddrs map[string]ServiceAddress
if len(s.ServiceTaggedAddresses) > 0 {
svcTaggedAddrs = make(map[string]ServiceAddress)
for k, v := range s.ServiceTaggedAddresses {
svcTaggedAddrs[k] = v
}
}
return &ServiceNode{
// Skip ID, see above.
Node: s.Node,
// Skip Address, see above.
// Skip TaggedAddresses, see above.
ServiceKind: s.ServiceKind,
ServiceID: s.ServiceID,
ServiceName: s.ServiceName,
ServiceTags: tags,
ServiceAddress: s.ServiceAddress,
ServiceSocketPath: s.ServiceSocketPath,
ServiceTaggedAddresses: svcTaggedAddrs,
ServicePort: s.ServicePort,
ServiceMeta: nsmeta,
ServiceWeights: s.ServiceWeights,
ServiceEnableTagOverride: s.ServiceEnableTagOverride,
ServiceProxy: s.ServiceProxy,
ServiceConnect: s.ServiceConnect,
ServiceLocality: s.ServiceLocality,
RaftIndex: RaftIndex{
CreateIndex: s.CreateIndex,
ModifyIndex: s.ModifyIndex,
},
EnterpriseMeta: s.EnterpriseMeta,
PeerName: s.PeerName,
}
}
// ToNodeService converts the given service node to a node service.
func (s *ServiceNode) ToNodeService() *NodeService {
return &NodeService{
Kind: s.ServiceKind,
ID: s.ServiceID,
Service: s.ServiceName,
Tags: s.ServiceTags,
Address: s.ServiceAddress,
TaggedAddresses: s.ServiceTaggedAddresses,
Port: s.ServicePort,
SocketPath: s.ServiceSocketPath,
Meta: s.ServiceMeta,
Weights: &s.ServiceWeights,
EnableTagOverride: s.ServiceEnableTagOverride,
Proxy: s.ServiceProxy,
Connect: s.ServiceConnect,
PeerName: s.PeerName,
EnterpriseMeta: s.EnterpriseMeta,
Locality: s.ServiceLocality,
RaftIndex: RaftIndex{
CreateIndex: s.CreateIndex,
ModifyIndex: s.ModifyIndex,
},
}
}
func (sn *ServiceNode) CompoundServiceID() ServiceID {
id := sn.ServiceID
if id == "" {
id = sn.ServiceName
}
// copy the ent meta and normalize it
entMeta := sn.EnterpriseMeta
entMeta.Normalize()
return ServiceID{
ID: id,
EnterpriseMeta: entMeta,
}
}
func (sn *ServiceNode) CompoundServiceName() PeeredServiceName {
name := sn.ServiceName
if name == "" {
name = sn.ServiceID
}
// copy the ent meta and normalize it
entMeta := sn.EnterpriseMeta
entMeta.Normalize()
return PeeredServiceName{
ServiceName: ServiceName{
Name: name,
EnterpriseMeta: entMeta,
},
Peer: sn.PeerName,
}
}
// Weights represent the weight used by DNS for a given status
type Weights struct {
Passing int
Warning int
}
type ServiceNodes []*ServiceNode
// ServiceKind is the kind of service being registered.
type ServiceKind string
func (k ServiceKind) Normalized() string {
if k == ServiceKindTypical {
return "typical"
}
return string(k)
}
// IsProxy returns whether the ServiceKind is a connect proxy or gateway.
func (k ServiceKind) IsProxy() bool {
switch k {
case ServiceKindConnectProxy,
ServiceKindMeshGateway,
ServiceKindTerminatingGateway,
ServiceKindIngressGateway,
ServiceKindAPIGateway:
return true
}
return false
}
const (
// ServiceKindTypical is a typical, classic Consul service. This is
// represented by the absence of a value. This was chosen for ease of
// backwards compatibility: existing services in the catalog would
// default to the typical service.
ServiceKindTypical ServiceKind = ""
// ServiceKindConnectProxy is a proxy for the Consul Service Mesh. This
// service proxies another service within Consul and speaks the connect
// protocol.
ServiceKindConnectProxy ServiceKind = "connect-proxy"
// ServiceKindMeshGateway is a Mesh Gateway for the Consul Service Mesh.
// This service will proxy connections based off the SNI header set by other
// connect proxies
ServiceKindMeshGateway ServiceKind = "mesh-gateway"
// ServiceKindTerminatingGateway is a Terminating Gateway for the Consul Service
// Mesh feature. This service will proxy connections to services outside the mesh.
ServiceKindTerminatingGateway ServiceKind = "terminating-gateway"
// ServiceKindIngressGateway is an Ingress Gateway for the Consul Service Mesh.
// This service allows external traffic to enter the mesh based on
// centralized configuration.
ServiceKindIngressGateway ServiceKind = "ingress-gateway"
// ServiceKindAPIGateway is an API Gateway for the Consul Service Mesh.
// This service allows external traffic to enter the mesh based on
// centralized configuration.
ServiceKindAPIGateway ServiceKind = "api-gateway"
// ServiceKindDestination is a Destination for the Consul Service Mesh feature.
// This service allows external traffic to exit the mesh through a terminating gateway
// based on centralized configuration.
ServiceKindDestination ServiceKind = "destination"
// ServiceKindConnectEnabled is used to indicate whether a service is either
// connect-native or if the service has a corresponding sidecar. It is used for
// internal query purposes and should not be exposed to users as a valid Kind
// option.
ServiceKindConnectEnabled ServiceKind = "connect-enabled"
)
// Type to hold a address and port of a service
type ServiceAddress struct {
Address string
Port int
}
func (a ServiceAddress) ToAPIServiceAddress() api.ServiceAddress {
return api.ServiceAddress{Address: a.Address, Port: a.Port}
}
const SidecarProxySuffix = "-sidecar-proxy"
// NodeService is a service provided by a node
type NodeService struct {
// Kind is the kind of service this is. Different kinds of services may
// have differing validation, DNS behavior, etc. An empty kind will default
// to the Default kind. See ServiceKind for the full list of kinds.
Kind ServiceKind `json:",omitempty"`
ID string
Service string
Tags []string
Address string
TaggedAddresses map[string]ServiceAddress `json:",omitempty"`
Meta map[string]string
Port int `json:",omitempty"`
SocketPath string `json:",omitempty"` // TODO This might be integrated into Address somehow, but not sure about the ergonomics. Only one of (address,port) or socketpath can be defined.
Weights *Weights
EnableTagOverride bool
Locality *Locality `json:",omitempty" bexpr:"-"`
// Proxy is the configuration set for Kind = connect-proxy. It is mandatory in
// that case and an error to be set for any other kind. This config is part of
// a proxy service definition. ProxyConfig may be a more natural name here, but
// it's confusing for the UX because one of the fields in ConnectProxyConfig is
// also called just "Config"
Proxy ConnectProxyConfig
// Connect are the Connect settings for a service. This is purposely NOT
// a pointer so that we never have to nil-check this.
Connect ServiceConnect
// TODO: rename to reflect that this is used to express future intent to register.
// LocallyRegisteredAsSidecar is private as it is only used by a local agent
// state to track if the service was or will be registered from a nested sidecar_service
// block. We need to track that so we can know whether we need to deregister
// it automatically too if it's removed from the service definition or if the
// parent service is deregistered. Relying only on ID would cause us to
// deregister regular services if they happen to be registered using the same
// ID scheme as our sidecars do by default. We could use meta but that gets
// unpleasant because we can't use the consul- prefix from an agent (reserved
// for use internally but in practice that means within the state store or in
// responses only), and it leaks the detail publicly which people might rely
// on which is a bit unpleasant for something that is meant to be config-file
// syntax sugar. Note this is not translated to ServiceNode and friends and
// may not be set on a NodeService that isn't the one the agent registered and
// keeps in it's local state. We never want this rendered in JSON as it's
// internal only. Right now our agent endpoints return api structs which don't
// include it but this is a safety net incase we change that or there is
// somewhere this is used in API output.
LocallyRegisteredAsSidecar bool `json:"-" bexpr:"-"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash" bexpr:"-"`
// If not empty, PeerName represents the peer that the NodeService was imported from.
PeerName string
RaftIndex `bexpr:"-"`
}
// PeeringServiceMeta is read-only information provided from an exported peer.
type PeeringServiceMeta struct {
SNI []string `json:",omitempty"`
SpiffeID []string `json:",omitempty"`
Protocol string `json:",omitempty"`
}
func (m *PeeringServiceMeta) PrimarySNI() string {
if m == nil || len(m.SNI) == 0 {
return ""
}
return m.SNI[0]
}
func (ns *NodeService) FillAuthzContext(ctx *acl.AuthorizerContext) {
if ctx == nil {
return
}
ctx.Peer = ns.PeerName
ns.EnterpriseMeta.FillAuthzContext(ctx)
}
func (ns *NodeService) BestAddress(wan bool) (string, int) {
addr := ns.Address
port := ns.Port
if wan {
if wan, ok := ns.TaggedAddresses[TaggedAddressWAN]; ok {
addr = wan.Address
if wan.Port != 0 {
port = wan.Port
}
}
}
return addr, port
}
func (ns *NodeService) CompoundServiceID() ServiceID {
id := ns.ID
if id == "" {
id = ns.Service
}
// copy the ent meta and normalize it
entMeta := ns.EnterpriseMeta
entMeta.Normalize()
return ServiceID{
ID: id,
EnterpriseMeta: entMeta,
}
}
func (ns *NodeService) CompoundServiceName() ServiceName {
name := ns.Service
if name == "" {
name = ns.ID
}
// copy the ent meta and normalize it
entMeta := ns.EnterpriseMeta
entMeta.Normalize()
return ServiceName{
Name: name,
EnterpriseMeta: entMeta,
}
}
// UniqueID is a unique identifier for a service instance within a datacenter by encoding:
// node/namespace/service_id
//
// Note: We do not have strict character restrictions in all node names, so this should NOT be split on / to retrieve components.
func UniqueID(node string, compoundID string) string {
// TODO(partitions)
return fmt.Sprintf("%s/%s", node, compoundID)
}
// ServiceConnect are the shared Connect settings between all service
// definitions from the agent to the state store.
type ServiceConnect struct {
// Native is true when this service can natively understand Connect.
Native bool `json:",omitempty"`
// SidecarService is a nested Service Definition to register at the same time.
// It's purely a convenience mechanism to allow specifying a sidecar service
// along with the application service definition. It's nested nature allows
// all of the fields to be defaulted which can reduce the amount of
// boilerplate needed to register a sidecar service separately, but the end
// result is identical to just making a second service registration via any
// other means.
SidecarService *ServiceDefinition `json:",omitempty" bexpr:"-"`
PeerMeta *PeeringServiceMeta `json:",omitempty" bexpr:"-"`
}
func (t *ServiceConnect) UnmarshalJSON(data []byte) (err error) {
type Alias ServiceConnect
aux := &struct {
SidecarServiceSnake *ServiceDefinition `json:"sidecar_service"`
*Alias
}{
Alias: (*Alias)(t),
}
if err = json.Unmarshal(data, &aux); err != nil {
return err
}
if t.SidecarService == nil && aux != nil {
t.SidecarService = aux.SidecarServiceSnake
}
return nil
}
// IsSidecarProxy returns true if the NodeService is a sidecar proxy.
func (s *NodeService) IsSidecarProxy() bool {
return s.Kind == ServiceKindConnectProxy &&
(s.Proxy.DestinationServiceID != "" || s.Proxy.DestinationServiceName != "")
}
func (s *NodeService) IsGateway() bool {
return s.Kind == ServiceKindMeshGateway ||
s.Kind == ServiceKindTerminatingGateway ||
s.Kind == ServiceKindIngressGateway ||
s.Kind == ServiceKindAPIGateway
}
// Validate validates the node service configuration.
//
// NOTE(mitchellh): This currently only validates fields for a ConnectProxy.
// Historically validation has been directly in the Catalog.Register RPC.
// ConnectProxy validation was moved here for easier table testing, but
// other validation still exists in Catalog.Register.
func (s *NodeService) Validate() error {
var result error
if err := s.Locality.Validate(); err != nil {
result = multierror.Append(result, err)
}
if s.Kind == ServiceKindConnectProxy {
if s.Port == 0 && s.SocketPath == "" {
result = multierror.Append(result, fmt.Errorf("Port or SocketPath must be set for a %s", s.Kind))
}
}
commonValidation := s.ValidateForAgent()
if commonValidation != nil {
result = multierror.Append(result, commonValidation)
}
return result
}
// ValidateForAgent does a subset validation, with the assumption that a local agent can assist with missing values.
//
// I.e. in the catalog case, a local agent cannot be assumed to facilitate auto-assignment of port or socket path,
// so additional checks are needed.
func (s *NodeService) ValidateForAgent() error {
var result error
// TODO(partitions): remember to double check that this doesn't cross partition boundaries
// ConnectProxy validation
if s.Kind == ServiceKindConnectProxy {
if strings.TrimSpace(s.Proxy.DestinationServiceName) == "" {
result = multierror.Append(result, fmt.Errorf(
"Proxy.DestinationServiceName must be non-empty for Connect proxy "+
"services"))
}
if s.Proxy.DestinationServiceName == WildcardSpecifier {
result = multierror.Append(result, fmt.Errorf(
"Proxy.DestinationServiceName must not be a wildcard for Connect proxy "+
"services"))
}
if s.Connect.Native {
result = multierror.Append(result, fmt.Errorf(
"A Proxy cannot also be Connect Native, only typical services"))
}
if err := validateOpaqueProxyConfig(s.Proxy.Config); err != nil {
result = multierror.Append(result, fmt.Errorf("Proxy.Config: %w", err))
}
// ensure we don't have multiple upstreams for the same service
var (
upstreamKeys = make(map[UpstreamKey]struct{})
bindAddrs = make(map[string]struct{})
)
for _, u := range s.Proxy.Upstreams {
destinationPartition := u.DestinationPartition
if destinationPartition == "" {
// If we have a set DestinationPeer, then DestinationPartition
// must match the NodeService's Partition
if u.DestinationPeer != "" {
destinationPartition = s.PartitionOrDefault()
} else {
destinationPartition = acl.DefaultPartitionName
}
}
if u.DestinationPeer == "" {
// cross DC Upstreams are only allowed for non "default" partitions
if u.Datacenter != "" && (destinationPartition != acl.DefaultPartitionName || s.PartitionOrDefault() != "default") {
result = multierror.Append(result, fmt.Errorf(
"upstreams cannot target another datacenter in non default partition"))
continue
}
} else {
if destinationPartition != s.PartitionOrDefault() {
result = multierror.Append(result, fmt.Errorf(
"upstreams must target peers in the same partition as the service"))
continue
}
}
if err := u.Validate(); err != nil {
result = multierror.Append(result, err)
continue
}
uk := u.ToKey()
if _, ok := upstreamKeys[uk]; ok {
result = multierror.Append(result, fmt.Errorf(
"upstreams cannot contain duplicates of %s", uk))
continue
}
upstreamKeys[uk] = struct{}{}
addr := u.UpstreamAddressToString()
// Centrally configured upstreams will fail this check if there are multiple because they do not have an address/port.
// Only consider non-centrally configured upstreams in this check since those are the ones we create listeners for.
if _, ok := bindAddrs[addr]; ok && !u.CentrallyConfigured {
result = multierror.Append(result, fmt.Errorf(
"upstreams cannot contain duplicates by local bind address and port or unix path; %q is specified twice", addr))
continue
}
bindAddrs[addr] = struct{}{}
}
var knownListeners = make(map[int]bool)
for _, path := range s.Proxy.Expose.Paths {
if path.Path == "" {
result = multierror.Append(result, fmt.Errorf("expose.paths: empty path exposed"))
}
if seen := knownListeners[path.ListenerPort]; seen {
result = multierror.Append(result, fmt.Errorf("expose.paths: duplicate listener ports exposed"))
}
knownListeners[path.ListenerPort] = true
if path.ListenerPort <= 0 || path.ListenerPort > 65535 {
result = multierror.Append(result, fmt.Errorf("expose.paths: invalid listener port: %d", path.ListenerPort))
}
path.Protocol = strings.ToLower(path.Protocol)
if ok := allowedExposeProtocols[path.Protocol]; !ok && path.Protocol != "" {
protocols := make([]string, 0)
for p := range allowedExposeProtocols {
protocols = append(protocols, p)
}
result = multierror.Append(result,
fmt.Errorf("protocol '%s' not supported for path: %s, must be in: %v",
path.Protocol, path.Path, protocols))
}
}
}
// Gateway validation
if s.IsGateway() {
// Non-ingress gateways must have a port
if s.Port == 0 && s.Kind != ServiceKindIngressGateway && s.Kind != ServiceKindAPIGateway {
result = multierror.Append(result, fmt.Errorf("Port must be non-zero for a %s", s.Kind))
}
// Gateways cannot have sidecars
if s.Connect.SidecarService != nil {
result = multierror.Append(result, fmt.Errorf("A %s cannot have a sidecar service defined", s.Kind))
}
if s.Proxy.DestinationServiceName != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceName configuration is invalid for a %s", s.Kind))
}
if s.Proxy.DestinationServiceID != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceID configuration is invalid for a %s", s.Kind))
}
if s.Proxy.LocalServiceAddress != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServiceAddress configuration is invalid for a %s", s.Kind))
}
if s.Proxy.LocalServicePort != 0 {
result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServicePort configuration is invalid for a %s", s.Kind))
}
if s.Proxy.LocalServiceSocketPath != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServiceSocketPath configuration is invalid for a %s", s.Kind))
}
if len(s.Proxy.Upstreams) != 0 {
result = multierror.Append(result, fmt.Errorf("The Proxy.Upstreams configuration is invalid for a %s", s.Kind))
}
}
// Nested sidecar validation
if s.Connect.SidecarService != nil {
if s.Connect.SidecarService.ID != "" {
result = multierror.Append(result, fmt.Errorf(
"A SidecarService cannot specify an ID as this is managed by the "+
"agent"))
}
if s.Connect.SidecarService.Connect != nil {
if s.Connect.SidecarService.Connect.SidecarService != nil {
result = multierror.Append(result, fmt.Errorf(
"A SidecarService cannot have a nested SidecarService"))
}
}
}
if s.Connect.Native && s.Port == 0 && s.SocketPath == "" {
result = multierror.Append(result, fmt.Errorf(
"Port or SocketPath must be set for a Connect native service."))
}
return result
}
// IsSame checks if one NodeService is the same as another, without looking
// at the Raft information (that's why we didn't call it IsEqual). This is
// useful for seeing if an update would be idempotent for all the functional
// parts of the structure.
func (s *NodeService) IsSame(other *NodeService) bool {
if s.ID != other.ID ||
s.Service != other.Service ||
!reflect.DeepEqual(s.Tags, other.Tags) ||
s.Address != other.Address ||
s.Port != other.Port ||
s.SocketPath != other.SocketPath ||
!reflect.DeepEqual(s.TaggedAddresses, other.TaggedAddresses) ||
!reflect.DeepEqual(s.Weights, other.Weights) ||
!reflect.DeepEqual(s.Meta, other.Meta) ||
!reflect.DeepEqual(s.Locality, other.Locality) ||
s.EnableTagOverride != other.EnableTagOverride ||
s.Kind != other.Kind ||
!reflect.DeepEqual(s.Proxy, other.Proxy) ||
s.Connect != other.Connect ||
s.PeerName != other.PeerName ||
!s.EnterpriseMeta.IsSame(&other.EnterpriseMeta) {
return false
}
return true
}
// IsSameService checks if one Service of a ServiceNode is the same as another,
// without looking at the Raft information or Node information (that's why we
// didn't call it IsEqual).
// This is useful for seeing if an update would be idempotent for all the functional
// parts of the structure.
// In a similar fashion as ToNodeService(), fields related to Node are ignored
// see ServiceNode for more information.
func (s *ServiceNode) IsSameService(other *ServiceNode) bool {
// Skip the following fields, see ServiceNode definition
// Address string
// Datacenter string
// TaggedAddresses map[string]string
// NodeMeta map[string]string
if s.ID != other.ID ||
!strings.EqualFold(s.Node, other.Node) ||
s.ServiceKind != other.ServiceKind ||
s.ServiceID != other.ServiceID ||
s.ServiceName != other.ServiceName ||
!reflect.DeepEqual(s.ServiceTags, other.ServiceTags) ||
s.ServiceAddress != other.ServiceAddress ||
!reflect.DeepEqual(s.ServiceTaggedAddresses, other.ServiceTaggedAddresses) ||
s.ServicePort != other.ServicePort ||
!reflect.DeepEqual(s.ServiceMeta, other.ServiceMeta) ||
!reflect.DeepEqual(s.ServiceWeights, other.ServiceWeights) ||
s.ServiceEnableTagOverride != other.ServiceEnableTagOverride ||
!reflect.DeepEqual(s.ServiceProxy, other.ServiceProxy) ||
!reflect.DeepEqual(s.ServiceConnect, other.ServiceConnect) ||
!s.EnterpriseMeta.IsSame(&other.EnterpriseMeta) {
return false
}
return true
}
// ToServiceNode converts the given node service to a service node.
func (s *NodeService) ToServiceNode(node string) *ServiceNode {
theWeights := Weights{
Passing: 1,
Warning: 1,
}
if s.Weights != nil {
if err := ValidateWeights(s.Weights); err == nil {
theWeights = *s.Weights
}
}
return &ServiceNode{
// Skip ID, see ServiceNode definition.
Node: node,
// Skip Address, see ServiceNode definition.
// Skip TaggedAddresses, see ServiceNode definition.
ServiceKind: s.Kind,
ServiceID: s.ID,
ServiceName: s.Service,
ServiceTags: s.Tags,
ServiceAddress: s.Address,
ServiceTaggedAddresses: s.TaggedAddresses,
ServicePort: s.Port,
ServiceSocketPath: s.SocketPath,
ServiceMeta: s.Meta,
ServiceWeights: theWeights,
ServiceEnableTagOverride: s.EnableTagOverride,
ServiceProxy: s.Proxy,
ServiceConnect: s.Connect,
ServiceLocality: s.Locality,
EnterpriseMeta: s.EnterpriseMeta,
PeerName: s.PeerName,
RaftIndex: RaftIndex{
CreateIndex: s.CreateIndex,
ModifyIndex: s.ModifyIndex,
},
}
}
// NodeServices represents services provided by Node.
// Services is a map of service IDs to services.
type NodeServices struct {
Node *Node
Services map[string]*NodeService
}
// NodeServiceList represents services provided by Node.
// Services is a list of services.
type NodeServiceList struct {
Node *Node
Services []*NodeService
}
// HealthCheck represents a single check on a given node.
type HealthCheck struct {
Node string
CheckID types.CheckID // Unique per-node ID
Name string // Check name
Status string // The current check status
Notes string // Additional notes with the status
Output string // Holds output of script runs
ServiceID string // optional associated service
ServiceName string // optional service name
ServiceTags []string // optional service tags
Type string // Check type: http/ttl/tcp/udp/etc
Interval string // from definition
Timeout string // from definition
// ExposedPort is the port of the exposed Envoy listener representing the
// HTTP or GRPC health check of the service.
ExposedPort int
// PeerName is the name of the peer the check was imported from.
// It is empty if the check was registered locally.
PeerName string `json:",omitempty"`
Definition HealthCheckDefinition `bexpr:"-"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash" bexpr:"-"`
RaftIndex `bexpr:"-"`
}
func (hc *HealthCheck) FillAuthzContext(ctx *acl.AuthorizerContext) {
if ctx == nil {
return
}
ctx.Peer = hc.PeerName
hc.EnterpriseMeta.FillAuthzContext(ctx)
}
func (hc *HealthCheck) PeerOrEmpty() string {
return hc.PeerName
}
func (hc *HealthCheck) NodeIdentity() Identity {
return Identity{
ID: hc.Node,
EnterpriseMeta: *NodeEnterpriseMetaInPartition(hc.PartitionOrDefault()),
}
}
func (hc *HealthCheck) CompoundServiceID() ServiceID {
id := hc.ServiceID
if id == "" {
id = hc.ServiceName
}
entMeta := hc.EnterpriseMeta
entMeta.Normalize()
return ServiceID{
ID: id,
EnterpriseMeta: entMeta,
}
}
func (hc *HealthCheck) CompoundCheckID() CheckID {
entMeta := hc.EnterpriseMeta
entMeta.Normalize()
return CheckID{
ID: hc.CheckID,
EnterpriseMeta: entMeta,
}
}
type HealthCheckDefinition struct {
HTTP string `json:",omitempty"`
TLSServerName string `json:",omitempty"`
TLSSkipVerify bool `json:",omitempty"`
Header map[string][]string `json:",omitempty"`
Method string `json:",omitempty"`
Body string `json:",omitempty"`
DisableRedirects bool `json:",omitempty"`
TCP string `json:",omitempty"`
TCPUseTLS bool `json:",omitempty"`
UDP string `json:",omitempty"`
H2PING string `json:",omitempty"`
OSService string `json:",omitempty"`
H2PingUseTLS bool `json:",omitempty"`
Interval time.Duration `json:",omitempty"`
OutputMaxSize uint `json:",omitempty"`
Timeout time.Duration `json:",omitempty"`
DeregisterCriticalServiceAfter time.Duration `json:",omitempty"`
ScriptArgs []string `json:",omitempty"`
DockerContainerID string `json:",omitempty"`
Shell string `json:",omitempty"`
GRPC string `json:",omitempty"`
GRPCUseTLS bool `json:",omitempty"`
AliasNode string `json:",omitempty"`
AliasService string `json:",omitempty"`
TTL time.Duration `json:",omitempty"`
}
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
type Alias HealthCheckDefinition
exported := &struct {
Interval string `json:",omitempty"`
OutputMaxSize uint `json:",omitempty"`
Timeout string `json:",omitempty"`
DeregisterCriticalServiceAfter string `json:",omitempty"`
*Alias
}{
Interval: d.Interval.String(),
OutputMaxSize: d.OutputMaxSize,
Timeout: d.Timeout.String(),
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
Alias: (*Alias)(d),
}
if d.Interval == 0 {
exported.Interval = ""
}
if d.Timeout == 0 {
exported.Timeout = ""
}
if d.DeregisterCriticalServiceAfter == 0 {
exported.DeregisterCriticalServiceAfter = ""
}
return json.Marshal(exported)
}
func (t *HealthCheckDefinition) UnmarshalJSON(data []byte) (err error) {
type Alias HealthCheckDefinition
aux := &struct {
Interval interface{}
Timeout interface{}
DeregisterCriticalServiceAfter interface{}
TTL interface{}
*Alias
}{
Alias: (*Alias)(t),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Interval != nil {
switch v := aux.Interval.(type) {
case string:
if t.Interval, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
t.Interval = time.Duration(v)
}
}
if aux.Timeout != nil {
switch v := aux.Timeout.(type) {
case string:
if t.Timeout, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
t.Timeout = time.Duration(v)
}
}
if aux.DeregisterCriticalServiceAfter != nil {
switch v := aux.DeregisterCriticalServiceAfter.(type) {
case string:
if t.DeregisterCriticalServiceAfter, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
t.DeregisterCriticalServiceAfter = time.Duration(v)
}
}
if aux.TTL != nil {
switch v := aux.TTL.(type) {
case string:
if t.TTL, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
t.TTL = time.Duration(v)
}
}
return nil
}
// IsSame checks if one HealthCheck is the same as another, without looking
// at the Raft information (that's why we didn't call it IsEqual). This is
// useful for seeing if an update would be idempotent for all the functional
// parts of the structure.
func (c *HealthCheck) IsSame(other *HealthCheck) bool {
if !strings.EqualFold(c.Node, other.Node) ||
c.CheckID != other.CheckID ||
c.Name != other.Name ||
c.Status != other.Status ||
c.Notes != other.Notes ||
c.Output != other.Output ||
c.ServiceID != other.ServiceID ||
c.ServiceName != other.ServiceName ||
!reflect.DeepEqual(c.ServiceTags, other.ServiceTags) ||
!reflect.DeepEqual(c.Definition, other.Definition) ||
c.PeerName != other.PeerName ||
!c.EnterpriseMeta.IsSame(&other.EnterpriseMeta) {
return false
}
return true
}
// Clone returns a distinct clone of the HealthCheck. Note that the
// "ServiceTags" and "Definition.Header" field are not deep copied.
func (c *HealthCheck) Clone() *HealthCheck {
clone := new(HealthCheck)
*clone = *c
return clone
}
func (c *HealthCheck) CheckType() *CheckType {
return &CheckType{
CheckID: c.CheckID,
Name: c.Name,
Status: c.Status,
Notes: c.Notes,
ScriptArgs: c.Definition.ScriptArgs,
AliasNode: c.Definition.AliasNode,
AliasService: c.Definition.AliasService,
HTTP: c.Definition.HTTP,
GRPC: c.Definition.GRPC,
GRPCUseTLS: c.Definition.GRPCUseTLS,
Header: c.Definition.Header,
Method: c.Definition.Method,
Body: c.Definition.Body,
DisableRedirects: c.Definition.DisableRedirects,
TCP: c.Definition.TCP,
TCPUseTLS: c.Definition.TCPUseTLS,
UDP: c.Definition.UDP,
H2PING: c.Definition.H2PING,
OSService: c.Definition.OSService,
H2PingUseTLS: c.Definition.H2PingUseTLS,
Interval: c.Definition.Interval,
DockerContainerID: c.Definition.DockerContainerID,
Shell: c.Definition.Shell,
TLSServerName: c.Definition.TLSServerName,
TLSSkipVerify: c.Definition.TLSSkipVerify,
Timeout: c.Definition.Timeout,
TTL: c.Definition.TTL,
DeregisterCriticalServiceAfter: c.Definition.DeregisterCriticalServiceAfter,
}
}
// HealthChecks is a collection of HealthCheck structs.
type HealthChecks []*HealthCheck
// CheckServiceNode is used to provide the node, its service
// definition, as well as a HealthCheck that is associated.
type CheckServiceNode struct {
Node *Node
Service *NodeService
Checks HealthChecks
}
func (csn *CheckServiceNode) BestAddress(wan bool) (uint64, string, int) {
// TODO (mesh-gateway) needs a test
// best address
// wan
// wan svc addr
// svc addr
// wan node addr
// node addr
// lan
// svc addr
// node addr
addr, port := csn.Service.BestAddress(wan)
idx := csn.Service.ModifyIndex
if addr == "" {
addr = csn.Node.BestAddress(wan)
idx = csn.Node.ModifyIndex
}
return idx, addr, port
}
func (csn *CheckServiceNode) CanRead(authz acl.Authorizer) acl.EnforcementDecision {
if csn.Node == nil || csn.Service == nil {
return acl.Deny
}
var authzContext acl.AuthorizerContext
csn.Service.FillAuthzContext(&authzContext)
if authz.NodeRead(csn.Node.Node, &authzContext) != acl.Allow {
return acl.Deny
}
if authz.ServiceRead(csn.Service.Service, &authzContext) != acl.Allow {
return acl.Deny
}
return acl.Allow
}
func (csn *CheckServiceNode) Locality() *Locality {
if csn.Service != nil && csn.Service.Locality != nil {
return csn.Service.Locality
}
if csn.Node != nil && csn.Node.Locality != nil {
return csn.Node.Locality
}
return nil
}
func (csn *CheckServiceNode) ExcludeBasedOnChecks(opts CheckServiceNodeFilterOptions) bool {
for _, check := range csn.Checks {
if slices.Contains(opts.IgnoreCheckIDs, check.CheckID) {
// Skip this _check_ but keep looking at other checks for this node.
continue
}
if opts.FilterType.ExcludeBasedOnStatus(check.Status) {
return true
}
}
return false
}
type CheckServiceNodes []CheckServiceNode
func (csns CheckServiceNodes) DeepCopy() CheckServiceNodes {
dup := make(CheckServiceNodes, len(csns))
for idx, v := range csns {
dup[idx] = *v.DeepCopy()
}
return dup
}
// Shuffle does an in-place random shuffle using the Fisher-Yates algorithm.
func (nodes CheckServiceNodes) Shuffle() {
for i := len(nodes) - 1; i > 0; i-- {
j := rand.Int31n(int32(i + 1))
nodes[i], nodes[j] = nodes[j], nodes[i]
}
}
func (nodes CheckServiceNodes) ToServiceDump() ServiceDump {
var ret ServiceDump
for i := range nodes {
svc := ServiceInfo{
Node: nodes[i].Node,
Service: nodes[i].Service,
Checks: nodes[i].Checks,
GatewayService: nil,
}
ret = append(ret, &svc)
}
return ret
}
// ShallowClone duplicates the slice and underlying array.
func (nodes CheckServiceNodes) ShallowClone() CheckServiceNodes {
dup := make(CheckServiceNodes, len(nodes))
copy(dup, nodes)
return dup
}
// HealthFilterType is used to filter nodes based on their health status.
type HealthFilterType int32
func (h HealthFilterType) ExcludeBasedOnStatus(status string) bool {
switch {
case h == HealthFilterExcludeCritical && status == api.HealthCritical:
return true
case h == HealthFilterIncludeOnlyPassing && status != api.HealthPassing:
return true
}
return false
}
// These are listed from most to least inclusive.
const (
HealthFilterIncludeAll HealthFilterType = 0
HealthFilterExcludeCritical HealthFilterType = 1
HealthFilterIncludeOnlyPassing HealthFilterType = 2
)
type CheckServiceNodeFilterOptions struct {
FilterType HealthFilterType
IgnoreCheckIDs []types.CheckID
disableReceiverModification bool
}
// Filter removes nodes that are failing health checks (and any non-passing
// check if that option is selected). Note that this returns the filtered
// results AND modifies the receiver for performance.
func (nodes CheckServiceNodes) Filter(opts CheckServiceNodeFilterOptions) CheckServiceNodes {
n := len(nodes)
for i := 0; i < n; i++ {
if nodes[i].ExcludeBasedOnChecks(opts) {
nodes[i], nodes[n-1] = nodes[n-1], CheckServiceNode{}
n--
i--
}
}
return nodes[:n]
}
// NodeInfo is used to dump all associated information about
// a node. This is currently used for the UI only, as it is
// rather expensive to generate.
type NodeInfo struct {
ID types.NodeID
Node string
Partition string `json:",omitempty"`
PeerName string `json:",omitempty"`
Address string
TaggedAddresses map[string]string
Meta map[string]string
Services []*NodeService
Checks HealthChecks
}
func (n *NodeInfo) GetEnterpriseMeta() *acl.EnterpriseMeta {
return NodeEnterpriseMetaInPartition(n.Partition)
}
func (n *NodeInfo) PartitionOrDefault() string {
return acl.PartitionOrDefault(n.Partition)
}
// NodeDump is used to dump all the nodes with all their
// associated data. This is currently used for the UI only,
// as it is rather expensive to generate.
type NodeDump []*NodeInfo
type ServiceInfo struct {
Node *Node
Service *NodeService
Checks HealthChecks
GatewayService *GatewayService
}
type ServiceDump []*ServiceInfo
type CheckID struct {
ID types.CheckID
acl.EnterpriseMeta
}
// NamespaceOrDefault exists because acl.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (c CheckID) NamespaceOrDefault() string {
return c.EnterpriseMeta.NamespaceOrDefault()
}
// PartitionOrDefault exists because acl.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (c CheckID) PartitionOrDefault() string {
return c.EnterpriseMeta.PartitionOrDefault()
}
func NewCheckID(id types.CheckID, entMeta *acl.EnterpriseMeta) CheckID {
var cid CheckID
cid.ID = id
if entMeta == nil {
entMeta = DefaultEnterpriseMetaInDefaultPartition()
}
cid.EnterpriseMeta = *entMeta
cid.EnterpriseMeta.Normalize()
return cid
}
// StringHashMD5 is used mainly to populate part of the filename of a check
// definition persisted on the local agent (deprecated in favor of StringHashSHA256)
// Kept around for backwards compatibility
func (cid CheckID) StringHashMD5() string {
hasher := md5.New()
hasher.Write([]byte(cid.ID))
cid.EnterpriseMeta.AddToHash(hasher, true)
return fmt.Sprintf("%x", hasher.Sum(nil))
}
// StringHashSHA256 is used mainly to populate part of the filename of a check
// definition persisted on the local agent
func (cid CheckID) StringHashSHA256() string {
hasher := sha256.New()
hasher.Write([]byte(cid.ID))
cid.EnterpriseMeta.AddToHash(hasher, true)
return fmt.Sprintf("%x", hasher.Sum(nil))
}
type ServiceID struct {
ID string
acl.EnterpriseMeta
}
func NewServiceID(id string, entMeta *acl.EnterpriseMeta) ServiceID {
var sid ServiceID
sid.ID = id
if entMeta == nil {
entMeta = DefaultEnterpriseMetaInDefaultPartition()
}
sid.EnterpriseMeta = *entMeta
sid.EnterpriseMeta.Normalize()
return sid
}
func (sid ServiceID) Matches(other ServiceID) bool {
return sid.ID == other.ID && sid.EnterpriseMeta.Matches(&other.EnterpriseMeta)
}
// StringHashSHA256 is used mainly to populate part of the filename of a service
// definition persisted on the local agent
func (sid ServiceID) StringHashSHA256() string {
hasher := sha256.New()
hasher.Write([]byte(sid.ID))
sid.EnterpriseMeta.AddToHash(hasher, true)
return fmt.Sprintf("%x", hasher.Sum(nil))
}
type IndexedNodes struct {
Nodes Nodes
QueryMeta
}
type IndexedServices struct {
Services Services
// In various situations we need to know the meta that the services are for - in particular
// this is needed to be able to properly filter the list based on ACLs
acl.EnterpriseMeta
QueryMeta
}
type Usage struct {
Usage map[string]ServiceUsage
QueryMeta
}
// ServiceUsage contains all of the usage data related to services
type ServiceUsage struct {
Services int
ServiceInstances int
ConnectServiceInstances map[string]int
BillableServiceInstances int
Nodes int
EnterpriseServiceUsage
}
// PeeredServiceName is a basic tuple of ServiceName and peer
type PeeredServiceName struct {
ServiceName ServiceName
Peer string
}
func (psn PeeredServiceName) String() string {
return fmt.Sprintf("%v:%v", psn.ServiceName.String(), psn.Peer)
}
type ServiceNameWithSamenessGroup struct {
SamenessGroup string
ServiceName
}
type ServiceName struct {
Name string
acl.EnterpriseMeta `mapstructure:",squash"`
}
func NewServiceName(name string, entMeta *acl.EnterpriseMeta) ServiceName {
var ret ServiceName
ret.Name = name
if entMeta == nil {
entMeta = DefaultEnterpriseMetaInDefaultPartition()
}
ret.EnterpriseMeta = *entMeta
ret.EnterpriseMeta.Normalize()
return ret
}
func (n ServiceName) Matches(o ServiceName) bool {
return n.Name == o.Name && n.EnterpriseMeta.Matches(&o.EnterpriseMeta)
}
func (n ServiceName) ToServiceID() ServiceID {
return ServiceID{ID: n.Name, EnterpriseMeta: n.EnterpriseMeta}
}
func ServiceGatewayVirtualIPTag(sn ServiceName) string {
return fmt.Sprintf("%s:%s", TaggedAddressVirtualIP, sn.String())
}
type ServiceList []ServiceName
// Len implements sort.Interface.
func (s ServiceList) Len() int { return len(s) }
// Swap implements sort.Interface.
func (s ServiceList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ServiceList) Sort() { sort.Sort(s) }
type IndexedServiceList struct {
Services ServiceList
QueryMeta
}
type IndexedPeeredServiceList struct {
Services []PeeredServiceName
QueryMeta
}
type IndexedServiceNodes struct {
ServiceNodes ServiceNodes
QueryMeta
}
type IndexedNodeServices struct {
// TODO: This should not be a pointer, see comments in
// agent/catalog_endpoint.go.
NodeServices *NodeServices
QueryMeta
}
type IndexedNodeServiceList struct {
NodeServices NodeServiceList
QueryMeta
}
type IndexedHealthChecks struct {
HealthChecks HealthChecks
QueryMeta
}
type IndexedCheckServiceNodes struct {
Nodes CheckServiceNodes
QueryMeta
}
type IndexedNodesWithGateways struct {
ImportedNodes CheckServiceNodes
Nodes CheckServiceNodes
Gateways GatewayServices
QueryMeta
}
type DatacenterIndexedCheckServiceNodes struct {
DatacenterNodes map[string]CheckServiceNodes
QueryMeta
}
type IndexedNodeDump struct {
ImportedDump NodeDump
Dump NodeDump
QueryMeta
}
type IndexedServiceDump struct {
Dump ServiceDump
QueryMeta
}
type IndexedGatewayServices struct {
Services GatewayServices
QueryMeta
}
type IndexedServiceTopology struct {
ServiceTopology *ServiceTopology
FilteredByACLs bool
QueryMeta
}
type ServiceTopology struct {
Upstreams CheckServiceNodes
Downstreams CheckServiceNodes
UpstreamDecisions map[string]IntentionDecisionSummary
DownstreamDecisions map[string]IntentionDecisionSummary
// MetricsProtocol is the protocol of the service being queried
MetricsProtocol string
// TransparentProxy describes whether all instances of the proxy
// service are in transparent mode.
TransparentProxy bool
// (Up|Down)streamSources are maps with labels for why each service is being
// returned. Services can be upstreams or downstreams due to
// explicit upstream definition or various types of intention policies:
// specific, wildcard, or default allow.
UpstreamSources map[string]string
DownstreamSources map[string]string
}
// IndexedConfigEntries has its own encoding logic which differs from
// ConfigEntryRequest as it has to send a slice of ConfigEntry.
type IndexedConfigEntries struct {
Kind string
Entries []ConfigEntry
QueryMeta
}
func (c *IndexedConfigEntries) MarshalBinary() (data []byte, err error) {
// bs will grow if needed but allocate enough to avoid reallocation in common
// case.
bs := make([]byte, 128)
enc := codec.NewEncoderBytes(&bs, MsgpackHandle)
// Encode length.
err = enc.Encode(len(c.Entries))
if err != nil {
return nil, err
}
// Encode kind.
err = enc.Encode(c.Kind)
if err != nil {
return nil, err
}
// Then actual value using alias trick to avoid infinite recursion
type Alias IndexedConfigEntries
err = enc.Encode(struct {
*Alias
}{
Alias: (*Alias)(c),
})
if err != nil {
return nil, err
}
return bs, nil
}
func (c *IndexedConfigEntries) UnmarshalBinary(data []byte) error {
// First decode the number of entries.
var numEntries int
dec := codec.NewDecoderBytes(data, MsgpackHandle)
if err := dec.Decode(&numEntries); err != nil {
return err
}
// Next decode the kind.
var kind string
if err := dec.Decode(&kind); err != nil {
return err
}
// Then decode the slice of ConfigEntries
c.Entries = make([]ConfigEntry, numEntries)
for i := 0; i < numEntries; i++ {
entry, err := MakeConfigEntry(kind, "")
if err != nil {
return err
}
c.Entries[i] = entry
}
// Alias juggling to prevent infinite recursive calls back to this decode
// method.
type Alias IndexedConfigEntries
as := struct {
*Alias
}{
Alias: (*Alias)(c),
}
if err := dec.Decode(&as); err != nil {
return err
}
return nil
}
type IndexedGenericConfigEntries struct {
Entries []ConfigEntry
QueryMeta
}
func (c *IndexedGenericConfigEntries) MarshalBinary() (data []byte, err error) {
// bs will grow if needed but allocate enough to avoid reallocation in common
// case.
bs := make([]byte, 128)
enc := codec.NewEncoderBytes(&bs, MsgpackHandle)
if err := enc.Encode(len(c.Entries)); err != nil {
return nil, err
}
for _, entry := range c.Entries {
if err := enc.Encode(entry.GetKind()); err != nil {
return nil, err
}
if err := enc.Encode(entry); err != nil {
return nil, err
}
}
if err := enc.Encode(c.QueryMeta); err != nil {
return nil, err
}
return bs, nil
}
func (c *IndexedGenericConfigEntries) UnmarshalBinary(data []byte) error {
// First decode the number of entries.
var numEntries int
dec := codec.NewDecoderBytes(data, MsgpackHandle)
if err := dec.Decode(&numEntries); err != nil {
return err
}
// Then decode the slice of ConfigEntries
c.Entries = make([]ConfigEntry, numEntries)
for i := 0; i < numEntries; i++ {
var kind string
if err := dec.Decode(&kind); err != nil {
return err
}
entry, err := MakeConfigEntry(kind, "")
if err != nil {
return err
}
if err := dec.Decode(entry); err != nil {
return err
}
c.Entries[i] = entry
}
if err := dec.Decode(&c.QueryMeta); err != nil {
return err
}
return nil
}
// DirEntry is used to represent a directory entry. This is
// used for values in our Key-Value store.
type DirEntry struct {
LockIndex uint64
Key string
Flags uint64
Value []byte
Session string `json:",omitempty"`
acl.EnterpriseMeta `bexpr:"-"`
RaftIndex
}
// Returns a clone of the given directory entry.
func (d *DirEntry) Clone() *DirEntry {
return &DirEntry{
LockIndex: d.LockIndex,
Key: d.Key,
Flags: d.Flags,
Value: d.Value,
Session: d.Session,
RaftIndex: RaftIndex{
CreateIndex: d.CreateIndex,
ModifyIndex: d.ModifyIndex,
},
EnterpriseMeta: d.EnterpriseMeta,
}
}
func (d *DirEntry) Equal(o *DirEntry) bool {
return d.LockIndex == o.LockIndex &&
d.Key == o.Key &&
d.Flags == o.Flags &&
bytes.Equal(d.Value, o.Value) &&
d.Session == o.Session
}
// IDValue implements the state.singleValueID interface for indexing.
func (d *DirEntry) IDValue() string {
return d.Key
}
type DirEntries []*DirEntry
// KVSRequest is used to operate on the Key-Value store
type KVSRequest struct {
Datacenter string
Op api.KVOp // Which operation are we performing
DirEnt DirEntry // Which directory entry
WriteRequest
}
func (r *KVSRequest) RequestDatacenter() string {
return r.Datacenter
}
// KeyRequest is used to request a key, or key prefix
type KeyRequest struct {
Datacenter string
Key string
acl.EnterpriseMeta
QueryOptions
}
func (r *KeyRequest) RequestDatacenter() string {
return r.Datacenter
}
// KeyListRequest is used to list keys
type KeyListRequest struct {
Datacenter string
Prefix string
Seperator string
QueryOptions
acl.EnterpriseMeta
}
func (r *KeyListRequest) RequestDatacenter() string {
return r.Datacenter
}
type IndexedDirEntries struct {
Entries DirEntries
QueryMeta
}
type IndexedKeyList struct {
Keys []string
QueryMeta
}
type SessionBehavior string
const (
SessionKeysRelease SessionBehavior = "release"
SessionKeysDelete = "delete"
)
const (
SessionTTLMax = 24 * time.Hour
SessionTTLMultiplier = 2
)
type Sessions []*Session
// Session is used to represent an open session in the KV store.
// This issued to associate node checks with acquired locks.
type Session struct {
ID string
Name string
Node string // TODO(partitions): ensure that the entmeta interacts with this node field properly
LockDelay time.Duration
Behavior SessionBehavior // What to do when session is invalidated
TTL string
NodeChecks []string
ServiceChecks []ServiceCheck
// Deprecated v1.7.0.
Checks []types.CheckID `json:",omitempty"`
acl.EnterpriseMeta
RaftIndex
}
type ServiceCheck struct {
ID string
Namespace string
}
// IDValue implements the state.singleValueID interface for indexing.
func (s *Session) IDValue() string {
return s.ID
}
func (s *Session) UnmarshalJSON(data []byte) (err error) {
type Alias Session
aux := &struct {
LockDelay interface{}
*Alias
}{
Alias: (*Alias)(s),
}
if err = json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.LockDelay != nil {
var dur time.Duration
switch v := aux.LockDelay.(type) {
case string:
if dur, err = time.ParseDuration(v); err != nil {
return err
}
case float64:
dur = time.Duration(v)
}
// Convert low value integers into seconds
if dur < lockDelayMinThreshold {
dur = dur * time.Second
}
s.LockDelay = dur
}
return nil
}
type SessionOp string
const (
SessionCreate SessionOp = "create"
SessionDestroy = "destroy"
)
// SessionRequest is used to operate on sessions
type SessionRequest struct {
Datacenter string
Op SessionOp // Which operation are we performing
Session Session // Which session
WriteRequest
}
func (r *SessionRequest) RequestDatacenter() string {
return r.Datacenter
}
// SessionSpecificRequest is used to request a session by ID
type SessionSpecificRequest struct {
Datacenter string
SessionID string
// DEPRECATED in 1.7.0
Session string
acl.EnterpriseMeta
QueryOptions
}
func (r *SessionSpecificRequest) RequestDatacenter() string {
return r.Datacenter
}
type IndexedSessions struct {
Sessions Sessions
QueryMeta
}
// Coordinate stores a node name with its associated network coordinate.
type Coordinate struct {
Node string
Segment string
Partition string `json:",omitempty"` // TODO(partitions): fully thread this needle
Coord *coordinate.Coordinate
}
func (c *Coordinate) GetEnterpriseMeta() *acl.EnterpriseMeta {
return NodeEnterpriseMetaInPartition(c.Partition)
}
func (c *Coordinate) PartitionOrDefault() string {
return acl.PartitionOrDefault(c.Partition)
}
type Coordinates []*Coordinate
// IndexedCoordinate is used to represent a single node's coordinate from the state
// store.
type IndexedCoordinate struct {
Coord *coordinate.Coordinate
QueryMeta
}
// IndexedCoordinates is used to represent a list of nodes and their
// corresponding raw coordinates.
type IndexedCoordinates struct {
Coordinates Coordinates
QueryMeta
}
// DatacenterMap is used to represent a list of nodes with their raw coordinates,
// associated with a datacenter. Coordinates are only compatible between nodes in
// the same area.
type DatacenterMap struct {
Datacenter string
AreaID types.AreaID
Coordinates Coordinates
}
// CoordinateUpdateRequest is used to update the network coordinate of a given
// node.
type CoordinateUpdateRequest struct {
Datacenter string
Node string
Segment string
Coord *coordinate.Coordinate
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
WriteRequest
}
// RequestDatacenter returns the datacenter for a given update request.
func (c *CoordinateUpdateRequest) RequestDatacenter() string {
return c.Datacenter
}
// EventFireRequest is used to ask a server to fire
// a Serf event. It is a bit odd, since it doesn't depend on
// the catalog or leader. Any node can respond, so it's not quite
// like a standard write request. This is used only internally.
type EventFireRequest struct {
Datacenter string
Name string
Payload []byte
// Not using WriteRequest so that any server can process
// the request. It is a bit unusual...
QueryOptions
}
func (r *EventFireRequest) RequestDatacenter() string {
return r.Datacenter
}
// EventFireResponse is used to respond to a fire request.
type EventFireResponse struct {
QueryMeta
}
type TombstoneOp string
const (
TombstoneReap TombstoneOp = "reap"
)
// TombstoneRequest is used to trigger a reaping of the tombstones
type TombstoneRequest struct {
Datacenter string
Op TombstoneOp
ReapIndex uint64
WriteRequest
}
func (r *TombstoneRequest) RequestDatacenter() string {
return r.Datacenter
}
// MsgpackHandle is a shared handle for encoding/decoding msgpack payloads
var MsgpackHandle = &codec.MsgpackHandle{
RawToString: true,
BasicHandle: codec.BasicHandle{
DecodeOptions: codec.DecodeOptions{
MapType: reflect.TypeOf(map[string]interface{}{}),
},
},
}
// Decode is used to decode a MsgPack encoded object
func Decode(buf []byte, out interface{}) error {
return codec.NewDecoder(bytes.NewReader(buf), MsgpackHandle).Decode(out)
}
// Encode is used to encode a MsgPack object with type prefix
func Encode(t MessageType, msg interface{}) ([]byte, error) {
var buf bytes.Buffer
buf.WriteByte(uint8(t))
err := codec.NewEncoder(&buf, MsgpackHandle).Encode(msg)
return buf.Bytes(), err
}
func EncodeProtoInterface(t MessageType, message interface{}) ([]byte, error) {
if marshaller, ok := message.(proto.Message); ok {
return EncodeProto(t, marshaller)
}
return nil, fmt.Errorf("message does not implement proto.Message: %T", message)
}
func EncodeProto(t MessageType, pb proto.Message) ([]byte, error) {
data := make([]byte, 0, proto.Size(pb)+1)
data = append(data, uint8(t))
var err error
data, err = proto.MarshalOptions{}.MarshalAppend(data, pb)
if err != nil {
return nil, err
}
return data, nil
}
func DecodeProto(buf []byte, pb proto.Message) error {
// Note that this assumes the leading byte indicating the type as already been stripped off.
return proto.Unmarshal(buf, pb)
}
// CompoundResponse is an interface for gathering multiple responses. It is
// used in cross-datacenter RPC calls where more than 1 datacenter is
// expected to reply.
type CompoundResponse interface {
// Add adds a new response to the compound response
Add(interface{})
// New returns an empty response object which can be passed around by
// reference, and then passed to Add() later on.
New() interface{}
}
type KeyringOp string
const (
KeyringList KeyringOp = "list"
KeyringInstall = "install"
KeyringUse = "use"
KeyringRemove = "remove"
)
// KeyringRequest encapsulates a request to modify an encryption keyring.
// It can be used for install, remove, or use key type operations.
type KeyringRequest struct {
Operation KeyringOp
Key string
Datacenter string
Forwarded bool
RelayFactor uint8
LocalOnly bool
QueryOptions
}
func (r *KeyringRequest) RequestDatacenter() string {
return r.Datacenter
}
// KeyringResponse is a unified key response and can be used for install,
// remove, use, as well as listing key queries.
type KeyringResponse struct {
WAN bool
Datacenter string
Segment string
Partition string `json:",omitempty"`
Messages map[string]string `json:",omitempty"`
Keys map[string]int
PrimaryKeys map[string]int
NumNodes int
Error string `json:",omitempty"`
}
func (r *KeyringResponse) PartitionOrDefault() string {
return acl.PartitionOrDefault(r.Partition)
}
// KeyringResponses holds multiple responses to keyring queries. Each
// datacenter replies independently, and KeyringResponses is used as a
// container for the set of all responses.
type KeyringResponses struct {
Responses []*KeyringResponse
QueryMeta
}
func (r *KeyringResponses) Add(v interface{}) {
val := v.(*KeyringResponses)
r.Responses = append(r.Responses, val.Responses...)
}
func (r *KeyringResponses) New() interface{} {
return new(KeyringResponses)
}
// String converts message type int to string
func (m MessageType) String() string {
s, ok := requestTypeStrings[m]
if ok {
return s
}
s, ok = enterpriseRequestType(m)
if ok {
return s
}
return "Unknown(" + strconv.Itoa(int(m)) + ")"
}
// This should only be used for conversions generated by MOG
func DurationToProto(d time.Duration) *durationpb.Duration {
return durationpb.New(d)
}
// This should only be used for conversions generated by MOG
func DurationFromProto(d *durationpb.Duration) time.Duration {
return d.AsDuration()
}
// This should only be used for conversions generated by MOG
func DurationPointerToProto(d *time.Duration) *durationpb.Duration {
if d == nil {
return nil
}
return durationpb.New(*d)
}
// This should only be used for conversions generated by MOG
func DurationPointerFromProto(d *durationpb.Duration) *time.Duration {
if d == nil {
return nil
}
return DurationPointer(d.AsDuration())
}
func DurationPointer(d time.Duration) *time.Duration {
return &d
}
// This should only be used for conversions generated by MOG
func TimeFromProto(s *timestamppb.Timestamp) time.Time {
return s.AsTime()
}
// This should only be used for conversions generated by MOG
func TimeToProto(s time.Time) *timestamppb.Timestamp {
return timestamppb.New(s)
}
// IsZeroProtoTime returns true if the time is the minimum protobuf timestamp
// (the Unix epoch).
func IsZeroProtoTime(t *timestamppb.Timestamp) bool {
return t.Seconds == 0 && t.Nanos == 0
}
// Locality identifies where a given entity is running.
type Locality struct {
// Region is region the zone belongs to.
Region string `json:",omitempty"`
// Zone is the zone the entity is running in.
Zone string `json:",omitempty"`
}
// ToAPI converts a struct Locality to an API Locality.
func (l *Locality) ToAPI() *api.Locality {
if l == nil {
return nil
}
return &api.Locality{
Region: l.Region,
Zone: l.Zone,
}
}
func (l *Locality) GetRegion() string {
if l == nil {
return ""
}
return l.Region
}
func (l *Locality) Validate() error {
if l == nil {
return nil
}
if l.Region == "" && l.Zone != "" {
return fmt.Errorf("zone cannot be set without region")
}
return nil
}