mirror of https://github.com/status-im/consul.git
feat: add endpoint struct to ServiceConfigEntry
This commit is contained in:
parent
876f3bb971
commit
147fd96d97
|
@ -1,7 +1,9 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -103,6 +105,7 @@ type ServiceConfigEntry struct {
|
|||
Expose ExposeConfig `json:",omitempty"`
|
||||
ExternalSNI string `json:",omitempty" alias:"external_sni"`
|
||||
UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"`
|
||||
Endpoint *EndpointConfig `json:",omitempty"`
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||
|
@ -175,6 +178,12 @@ func (e *ServiceConfigEntry) Validate() error {
|
|||
|
||||
validationErr := validateConfigEntryMeta(e.Meta)
|
||||
|
||||
// External endpoints are invalid with an existing service's upstream configuration
|
||||
if e.UpstreamConfig != nil && e.Endpoint != nil {
|
||||
validationErr = multierror.Append(validationErr, errors.New("UpstreamConfig and Endpoint are mutually exclusive for service defaults"))
|
||||
return validationErr
|
||||
}
|
||||
|
||||
if e.UpstreamConfig != nil {
|
||||
for _, override := range e.UpstreamConfig.Overrides {
|
||||
err := override.ValidateWithName()
|
||||
|
@ -190,9 +199,61 @@ func (e *ServiceConfigEntry) Validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
if e.Endpoint != nil {
|
||||
if err := validateEndpointAddress(e.Endpoint.Address); err != nil {
|
||||
validationErr = multierror.Append(validationErr, fmt.Errorf("Endpoint address is invalid %w", err))
|
||||
}
|
||||
|
||||
if e.Endpoint.Port < 1 || e.Endpoint.Port > 65535 {
|
||||
validationErr = multierror.Append(validationErr, fmt.Errorf("Invalid Port number %d", e.Endpoint.Port))
|
||||
}
|
||||
|
||||
// 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 (e.Endpoint.CertFile != "" || e.Endpoint.KeyFile != "") &&
|
||||
!(e.Endpoint.CAFile != "" && e.Endpoint.CertFile != "" && e.Endpoint.KeyFile != "") {
|
||||
validationErr = multierror.Append(validationErr, errors.New("Endpoint must have a CertFile, CAFile, and KeyFile specified for TLS origination"))
|
||||
}
|
||||
}
|
||||
|
||||
return validationErr
|
||||
}
|
||||
|
||||
func validateEndpointAddress(address string) error {
|
||||
var valid bool
|
||||
|
||||
ip := net.ParseIP(address)
|
||||
valid = ip != nil
|
||||
|
||||
_, _, err := net.ParseCIDR(address)
|
||||
valid = valid || err == nil
|
||||
|
||||
// Since we don't know if this will be a TLS connection, setting tlsEnabled to false will be more permissive with wildcards
|
||||
err = validateHost(false, address)
|
||||
valid = valid || err == nil
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Could not validate address %s as an IP, CIDR block or Hostname", address)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ServiceConfigEntry) Warnings() []string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if e.Endpoint != nil {
|
||||
if (e.Endpoint.CAFile != "" || e.Endpoint.CertFile != "" || e.Endpoint.KeyFile != "") && e.Endpoint.SNI == "" {
|
||||
warning := fmt.Sprintf("TLS is configured but SNI is not set for the endpoint. Enabling SNI is strongly recommended when using TLS.")
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||
var authzContext acl.AuthorizerContext
|
||||
e.FillAuthzContext(&authzContext)
|
||||
|
@ -253,6 +314,30 @@ func (c *UpstreamConfiguration) Clone() *UpstreamConfiguration {
|
|||
return &c2
|
||||
}
|
||||
|
||||
// EndpointConfig represents a virtual service, i.e. one that is external to Consul
|
||||
type EndpointConfig struct {
|
||||
// Address of the endpoint; hostname, IP, or CIDR
|
||||
Address string `json:",omitempty"`
|
||||
|
||||
// Port allowed within this endpoint
|
||||
Port int `json:",omitempty"`
|
||||
|
||||
// CAFile is the optional path to a CA certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CAFile string `json:",omitempty" alias:"ca_file"`
|
||||
|
||||
// CertFile is the optional path to a client certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CertFile string `json:",omitempty" alias:"cert_file"`
|
||||
|
||||
// KeyFile is the optional path to a private key to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
KeyFile string `json:",omitempty" alias:"key_file"`
|
||||
|
||||
// SNI is the optional name to specify during the TLS handshake with a linked service.
|
||||
SNI string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ProxyConfigEntry is the top-level struct for global proxy configuration defaults.
|
||||
type ProxyConfigEntry struct {
|
||||
Kind string
|
||||
|
|
|
@ -427,6 +427,48 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-defaults-with-endpoint",
|
||||
snake: `
|
||||
kind = "service-defaults"
|
||||
name = "external"
|
||||
protocol = "tcp"
|
||||
endpoint {
|
||||
address = "1.2.3.4/24"
|
||||
port = 8080
|
||||
ca_file = "ca.pem"
|
||||
cert_file = "cert.pem"
|
||||
key_file = "key.pem"
|
||||
sni = "external.com"
|
||||
}
|
||||
`,
|
||||
camel: `
|
||||
Kind = "service-defaults"
|
||||
Name = "external"
|
||||
Protocol = "tcp"
|
||||
Endpoint {
|
||||
Address = "1.2.3.4/24"
|
||||
Port = 8080
|
||||
CAFile = "ca.pem"
|
||||
CertFile = "cert.pem"
|
||||
KeyFile = "key.pem"
|
||||
SNI = "external.com"
|
||||
}
|
||||
`,
|
||||
expect: &ServiceConfigEntry{
|
||||
Kind: "service-defaults",
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "1.2.3.4/24",
|
||||
Port: 8080,
|
||||
CAFile: "ca.pem",
|
||||
CertFile: "cert.pem",
|
||||
KeyFile: "key.pem",
|
||||
SNI: "external.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-router: kitchen sink",
|
||||
snake: `
|
||||
|
@ -2391,6 +2433,195 @@ func TestServiceConfigEntry(t *testing.T) {
|
|||
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
||||
},
|
||||
},
|
||||
"validate: missing endpoint address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
validateErr: "Could not validate address",
|
||||
},
|
||||
"validate: endpoint ipv4 address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "1.2.3.4",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: endpoint ipv4 CIDR address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "10.0.0.1/16",
|
||||
Port: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: endpoint ipv6 address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:0db8:0000:8a2e:0370:7334:1234:5678",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
"valid endpoint shortened ipv6 address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: endpoint ipv6 CIDR address": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: invalid endpoint port": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
},
|
||||
},
|
||||
validateErr: "Invalid Port number",
|
||||
},
|
||||
"validate: not all TLS options provided-1": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
Port: 443,
|
||||
CertFile: "client.crt",
|
||||
},
|
||||
},
|
||||
validateErr: "must have a CertFile, CAFile, and KeyFile",
|
||||
},
|
||||
"validate: not all TLS options provided-2": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
Port: 443,
|
||||
KeyFile: "tls.key",
|
||||
},
|
||||
},
|
||||
validateErr: "must have a CertFile, CAFile, and KeyFile",
|
||||
},
|
||||
"validate: all TLS options provided": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
Port: 443,
|
||||
CAFile: "ca.crt",
|
||||
CertFile: "client.crt",
|
||||
KeyFile: "tls.key",
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: only providing ca file is allowed": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "2001:db8::8a2e:370:7334/64",
|
||||
Port: 443,
|
||||
CAFile: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: wildcard is allowed for hostname": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "*.external.com",
|
||||
Port: 443,
|
||||
CAFile: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: hostname": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "api.external.com",
|
||||
Port: 443,
|
||||
CAFile: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"validate: invalid hostname 1": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "*external.com",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
validateErr: "Could not validate address",
|
||||
},
|
||||
"validate: invalid hostname 2": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "tcp",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "..hello.",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
validateErr: "Could not validate address",
|
||||
},
|
||||
"validate: all web traffic allowed": {
|
||||
entry: &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "external",
|
||||
Protocol: "http",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "*",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testConfigEntryNormalizeAndValidate(t, cases)
|
||||
}
|
||||
|
|
|
@ -179,6 +179,30 @@ type UpstreamConfig struct {
|
|||
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway" `
|
||||
}
|
||||
|
||||
// EndpointConfig represents a virtual service, i.e. one that is external to Consul
|
||||
type EndpointConfig struct {
|
||||
// Address of the endpoint; hostname, IP, or CIDR
|
||||
Address string `json:",omitempty"`
|
||||
|
||||
// Port allowed within this endpoint
|
||||
Port int `json:",omitempty"`
|
||||
|
||||
// CAFile is the optional path to a CA certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CAFile string `json:",omitempty" alias:"ca_file"`
|
||||
|
||||
// CertFile is the optional path to a client certificate to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
CertFile string `json:",omitempty" alias:"cert_file"`
|
||||
|
||||
// KeyFile is the optional path to a private key to use for TLS connections
|
||||
// from the gateway to the linked service
|
||||
KeyFile string `json:",omitempty" alias:"key_file"`
|
||||
|
||||
// SNI is the optional name to specify during the TLS handshake with a linked service.
|
||||
SNI string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type PassiveHealthCheck struct {
|
||||
// Interval between health check analysis sweeps. Each sweep may remove
|
||||
// hosts or return hosts to the pool.
|
||||
|
@ -220,10 +244,10 @@ type ServiceConfigEntry struct {
|
|||
Expose ExposeConfig `json:",omitempty"`
|
||||
ExternalSNI string `json:",omitempty" alias:"external_sni"`
|
||||
UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"`
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
Endpoint *EndpointConfig `json:",omitempty"`
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetKind() string { return s.Kind }
|
||||
|
|
|
@ -106,10 +106,16 @@ func TestAPI_ConfigEntries(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
endpoint := &EndpointConfig{
|
||||
Address: "my.example.com",
|
||||
Port: 80,
|
||||
}
|
||||
|
||||
service2 := &ServiceConfigEntry{
|
||||
Kind: ServiceDefaults,
|
||||
Name: "bar",
|
||||
Protocol: "tcp",
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
|
||||
// set it
|
||||
|
@ -185,6 +191,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
|
|||
require.Equal(t, service2.Kind, readService.Kind)
|
||||
require.Equal(t, service2.Name, readService.Name)
|
||||
require.Equal(t, service2.Protocol, readService.Protocol)
|
||||
require.Equal(t, endpoint, readService.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -516,6 +523,37 @@ func TestDecodeConfigEntry(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-defaults-endpoint",
|
||||
body: `
|
||||
{
|
||||
"Kind": "service-defaults",
|
||||
"Name": "external",
|
||||
"Protocol": "http",
|
||||
"Endpoint": {
|
||||
"Address": "1.2.3.4/24",
|
||||
"Port": 443,
|
||||
"CAFile": "ca.pem",
|
||||
"CertFile": "crt.pem",
|
||||
"KeyFile": "key.pem",
|
||||
"SNI": "external.com"
|
||||
}
|
||||
}
|
||||
`,
|
||||
expect: &ServiceConfigEntry{
|
||||
Kind: "service-defaults",
|
||||
Name: "external",
|
||||
Protocol: "http",
|
||||
Endpoint: &EndpointConfig{
|
||||
Address: "1.2.3.4/24",
|
||||
Port: 443,
|
||||
CAFile: "ca.pem",
|
||||
CertFile: "crt.pem",
|
||||
KeyFile: "key.pem",
|
||||
SNI: "external.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-router: kitchen sink",
|
||||
body: `
|
||||
|
|
Loading…
Reference in New Issue