consul/agent/discovery/discovery.go

232 lines
7.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package discovery
import (
"fmt"
"net"
"github.com/hashicorp/consul/agent/config"
)
var (
ErrECSNotGlobal = fmt.Errorf("ECS response is not global")
ErrNoData = fmt.Errorf("no data")
ErrNotFound = fmt.Errorf("not found")
ErrNotSupported = fmt.Errorf("not supported")
ErrNoPathToDatacenter = fmt.Errorf("no path to datacenter")
)
// ECSNotGlobalError may be used to wrap an error or nil, to indicate that the
// EDNS client subnet source scope is not global.
// TODO (v2-dns): prepared queries errors are wrapped by this
type ECSNotGlobalError struct {
error
}
func (e ECSNotGlobalError) Error() string {
if e.error == nil {
return ""
}
return e.error.Error()
}
func (e ECSNotGlobalError) Is(other error) bool {
return other == ErrECSNotGlobal
}
func (e ECSNotGlobalError) Unwrap() error {
return e.error
}
// Query is used to request a name-based Service Discovery lookup.
type Query struct {
QueryType QueryType
QueryPayload QueryPayload
}
// QueryType is used to filter service endpoints.
// This is needed by the V1 catalog because of the
// overlapping lookups through the service endpoint.
type QueryType string
const (
QueryTypeConnect QueryType = "CONNECT" // deprecated: use for V1 only
QueryTypeIngress QueryType = "INGRESS" // deprecated: use for V1 only
QueryTypeInvalid QueryType = "INVALID"
QueryTypeNode QueryType = "NODE"
QueryTypePreparedQuery QueryType = "PREPARED_QUERY" // deprecated: use for V1 only
QueryTypeService QueryType = "SERVICE"
QueryTypeVirtual QueryType = "VIRTUAL"
QueryTypeWorkload QueryType = "WORKLOAD" // V2-only
)
// Context is used to pass information about the request.
type Context struct {
Token string
}
// QueryTenancy is used to filter catalog data based on tenancy.
type QueryTenancy struct {
Namespace string
Partition string
SamenessGroup string
Peer string
Datacenter string
}
// QueryPayload represents all information needed by the data backend
// to decide which records to include.
type QueryPayload struct {
Name string
PortName string // v1 - this could optionally be "connect" or "ingress"; v2 - this is the service port name
Tag string // deprecated: use for V1 only
SourceIP net.IP // deprecated: used for prepared queries
Tenancy QueryTenancy // tenancy includes any additional labels specified before the domain
Limit int // The maximum number of records to return
// v2 fields only
EnableFailover bool
}
// ResultType indicates the Consul resource that a discovery record represents.
// This is useful for things like adding TTLs for different objects in the DNS.
type ResultType string
const (
ResultTypeService ResultType = "SERVICE"
ResultTypeNode ResultType = "NODE"
ResultTypeVirtual ResultType = "VIRTUAL"
ResultTypeWorkload ResultType = "WORKLOAD"
)
// Result is a generic format of targets that could be returned in a query.
// It is the responsibility of the DNS encoder to know what to do with
// each Result, based on the query type.
type Result struct {
Service *Location // The name and address of the service.
Node *Location // The name and address of the node.
Weight uint32 // SRV queries
PortName string // Used to generate a fgdn when a specifc port was queried
PortNumber uint32 // SRV queries
Metadata map[string]string // Used to collect metadata into TXT Records
Type ResultType // Used to reconstruct the fqdn name of the resource
Tenancy ResultTenancy
}
// Location is used to represent a service, node, or workload.
type Location struct {
Name string
Address string
}
// ResultTenancy is used to reconstruct the fqdn name of the resource.
type ResultTenancy struct {
Namespace string
Partition string
PeerName string
Datacenter string
}
// LookupType is used by the CatalogDataFetcher to properly filter endpoints.
type LookupType string
const (
LookupTypeService LookupType = "SERVICE"
LookupTypeConnect LookupType = "CONNECT"
LookupTypeIngress LookupType = "INGRESS"
)
// CatalogDataFetcher is an interface that abstracts data collection
// for Discovery queries. It is assumed that the instantiation also
// includes any agent configuration that influences catalog queries.
//
//go:generate mockery --name CatalogDataFetcher --inpackage
type CatalogDataFetcher interface {
// LoadConfig is used to hot-reload the data fetcher with new agent config.
LoadConfig(config *config.RuntimeConfig)
// FetchNodes fetches A/AAAA/CNAME
FetchNodes(ctx Context, req *QueryPayload) ([]*Result, error)
// FetchEndpoints fetches records for A/AAAA/CNAME or SRV requests for services
FetchEndpoints(ctx Context, req *QueryPayload, lookupType LookupType) ([]*Result, error)
// FetchVirtualIP fetches A/AAAA records for virtual IPs
FetchVirtualIP(ctx Context, req *QueryPayload) (*Result, error)
// FetchRecordsByIp is used for PTR requests
// to look up a service/node from an IP.
FetchRecordsByIp(ctx Context, ip net.IP) ([]*Result, error)
// FetchWorkload fetches a single Result associated with
// V2 Workload. V2-only.
FetchWorkload(ctx Context, req *QueryPayload) (*Result, error)
// FetchPreparedQuery evaluates the results of a prepared query.
// deprecated in V2
FetchPreparedQuery(ctx Context, req *QueryPayload) ([]*Result, error)
// NormalizeRequest mutates the original request based on data fetcher configuration, like
// defaulting tenancy to the agent's partition.
NormalizeRequest(req *QueryPayload)
// ValidateRequest throws an error is any of the input fields are invalid for this data fetcher.
ValidateRequest(ctx Context, req *QueryPayload) error
}
// QueryProcessor is used to process a Discovery Query and return the results.
type QueryProcessor struct {
dataFetcher CatalogDataFetcher
}
// NewQueryProcessor creates a new QueryProcessor.
func NewQueryProcessor(dataFetcher CatalogDataFetcher) *QueryProcessor {
return &QueryProcessor{
dataFetcher: dataFetcher,
}
}
// QueryByName is used to look up a service, node, workload, or prepared query.
func (p *QueryProcessor) QueryByName(query *Query, ctx Context) ([]*Result, error) {
if err := p.dataFetcher.ValidateRequest(ctx, &query.QueryPayload); err != nil {
return nil, err
}
p.dataFetcher.NormalizeRequest(&query.QueryPayload)
switch query.QueryType {
case QueryTypeNode:
return p.dataFetcher.FetchNodes(ctx, &query.QueryPayload)
case QueryTypeService:
return p.dataFetcher.FetchEndpoints(ctx, &query.QueryPayload, LookupTypeService)
case QueryTypeConnect:
return p.dataFetcher.FetchEndpoints(ctx, &query.QueryPayload, LookupTypeConnect)
case QueryTypeIngress:
return p.dataFetcher.FetchEndpoints(ctx, &query.QueryPayload, LookupTypeIngress)
case QueryTypeVirtual:
result, err := p.dataFetcher.FetchVirtualIP(ctx, &query.QueryPayload)
if err != nil {
return nil, err
}
return []*Result{result}, nil
case QueryTypeWorkload:
result, err := p.dataFetcher.FetchWorkload(ctx, &query.QueryPayload)
if err != nil {
return nil, err
}
return []*Result{result}, nil
case QueryTypePreparedQuery:
return p.dataFetcher.FetchPreparedQuery(ctx, &query.QueryPayload)
default:
return nil, fmt.Errorf("unknown query type: %s", query.QueryType)
}
}
// QueryByIP is used to look up a service or node from an IP address.
func (p *QueryProcessor) QueryByIP(ip net.IP, reqCtx Context) ([]*Result, error) {
return p.dataFetcher.FetchRecordsByIp(reqCtx, ip)
}