2020-03-31 16:59:10 +00:00
|
|
|
|
package structs
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2023-02-21 19:34:04 +00:00
|
|
|
|
"regexp"
|
2020-08-12 16:19:20 +00:00
|
|
|
|
"sort"
|
2020-03-31 16:59:10 +00:00
|
|
|
|
"strings"
|
|
|
|
|
|
2020-12-11 21:10:00 +00:00
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
"github.com/hashicorp/consul/acl"
|
2020-05-27 16:47:32 +00:00
|
|
|
|
"github.com/hashicorp/consul/lib/stringslice"
|
2022-01-11 16:46:42 +00:00
|
|
|
|
"github.com/hashicorp/consul/types"
|
2020-03-31 16:59:10 +00:00
|
|
|
|
)
|
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
const (
|
|
|
|
|
wildcardPrefix = "*."
|
|
|
|
|
)
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
// IngressGatewayConfigEntry manages the configuration for an ingress service
|
|
|
|
|
// with the given name.
|
|
|
|
|
type IngressGatewayConfigEntry struct {
|
|
|
|
|
// Kind of the config entry. This will be set to structs.IngressGateway.
|
|
|
|
|
Kind string
|
|
|
|
|
|
|
|
|
|
// Name is used to match the config entry with its associated ingress gateway
|
|
|
|
|
// service. This should match the name provided in the service definition.
|
|
|
|
|
Name string
|
|
|
|
|
|
2021-08-17 11:27:31 +00:00
|
|
|
|
// TLS holds the TLS configuration for this gateway. It would be nicer if it
|
|
|
|
|
// were a pointer so it could be omitempty when read back in JSON but that
|
|
|
|
|
// would be a breaking API change now as we currently always return it.
|
2020-04-27 23:36:20 +00:00
|
|
|
|
TLS GatewayTLSConfig
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
// Listeners declares what ports the ingress gateway should listen on, and
|
|
|
|
|
// what services to associated to those ports.
|
|
|
|
|
Listeners []IngressListener
|
|
|
|
|
|
2022-09-28 18:56:46 +00:00
|
|
|
|
// Defaults contains default configuration for all upstream service instances
|
|
|
|
|
Defaults *IngressServiceConfig `json:",omitempty"`
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
2020-03-31 16:59:10 +00:00
|
|
|
|
RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-28 18:56:46 +00:00
|
|
|
|
type IngressServiceConfig struct {
|
|
|
|
|
MaxConnections uint32
|
|
|
|
|
MaxPendingRequests uint32
|
|
|
|
|
MaxConcurrentRequests uint32
|
2022-12-13 16:51:37 +00:00
|
|
|
|
|
|
|
|
|
// PassiveHealthCheck configuration determines how upstream proxy instances will
|
|
|
|
|
// be monitored for removal from the load balancing pool.
|
|
|
|
|
PassiveHealthCheck *PassiveHealthCheck `json:",omitempty" alias:"passive_health_check"`
|
2022-09-28 18:56:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
type IngressListener struct {
|
|
|
|
|
// Port declares the port on which the ingress gateway should listen for traffic.
|
|
|
|
|
Port int
|
|
|
|
|
|
|
|
|
|
// Protocol declares what type of traffic this listener is expected to
|
|
|
|
|
// receive. Depending on the protocol, a listener might support multiplexing
|
|
|
|
|
// services over a single port, or additional discovery chain features. The
|
2020-08-27 21:34:08 +00:00
|
|
|
|
// current supported values are: (tcp | http | http2 | grpc).
|
2020-03-31 16:59:10 +00:00
|
|
|
|
Protocol string
|
|
|
|
|
|
2021-08-17 11:27:31 +00:00
|
|
|
|
// TLS config for this listener.
|
|
|
|
|
TLS *GatewayTLSConfig `json:",omitempty"`
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
// Services declares the set of services to which the listener forwards
|
|
|
|
|
// traffic.
|
|
|
|
|
//
|
|
|
|
|
// For "tcp" protocol listeners, only a single service is allowed.
|
|
|
|
|
// For "http" listeners, multiple services can be declared.
|
|
|
|
|
Services []IngressService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type IngressService struct {
|
|
|
|
|
// Name declares the service to which traffic should be forwarded.
|
|
|
|
|
//
|
|
|
|
|
// This can either be a specific service, or the wildcard specifier,
|
|
|
|
|
// "*". If the wildcard specifier is provided, the listener must be of "http"
|
|
|
|
|
// protocol and means that the listener will forward traffic to all services.
|
2020-05-04 16:47:46 +00:00
|
|
|
|
//
|
|
|
|
|
// A name can be specified on multiple listeners, and will be exposed on both
|
|
|
|
|
// of the listeners
|
2020-03-31 16:59:10 +00:00
|
|
|
|
Name string
|
|
|
|
|
|
2020-04-23 15:06:19 +00:00
|
|
|
|
// Hosts is a list of hostnames which should be associated to this service on
|
|
|
|
|
// the defined listener. Only allowed on layer 7 protocols, this will be used
|
|
|
|
|
// to route traffic to the service by matching the Host header of the HTTP
|
|
|
|
|
// request.
|
|
|
|
|
//
|
2020-05-04 16:47:46 +00:00
|
|
|
|
// If a host is provided for a service that also has a wildcard specifier
|
|
|
|
|
// defined, the host will override the wildcard-specifier-provided
|
|
|
|
|
// "<service-name>.*" domain for that listener.
|
|
|
|
|
//
|
2020-04-23 15:06:19 +00:00
|
|
|
|
// This cannot be specified when using the wildcard specifier, "*", or when
|
|
|
|
|
// using a "tcp" listener.
|
|
|
|
|
Hosts []string
|
|
|
|
|
|
2021-08-17 11:27:31 +00:00
|
|
|
|
// TLS configuration overrides for this service. At least one entry must exist
|
|
|
|
|
// in Hosts to use set and the Listener must also have a default Cert loaded
|
|
|
|
|
// from SDS.
|
2021-09-22 15:03:50 +00:00
|
|
|
|
TLS *GatewayServiceTLSConfig `json:",omitempty"`
|
2021-08-17 11:27:31 +00:00
|
|
|
|
|
2021-07-13 11:13:18 +00:00
|
|
|
|
// Allow HTTP header manipulation to be configured.
|
|
|
|
|
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
|
|
|
|
|
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
|
|
|
|
|
|
2022-09-28 18:56:46 +00:00
|
|
|
|
MaxConnections uint32 `json:",omitempty" alias:"max_connections"`
|
|
|
|
|
MaxPendingRequests uint32 `json:",omitempty" alias:"max_pending_requests"`
|
|
|
|
|
MaxConcurrentRequests uint32 `json:",omitempty" alias:"max_concurrent_requests"`
|
|
|
|
|
|
2022-12-13 16:51:37 +00:00
|
|
|
|
// PassiveHealthCheck configuration determines how upstream proxy instances will
|
|
|
|
|
// be monitored for removal from the load balancing pool.
|
|
|
|
|
PassiveHealthCheck *PassiveHealthCheck `json:",omitempty" alias:"passive_health_check"`
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-27 23:36:20 +00:00
|
|
|
|
type GatewayTLSConfig struct {
|
2021-08-17 11:27:31 +00:00
|
|
|
|
// Indicates that TLS should be enabled for this gateway or listener
|
2020-04-27 23:36:20 +00:00
|
|
|
|
Enabled bool
|
2021-08-17 11:27:31 +00:00
|
|
|
|
|
|
|
|
|
// SDS allows configuring TLS certificate from an SDS service.
|
|
|
|
|
SDS *GatewayTLSSDSConfig `json:",omitempty"`
|
2022-01-11 16:46:42 +00:00
|
|
|
|
|
|
|
|
|
TLSMinVersion types.TLSVersion `json:",omitempty" alias:"tls_min_version"`
|
|
|
|
|
TLSMaxVersion types.TLSVersion `json:",omitempty" alias:"tls_max_version"`
|
|
|
|
|
|
|
|
|
|
// Define a subset of cipher suites to restrict
|
|
|
|
|
// Only applicable to connections negotiated via TLS 1.2 or earlier
|
|
|
|
|
CipherSuites []types.TLSCipherSuite `json:",omitempty" alias:"cipher_suites"`
|
2021-08-17 11:27:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GatewayServiceTLSConfig struct {
|
|
|
|
|
// Note no Enabled field here since it doesn't make sense to disable TLS on
|
|
|
|
|
// one host on a TLS-configured listener.
|
|
|
|
|
|
|
|
|
|
// SDS allows configuring TLS certificate from an SDS service.
|
|
|
|
|
SDS *GatewayTLSSDSConfig `json:",omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GatewayTLSSDSConfig struct {
|
|
|
|
|
ClusterName string `json:",omitempty" alias:"cluster_name"`
|
|
|
|
|
CertResource string `json:",omitempty" alias:"cert_resource"`
|
2020-04-27 23:36:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) GetKind() string {
|
|
|
|
|
return IngressGateway
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *IngressGatewayConfigEntry) GetName() string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return e.Name
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-02 19:10:25 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) GetMeta() map[string]string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.Meta
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) Normalize() error {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return fmt.Errorf("config entry is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Kind = IngressGateway
|
2020-05-08 15:44:34 +00:00
|
|
|
|
e.EnterpriseMeta.Normalize()
|
|
|
|
|
|
2020-04-16 21:00:48 +00:00
|
|
|
|
for i, listener := range e.Listeners {
|
|
|
|
|
if listener.Protocol == "" {
|
|
|
|
|
listener.Protocol = "tcp"
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
listener.Protocol = strings.ToLower(listener.Protocol)
|
|
|
|
|
for i := range listener.Services {
|
2020-05-08 15:44:34 +00:00
|
|
|
|
listener.Services[i].EnterpriseMeta.Merge(&e.EnterpriseMeta)
|
2020-03-31 16:59:10 +00:00
|
|
|
|
listener.Services[i].EnterpriseMeta.Normalize()
|
|
|
|
|
}
|
2020-04-16 21:00:48 +00:00
|
|
|
|
|
|
|
|
|
// Make sure to set the item back into the array, since we are not using
|
|
|
|
|
// pointers to structs
|
|
|
|
|
e.Listeners[i] = listener
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-22 15:03:50 +00:00
|
|
|
|
// validateServiceSDS validates the SDS config for a specific service on a
|
|
|
|
|
// specific listener. It checks inherited config properties from listener and
|
|
|
|
|
// gateway level and ensures they are valid all the way down. If called on
|
|
|
|
|
// several services some of these checks will be duplicated but that isn't a big
|
|
|
|
|
// deal and it's significantly easier to reason about and read if this is in one
|
|
|
|
|
// place rather than threaded through the multi-level loop in Validate with
|
|
|
|
|
// other checks.
|
|
|
|
|
func (e *IngressGatewayConfigEntry) validateServiceSDS(lis IngressListener, svc IngressService) error {
|
|
|
|
|
// First work out if there is valid gateway-level SDS config
|
2021-08-17 11:27:31 +00:00
|
|
|
|
gwSDSClusterSet := false
|
|
|
|
|
gwSDSCertSet := false
|
|
|
|
|
if e.TLS.SDS != nil {
|
|
|
|
|
// Gateway level SDS config must set ClusterName if it specifies a default
|
|
|
|
|
// certificate. Just a clustername is OK though if certs are specified
|
|
|
|
|
// per-listener.
|
|
|
|
|
if e.TLS.SDS.ClusterName == "" && e.TLS.SDS.CertResource != "" {
|
|
|
|
|
return fmt.Errorf("TLS.SDS.ClusterName is required if CertResource is set")
|
|
|
|
|
}
|
|
|
|
|
// Note we rely on the fact that ClusterName must be non-empty if any SDS
|
2021-09-22 15:03:50 +00:00
|
|
|
|
// properties are defined at this level (as validated above) in validation
|
2021-08-17 11:27:31 +00:00
|
|
|
|
// below that uses this variable. If that changes we will need to change the
|
|
|
|
|
// code below too.
|
|
|
|
|
gwSDSClusterSet = (e.TLS.SDS.ClusterName != "")
|
|
|
|
|
gwSDSCertSet = (e.TLS.SDS.CertResource != "")
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-22 15:03:50 +00:00
|
|
|
|
// Validate listener-level SDS config.
|
|
|
|
|
lisSDSCertSet := false
|
|
|
|
|
lisSDSClusterSet := false
|
|
|
|
|
if lis.TLS != nil && lis.TLS.SDS != nil {
|
|
|
|
|
lisSDSCertSet = (lis.TLS.SDS.CertResource != "")
|
|
|
|
|
lisSDSClusterSet = (lis.TLS.SDS.ClusterName != "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If SDS was setup at gw level but without a default CertResource, the
|
|
|
|
|
// listener MUST set a CertResource.
|
|
|
|
|
if gwSDSClusterSet && !gwSDSCertSet && !lisSDSCertSet {
|
|
|
|
|
return fmt.Errorf("TLS.SDS.CertResource is required if ClusterName is set for gateway (listener on port %d)", lis.Port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If listener set a cluster name then it requires a cert resource too.
|
|
|
|
|
if lisSDSClusterSet && !lisSDSCertSet {
|
|
|
|
|
return fmt.Errorf("TLS.SDS.CertResource is required if ClusterName is set for listener (listener on port %d)", lis.Port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If a listener-level cert is given, we need a cluster from at least one
|
|
|
|
|
// level.
|
|
|
|
|
if lisSDSCertSet && !lisSDSClusterSet && !gwSDSClusterSet {
|
|
|
|
|
return fmt.Errorf("TLS.SDS.ClusterName is required if CertResource is set (listener on port %d)", lis.Port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate service-level SDS config
|
|
|
|
|
svcSDSSet := (svc.TLS != nil && svc.TLS.SDS != nil && svc.TLS.SDS.CertResource != "")
|
|
|
|
|
|
|
|
|
|
// Service SDS is only supported with Host names because we need to bind
|
|
|
|
|
// specific service certs to one or more SNI hostnames.
|
|
|
|
|
if svcSDSSet && len(svc.Hosts) < 1 {
|
2021-09-23 09:05:42 +00:00
|
|
|
|
sid := NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
2021-09-22 15:03:50 +00:00
|
|
|
|
return fmt.Errorf("A service specifying TLS.SDS.CertResource must have at least one item in Hosts (service %q on listener on port %d)",
|
|
|
|
|
sid.String(), lis.Port)
|
|
|
|
|
}
|
|
|
|
|
// If this service specified a certificate, there must be an SDS cluster set
|
|
|
|
|
// at one of the three levels.
|
|
|
|
|
if svcSDSSet && svc.TLS.SDS.ClusterName == "" && !lisSDSClusterSet && !gwSDSClusterSet {
|
2021-09-23 09:05:42 +00:00
|
|
|
|
sid := NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
2021-09-22 15:03:50 +00:00
|
|
|
|
return fmt.Errorf("TLS.SDS.ClusterName is required if CertResource is set (service %q on listener on port %d)",
|
|
|
|
|
sid.String(), lis.Port)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-11 16:46:42 +00:00
|
|
|
|
func validateGatewayTLSConfig(tlsCfg GatewayTLSConfig) error {
|
2022-03-30 18:43:59 +00:00
|
|
|
|
return validateTLSConfig(tlsCfg.TLSMinVersion, tlsCfg.TLSMaxVersion, tlsCfg.CipherSuites)
|
2022-01-11 16:46:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-22 15:03:50 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) Validate() error {
|
|
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-11 16:46:42 +00:00
|
|
|
|
if err := validateGatewayTLSConfig(e.TLS); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-22 15:03:50 +00:00
|
|
|
|
validProtocols := map[string]bool{
|
|
|
|
|
"tcp": true,
|
|
|
|
|
"http": true,
|
|
|
|
|
"http2": true,
|
|
|
|
|
"grpc": true,
|
|
|
|
|
}
|
|
|
|
|
declaredPorts := make(map[int]bool)
|
|
|
|
|
|
2020-03-31 16:59:10 +00:00
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
if _, ok := declaredPorts[listener.Port]; ok {
|
|
|
|
|
return fmt.Errorf("port %d declared on two listeners", listener.Port)
|
|
|
|
|
}
|
|
|
|
|
declaredPorts[listener.Port] = true
|
|
|
|
|
|
|
|
|
|
if _, ok := validProtocols[listener.Protocol]; !ok {
|
2020-08-27 21:34:08 +00:00
|
|
|
|
return fmt.Errorf("protocol must be 'tcp', 'http', 'http2', or 'grpc'. '%s' is an unsupported protocol", listener.Protocol)
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 15:06:19 +00:00
|
|
|
|
if len(listener.Services) == 0 {
|
|
|
|
|
return fmt.Errorf("No service declared for listener with port %d", listener.Port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that http features aren't being used with tcp or another non-supported protocol.
|
2022-05-18 21:55:59 +00:00
|
|
|
|
if !IsProtocolHTTPLike(listener.Protocol) && len(listener.Services) > 1 {
|
|
|
|
|
return fmt.Errorf("Multiple services per listener are only supported for L7 protocols, 'http', 'grpc' and 'http2' (listener on port %d)",
|
2020-04-23 15:06:19 +00:00
|
|
|
|
listener.Port)
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-11 16:46:42 +00:00
|
|
|
|
if listener.TLS != nil {
|
|
|
|
|
if err := validateGatewayTLSConfig(*listener.TLS); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 15:06:19 +00:00
|
|
|
|
declaredHosts := make(map[string]bool)
|
2021-07-13 12:53:59 +00:00
|
|
|
|
serviceNames := make(map[ServiceID]struct{})
|
2021-11-08 17:40:09 +00:00
|
|
|
|
for _, s := range listener.Services {
|
2021-09-10 19:58:51 +00:00
|
|
|
|
sn := NewServiceName(s.Name, &s.EnterpriseMeta)
|
2021-07-13 11:13:18 +00:00
|
|
|
|
if err := s.RequestHeaders.Validate(listener.Protocol); err != nil {
|
2021-09-10 19:58:51 +00:00
|
|
|
|
return fmt.Errorf("request headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port)
|
2021-07-13 11:13:18 +00:00
|
|
|
|
}
|
|
|
|
|
if err := s.ResponseHeaders.Validate(listener.Protocol); err != nil {
|
2021-09-10 19:58:51 +00:00
|
|
|
|
return fmt.Errorf("response headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port)
|
2021-07-13 11:13:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 15:06:19 +00:00
|
|
|
|
if listener.Protocol == "tcp" {
|
|
|
|
|
if s.Name == WildcardSpecifier {
|
|
|
|
|
return fmt.Errorf("Wildcard service name is only valid for protocol = 'http' (listener on port %d)", listener.Port)
|
|
|
|
|
}
|
|
|
|
|
if len(s.Hosts) != 0 {
|
|
|
|
|
return fmt.Errorf("Associating hosts to a service is not supported for the %s protocol (listener on port %d)", listener.Protocol, listener.Port)
|
|
|
|
|
}
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
if s.Name == "" {
|
|
|
|
|
return fmt.Errorf("Service name cannot be blank (listener on port %d)", listener.Port)
|
|
|
|
|
}
|
2020-04-23 15:06:19 +00:00
|
|
|
|
if s.Name == WildcardSpecifier && len(s.Hosts) != 0 {
|
|
|
|
|
return fmt.Errorf("Associating hosts to a wildcard service is not supported (listener on port %d)", listener.Port)
|
|
|
|
|
}
|
2020-03-31 16:59:10 +00:00
|
|
|
|
if s.NamespaceOrDefault() == WildcardSpecifier {
|
|
|
|
|
return fmt.Errorf("Wildcard namespace is not supported for ingress services (listener on port %d)", listener.Port)
|
|
|
|
|
}
|
2021-07-13 12:53:59 +00:00
|
|
|
|
sid := NewServiceID(s.Name, &s.EnterpriseMeta)
|
|
|
|
|
if _, ok := serviceNames[sid]; ok {
|
|
|
|
|
return fmt.Errorf("Service %s cannot be added multiple times (listener on port %d)", sid, listener.Port)
|
|
|
|
|
}
|
|
|
|
|
serviceNames[sid] = struct{}{}
|
2020-03-31 16:59:10 +00:00
|
|
|
|
|
2021-09-22 15:03:50 +00:00
|
|
|
|
// Validate SDS configuration for this service
|
|
|
|
|
if err := e.validateServiceSDS(listener, s); err != nil {
|
|
|
|
|
return err
|
2021-08-17 11:27:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 15:06:19 +00:00
|
|
|
|
for _, h := range s.Hosts {
|
|
|
|
|
if declaredHosts[h] {
|
|
|
|
|
return fmt.Errorf("Hosts must be unique within a specific listener (listener on port %d)", listener.Port)
|
|
|
|
|
}
|
|
|
|
|
declaredHosts[h] = true
|
2020-06-11 15:03:06 +00:00
|
|
|
|
if err := validateHost(e.TLS.Enabled, h); err != nil {
|
2020-04-28 20:20:53 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
2020-04-23 15:06:19 +00:00
|
|
|
|
}
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 15:03:06 +00:00
|
|
|
|
func validateHost(tlsEnabled bool, host string) error {
|
|
|
|
|
// Special case '*' so that non-TLS ingress gateways can use it. This allows
|
|
|
|
|
// an easy demo/testing experience.
|
|
|
|
|
if host == "*" {
|
|
|
|
|
if tlsEnabled {
|
|
|
|
|
return fmt.Errorf("Host '*' is not allowed when TLS is enabled, all hosts must be valid DNS records to add as a DNSSAN")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 20:20:53 +00:00
|
|
|
|
if _, ok := dns.IsDomainName(host); !ok {
|
|
|
|
|
return fmt.Errorf("Host %q must be a valid DNS hostname", host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.ContainsRune(strings.TrimPrefix(host, wildcardPrefix), '*') {
|
|
|
|
|
return fmt.Errorf("Host %q is not valid, a wildcard specifier is only allowed as the leftmost label", host)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-12 16:19:20 +00:00
|
|
|
|
// ListRelatedServices implements discoveryChainConfigEntry
|
|
|
|
|
//
|
|
|
|
|
// For ingress-gateway config entries this only finds services that are
|
|
|
|
|
// explicitly linked in the ingress-gateway config entry. Wildcards will not
|
|
|
|
|
// expand to all services.
|
|
|
|
|
//
|
|
|
|
|
// This function is used during discovery chain graph validation to prevent
|
|
|
|
|
// erroneous sets of config entries from being created. Wildcard ingress
|
|
|
|
|
// filters out sets with protocol mismatch elsewhere so it isn't an issue here
|
|
|
|
|
// that needs fixing.
|
|
|
|
|
func (e *IngressGatewayConfigEntry) ListRelatedServices() []ServiceID {
|
|
|
|
|
found := make(map[ServiceID]struct{})
|
|
|
|
|
|
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
for _, service := range listener.Services {
|
|
|
|
|
if service.Name == WildcardSpecifier {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
svcID := NewServiceID(service.Name, &service.EnterpriseMeta)
|
|
|
|
|
found[svcID] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(found) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out := make([]ServiceID, 0, len(found))
|
|
|
|
|
for svc := range found {
|
|
|
|
|
out = append(out, svc)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(out, func(i, j int) bool {
|
|
|
|
|
return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
|
|
|
|
|
out[i].ID < out[j].ID
|
|
|
|
|
})
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-11 21:45:51 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
2020-03-31 16:59:10 +00:00
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
2022-03-11 21:45:51 +00:00
|
|
|
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-11 21:45:51 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) error {
|
2020-03-31 16:59:10 +00:00
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
2022-03-11 21:45:51 +00:00
|
|
|
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
2020-03-31 16:59:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return &RaftIndex{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &e.RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
func (e *IngressGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
2020-03-31 16:59:10 +00:00
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &e.EnterpriseMeta
|
|
|
|
|
}
|
2020-03-31 19:27:32 +00:00
|
|
|
|
|
2020-06-12 14:57:41 +00:00
|
|
|
|
func (s *IngressService) ToServiceName() ServiceName {
|
|
|
|
|
return NewServiceName(s.Name, &s.EnterpriseMeta)
|
2020-04-16 21:00:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 19:27:32 +00:00
|
|
|
|
// TerminatingGatewayConfigEntry manages the configuration for a terminating service
|
|
|
|
|
// with the given name.
|
|
|
|
|
type TerminatingGatewayConfigEntry struct {
|
|
|
|
|
Kind string
|
|
|
|
|
Name string
|
|
|
|
|
Services []LinkedService
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
2020-03-31 19:27:32 +00:00
|
|
|
|
RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A LinkedService is a service represented by a terminating gateway
|
|
|
|
|
type LinkedService struct {
|
|
|
|
|
// Name is the name of the service, as defined in Consul's catalog
|
|
|
|
|
Name string `json:",omitempty"`
|
|
|
|
|
|
|
|
|
|
// CAFile is the optional path to a CA certificate to use for TLS connections
|
|
|
|
|
// from the gateway to the linked service
|
2020-05-27 18:28:28 +00:00
|
|
|
|
CAFile string `json:",omitempty" alias:"ca_file"`
|
2020-03-31 19:27:32 +00:00
|
|
|
|
|
|
|
|
|
// CertFile is the optional path to a client certificate to use for TLS connections
|
|
|
|
|
// from the gateway to the linked service
|
2020-05-27 18:28:28 +00:00
|
|
|
|
CertFile string `json:",omitempty" alias:"cert_file"`
|
2020-03-31 19:27:32 +00:00
|
|
|
|
|
|
|
|
|
// KeyFile is the optional path to a private key to use for TLS connections
|
|
|
|
|
// from the gateway to the linked service
|
2020-05-27 18:28:28 +00:00
|
|
|
|
KeyFile string `json:",omitempty" alias:"key_file"`
|
2020-03-31 19:27:32 +00:00
|
|
|
|
|
2020-04-27 22:25:37 +00:00
|
|
|
|
// SNI is the optional name to specify during the TLS handshake with a linked service
|
|
|
|
|
SNI string `json:",omitempty"`
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
2020-03-31 19:27:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *TerminatingGatewayConfigEntry) GetKind() string {
|
|
|
|
|
return TerminatingGateway
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *TerminatingGatewayConfigEntry) GetName() string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return e.Name
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-02 19:10:25 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) GetMeta() map[string]string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.Meta
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 19:27:32 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) Normalize() error {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return fmt.Errorf("config entry is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Kind = TerminatingGateway
|
2020-05-08 15:44:34 +00:00
|
|
|
|
e.EnterpriseMeta.Normalize()
|
2020-03-31 19:27:32 +00:00
|
|
|
|
|
|
|
|
|
for i := range e.Services {
|
2020-05-08 15:44:34 +00:00
|
|
|
|
e.Services[i].EnterpriseMeta.Merge(&e.EnterpriseMeta)
|
2020-03-31 19:27:32 +00:00
|
|
|
|
e.Services[i].EnterpriseMeta.Normalize()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *TerminatingGatewayConfigEntry) Validate() error {
|
2020-09-02 19:10:25 +00:00
|
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 19:27:32 +00:00
|
|
|
|
seen := make(map[ServiceID]bool)
|
|
|
|
|
|
|
|
|
|
for _, svc := range e.Services {
|
|
|
|
|
if svc.Name == "" {
|
|
|
|
|
return fmt.Errorf("Service name cannot be blank.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ns := svc.NamespaceOrDefault()
|
|
|
|
|
if ns == WildcardSpecifier {
|
|
|
|
|
return fmt.Errorf("Wildcard namespace is not supported for terminating gateway services")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cid := NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
2021-06-23 21:44:10 +00:00
|
|
|
|
|
|
|
|
|
if err := validateInnerEnterpriseMeta(&svc.EnterpriseMeta, &e.EnterpriseMeta); err != nil {
|
2021-09-14 17:08:06 +00:00
|
|
|
|
return fmt.Errorf("service %q: %w", cid, err)
|
2021-06-23 21:44:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for duplicates within the entry
|
2020-03-31 19:27:32 +00:00
|
|
|
|
if ok := seen[cid]; ok {
|
|
|
|
|
return fmt.Errorf("Service %q was specified more than once within a namespace", cid.String())
|
|
|
|
|
}
|
|
|
|
|
seen[cid] = true
|
|
|
|
|
|
2020-04-27 22:25:37 +00:00
|
|
|
|
// If either client cert config file was specified then the CA file, client cert, and key file must be specified
|
|
|
|
|
// Specifying only a CAFile is allowed for one-way TLS
|
|
|
|
|
if (svc.CertFile != "" || svc.KeyFile != "") &&
|
2020-03-31 19:27:32 +00:00
|
|
|
|
!(svc.CAFile != "" && svc.CertFile != "" && svc.KeyFile != "") {
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("Service %q must have a CertFile, CAFile, and KeyFile specified for TLS origination", svc.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-11 21:45:51 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
2020-03-31 19:27:32 +00:00
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
2022-03-11 21:45:51 +00:00
|
|
|
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
2020-03-31 19:27:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-11 21:45:51 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) CanWrite(authz acl.Authorizer) error {
|
2020-03-31 19:27:32 +00:00
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
2022-03-11 21:45:51 +00:00
|
|
|
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
2020-03-31 19:27:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *TerminatingGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return &RaftIndex{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &e.RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-13 03:55:53 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
2020-03-31 19:27:32 +00:00
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &e.EnterpriseMeta
|
|
|
|
|
}
|
2020-04-08 18:37:24 +00:00
|
|
|
|
|
2022-03-31 19:18:40 +00:00
|
|
|
|
func (e *TerminatingGatewayConfigEntry) Warnings() []string {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warnings := make([]string, 0)
|
|
|
|
|
for _, svc := range e.Services {
|
|
|
|
|
if (svc.CAFile != "" || svc.CertFile != "" || svc.KeyFile != "") && svc.SNI == "" {
|
|
|
|
|
warning := fmt.Sprintf("TLS is configured but SNI is not set for service %q. Enabling SNI is strongly recommended when using TLS.", svc.Name)
|
|
|
|
|
warnings = append(warnings, warning)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return warnings
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-31 20:20:12 +00:00
|
|
|
|
type GatewayServiceKind string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
GatewayServiceKindUnknown GatewayServiceKind = ""
|
|
|
|
|
GatewayServiceKindDestination GatewayServiceKind = "destination"
|
|
|
|
|
GatewayServiceKindService GatewayServiceKind = "service"
|
|
|
|
|
)
|
|
|
|
|
|
2020-04-08 18:37:24 +00:00
|
|
|
|
// GatewayService is used to associate gateways with their linked services.
|
|
|
|
|
type GatewayService struct {
|
2020-06-12 14:57:41 +00:00
|
|
|
|
Gateway ServiceName
|
|
|
|
|
Service ServiceName
|
2020-04-17 00:51:27 +00:00
|
|
|
|
GatewayKind ServiceKind
|
2022-05-31 20:20:12 +00:00
|
|
|
|
Port int `json:",omitempty"`
|
|
|
|
|
Protocol string `json:",omitempty"`
|
|
|
|
|
Hosts []string `json:",omitempty"`
|
|
|
|
|
CAFile string `json:",omitempty"`
|
|
|
|
|
CertFile string `json:",omitempty"`
|
|
|
|
|
KeyFile string `json:",omitempty"`
|
|
|
|
|
SNI string `json:",omitempty"`
|
|
|
|
|
FromWildcard bool `json:",omitempty"`
|
|
|
|
|
ServiceKind GatewayServiceKind `json:",omitempty"`
|
2020-04-16 21:00:48 +00:00
|
|
|
|
RaftIndex
|
2020-04-08 18:37:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GatewayServices []*GatewayService
|
|
|
|
|
|
2020-06-22 19:14:12 +00:00
|
|
|
|
func (g *GatewayService) Addresses(defaultHosts []string) []string {
|
|
|
|
|
if g.Port == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hosts := g.Hosts
|
|
|
|
|
if len(hosts) == 0 {
|
|
|
|
|
hosts = defaultHosts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var addresses []string
|
2021-02-25 09:34:47 +00:00
|
|
|
|
// loop through the hosts and format that into domain.name:port format,
|
|
|
|
|
// ensuring we trim any trailing DNS . characters from the domain name as we
|
|
|
|
|
// go
|
2020-06-22 19:14:12 +00:00
|
|
|
|
for _, h := range hosts {
|
2021-02-25 09:34:47 +00:00
|
|
|
|
addresses = append(addresses, fmt.Sprintf("%s:%d", strings.TrimRight(h, "."), g.Port))
|
2020-06-22 19:14:12 +00:00
|
|
|
|
}
|
|
|
|
|
return addresses
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 18:37:24 +00:00
|
|
|
|
func (g *GatewayService) IsSame(o *GatewayService) bool {
|
2020-12-11 21:10:00 +00:00
|
|
|
|
return g.Gateway.Matches(o.Gateway) &&
|
|
|
|
|
g.Service.Matches(o.Service) &&
|
2020-04-08 18:37:24 +00:00
|
|
|
|
g.GatewayKind == o.GatewayKind &&
|
2020-04-16 21:00:48 +00:00
|
|
|
|
g.Port == o.Port &&
|
2020-04-16 23:24:11 +00:00
|
|
|
|
g.Protocol == o.Protocol &&
|
2020-05-27 16:47:32 +00:00
|
|
|
|
stringslice.Equal(g.Hosts, o.Hosts) &&
|
2020-04-08 18:37:24 +00:00
|
|
|
|
g.CAFile == o.CAFile &&
|
|
|
|
|
g.CertFile == o.CertFile &&
|
2020-04-27 22:25:37 +00:00
|
|
|
|
g.KeyFile == o.KeyFile &&
|
|
|
|
|
g.SNI == o.SNI &&
|
2022-05-31 20:20:12 +00:00
|
|
|
|
g.ServiceKind == o.ServiceKind &&
|
2020-04-27 22:25:37 +00:00
|
|
|
|
g.FromWildcard == o.FromWildcard
|
2020-04-08 18:37:24 +00:00
|
|
|
|
}
|
2020-04-16 21:00:48 +00:00
|
|
|
|
|
|
|
|
|
func (g *GatewayService) Clone() *GatewayService {
|
|
|
|
|
return &GatewayService{
|
2020-04-23 15:06:19 +00:00
|
|
|
|
Gateway: g.Gateway,
|
|
|
|
|
Service: g.Service,
|
|
|
|
|
GatewayKind: g.GatewayKind,
|
|
|
|
|
Port: g.Port,
|
|
|
|
|
Protocol: g.Protocol,
|
|
|
|
|
// See https://github.com/go101/go101/wiki/How-to-efficiently-clone-a-slice%3F
|
|
|
|
|
Hosts: append(g.Hosts[:0:0], g.Hosts...),
|
2020-04-27 22:25:37 +00:00
|
|
|
|
CAFile: g.CAFile,
|
|
|
|
|
CertFile: g.CertFile,
|
|
|
|
|
KeyFile: g.KeyFile,
|
|
|
|
|
SNI: g.SNI,
|
|
|
|
|
FromWildcard: g.FromWildcard,
|
|
|
|
|
RaftIndex: g.RaftIndex,
|
2022-05-31 20:20:12 +00:00
|
|
|
|
ServiceKind: g.ServiceKind,
|
2020-04-16 21:00:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-18 22:14:34 +00:00
|
|
|
|
|
|
|
|
|
// APIGatewayConfigEntry manages the configuration for an API gateway service
|
|
|
|
|
// with the given name.
|
|
|
|
|
type APIGatewayConfigEntry struct {
|
|
|
|
|
// Kind of the config entry. This will be set to structs.APIGateway.
|
|
|
|
|
Kind string
|
|
|
|
|
|
|
|
|
|
// Name is used to match the config entry with its associated API gateway
|
|
|
|
|
// service. This should match the name provided in the service definition.
|
|
|
|
|
Name string
|
|
|
|
|
|
|
|
|
|
// Listeners is the set of listener configuration to which an API Gateway
|
|
|
|
|
// might bind.
|
|
|
|
|
Listeners []APIGatewayListener
|
2023-02-08 21:52:12 +00:00
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
// Status is the asynchronous status which an APIGateway propagates to the user.
|
|
|
|
|
Status Status
|
|
|
|
|
|
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
|
|
|
|
RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-17 21:03:30 +00:00
|
|
|
|
func (e *APIGatewayConfigEntry) GetKind() string { return APIGateway }
|
|
|
|
|
func (e *APIGatewayConfigEntry) GetName() string { return e.Name }
|
|
|
|
|
func (e *APIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta }
|
|
|
|
|
func (e *APIGatewayConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex }
|
|
|
|
|
func (e *APIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta }
|
|
|
|
|
|
|
|
|
|
var _ ControlledConfigEntry = (*APIGatewayConfigEntry)(nil)
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) GetStatus() Status { return e.Status }
|
|
|
|
|
func (e *APIGatewayConfigEntry) SetStatus(status Status) { e.Status = status }
|
|
|
|
|
func (e *APIGatewayConfigEntry) DefaultStatus() Status { return Status{} }
|
|
|
|
|
|
2023-02-10 17:34:01 +00:00
|
|
|
|
func (e *APIGatewayConfigEntry) ListenerIsReady(name string) bool {
|
|
|
|
|
for _, condition := range e.Status.Conditions {
|
|
|
|
|
if !condition.Resource.IsSame(&ResourceReference{
|
|
|
|
|
Kind: APIGateway,
|
|
|
|
|
SectionName: name,
|
|
|
|
|
Name: e.Name,
|
|
|
|
|
EnterpriseMeta: e.EnterpriseMeta,
|
|
|
|
|
}) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if condition.Type == "Conflicted" && condition.Status == "True" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
func (e *APIGatewayConfigEntry) Normalize() error {
|
|
|
|
|
for i, listener := range e.Listeners {
|
|
|
|
|
protocol := strings.ToLower(string(listener.Protocol))
|
|
|
|
|
listener.Protocol = APIGatewayListenerProtocol(protocol)
|
|
|
|
|
e.Listeners[i] = listener
|
2023-02-17 21:03:30 +00:00
|
|
|
|
|
|
|
|
|
for i, cert := range listener.TLS.Certificates {
|
|
|
|
|
if cert.Kind == "" {
|
|
|
|
|
cert.Kind = InlineCertificate
|
|
|
|
|
}
|
2023-02-17 23:23:16 +00:00
|
|
|
|
cert.EnterpriseMeta.Normalize()
|
|
|
|
|
|
2023-02-17 21:03:30 +00:00
|
|
|
|
listener.TLS.Certificates[i] = cert
|
|
|
|
|
}
|
2023-01-18 22:14:34 +00:00
|
|
|
|
}
|
2023-02-17 21:03:30 +00:00
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) Validate() error {
|
2023-02-17 21:03:30 +00:00
|
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-20 18:14:05 +00:00
|
|
|
|
if len(e.Listeners) == 0 {
|
|
|
|
|
return fmt.Errorf("api gateway must have at least one listener")
|
|
|
|
|
}
|
2023-01-18 22:14:34 +00:00
|
|
|
|
if err := e.validateListenerNames(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := e.validateMergedListeners(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return e.validateListeners()
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-21 19:34:04 +00:00
|
|
|
|
var listenerNameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
|
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
func (e *APIGatewayConfigEntry) validateListenerNames() error {
|
|
|
|
|
listeners := make(map[string]struct{})
|
|
|
|
|
for _, listener := range e.Listeners {
|
2023-02-21 19:34:04 +00:00
|
|
|
|
if len(listener.Name) < 1 || !listenerNameRegex.MatchString(listener.Name) {
|
|
|
|
|
return fmt.Errorf("listener name %q is invalid, must be at least 1 character and contain only letters, numbers, or dashes", listener.Name)
|
|
|
|
|
}
|
2023-01-18 22:14:34 +00:00
|
|
|
|
if _, found := listeners[listener.Name]; found {
|
|
|
|
|
return fmt.Errorf("found multiple listeners with the name %q", listener.Name)
|
|
|
|
|
}
|
|
|
|
|
listeners[listener.Name] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) validateMergedListeners() error {
|
|
|
|
|
listeners := make(map[int]APIGatewayListener)
|
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
merged, found := listeners[listener.Port]
|
|
|
|
|
if found && (merged.Hostname != listener.Hostname || merged.Protocol != listener.Protocol) {
|
|
|
|
|
return fmt.Errorf("listeners %q and %q cannot be merged", merged.Name, listener.Name)
|
|
|
|
|
}
|
|
|
|
|
listeners[listener.Port] = listener
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) validateListeners() error {
|
|
|
|
|
validProtocols := map[APIGatewayListenerProtocol]bool{
|
|
|
|
|
ListenerProtocolHTTP: true,
|
|
|
|
|
ListenerProtocolTCP: true,
|
|
|
|
|
}
|
|
|
|
|
allowedCertificateKinds := map[string]bool{
|
|
|
|
|
InlineCertificate: true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
if !validProtocols[listener.Protocol] {
|
|
|
|
|
return fmt.Errorf("unsupported listener protocol %q, must be one of 'tcp', or 'http'", listener.Protocol)
|
|
|
|
|
}
|
|
|
|
|
if listener.Protocol == ListenerProtocolTCP && listener.Hostname != "" {
|
|
|
|
|
// TODO: once we have SNI matching we should be able to implement this
|
|
|
|
|
return fmt.Errorf("hostname specification is not supported when using TCP")
|
|
|
|
|
}
|
|
|
|
|
if listener.Port <= 0 || listener.Port > 65535 {
|
|
|
|
|
return fmt.Errorf("listener port %d not in the range 1-65535", listener.Port)
|
|
|
|
|
}
|
|
|
|
|
if strings.ContainsRune(strings.TrimPrefix(listener.Hostname, wildcardPrefix), '*') {
|
|
|
|
|
return fmt.Errorf("host %q is not valid, a wildcard specifier is only allowed as the left-most label", listener.Hostname)
|
|
|
|
|
}
|
|
|
|
|
for _, certificate := range listener.TLS.Certificates {
|
|
|
|
|
if !allowedCertificateKinds[certificate.Kind] {
|
|
|
|
|
return fmt.Errorf("unsupported certificate kind: %q, must be 'inline-certificate'", certificate.Kind)
|
|
|
|
|
}
|
|
|
|
|
if certificate.Name == "" {
|
|
|
|
|
return fmt.Errorf("certificate reference must have a name")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := validateTLSConfig(listener.TLS.MinVersion, listener.TLS.MaxVersion, listener.TLS.CipherSuites); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
|
|
|
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *APIGatewayConfigEntry) CanWrite(authz acl.Authorizer) error {
|
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
|
|
|
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// APIGatewayListenerProtocol is the protocol that an APIGateway listener uses
|
|
|
|
|
type APIGatewayListenerProtocol string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
ListenerProtocolHTTP APIGatewayListenerProtocol = "http"
|
|
|
|
|
ListenerProtocolTCP APIGatewayListenerProtocol = "tcp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// APIGatewayListener represents an individual listener for an APIGateway
|
|
|
|
|
type APIGatewayListener struct {
|
2023-02-21 19:34:04 +00:00
|
|
|
|
// Name is the name of the listener in a given gateway. This must be
|
|
|
|
|
// unique within a gateway.
|
2023-01-18 22:14:34 +00:00
|
|
|
|
Name string
|
2023-02-08 21:52:12 +00:00
|
|
|
|
// Hostname is the host name that a listener should be bound to. If
|
2023-01-18 22:14:34 +00:00
|
|
|
|
// unspecified, the listener accepts requests for all hostnames.
|
|
|
|
|
Hostname string
|
|
|
|
|
// Port is the port at which this listener should bind.
|
|
|
|
|
Port int
|
2023-02-08 21:52:12 +00:00
|
|
|
|
// Protocol is the protocol that a listener should use. It must
|
|
|
|
|
// either be http or tcp.
|
2023-01-18 22:14:34 +00:00
|
|
|
|
Protocol APIGatewayListenerProtocol
|
|
|
|
|
// TLS is the TLS settings for the listener.
|
|
|
|
|
TLS APIGatewayTLSConfiguration
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-17 20:47:38 +00:00
|
|
|
|
func (l APIGatewayListener) GetHostname() string {
|
|
|
|
|
if l.Hostname != "" {
|
|
|
|
|
return l.Hostname
|
|
|
|
|
}
|
|
|
|
|
return "*"
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 22:14:34 +00:00
|
|
|
|
// APIGatewayTLSConfiguration specifies the configuration of a listener’s
|
|
|
|
|
// TLS settings.
|
|
|
|
|
type APIGatewayTLSConfiguration struct {
|
|
|
|
|
// Certificates is a set of references to certificates
|
|
|
|
|
// that a gateway listener uses for TLS termination.
|
|
|
|
|
Certificates []ResourceReference
|
|
|
|
|
// MaxVersion is the maximum TLS version that the listener
|
|
|
|
|
// should support.
|
|
|
|
|
MaxVersion types.TLSVersion
|
|
|
|
|
// MinVersion is the minimum TLS version that the listener
|
|
|
|
|
// should support.
|
|
|
|
|
MinVersion types.TLSVersion
|
|
|
|
|
// CipherSuites is the cipher suites that the listener should support.
|
|
|
|
|
CipherSuites []types.TLSCipherSuite
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BoundAPIGatewayConfigEntry manages the configuration for a bound API
|
|
|
|
|
// gateway with the given name. This type is never written from the client.
|
|
|
|
|
// It is only written by the controller in order to represent an API gateway
|
|
|
|
|
// and the resources that are bound to it.
|
|
|
|
|
type BoundAPIGatewayConfigEntry struct {
|
|
|
|
|
// Kind of the config entry. This will be set to structs.BoundAPIGateway.
|
|
|
|
|
Kind string
|
|
|
|
|
|
|
|
|
|
// Name is used to match the config entry with its associated API gateway
|
|
|
|
|
// service. This should match the name provided in the corresponding API
|
|
|
|
|
// gateway service definition.
|
|
|
|
|
Name string
|
|
|
|
|
|
|
|
|
|
// Listeners are the valid listeners of an APIGateway with information about
|
|
|
|
|
// what certificates and routes have successfully bound to it.
|
|
|
|
|
Listeners []BoundAPIGatewayListener
|
|
|
|
|
|
|
|
|
|
Meta map[string]string `json:",omitempty"`
|
|
|
|
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
|
|
|
|
RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 15:17:25 +00:00
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) IsSame(other *BoundAPIGatewayConfigEntry) bool {
|
|
|
|
|
listeners := map[string]BoundAPIGatewayListener{}
|
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
listeners[listener.Name] = listener
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
otherListeners := map[string]BoundAPIGatewayListener{}
|
|
|
|
|
for _, listener := range other.Listeners {
|
|
|
|
|
otherListeners[listener.Name] = listener
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(listeners) != len(otherListeners) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, listener := range listeners {
|
|
|
|
|
otherListener, found := otherListeners[name]
|
|
|
|
|
if !found {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if !listener.IsSame(otherListener) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-08 19:50:17 +00:00
|
|
|
|
// IsInitializedForGateway returns whether or not this bound api gateway is initialized with the given api gateway
|
|
|
|
|
// including having corresponding listener entries for the gateway.
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) IsInitializedForGateway(gateway *APIGatewayConfigEntry) bool {
|
|
|
|
|
if e.Name != gateway.Name || !e.EnterpriseMeta.IsSame(&gateway.EnterpriseMeta) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensure that this has the same listener data (i.e. it's been reconciled)
|
|
|
|
|
if len(gateway.Listeners) != len(e.Listeners) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, listener := range e.Listeners {
|
|
|
|
|
if listener.Name != gateway.Listeners[i].Name {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-17 21:03:30 +00:00
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetKind() string { return BoundAPIGateway }
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetName() string { return e.Name }
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetMeta() map[string]string { return e.Meta }
|
2023-02-17 23:23:16 +00:00
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) Normalize() error {
|
|
|
|
|
for i, listener := range e.Listeners {
|
|
|
|
|
for j, route := range listener.Routes {
|
|
|
|
|
route.EnterpriseMeta.Normalize()
|
|
|
|
|
|
|
|
|
|
listener.Routes[j] = route
|
|
|
|
|
}
|
|
|
|
|
for j, cert := range listener.Certificates {
|
|
|
|
|
cert.EnterpriseMeta.Normalize()
|
|
|
|
|
|
|
|
|
|
listener.Certificates[j] = cert
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Listeners[i] = listener
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2023-01-18 22:14:34 +00:00
|
|
|
|
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) Validate() error {
|
|
|
|
|
allowedCertificateKinds := map[string]bool{
|
|
|
|
|
InlineCertificate: true,
|
|
|
|
|
}
|
|
|
|
|
allowedRouteKinds := map[string]bool{
|
|
|
|
|
HTTPRoute: true,
|
|
|
|
|
TCPRoute: true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These should already be validated by upstream validation
|
|
|
|
|
// logic in the gateways/routes, but just in case we validate
|
|
|
|
|
// here as well.
|
|
|
|
|
for _, listener := range e.Listeners {
|
|
|
|
|
for _, certificate := range listener.Certificates {
|
|
|
|
|
if !allowedCertificateKinds[certificate.Kind] {
|
|
|
|
|
return fmt.Errorf("unsupported certificate kind: %q, must be 'inline-certificate'", certificate.Kind)
|
|
|
|
|
}
|
|
|
|
|
if certificate.Name == "" {
|
|
|
|
|
return fmt.Errorf("certificate reference must have a name")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, route := range listener.Routes {
|
|
|
|
|
if !allowedRouteKinds[route.Kind] {
|
|
|
|
|
return fmt.Errorf("unsupported route kind: %q, must be one of 'http-route', or 'tcp-route'", route.Kind)
|
|
|
|
|
}
|
|
|
|
|
if route.Name == "" {
|
|
|
|
|
return fmt.Errorf("route reference must have a name")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) CanRead(authz acl.Authorizer) error {
|
|
|
|
|
var authzContext acl.AuthorizerContext
|
|
|
|
|
e.FillAuthzContext(&authzContext)
|
|
|
|
|
return authz.ToAllowAuthorizer().ServiceReadAllowed(e.Name, &authzContext)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) CanWrite(_ acl.Authorizer) error {
|
|
|
|
|
return acl.PermissionDenied("only writeable by controller")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return &RaftIndex{}
|
|
|
|
|
}
|
|
|
|
|
return &e.RaftIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
|
|
|
|
if e == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &e.EnterpriseMeta
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BoundAPIGatewayListener is an API gateway listener with information
|
|
|
|
|
// about the routes and certificates that have successfully bound to it.
|
|
|
|
|
type BoundAPIGatewayListener struct {
|
|
|
|
|
Name string
|
|
|
|
|
Routes []ResourceReference
|
|
|
|
|
Certificates []ResourceReference
|
|
|
|
|
}
|
2023-01-20 20:11:16 +00:00
|
|
|
|
|
2023-02-08 19:50:17 +00:00
|
|
|
|
func sameResources(first, second []ResourceReference) bool {
|
|
|
|
|
if len(first) != len(second) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, firstRef := range first {
|
|
|
|
|
found := false
|
|
|
|
|
for _, secondRef := range second {
|
|
|
|
|
if firstRef.IsSame(&secondRef) {
|
|
|
|
|
found = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l BoundAPIGatewayListener) IsSame(other BoundAPIGatewayListener) bool {
|
|
|
|
|
if l.Name != other.Name {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if !sameResources(l.Certificates, other.Certificates) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return sameResources(l.Routes, other.Routes)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-20 20:11:16 +00:00
|
|
|
|
// BindRoute is used to create or update a route on the listener.
|
|
|
|
|
// It returns true if the route was able to be bound to the listener.
|
2023-01-27 14:41:03 +00:00
|
|
|
|
// Routes should only bind to listeners with their same section name
|
|
|
|
|
// and protocol. Be sure to check both of these before attempting
|
|
|
|
|
// to bind a route to the listener.
|
2023-02-08 19:50:17 +00:00
|
|
|
|
func (l *BoundAPIGatewayListener) BindRoute(routeRef ResourceReference) bool {
|
2023-01-20 20:11:16 +00:00
|
|
|
|
// If the listener has no routes, create a new slice of routes with the given route.
|
|
|
|
|
if l.Routes == nil {
|
|
|
|
|
l.Routes = []ResourceReference{routeRef}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the route matches an existing route, update it and return.
|
|
|
|
|
for i, listenerRoute := range l.Routes {
|
|
|
|
|
if listenerRoute.Kind == routeRef.Kind && listenerRoute.Name == routeRef.Name && listenerRoute.EnterpriseMeta.IsSame(&routeRef.EnterpriseMeta) {
|
|
|
|
|
l.Routes[i] = routeRef
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the route is new to the listener, append it.
|
|
|
|
|
l.Routes = append(l.Routes, routeRef)
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-08 19:50:17 +00:00
|
|
|
|
func (l *BoundAPIGatewayListener) UnbindRoute(route ResourceReference) bool {
|
2023-01-20 20:11:16 +00:00
|
|
|
|
if l == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, listenerRoute := range l.Routes {
|
2023-02-08 19:50:17 +00:00
|
|
|
|
if listenerRoute.Kind == route.Kind && listenerRoute.Name == route.Name && listenerRoute.EnterpriseMeta.IsSame(&route.EnterpriseMeta) {
|
2023-01-20 20:11:16 +00:00
|
|
|
|
l.Routes = append(l.Routes[:i], l.Routes[i+1:]...)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
2023-02-08 19:50:17 +00:00
|
|
|
|
|
2023-02-17 23:23:16 +00:00
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) GetStatus() Status { return Status{} }
|
2023-02-08 19:50:17 +00:00
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) SetStatus(status Status) {}
|
|
|
|
|
func (e *BoundAPIGatewayConfigEntry) DefaultStatus() Status { return Status{} }
|