consul/agent/structs/connect_proxy_config.go

653 lines
22 KiB
Go

package structs
import (
"encoding/json"
"fmt"
"net"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
)
const (
defaultExposeProtocol = "http"
)
var allowedExposeProtocols = map[string]bool{"http": true, "http2": true}
type MeshGatewayMode string
const (
// MeshGatewayModeDefault represents no specific mode and should
// be used to indicate that a different layer of the configuration
// chain should take precedence
MeshGatewayModeDefault MeshGatewayMode = ""
// MeshGatewayModeNone represents that the Upstream Connect connections
// should be direct and not flow through a mesh gateway.
MeshGatewayModeNone MeshGatewayMode = "none"
// MeshGatewayModeLocal represents that the Upstream Connect connections
// should be made to a mesh gateway in the local datacenter.
MeshGatewayModeLocal MeshGatewayMode = "local"
// MeshGatewayModeRemote represents that the Upstream Connect connections
// should be made to a mesh gateway in a remote datacenter.
MeshGatewayModeRemote MeshGatewayMode = "remote"
)
const (
// TODO (freddy) Should we have a TopologySourceMixed when there is a mix of proxy reg and tproxy?
// Currently we label as proxy-registration if ANY instance has the explicit upstream definition.
// TopologySourceRegistration is used to label upstreams or downstreams from explicit upstream definitions
TopologySourceRegistration = "proxy-registration"
// TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions
TopologySourceSpecificIntention = "specific-intention"
// TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions
TopologySourceWildcardIntention = "wildcard-intention"
// TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy
TopologySourceDefaultAllow = "default-allow"
)
// MeshGatewayConfig controls how Mesh Gateways are configured and used
// This is a struct to allow for future additions without having more free-hanging
// configuration items all over the place
type MeshGatewayConfig struct {
// The Mesh Gateway routing mode
Mode MeshGatewayMode `json:",omitempty"`
}
func (c *MeshGatewayConfig) IsZero() bool {
zeroVal := MeshGatewayConfig{}
return *c == zeroVal
}
func (base *MeshGatewayConfig) OverlayWith(overlay MeshGatewayConfig) MeshGatewayConfig {
out := *base
if overlay.Mode != MeshGatewayModeDefault {
out.Mode = overlay.Mode
}
return out
}
func ValidateMeshGatewayMode(mode string) (MeshGatewayMode, error) {
switch MeshGatewayMode(mode) {
case MeshGatewayModeNone:
return MeshGatewayModeNone, nil
case MeshGatewayModeDefault:
return MeshGatewayModeDefault, nil
case MeshGatewayModeLocal:
return MeshGatewayModeLocal, nil
case MeshGatewayModeRemote:
return MeshGatewayModeRemote, nil
default:
return MeshGatewayModeDefault, fmt.Errorf("Invalid Mesh Gateway Mode: %q", mode)
}
}
func (c *MeshGatewayConfig) ToAPI() api.MeshGatewayConfig {
return api.MeshGatewayConfig{Mode: api.MeshGatewayMode(c.Mode)}
}
type ProxyMode string
const (
// ProxyModeDefault represents no specific mode and should
// be used to indicate that a different layer of the configuration
// chain should take precedence
ProxyModeDefault ProxyMode = ""
// ProxyModeTransparent represents that inbound and outbound application
// traffic is being captured and redirected through the proxy.
ProxyModeTransparent ProxyMode = "transparent"
// ProxyModeDirect represents that the proxy's listeners must be dialed directly
// by the local application and other proxies.
ProxyModeDirect ProxyMode = "direct"
)
func ValidateProxyMode(mode string) (ProxyMode, error) {
switch ProxyMode(mode) {
case ProxyModeDefault:
return ProxyModeDefault, nil
case ProxyModeDirect:
return ProxyModeDirect, nil
case ProxyModeTransparent:
return ProxyModeTransparent, nil
default:
return ProxyModeDefault, fmt.Errorf("Invalid Proxy Mode: %q", mode)
}
}
type TransparentProxyConfig struct {
// The port of the listener where outbound application traffic is being redirected to.
OutboundListenerPort int `json:",omitempty" alias:"outbound_listener_port"`
// DialedDirectly indicates whether transparent proxies can dial this proxy instance directly.
// The discovery chain is not considered when dialing a service instance directly.
// This setting is useful when addressing stateful services, such as a database cluster with a leader node.
DialedDirectly bool `json:",omitempty" alias:"dialed_directly"`
}
func (c TransparentProxyConfig) ToAPI() *api.TransparentProxyConfig {
if c.IsZero() {
return nil
}
return &api.TransparentProxyConfig{
OutboundListenerPort: c.OutboundListenerPort,
DialedDirectly: c.DialedDirectly,
}
}
func (c *TransparentProxyConfig) IsZero() bool {
zeroVal := TransparentProxyConfig{}
return *c == zeroVal
}
// ConnectProxyConfig describes the configuration needed for any proxy managed
// or unmanaged. It describes a single logical service's listener and optionally
// upstreams and sidecar-related config for a single instance. To describe a
// centralized proxy that routed traffic for multiple services, a different one
// of these would be needed for each, sharing the same LogicalProxyID.
type ConnectProxyConfig struct {
// DestinationServiceName is required and is the name of the service to accept
// traffic for.
DestinationServiceName string `json:",omitempty" alias:"destination_service_name"`
// DestinationServiceID is optional and should only be specified for
// "side-car" style proxies where the proxy is in front of just a single
// instance of the service. It should be set to the service ID of the instance
// being represented which must be registered to the same agent. It's valid to
// provide a service ID that does not yet exist to avoid timing issues when
// bootstrapping a service with a proxy.
DestinationServiceID string `json:",omitempty" alias:"destination_service_id"`
// LocalServiceAddress is the address of the local service instance. It is
// optional and should only be specified for "side-car" style proxies. It will
// default to 127.0.0.1 if the proxy is a "side-car" (DestinationServiceID is
// set) but otherwise will be ignored.
LocalServiceAddress string `json:",omitempty" alias:"local_service_address"`
// LocalServicePort is the port of the local service instance. It is optional
// and should only be specified for "side-car" style proxies. It will default
// to the registered port for the instance if the proxy is a "side-car"
// (DestinationServiceID is set) but otherwise will be ignored.
LocalServicePort int `json:",omitempty" alias:"local_service_port"`
// LocalServiceSocketPath is the socket of the local service instance. It is optional
// and should only be specified for "side-car" style proxies.
LocalServiceSocketPath string `json:",omitempty" alias:"local_service_socket_path"`
// Mode represents how the proxy's inbound and upstream listeners are dialed.
Mode ProxyMode
// Config is the arbitrary configuration data provided with the proxy
// registration.
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
// Upstreams describes any upstream dependencies the proxy instance should
// setup.
Upstreams Upstreams `json:",omitempty"`
// MeshGateway defines the mesh gateway configuration for this upstream
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
// Expose defines whether checks or paths are exposed through the proxy
Expose ExposeConfig `json:",omitempty"`
// TransparentProxy defines configuration for when the proxy is in
// transparent mode.
TransparentProxy TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
}
func (t *ConnectProxyConfig) UnmarshalJSON(data []byte) (err error) {
type Alias ConnectProxyConfig
aux := &struct {
DestinationServiceNameSnake string `json:"destination_service_name"`
DestinationServiceIDSnake string `json:"destination_service_id"`
LocalServiceAddressSnake string `json:"local_service_address"`
LocalServicePortSnake int `json:"local_service_port"`
LocalServiceSocketPathSnake string `json:"local_service_socket_path"`
MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"`
TransparentProxySnake TransparentProxyConfig `json:"transparent_proxy"`
*Alias
}{
Alias: (*Alias)(t),
}
if err = lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
if t.DestinationServiceName == "" {
t.DestinationServiceName = aux.DestinationServiceNameSnake
}
if t.DestinationServiceID == "" {
t.DestinationServiceID = aux.DestinationServiceIDSnake
}
if t.LocalServiceAddress == "" {
t.LocalServiceAddress = aux.LocalServiceAddressSnake
}
if t.LocalServicePort == 0 {
t.LocalServicePort = aux.LocalServicePortSnake
}
if t.LocalServiceSocketPath == "" {
t.LocalServiceSocketPath = aux.LocalServiceSocketPathSnake
}
if t.MeshGateway.Mode == "" {
t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode
}
if t.TransparentProxy.OutboundListenerPort == 0 {
t.TransparentProxy.OutboundListenerPort = aux.TransparentProxySnake.OutboundListenerPort
}
if !t.TransparentProxy.DialedDirectly {
t.TransparentProxy.DialedDirectly = aux.TransparentProxySnake.DialedDirectly
}
return nil
}
func (c *ConnectProxyConfig) MarshalJSON() ([]byte, error) {
type Alias ConnectProxyConfig
out := struct {
TransparentProxy *TransparentProxyConfig `json:",omitempty"`
Alias
}{
Alias: (Alias)(*c),
}
proxyConfig, err := lib.MapWalk(c.Config)
if err != nil {
return nil, err
}
out.Alias.Config = proxyConfig
if !c.TransparentProxy.IsZero() {
out.TransparentProxy = &out.Alias.TransparentProxy
}
return json.Marshal(&out)
}
// ToAPI returns the api struct with the same fields. We have duplicates to
// avoid the api package depending on this one which imports a ton of Consul's
// core which you don't want if you are just trying to use our client in your
// app.
func (c *ConnectProxyConfig) ToAPI() *api.AgentServiceConnectProxyConfig {
return &api.AgentServiceConnectProxyConfig{
DestinationServiceName: c.DestinationServiceName,
DestinationServiceID: c.DestinationServiceID,
LocalServiceAddress: c.LocalServiceAddress,
LocalServicePort: c.LocalServicePort,
LocalServiceSocketPath: c.LocalServiceSocketPath,
Mode: api.ProxyMode(c.Mode),
TransparentProxy: c.TransparentProxy.ToAPI(),
Config: c.Config,
Upstreams: c.Upstreams.ToAPI(),
MeshGateway: c.MeshGateway.ToAPI(),
Expose: c.Expose.ToAPI(),
}
}
const (
UpstreamDestTypeService = "service"
UpstreamDestTypePreparedQuery = "prepared_query"
)
// Upstreams is a list of upstreams. Aliased to allow ToAPI method.
type Upstreams []Upstream
// ToAPI returns the api structs with the same fields. We have duplicates to
// avoid the api package depending on this one which imports a ton of Consul's
// core which you don't want if you are just trying to use our client in your
// app.
func (us Upstreams) ToAPI() []api.Upstream {
a := make([]api.Upstream, len(us))
for i, u := range us {
a[i] = u.ToAPI()
}
return a
}
func (us Upstreams) ToMap() map[string]*Upstream {
upstreamMap := make(map[string]*Upstream)
for i := range us {
upstreamMap[us[i].Identifier()] = &us[i]
}
return upstreamMap
}
// UpstreamsFromAPI is a helper for converting api.Upstream to Upstream.
func UpstreamsFromAPI(us []api.Upstream) Upstreams {
a := make([]Upstream, len(us))
for i, u := range us {
a[i] = UpstreamFromAPI(u)
}
return a
}
// Upstream represents a single upstream dependency for a service or proxy. It
// describes the mechanism used to discover instances to communicate with (the
// Target) as well as any potential client configuration that may be useful such
// as load balancer options, timeouts etc.
type Upstream struct {
// Destination fields are the required ones for determining what this upstream
// points to. Depending on DestinationType some other fields below might
// further restrict the set of instances allowable.
//
// DestinationType would be better as an int constant but even with custom
// JSON marshallers it causes havoc with all the mapstructure mangling we do
// on service definitions in various places.
DestinationType string `alias:"destination_type"`
DestinationNamespace string `json:",omitempty" alias:"destination_namespace"`
DestinationName string `alias:"destination_name"`
// Datacenter that the service discovery request should be run against. Note
// for prepared queries, the actual results might be from a different
// datacenter.
Datacenter string
// LocalBindAddress is the ip address a side-car proxy should listen on for
// traffic destined for this upstream service. Default if empty is 127.0.0.1.
LocalBindAddress string `json:",omitempty" alias:"local_bind_address"`
// LocalBindPort is the ip address a side-car proxy should listen on for traffic
// destined for this upstream service. Required.
LocalBindPort int `json:",omitempty" alias:"local_bind_port"`
// These are exclusive with LocalBindAddress/LocalBindPort
LocalBindSocketPath string `json:",omitempty" alias:"local_bind_socket_path"`
// This might be represented as an int, but because it's octal outputs can be a bit strange.
LocalBindSocketMode string `json:",omitempty" alias:"local_bind_socket_mode"`
// Config is an opaque config that is specific to the proxy process being run.
// It can be used to pass arbitrary configuration for this specific upstream
// to the proxy.
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
// MeshGateway is the configuration for mesh gateway usage of this upstream
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
// IngressHosts are a list of hosts that should route to this upstream from
// an ingress gateway. This cannot and should not be set by a user, it is
// used internally to store the association of hosts to an upstream service.
IngressHosts []string `json:"-" bexpr:"-"`
// CentrallyConfigured indicates whether the upstream was defined in a proxy
// instance registration or whether it was generated from a config entry.
CentrallyConfigured bool `json:",omitempty" bexpr:"-"`
}
func (t *Upstream) UnmarshalJSON(data []byte) (err error) {
type Alias Upstream
aux := &struct {
DestinationTypeSnake string `json:"destination_type"`
DestinationNamespaceSnake string `json:"destination_namespace"`
DestinationNameSnake string `json:"destination_name"`
LocalBindAddressSnake string `json:"local_bind_address"`
LocalBindPortSnake int `json:"local_bind_port"`
LocalBindSocketPathSnake string `json:"local_bind_socket_path"`
LocalBindSocketModeSnake string `json:"local_bind_socket_mode"`
MeshGatewaySnake MeshGatewayConfig `json:"mesh_gateway"`
*Alias
}{
Alias: (*Alias)(t),
}
if err = lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
if t.DestinationType == "" {
t.DestinationType = aux.DestinationTypeSnake
}
if t.DestinationNamespace == "" {
t.DestinationNamespace = aux.DestinationNamespaceSnake
}
if t.DestinationName == "" {
t.DestinationName = aux.DestinationNameSnake
}
if t.LocalBindAddress == "" {
t.LocalBindAddress = aux.LocalBindAddressSnake
}
if t.LocalBindPort == 0 {
t.LocalBindPort = aux.LocalBindPortSnake
}
if t.LocalBindSocketPath == "" {
t.LocalBindSocketPath = aux.LocalBindSocketPathSnake
}
if t.LocalBindSocketMode == "" {
t.LocalBindSocketMode = aux.LocalBindSocketModeSnake
}
if t.MeshGateway.Mode == "" {
t.MeshGateway.Mode = aux.MeshGatewaySnake.Mode
}
return nil
}
// Validate sanity checks the struct is valid
func (u *Upstream) Validate() error {
switch u.DestinationType {
case UpstreamDestTypePreparedQuery:
case UpstreamDestTypeService, "":
default:
return fmt.Errorf("unknown upstream destination type: %q", u.DestinationType)
}
if u.DestinationName == "" {
return fmt.Errorf("upstream destination name cannot be empty")
}
if u.DestinationName == WildcardSpecifier && !u.CentrallyConfigured {
return fmt.Errorf("upstream destination name cannot be a wildcard")
}
if u.LocalBindPort == 0 && u.LocalBindSocketPath == "" && !u.CentrallyConfigured {
return fmt.Errorf("upstream local bind port or local socket path must be defined and nonzero")
}
if u.LocalBindPort != 0 && u.LocalBindSocketPath != "" && !u.CentrallyConfigured {
return fmt.Errorf("only one of upstream local bind port or local socket path can be defined and nonzero")
}
return nil
}
// ToAPI returns the api structs with the same fields. We have duplicates to
// avoid the api package depending on this one which imports a ton of Consul's
// core which you don't want if you are just trying to use our client in your
// app.
func (u *Upstream) ToAPI() api.Upstream {
return api.Upstream{
DestinationType: api.UpstreamDestType(u.DestinationType),
DestinationNamespace: u.DestinationNamespace,
DestinationName: u.DestinationName,
Datacenter: u.Datacenter,
LocalBindAddress: u.LocalBindAddress,
LocalBindPort: u.LocalBindPort,
LocalBindSocketPath: u.LocalBindSocketPath,
LocalBindSocketMode: u.LocalBindSocketMode,
Config: u.Config,
MeshGateway: u.MeshGateway.ToAPI(),
}
}
// ToKey returns a value-type representation that uniquely identifies the
// upstream in a canonical way. Set and unset values are deliberately handled
// differently.
//
// These fields should be user-specificed explicit values and not inferred
// values.
func (u *Upstream) ToKey() UpstreamKey {
return UpstreamKey{
DestinationType: u.DestinationType,
DestinationNamespace: u.DestinationNamespace,
DestinationName: u.DestinationName,
Datacenter: u.Datacenter,
}
}
func (u Upstream) HasLocalPortOrSocket() bool {
return (u.LocalBindPort != 0 || u.LocalBindSocketPath != "")
}
func (u Upstream) UpstreamIsUnixSocket() bool {
return (u.LocalBindPort == 0 && u.LocalBindAddress == "" && u.LocalBindSocketPath != "")
}
func (u Upstream) UpstreamAddressToString() string {
if u.UpstreamIsUnixSocket() {
return u.LocalBindSocketPath
}
addr := u.LocalBindAddress
if addr == "" {
addr = "127.0.0.1"
}
return net.JoinHostPort(addr, fmt.Sprintf("%d", u.LocalBindPort))
}
type UpstreamKey struct {
DestinationType string
DestinationName string
DestinationNamespace string
Datacenter string
}
func (k UpstreamKey) String() string {
return fmt.Sprintf(
"[type=%q, name=%q, namespace=%q, datacenter=%q]",
k.DestinationType,
k.DestinationName,
k.DestinationNamespace,
k.Datacenter,
)
}
// String implements Stringer by returning the Identifier.
func (u *Upstream) String() string {
return u.Identifier()
}
// UpstreamFromAPI is a helper for converting api.Upstream to Upstream.
func UpstreamFromAPI(u api.Upstream) Upstream {
return Upstream{
DestinationType: string(u.DestinationType),
DestinationNamespace: u.DestinationNamespace,
DestinationName: u.DestinationName,
Datacenter: u.Datacenter,
LocalBindAddress: u.LocalBindAddress,
LocalBindPort: u.LocalBindPort,
LocalBindSocketPath: u.LocalBindSocketPath,
LocalBindSocketMode: u.LocalBindSocketMode,
Config: u.Config,
}
}
// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
type ExposeConfig struct {
// Checks defines whether paths associated with Consul checks will be exposed.
// This flag triggers exposing all HTTP and GRPC check paths registered for the service.
Checks bool `json:",omitempty"`
// Paths is the list of paths exposed through the proxy.
Paths []ExposePath `json:",omitempty"`
}
func (e ExposeConfig) Clone() ExposeConfig {
e2 := e
if len(e.Paths) > 0 {
e2.Paths = make([]ExposePath, 0, len(e.Paths))
for _, p := range e.Paths {
e2.Paths = append(e2.Paths, p)
}
}
return e2
}
type ExposePath struct {
// ListenerPort defines the port of the proxy's listener for exposed paths.
ListenerPort int `json:",omitempty" alias:"listener_port"`
// Path is the path to expose through the proxy, ie. "/metrics."
Path string `json:",omitempty"`
// LocalPathPort is the port that the service is listening on for the given path.
LocalPathPort int `json:",omitempty" alias:"local_path_port"`
// Protocol describes the upstream's service protocol.
// Valid values are "http" and "http2", defaults to "http"
Protocol string `json:",omitempty"`
// ParsedFromCheck is set if this path was parsed from a registered check
ParsedFromCheck bool `json:",omitempty" alias:"parsed_from_check"`
}
func (t *ExposePath) UnmarshalJSON(data []byte) (err error) {
type Alias ExposePath
aux := &struct {
ListenerPortSnake int `json:"listener_port"`
LocalPathPortSnake int `json:"local_path_port"`
ParsedFromCheckSnake bool `json:"parsed_from_check"`
*Alias
}{
Alias: (*Alias)(t),
}
if err = lib.UnmarshalJSON(data, &aux); err != nil {
return err
}
if t.LocalPathPort == 0 {
t.LocalPathPort = aux.LocalPathPortSnake
}
if t.ListenerPort == 0 {
t.ListenerPort = aux.ListenerPortSnake
}
if aux.ParsedFromCheckSnake {
t.ParsedFromCheck = true
}
return nil
}
func (e *ExposeConfig) ToAPI() api.ExposeConfig {
paths := make([]api.ExposePath, 0)
for _, p := range e.Paths {
paths = append(paths, p.ToAPI())
}
if e.Paths == nil {
paths = nil
}
return api.ExposeConfig{
Checks: e.Checks,
Paths: paths,
}
}
func (p *ExposePath) ToAPI() api.ExposePath {
return api.ExposePath{
ListenerPort: p.ListenerPort,
Path: p.Path,
LocalPathPort: p.LocalPathPort,
Protocol: p.Protocol,
ParsedFromCheck: p.ParsedFromCheck,
}
}
// Finalize validates ExposeConfig and sets default values
func (e *ExposeConfig) Finalize() {
for i := 0; i < len(e.Paths); i++ {
path := &e.Paths[i]
if path.Protocol == "" {
path.Protocol = defaultExposeProtocol
}
}
}