mirror of https://github.com/hashicorp/consul
697 lines
20 KiB
Go
697 lines
20 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package extauthz
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
|
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
|
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
|
envoy_http_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
|
|
envoy_ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3"
|
|
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
|
envoy_upstreams_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
|
|
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
|
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
|
|
"github.com/hashicorp/go-multierror"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/api"
|
|
cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
|
|
)
|
|
|
|
const (
|
|
LocalExtAuthzClusterName = "local_ext_authz"
|
|
|
|
defaultMetadataNS = "consul"
|
|
defaultStatPrefix = "response"
|
|
defaultStatusOnError = 403
|
|
localhost = "localhost"
|
|
localhostIPv4 = "127.0.0.1"
|
|
localhostIPv6 = "::1"
|
|
)
|
|
|
|
type extAuthzConfig struct {
|
|
BootstrapMetadataLabelsKey string
|
|
ClearRouteCache *bool
|
|
GrpcService *GrpcService
|
|
HttpService *HttpService
|
|
IncludePeerCertificate *bool
|
|
MetadataContextNamespaces []string
|
|
StatusOnError *int
|
|
StatPrefix string
|
|
WithRequestBody *BufferSettings
|
|
|
|
failureModeAllow bool
|
|
}
|
|
|
|
func (c *extAuthzConfig) normalize() {
|
|
if c.StatPrefix == "" {
|
|
c.StatPrefix = defaultStatPrefix
|
|
}
|
|
if c.isGRPC() {
|
|
c.GrpcService.normalize()
|
|
}
|
|
if c.isHTTP() {
|
|
c.HttpService.normalize()
|
|
}
|
|
}
|
|
|
|
func (c *extAuthzConfig) validate() error {
|
|
c.normalize()
|
|
|
|
var resultErr error
|
|
if c.isGRPC() == c.isHTTP() {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("exactly one of GrpcService or HttpService must be set"))
|
|
}
|
|
|
|
var field string
|
|
var validate func() error
|
|
if c.isHTTP() {
|
|
field = "HttpService"
|
|
validate = c.HttpService.validate
|
|
} else {
|
|
field = "GrpcService"
|
|
validate = c.GrpcService.validate
|
|
}
|
|
|
|
if err := validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to validate Config.%s: %w", field, err))
|
|
}
|
|
|
|
if c.StatusOnError != nil {
|
|
if _, ok := envoy_type_v3.StatusCode_name[int32(*c.StatusOnError)]; !ok {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to validate Config.StatusOnError:"+
|
|
"status code %d is not supported by Envoy, please refer to the Envoy documentation for supported status codes",
|
|
*c.StatusOnError))
|
|
}
|
|
}
|
|
|
|
return resultErr
|
|
}
|
|
|
|
func (c extAuthzConfig) envoyGrpcService(cfg *cmn.RuntimeConfig) (*envoy_core_v3.GrpcService, error) {
|
|
target := c.GrpcService.Target
|
|
clusterName, err := c.getClusterName(cfg, target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var initialMetadata []*envoy_core_v3.HeaderValue
|
|
for _, meta := range c.GrpcService.InitialMetadata {
|
|
initialMetadata = append(initialMetadata, meta.toEnvoy())
|
|
}
|
|
|
|
return &envoy_core_v3.GrpcService{
|
|
TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{
|
|
EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{
|
|
ClusterName: clusterName,
|
|
Authority: c.GrpcService.Authority,
|
|
},
|
|
},
|
|
Timeout: target.timeoutDurationPB(),
|
|
InitialMetadata: initialMetadata,
|
|
}, nil
|
|
}
|
|
|
|
func (c extAuthzConfig) envoyHttpService(cfg *cmn.RuntimeConfig) (*envoy_http_ext_authz_v3.HttpService, error) {
|
|
clusterName, err := c.getClusterName(cfg, c.HttpService.Target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &envoy_http_ext_authz_v3.HttpService{
|
|
ServerUri: &envoy_core_v3.HttpUri{
|
|
Uri: clusterName, // not used by Envoy, set to cluster
|
|
HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{Cluster: clusterName},
|
|
Timeout: c.HttpService.Target.timeoutDurationPB(),
|
|
},
|
|
PathPrefix: c.HttpService.PathPrefix,
|
|
AuthorizationRequest: c.HttpService.AuthorizationRequest.toEnvoy(),
|
|
AuthorizationResponse: c.HttpService.AuthorizationResponse.toEnvoy(),
|
|
}, nil
|
|
}
|
|
|
|
// getClusterName returns the name of the cluster for the external authorization service.
|
|
// If the extension is configured with an upstream ext-authz service then the name of the cluster for
|
|
// that upstream is returned. If the extension is configured with a URI, the only allowed host is `localhost`
|
|
// and the extension will insert a new cluster with the name "local_ext_authz", so we use that name.
|
|
func (c extAuthzConfig) getClusterName(cfg *cmn.RuntimeConfig, target *Target) (string, error) {
|
|
var err error
|
|
clusterName := LocalExtAuthzClusterName
|
|
if target.isService() {
|
|
if clusterName, err = target.clusterName(cfg); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return clusterName, nil
|
|
}
|
|
|
|
func (c extAuthzConfig) isGRPC() bool {
|
|
return c.GrpcService != nil
|
|
}
|
|
|
|
func (c extAuthzConfig) isHTTP() bool {
|
|
return c.HttpService != nil
|
|
}
|
|
|
|
// toEnvoyCluster returns an Envoy cluster for connecting to the ext_authz service.
|
|
// If the extension is configured with the ext_authz service locally via the URI set to localhost,
|
|
// this func will return a new cluster definition that will allow the proxy to connect to the ext_authz
|
|
// service running on localhost on the configured port.
|
|
//
|
|
// If the extension is configured with the ext_authz service as an upstream there is no need to insert
|
|
// a new cluster so this method returns nil.
|
|
func (c *extAuthzConfig) toEnvoyCluster(_ *cmn.RuntimeConfig) (*envoy_cluster_v3.Cluster, error) {
|
|
var target *Target
|
|
if c.isHTTP() {
|
|
target = c.HttpService.Target
|
|
} else {
|
|
target = c.GrpcService.Target
|
|
}
|
|
|
|
// If the target is an upstream we do not need to create a cluster. We will use the cluster of the upstream.
|
|
if target.isService() {
|
|
return nil, nil
|
|
}
|
|
|
|
host, port, err := target.addr()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clusterType := &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_STATIC}
|
|
if host == localhost {
|
|
// If the host is "localhost" use a STRICT_DNS cluster type to perform DNS lookup.
|
|
clusterType = &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_STRICT_DNS}
|
|
}
|
|
|
|
var typedExtProtoOpts map[string]*anypb.Any
|
|
if c.isGRPC() {
|
|
// By default HTTP/1.1 is used for the transport protocol. gRPC requires that we explicitly configure HTTP/2
|
|
httpProtoOpts := &envoy_upstreams_http_v3.HttpProtocolOptions{
|
|
UpstreamProtocolOptions: &envoy_upstreams_http_v3.HttpProtocolOptions_ExplicitHttpConfig_{
|
|
ExplicitHttpConfig: &envoy_upstreams_http_v3.HttpProtocolOptions_ExplicitHttpConfig{
|
|
ProtocolConfig: &envoy_upstreams_http_v3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{},
|
|
},
|
|
},
|
|
}
|
|
httpProtoOptsAny, err := anypb.New(httpProtoOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
typedExtProtoOpts = make(map[string]*anypb.Any)
|
|
typedExtProtoOpts["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] = httpProtoOptsAny
|
|
}
|
|
|
|
return &envoy_cluster_v3.Cluster{
|
|
Name: LocalExtAuthzClusterName,
|
|
ClusterDiscoveryType: clusterType,
|
|
ConnectTimeout: target.timeoutDurationPB(),
|
|
LoadAssignment: &envoy_endpoint_v3.ClusterLoadAssignment{
|
|
ClusterName: LocalExtAuthzClusterName,
|
|
Endpoints: []*envoy_endpoint_v3.LocalityLbEndpoints{
|
|
{
|
|
LbEndpoints: []*envoy_endpoint_v3.LbEndpoint{{
|
|
HostIdentifier: &envoy_endpoint_v3.LbEndpoint_Endpoint{
|
|
Endpoint: &envoy_endpoint_v3.Endpoint{
|
|
Address: &envoy_core_v3.Address{
|
|
Address: &envoy_core_v3.Address_SocketAddress{
|
|
SocketAddress: &envoy_core_v3.SocketAddress{
|
|
Address: host,
|
|
PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
|
|
PortValue: uint32(port),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
TypedExtensionProtocolOptions: typedExtProtoOpts,
|
|
}, nil
|
|
}
|
|
|
|
func (c extAuthzConfig) toEnvoyHttpFilter(cfg *cmn.RuntimeConfig) (*envoy_http_v3.HttpFilter, error) {
|
|
extAuthzFilter := &envoy_http_ext_authz_v3.ExtAuthz{
|
|
StatPrefix: c.StatPrefix,
|
|
WithRequestBody: c.WithRequestBody.toEnvoy(),
|
|
TransportApiVersion: envoy_core_v3.ApiVersion_V3,
|
|
MetadataContextNamespaces: append(c.MetadataContextNamespaces, defaultMetadataNS),
|
|
FailureModeAllow: c.failureModeAllow,
|
|
BootstrapMetadataLabelsKey: c.BootstrapMetadataLabelsKey,
|
|
}
|
|
if c.isHTTP() {
|
|
httpSvc, err := c.envoyHttpService(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
extAuthzFilter.Services = &envoy_http_ext_authz_v3.ExtAuthz_HttpService{HttpService: httpSvc}
|
|
} else {
|
|
grpcSvc, err := c.envoyGrpcService(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
extAuthzFilter.Services = &envoy_http_ext_authz_v3.ExtAuthz_GrpcService{GrpcService: grpcSvc}
|
|
}
|
|
|
|
if c.ClearRouteCache != nil {
|
|
extAuthzFilter.ClearRouteCache = *c.ClearRouteCache
|
|
}
|
|
if c.IncludePeerCertificate != nil {
|
|
extAuthzFilter.IncludePeerCertificate = *c.IncludePeerCertificate
|
|
}
|
|
if c.StatusOnError != nil {
|
|
extAuthzFilter.StatusOnError = &envoy_type_v3.HttpStatus{
|
|
Code: envoy_type_v3.StatusCode(*c.StatusOnError),
|
|
}
|
|
}
|
|
|
|
return cmn.MakeEnvoyHTTPFilter("envoy.filters.http.ext_authz", extAuthzFilter)
|
|
}
|
|
|
|
func (c extAuthzConfig) toEnvoyNetworkFilter(cfg *cmn.RuntimeConfig) (*envoy_listener_v3.Filter, error) {
|
|
grpcSvc, err := c.envoyGrpcService(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
extAuthzFilter := &envoy_ext_authz_v3.ExtAuthz{
|
|
GrpcService: grpcSvc,
|
|
StatPrefix: c.StatPrefix,
|
|
TransportApiVersion: envoy_core_v3.ApiVersion_V3,
|
|
FailureModeAllow: c.failureModeAllow,
|
|
}
|
|
|
|
if c.IncludePeerCertificate != nil {
|
|
extAuthzFilter.IncludePeerCertificate = *c.IncludePeerCertificate
|
|
}
|
|
|
|
return cmn.MakeFilter("envoy.filters.network.ext_authz", extAuthzFilter)
|
|
}
|
|
|
|
type validator interface {
|
|
validate() error
|
|
}
|
|
|
|
type AuthorizationRequest struct {
|
|
AllowedHeaders ListStringMatcher
|
|
HeadersToAdd []*HeaderValue
|
|
}
|
|
|
|
func (r *AuthorizationRequest) toEnvoy() *envoy_http_ext_authz_v3.AuthorizationRequest {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
if len(r.AllowedHeaders) == 0 && len(r.HeadersToAdd) == 0 {
|
|
return nil
|
|
}
|
|
|
|
req := &envoy_http_ext_authz_v3.AuthorizationRequest{
|
|
AllowedHeaders: r.AllowedHeaders.toEnvoy(),
|
|
}
|
|
for _, header := range r.HeadersToAdd {
|
|
req.HeadersToAdd = append(req.HeadersToAdd, header.toEnvoy())
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
func (r *AuthorizationRequest) validate() error {
|
|
var resultErr error
|
|
if r == nil {
|
|
return resultErr
|
|
}
|
|
if err := r.AllowedHeaders.validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("validation failed for AuthorizationRequest.AllowedHeaders: %w", err))
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
type AuthorizationResponse struct {
|
|
AllowedUpstreamHeaders ListStringMatcher
|
|
AllowedUpstreamHeadersToAppend ListStringMatcher
|
|
AllowedClientHeaders ListStringMatcher
|
|
AllowedClientHeadersOnSuccess ListStringMatcher
|
|
DynamicMetadataFromHeaders ListStringMatcher
|
|
}
|
|
|
|
func (r *AuthorizationResponse) toEnvoy() *envoy_http_ext_authz_v3.AuthorizationResponse {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
|
|
return &envoy_http_ext_authz_v3.AuthorizationResponse{
|
|
AllowedUpstreamHeaders: r.AllowedUpstreamHeaders.toEnvoy(),
|
|
AllowedUpstreamHeadersToAppend: r.AllowedUpstreamHeadersToAppend.toEnvoy(),
|
|
AllowedClientHeaders: r.AllowedClientHeaders.toEnvoy(),
|
|
AllowedClientHeadersOnSuccess: r.AllowedClientHeadersOnSuccess.toEnvoy(),
|
|
DynamicMetadataFromHeaders: r.DynamicMetadataFromHeaders.toEnvoy(),
|
|
}
|
|
}
|
|
|
|
func (r *AuthorizationResponse) validate() error {
|
|
var resultErr error
|
|
if r == nil {
|
|
return resultErr
|
|
}
|
|
for field, matchers := range r.fieldMap() {
|
|
if err := matchers.validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("validation failed for AuthorizationResponse.%s: %w", field, err))
|
|
}
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
func (r *AuthorizationResponse) fieldMap() map[string]ListStringMatcher {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return map[string]ListStringMatcher{
|
|
"AllowedUpstreamHeaders": r.AllowedUpstreamHeaders,
|
|
"AllowedUpstreamHeadersToAppend": r.AllowedUpstreamHeadersToAppend,
|
|
"AllowedClientHeaders": r.AllowedClientHeaders,
|
|
"AllowedClientHeadersOnSuccess": r.AllowedClientHeadersOnSuccess,
|
|
"DynamicMetadataFromHeaders": r.DynamicMetadataFromHeaders,
|
|
}
|
|
}
|
|
|
|
type BufferSettings struct {
|
|
MaxRequestBytes *int64
|
|
AllowPartialMessage *bool
|
|
PackAsBytes *bool
|
|
}
|
|
|
|
func (b *BufferSettings) toEnvoy() *envoy_http_ext_authz_v3.BufferSettings {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
if b.AllowPartialMessage == nil &&
|
|
b.MaxRequestBytes == nil &&
|
|
b.PackAsBytes == nil {
|
|
return nil
|
|
}
|
|
|
|
bufSet := &envoy_http_ext_authz_v3.BufferSettings{}
|
|
if b.AllowPartialMessage != nil {
|
|
bufSet.AllowPartialMessage = *b.AllowPartialMessage
|
|
}
|
|
if b.MaxRequestBytes != nil {
|
|
bufSet.MaxRequestBytes = uint32(*b.MaxRequestBytes)
|
|
}
|
|
if b.PackAsBytes != nil {
|
|
bufSet.PackAsBytes = *b.PackAsBytes
|
|
}
|
|
|
|
return bufSet
|
|
}
|
|
|
|
type GrpcService struct {
|
|
Target *Target
|
|
Authority string
|
|
InitialMetadata []*HeaderValue
|
|
}
|
|
|
|
func (v *GrpcService) normalize() {
|
|
if v == nil {
|
|
return
|
|
}
|
|
v.Target.normalize()
|
|
}
|
|
|
|
func (v *GrpcService) validate() error {
|
|
var resultErr error
|
|
if v == nil {
|
|
return resultErr
|
|
}
|
|
|
|
if v.Target == nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("GrpcService.Target must be set"))
|
|
}
|
|
if err := v.Target.validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, err)
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
type HeaderValue struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
func (h *HeaderValue) toEnvoy() *envoy_core_v3.HeaderValue {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
return &envoy_core_v3.HeaderValue{Key: h.Key, Value: h.Value}
|
|
}
|
|
|
|
type HttpService struct {
|
|
Target *Target
|
|
PathPrefix string
|
|
AuthorizationRequest *AuthorizationRequest
|
|
AuthorizationResponse *AuthorizationResponse
|
|
}
|
|
|
|
func (v *HttpService) normalize() {
|
|
if v == nil {
|
|
return
|
|
}
|
|
v.Target.normalize()
|
|
}
|
|
|
|
func (v *HttpService) validate() error {
|
|
var resultErr error
|
|
if v == nil {
|
|
return resultErr
|
|
}
|
|
|
|
if v.Target == nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("HttpService.Target must be set"))
|
|
}
|
|
for _, val := range []validator{v.Target, v.AuthorizationRequest, v.AuthorizationResponse} {
|
|
if err := val.validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, err)
|
|
}
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
type ListStringMatcher []*StringMatcher
|
|
|
|
func (l ListStringMatcher) toEnvoy() *envoy_type_matcher_v3.ListStringMatcher {
|
|
if len(l) < 1 {
|
|
return nil
|
|
}
|
|
matchers := &envoy_type_matcher_v3.ListStringMatcher{}
|
|
for _, matcher := range l {
|
|
matchers.Patterns = append(matchers.Patterns, matcher.toEnvoy())
|
|
}
|
|
return matchers
|
|
}
|
|
|
|
func (l ListStringMatcher) validate() error {
|
|
var resultErr error
|
|
if len(l) < 1 {
|
|
return nil
|
|
}
|
|
for idx, matcher := range l {
|
|
if err := matcher.validate(); err != nil {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("validation failed for matcher at index %d: %w", idx, err))
|
|
}
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
type StringMatcher struct {
|
|
Contains string
|
|
Exact string
|
|
IgnoreCase bool
|
|
Prefix string
|
|
SafeRegex string
|
|
Suffix string
|
|
}
|
|
|
|
func (s *StringMatcher) toEnvoy() *envoy_type_matcher_v3.StringMatcher {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case s.Contains != "":
|
|
return &envoy_type_matcher_v3.StringMatcher{
|
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Contains{Contains: s.Contains},
|
|
IgnoreCase: s.IgnoreCase,
|
|
}
|
|
case s.Exact != "":
|
|
return &envoy_type_matcher_v3.StringMatcher{
|
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: s.Exact},
|
|
IgnoreCase: s.IgnoreCase,
|
|
}
|
|
case s.Prefix != "":
|
|
return &envoy_type_matcher_v3.StringMatcher{
|
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Prefix{Prefix: s.Prefix},
|
|
IgnoreCase: s.IgnoreCase,
|
|
}
|
|
case s.SafeRegex != "":
|
|
return &envoy_type_matcher_v3.StringMatcher{
|
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_SafeRegex{
|
|
SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
|
|
Regex: s.SafeRegex,
|
|
},
|
|
},
|
|
}
|
|
case s.Suffix != "":
|
|
return &envoy_type_matcher_v3.StringMatcher{
|
|
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Suffix{Suffix: s.Suffix},
|
|
IgnoreCase: s.IgnoreCase,
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *StringMatcher) validate() error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
|
|
set := 0
|
|
for _, s := range []string{s.Contains, s.Exact, s.Prefix, s.SafeRegex, s.Suffix} {
|
|
if s != "" {
|
|
set++
|
|
}
|
|
}
|
|
if set != 1 {
|
|
return fmt.Errorf("exactly one of Contains, Exact, Prefix, SafeRegex or Suffix must be set")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Target struct {
|
|
Service api.CompoundServiceName
|
|
URI string
|
|
Timeout string
|
|
|
|
timeout *time.Duration
|
|
host string
|
|
port int
|
|
}
|
|
|
|
// addr returns the host and port for the target when the target is a URI.
|
|
// It returns a non-nil error if the target is not a URI.
|
|
func (t Target) addr() (string, int, error) {
|
|
if !t.isURI() {
|
|
return "", 0, fmt.Errorf("target is not configured with a URI, set Target.URI")
|
|
}
|
|
return t.host, t.port, nil
|
|
}
|
|
|
|
// clusterName returns the cluster name for the target when the target is an upstream service.
|
|
// It searches through the upstreams in the provided runtime configuration and returns the name
|
|
// of the cluster for the first upstream service that matches the target service.
|
|
// It returns a non-nil error if a matching cluster is not found or if the target is not an
|
|
// upstream service.
|
|
func (t Target) clusterName(cfg *cmn.RuntimeConfig) (string, error) {
|
|
if !t.isService() {
|
|
return "", fmt.Errorf("target is not configured with an upstream service, set Target.Service")
|
|
}
|
|
|
|
for service, upstream := range cfg.Upstreams {
|
|
if service == t.Service {
|
|
for sni := range upstream.SNIs {
|
|
return sni, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no upstream definition found for service %q", t.Service.Name)
|
|
}
|
|
|
|
func (t Target) isService() bool {
|
|
return t.Service.Name != ""
|
|
}
|
|
|
|
func (t Target) isURI() bool {
|
|
return t.URI != ""
|
|
}
|
|
|
|
func (t *Target) normalize() {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.Service.Namespace = acl.NamespaceOrDefault(t.Service.Namespace)
|
|
t.Service.Partition = acl.PartitionOrDefault(t.Service.Partition)
|
|
}
|
|
|
|
// timeoutDurationPB returns the target's timeout as a *durationpb.Duration.
|
|
// It returns nil if the timeout has not been explicitly set.
|
|
func (t *Target) timeoutDurationPB() *durationpb.Duration {
|
|
if t == nil || t.timeout == nil {
|
|
return nil
|
|
}
|
|
return durationpb.New(*t.timeout)
|
|
}
|
|
|
|
func (t *Target) validate() error {
|
|
var err, resultErr error
|
|
if t == nil {
|
|
return resultErr
|
|
}
|
|
|
|
if t.isURI() == t.isService() {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("exactly one of Target.Service or Target.URI must be set"))
|
|
}
|
|
|
|
if t.isURI() {
|
|
t.host, t.port, err = parseAddr(t.URI)
|
|
if err == nil {
|
|
switch t.host {
|
|
case localhost, localhostIPv4, localhostIPv6:
|
|
default:
|
|
resultErr = multierror.Append(resultErr,
|
|
fmt.Errorf("invalid host for Target.URI %q: expected %q, %q, or %q", t.URI, localhost, localhostIPv4, localhostIPv6))
|
|
}
|
|
} else {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("invalid format for Target.URI %q: expected host:port", t.URI))
|
|
}
|
|
}
|
|
|
|
if t.Timeout != "" {
|
|
if d, err := time.ParseDuration(t.Timeout); err == nil {
|
|
t.timeout = &d
|
|
} else {
|
|
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse Target.Timeout %q as a duration: %w", t.Timeout, err))
|
|
}
|
|
}
|
|
return resultErr
|
|
}
|
|
|
|
func parseAddr(s string) (host string, port int, err error) {
|
|
// Strip the protocol if one was provided
|
|
if _, addr, hasProto := strings.Cut(s, "://"); hasProto {
|
|
s = addr
|
|
}
|
|
idx := strings.LastIndex(s, ":")
|
|
switch idx {
|
|
case -1, len(s) - 1:
|
|
err = fmt.Errorf("invalid input format %q: expected host:port", s)
|
|
case 0:
|
|
host = localhost
|
|
port, err = strconv.Atoi(s[idx+1:])
|
|
default:
|
|
host = s[:idx]
|
|
port, err = strconv.Atoi(s[idx+1:])
|
|
}
|
|
return
|
|
}
|