feat: add endpoint struct to ServiceConfigEntry

This commit is contained in:
DanStough 2022-05-19 18:15:57 -04:00 committed by Dan Stough
parent 876f3bb971
commit 147fd96d97
4 changed files with 382 additions and 4 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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 }

View File

@ -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: `