Add config entry/state for Ingress Gateways (#7483)
* Add Ingress gateway config entry and other relevant structs * Add api package tests for ingress gateways * Embed EnterpriseMeta into ingress service struct * Add namespace fields to api module and test consul config write decoding * Don't require a port for ingress gateways * Add snakeJSON and camelJSON cases in command test * Run Normalize on service's ent metadata Sadly cannot think of a way to test this in OSS. * Every protocol requires at least 1 service * Validate ingress protocols * Update agent/structs/config_entry_gateways.go Co-authored-by: Chris Piraino <cpiraino@hashicorp.com> Co-authored-by: Freddy <freddygv@users.noreply.github.com>
This commit is contained in:
parent
f26cc74533
commit
01a23b8eb4
|
@ -325,6 +325,7 @@ func (s *Store) validateProposedConfigEntryInGraph(
|
|||
case structs.ServiceRouter:
|
||||
case structs.ServiceSplitter:
|
||||
case structs.ServiceResolver:
|
||||
case structs.IngressGateway:
|
||||
default:
|
||||
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ const (
|
|||
ServiceRouter string = "service-router"
|
||||
ServiceSplitter string = "service-splitter"
|
||||
ServiceResolver string = "service-resolver"
|
||||
IngressGateway string = "ingress-gateway"
|
||||
|
||||
ProxyConfigGlobal string = "global"
|
||||
|
||||
|
@ -376,6 +377,15 @@ func ConfigEntryDecodeRulesForKind(kind string) (skipWhenPatching []string, tran
|
|||
"only_passing": "onlypassing",
|
||||
"service_subset": "servicesubset",
|
||||
}, nil
|
||||
case IngressGateway:
|
||||
return []string{
|
||||
"listeners",
|
||||
"Listeners",
|
||||
"listeners.services",
|
||||
"Listeners.Services",
|
||||
}, map[string]string{
|
||||
"service_subset": "servicesubset",
|
||||
}, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("kind %q should be explicitly handled here", kind)
|
||||
}
|
||||
|
@ -466,6 +476,8 @@ func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|||
return &ServiceSplitterConfigEntry{Name: name}, nil
|
||||
case ServiceResolver:
|
||||
return &ServiceResolverConfigEntry{Name: name}, nil
|
||||
case IngressGateway:
|
||||
return &IngressGatewayConfigEntry{Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
|
@ -477,6 +489,8 @@ func ValidateConfigEntryKind(kind string) bool {
|
|||
return true
|
||||
case ServiceRouter, ServiceSplitter, ServiceResolver:
|
||||
return true
|
||||
case IngressGateway:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
161
agent/structs/config_entry_gateways.go
Normal file
161
agent/structs/config_entry_gateways.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Listeners declares what ports the ingress gateway should listen on, and
|
||||
// what services to associated to those ports.
|
||||
Listeners []IngressListener
|
||||
|
||||
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||
RaftIndex
|
||||
}
|
||||
|
||||
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
|
||||
// current supported values are: (tcp | http).
|
||||
Protocol string
|
||||
|
||||
// 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.
|
||||
Name string
|
||||
|
||||
// ServiceSubset declares the specific service subset to which traffic should
|
||||
// be sent. This must match an existing service subset declared in a
|
||||
// service-resolver config entry.
|
||||
ServiceSubset string
|
||||
|
||||
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) GetKind() string {
|
||||
return IngressGateway
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) GetName() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return e.Name
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) Normalize() error {
|
||||
if e == nil {
|
||||
return fmt.Errorf("config entry is nil")
|
||||
}
|
||||
|
||||
e.Kind = IngressGateway
|
||||
for _, listener := range e.Listeners {
|
||||
listener.Protocol = strings.ToLower(listener.Protocol)
|
||||
for i := range listener.Services {
|
||||
listener.Services[i].EnterpriseMeta.Normalize()
|
||||
}
|
||||
}
|
||||
|
||||
e.EnterpriseMeta.Normalize()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) Validate() error {
|
||||
validProtocols := map[string]bool{
|
||||
"http": true,
|
||||
"tcp": true,
|
||||
}
|
||||
declaredPorts := make(map[int]bool)
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("Protocol must be either 'http' or 'tcp', '%s' is an unsupported protocol.", listener.Protocol)
|
||||
}
|
||||
|
||||
for _, s := range listener.Services {
|
||||
if s.Name == WildcardSpecifier && listener.Protocol != "http" {
|
||||
return fmt.Errorf("Wildcard service name is only valid for protocol = 'http' (listener on port %d)", listener.Port)
|
||||
}
|
||||
if s.Name == "" {
|
||||
return fmt.Errorf("Service name cannot be blank (listener on port %d)", listener.Port)
|
||||
}
|
||||
if s.NamespaceOrDefault() == WildcardSpecifier {
|
||||
return fmt.Errorf("Wildcard namespace is not supported for ingress services (listener on port %d)", listener.Port)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
if listener.Protocol != "http" && len(listener.Services) > 1 {
|
||||
return fmt.Errorf("Multiple services per listener are only supported for protocol = 'http' (listener on port %d)",
|
||||
listener.Port)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool {
|
||||
var authzContext acl.AuthorizerContext
|
||||
e.FillAuthzContext(&authzContext)
|
||||
return authz.OperatorRead(&authzContext) == acl.Allow
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
||||
var authzContext acl.AuthorizerContext
|
||||
e.FillAuthzContext(&authzContext)
|
||||
return authz.OperatorWrite(&authzContext) == acl.Allow
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex {
|
||||
if e == nil {
|
||||
return &RaftIndex{}
|
||||
}
|
||||
|
||||
return &e.RaftIndex
|
||||
}
|
||||
|
||||
func (e *IngressGatewayConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &e.EnterpriseMeta
|
||||
}
|
187
agent/structs/config_entry_gateways_test.go
Normal file
187
agent/structs/config_entry_gateways_test.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIngressConfigEntry_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
entry IngressGatewayConfigEntry
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "port conflict",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "mysql",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "postgres",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "port 1111 declared on two listeners",
|
||||
},
|
||||
{
|
||||
name: "http features: wildcard",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http features: wildcard service on invalid protocol",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "Wildcard service name is only valid for protocol",
|
||||
},
|
||||
{
|
||||
name: "http features: multiple services",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "db1",
|
||||
},
|
||||
{
|
||||
Name: "db2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "multiple services per listener are only supported for protocol",
|
||||
},
|
||||
{
|
||||
name: "tcp listener requires a defined service",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "no service declared for listener with port 1111",
|
||||
},
|
||||
{
|
||||
name: "http listener requires a defined service",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "no service declared for listener with port 1111",
|
||||
},
|
||||
{
|
||||
name: "empty service name not supported",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "Service name cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "protocol validation",
|
||||
entry: IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
{
|
||||
Port: 1111,
|
||||
Protocol: "asdf",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "db",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: "Protocol must be either 'http' or 'tcp', 'asdf' is an unsupported protocol.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
// We explicitly copy the variable for the range statement so that can run
|
||||
// tests in parallel.
|
||||
tc := test
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := tc.entry.Validate()
|
||||
if tc.expectErr != "" {
|
||||
require.Error(t, err)
|
||||
requireContainsLower(t, err.Error(), tc.expectErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -532,6 +532,121 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
Name: "main",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ingress-gateway: kitchen sink",
|
||||
snake: `
|
||||
kind = "ingress-gateway"
|
||||
name = "ingress-web"
|
||||
|
||||
listeners = [
|
||||
{
|
||||
port = 8080
|
||||
protocol = "http"
|
||||
services = [
|
||||
{
|
||||
name = "web"
|
||||
},
|
||||
{
|
||||
name = "db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
port = 9999
|
||||
protocol = "tcp"
|
||||
services = [
|
||||
{
|
||||
name = "mysql"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
port = 2234
|
||||
protocol = "tcp"
|
||||
services = [
|
||||
{
|
||||
name = "postgres"
|
||||
service_subset = "v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`,
|
||||
camel: `
|
||||
Kind = "ingress-gateway"
|
||||
Name = "ingress-web"
|
||||
Listeners = [
|
||||
{
|
||||
Port = 8080
|
||||
Protocol = "http"
|
||||
Services = [
|
||||
{
|
||||
Name = "web"
|
||||
},
|
||||
{
|
||||
Name = "db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Port = 9999
|
||||
Protocol = "tcp"
|
||||
Services = [
|
||||
{
|
||||
Name = "mysql"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
Port = 2234
|
||||
Protocol = "tcp"
|
||||
Services = [
|
||||
{
|
||||
Name = "postgres"
|
||||
ServiceSubset = "v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`,
|
||||
expect: &IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
IngressListener{
|
||||
Port: 8080,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{
|
||||
IngressService{
|
||||
Name: "web",
|
||||
},
|
||||
IngressService{
|
||||
Name: "db",
|
||||
},
|
||||
},
|
||||
},
|
||||
IngressListener{
|
||||
Port: 9999,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
IngressService{
|
||||
Name: "mysql",
|
||||
},
|
||||
},
|
||||
},
|
||||
IngressListener{
|
||||
Port: 2234,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
IngressService{
|
||||
Name: "postgres",
|
||||
ServiceSubset: "v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
|
||||
|
|
|
@ -358,7 +358,7 @@ func (u *Upstream) Identifier() string {
|
|||
name := u.DestinationName
|
||||
typ := u.DestinationType
|
||||
|
||||
if typ != UpstreamDestTypePreparedQuery && u.DestinationNamespace != "" && u.DestinationNamespace != "default" {
|
||||
if typ != UpstreamDestTypePreparedQuery && u.DestinationNamespace != "" && u.DestinationNamespace != IntentionDefaultNamespace {
|
||||
name = u.DestinationNamespace + "/" + u.DestinationName
|
||||
}
|
||||
if u.Datacenter != "" {
|
||||
|
|
|
@ -894,6 +894,11 @@ const (
|
|||
// ServiceKindTerminatingGateway is a Terminating Gateway for the Connect
|
||||
// feature. This service will proxy connections to services outside the mesh.
|
||||
ServiceKindTerminatingGateway ServiceKind = "terminating-gateway"
|
||||
|
||||
// ServiceKindIngressGateway is an Ingress Gateway for the Connect feature.
|
||||
// This service allows external traffic to enter the mesh based on
|
||||
// centralized configuration.
|
||||
ServiceKindIngressGateway ServiceKind = "ingress-gateway"
|
||||
)
|
||||
|
||||
// Type to hold a address and port of a service
|
||||
|
@ -1039,7 +1044,9 @@ func (s *NodeService) IsSidecarProxy() bool {
|
|||
}
|
||||
|
||||
func (s *NodeService) IsGateway() bool {
|
||||
return s.Kind == ServiceKindMeshGateway || s.Kind == ServiceKindTerminatingGateway
|
||||
return s.Kind == ServiceKindMeshGateway ||
|
||||
s.Kind == ServiceKindTerminatingGateway ||
|
||||
s.Kind == ServiceKindIngressGateway
|
||||
}
|
||||
|
||||
// Validate validates the node service configuration.
|
||||
|
@ -1138,8 +1145,8 @@ func (s *NodeService) Validate() error {
|
|||
|
||||
// Gateway validation
|
||||
if s.IsGateway() {
|
||||
// Gateways must have a port
|
||||
if s.Port == 0 {
|
||||
// Non-ingress gateways must have a port
|
||||
if s.Port == 0 && s.Kind != ServiceKindIngressGateway {
|
||||
result = multierror.Append(result, fmt.Errorf("Port must be non-zero for a %s", s.Kind))
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ func (m *EnterpriseMeta) LessThan(_ *EnterpriseMeta) bool {
|
|||
}
|
||||
|
||||
func (m *EnterpriseMeta) NamespaceOrDefault() string {
|
||||
return "default"
|
||||
return IntentionDefaultNamespace
|
||||
}
|
||||
|
||||
func EnterpriseMetaInitializer(_ string) EnterpriseMeta {
|
||||
|
|
|
@ -420,6 +420,7 @@ func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) {
|
|||
Modify func(*NodeService)
|
||||
Err string
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"valid": testCase{
|
||||
func(x *NodeService) {},
|
||||
|
@ -467,6 +468,59 @@ func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStructs_NodeService_ValidateIngressGateway(t *testing.T) {
|
||||
type testCase struct {
|
||||
Modify func(*NodeService)
|
||||
Err string
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"valid": testCase{
|
||||
func(x *NodeService) {},
|
||||
"",
|
||||
},
|
||||
"sidecar-service": testCase{
|
||||
func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} },
|
||||
"cannot have a sidecar service",
|
||||
},
|
||||
"proxy-destination-name": testCase{
|
||||
func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" },
|
||||
"Proxy.DestinationServiceName configuration is invalid",
|
||||
},
|
||||
"proxy-destination-id": testCase{
|
||||
func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" },
|
||||
"Proxy.DestinationServiceID configuration is invalid",
|
||||
},
|
||||
"proxy-local-address": testCase{
|
||||
func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" },
|
||||
"Proxy.LocalServiceAddress configuration is invalid",
|
||||
},
|
||||
"proxy-local-port": testCase{
|
||||
func(x *NodeService) { x.Proxy.LocalServicePort = 36 },
|
||||
"Proxy.LocalServicePort configuration is invalid",
|
||||
},
|
||||
"proxy-upstreams": testCase{
|
||||
func(x *NodeService) { x.Proxy.Upstreams = []Upstream{Upstream{}} },
|
||||
"Proxy.Upstreams configuration is invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ns := TestNodeServiceIngressGateway(t, "10.0.0.5")
|
||||
tc.Modify(ns)
|
||||
|
||||
err := ns.Validate()
|
||||
if tc.Err == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) {
|
||||
type testCase struct {
|
||||
Modify func(*NodeService)
|
||||
|
|
|
@ -112,6 +112,14 @@ func TestNodeServiceMeshGatewayWithAddrs(t testing.T, address string, port int,
|
|||
}
|
||||
}
|
||||
|
||||
func TestNodeServiceIngressGateway(t testing.T, address string) *NodeService {
|
||||
return &NodeService{
|
||||
Kind: ServiceKindIngressGateway,
|
||||
Service: "ingress-gateway",
|
||||
Address: address,
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeServiceSidecar returns a *NodeService representing a service
|
||||
// registration with a nested Sidecar registration.
|
||||
func TestNodeServiceSidecar(t testing.T) *NodeService {
|
||||
|
|
|
@ -17,6 +17,7 @@ const (
|
|||
ServiceRouter string = "service-router"
|
||||
ServiceSplitter string = "service-splitter"
|
||||
ServiceResolver string = "service-resolver"
|
||||
IngressGateway string = "ingress-gateway"
|
||||
|
||||
ProxyConfigGlobal string = "global"
|
||||
)
|
||||
|
@ -157,6 +158,8 @@ func makeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|||
return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil
|
||||
case ServiceResolver:
|
||||
return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil
|
||||
case IngressGateway:
|
||||
return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
|
|
84
api/config_entry_gateways.go
Normal file
84
api/config_entry_gateways.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package api
|
||||
|
||||
// IngressGatewayConfigEntry manages the configuration for an ingress service
|
||||
// with the given name.
|
||||
type IngressGatewayConfigEntry struct {
|
||||
// Kind of the config entry. This should be set to api.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
|
||||
|
||||
// Namespace is the namespace the IngressGateway is associated with
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// Listeners declares what ports the ingress gateway should listen on, and
|
||||
// what services to associated to those ports.
|
||||
Listeners []IngressListener
|
||||
|
||||
// CreateIndex is the Raft index this entry was created at. This is a
|
||||
// read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// IngressListener manages the configuration for a listener on a specific port.
|
||||
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
|
||||
// current supported values are: (tcp | http).
|
||||
Protocol string
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IngressService manages configuration for services that are exposed to
|
||||
// ingress traffic.
|
||||
type IngressService struct {
|
||||
// Name declares the service to which traffic should be forwarded.
|
||||
//
|
||||
// This can either be a specific service instance, 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.
|
||||
Name string
|
||||
|
||||
// Namespace is the namespace where the service is located.
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
|
||||
// ServiceSubset declares the specific service subset to which traffic should
|
||||
// be sent. This must match an existing service subset declared in a
|
||||
// service-resolver config entry.
|
||||
ServiceSubset string
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetKind() string {
|
||||
return i.Kind
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetName() string {
|
||||
return i.Name
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetCreateIndex() uint64 {
|
||||
return i.CreateIndex
|
||||
}
|
||||
|
||||
func (i *IngressGatewayConfigEntry) GetModifyIndex() uint64 {
|
||||
return i.ModifyIndex
|
||||
}
|
127
api/config_entry_gateways_test.go
Normal file
127
api/config_entry_gateways_test.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s := makeClient(t)
|
||||
defer s.Stop()
|
||||
|
||||
config_entries := c.ConfigEntries()
|
||||
|
||||
ingress1 := &IngressGatewayConfigEntry{
|
||||
Kind: IngressGateway,
|
||||
Name: "foo",
|
||||
}
|
||||
|
||||
ingress2 := &IngressGatewayConfigEntry{
|
||||
Kind: IngressGateway,
|
||||
Name: "bar",
|
||||
}
|
||||
|
||||
// set it
|
||||
_, wm, err := config_entries.Set(ingress1, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
|
||||
// also set the second one
|
||||
_, wm, err = config_entries.Set(ingress2, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
|
||||
// get it
|
||||
entry, qm, err := config_entries.Get(IngressGateway, "foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, qm)
|
||||
require.NotEqual(t, 0, qm.RequestTime)
|
||||
|
||||
// verify it
|
||||
readIngress, ok := entry.(*IngressGatewayConfigEntry)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ingress1.Kind, readIngress.Kind)
|
||||
require.Equal(t, ingress1.Name, readIngress.Name)
|
||||
|
||||
// update it
|
||||
ingress1.Listeners = []IngressListener{
|
||||
{
|
||||
Port: 2222,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "asdf",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// CAS fail
|
||||
written, _, err := config_entries.CAS(ingress1, 0, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, written)
|
||||
|
||||
// CAS success
|
||||
written, wm, err = config_entries.CAS(ingress1, readIngress.ModifyIndex, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
require.True(t, written)
|
||||
|
||||
// update no cas
|
||||
ingress2.Listeners = []IngressListener{
|
||||
{
|
||||
Port: 3333,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{
|
||||
{
|
||||
Name: "qwer",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, wm, err = config_entries.Set(ingress2, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
|
||||
// list them
|
||||
entries, qm, err := config_entries.List(IngressGateway, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, qm)
|
||||
require.NotEqual(t, 0, qm.RequestTime)
|
||||
require.Len(t, entries, 2)
|
||||
|
||||
for _, entry = range entries {
|
||||
switch entry.GetName() {
|
||||
case "foo":
|
||||
// this also verifies that the update value was persisted and
|
||||
// the updated values are seen
|
||||
readIngress, ok = entry.(*IngressGatewayConfigEntry)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ingress1.Kind, readIngress.Kind)
|
||||
require.Equal(t, ingress1.Name, readIngress.Name)
|
||||
require.Equal(t, ingress1.Listeners, readIngress.Listeners)
|
||||
case "bar":
|
||||
readIngress, ok = entry.(*IngressGatewayConfigEntry)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ingress2.Kind, readIngress.Kind)
|
||||
require.Equal(t, ingress2.Name, readIngress.Name)
|
||||
require.Equal(t, ingress2.Listeners, readIngress.Listeners)
|
||||
}
|
||||
}
|
||||
|
||||
// delete it
|
||||
wm, err = config_entries.Delete(IngressGateway, "foo", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
|
||||
// verify deletion
|
||||
entry, qm, err = config_entries.Get(IngressGateway, "foo", nil)
|
||||
require.Error(t, err)
|
||||
}
|
|
@ -613,6 +613,67 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
Name: "main",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ingress-gateway",
|
||||
body: `
|
||||
{
|
||||
"Kind": "ingress-gateway",
|
||||
"Name": "ingress-web",
|
||||
"Listeners": [
|
||||
{
|
||||
"Port": 8080,
|
||||
"Protocol": "http",
|
||||
"Services": [
|
||||
{
|
||||
"Name": "web",
|
||||
"Namespace": "foo"
|
||||
},
|
||||
{
|
||||
"Name": "db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Port": 9999,
|
||||
"Protocol": "tcp",
|
||||
"Services": [
|
||||
{
|
||||
"Name": "mysql"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
expect: &IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []IngressListener{
|
||||
IngressListener{
|
||||
Port: 8080,
|
||||
Protocol: "http",
|
||||
Services: []IngressService{
|
||||
IngressService{
|
||||
Name: "web",
|
||||
Namespace: "foo",
|
||||
},
|
||||
IngressService{
|
||||
Name: "db",
|
||||
},
|
||||
},
|
||||
},
|
||||
IngressListener{
|
||||
Port: 9999,
|
||||
Protocol: "tcp",
|
||||
Services: []IngressService{
|
||||
IngressService{
|
||||
Name: "mysql",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
|
||||
|
|
|
@ -1122,6 +1122,114 @@ func TestParseConfigEntry(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ingress-gateway: kitchen sink",
|
||||
snake: `
|
||||
kind = "ingress-gateway"
|
||||
name = "ingress-web"
|
||||
|
||||
listeners = [
|
||||
{
|
||||
port = 8080
|
||||
protocol = "http"
|
||||
services = [
|
||||
{
|
||||
name = "web"
|
||||
service_subset = "v1"
|
||||
},
|
||||
{
|
||||
name = "db"
|
||||
namespace = "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`,
|
||||
camel: `
|
||||
Kind = "ingress-gateway"
|
||||
Name = "ingress-web"
|
||||
Listeners = [
|
||||
{
|
||||
Port = 8080
|
||||
Protocol = "http"
|
||||
Services = [
|
||||
{
|
||||
Name = "web"
|
||||
ServiceSubset = "v1"
|
||||
},
|
||||
{
|
||||
Name = "db"
|
||||
Namespace = "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`,
|
||||
snakeJSON: `
|
||||
{
|
||||
"kind": "ingress-gateway",
|
||||
"name": "ingress-web",
|
||||
"listeners": [
|
||||
{
|
||||
"port": 8080,
|
||||
"protocol": "http",
|
||||
"services": [
|
||||
{
|
||||
"name": "web",
|
||||
"service_subset": "v1"
|
||||
},
|
||||
{
|
||||
"name": "db",
|
||||
"namespace": "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
camelJSON: `
|
||||
{
|
||||
"Kind": "ingress-gateway",
|
||||
"Name": "ingress-web",
|
||||
"Listeners": [
|
||||
{
|
||||
"Port": 8080,
|
||||
"Protocol": "http",
|
||||
"Services": [
|
||||
{
|
||||
"Name": "web",
|
||||
"ServiceSubset": "v1"
|
||||
},
|
||||
{
|
||||
"Name": "db",
|
||||
"Namespace": "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
expect: &api.IngressGatewayConfigEntry{
|
||||
Kind: "ingress-gateway",
|
||||
Name: "ingress-web",
|
||||
Listeners: []api.IngressListener{
|
||||
{
|
||||
Port: 8080,
|
||||
Protocol: "http",
|
||||
Services: []api.IngressService{
|
||||
{
|
||||
Name: "web",
|
||||
ServiceSubset: "v1",
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Namespace: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
|
||||
|
|
Loading…
Reference in a new issue