Merge pull request #11566 from hashicorp/ap/ingress

OSS Backport: Allow ingress gateways to target other partitions
This commit is contained in:
Freddy 2021-11-12 15:17:08 -07:00 committed by GitHub
commit f4cbde4086
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 218 additions and 106 deletions

3
.changelog/11566.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: **(Enterprise only)** Allow ingress gateways to target services in another partition
```

View File

@ -155,7 +155,7 @@ func makeUpstream(g *structs.GatewayService) structs.Upstream {
upstream := structs.Upstream{ upstream := structs.Upstream{
DestinationName: g.Service.Name, DestinationName: g.Service.Name,
DestinationNamespace: g.Service.NamespaceOrDefault(), DestinationNamespace: g.Service.NamespaceOrDefault(),
DestinationPartition: g.Gateway.PartitionOrDefault(), DestinationPartition: g.Service.PartitionOrDefault(),
LocalBindPort: g.Port, LocalBindPort: g.Port,
IngressHosts: g.Hosts, IngressHosts: g.Hosts,
// Pass the protocol that was configured on the ingress listener in order // Pass the protocol that was configured on the ingress listener in order
@ -232,6 +232,7 @@ func (s *handlerIngressGateway) generateIngressDNSSANs(snap *ConfigSnapshot) []s
} }
} }
// TODO(partitions): How should these be updated for partitions?
for ns := range namespaces { for ns := range namespaces {
// The default namespace is special cased in DNS resolution, so special // The default namespace is special cased in DNS resolution, so special
// case it here. // case it here.

View File

@ -266,10 +266,7 @@ func (e *IngressGatewayConfigEntry) Validate() error {
declaredHosts := make(map[string]bool) declaredHosts := make(map[string]bool)
serviceNames := make(map[ServiceID]struct{}) serviceNames := make(map[ServiceID]struct{})
for i, s := range listener.Services { for _, s := range listener.Services {
if err := validateInnerEnterpriseMeta(&s.EnterpriseMeta, &e.EnterpriseMeta); err != nil {
return fmt.Errorf("services[%d]: %w", i, err)
}
sn := NewServiceName(s.Name, &s.EnterpriseMeta) sn := NewServiceName(s.Name, &s.EnterpriseMeta)
if err := s.RequestHeaders.Validate(listener.Protocol); err != nil { if err := s.RequestHeaders.Validate(listener.Protocol); err != nil {
return fmt.Errorf("request headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port) return fmt.Errorf("request headers %s (service %q on listener on port %d)", err, sn.String(), listener.Port)

View File

@ -103,12 +103,14 @@ type IngressService struct {
// using a "tcp" listener. // using a "tcp" listener.
Hosts []string Hosts []string
// Referencing other partitions is not supported.
// Namespace is the namespace where the service is located. // Namespace is the namespace where the service is located.
// Namespacing is a Consul Enterprise feature. // Namespacing is a Consul Enterprise feature.
Namespace string `json:",omitempty"` Namespace string `json:",omitempty"`
// Partition is the partition where the service is located.
// Partitioning is a Consul Enterprise feature.
Partition string `json:",omitempty"`
// TLS allows specifying some TLS configuration per listener. // TLS allows specifying some TLS configuration per listener.
TLS *GatewayServiceTLSConfig `json:",omitempty"` TLS *GatewayServiceTLSConfig `json:",omitempty"`

View File

@ -157,8 +157,9 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
require.Len(t, readIngress.Listeners, 1) require.Len(t, readIngress.Listeners, 1)
require.Len(t, readIngress.Listeners[0].Services, 1) require.Len(t, readIngress.Listeners[0].Services, 1)
// Set namespace to blank so that OSS and ent can utilize the same tests // Set namespace and partition to blank so that OSS and ent can utilize the same tests
readIngress.Listeners[0].Services[0].Namespace = "" readIngress.Listeners[0].Services[0].Namespace = ""
readIngress.Listeners[0].Services[0].Partition = ""
require.Equal(t, ingress1.Listeners, readIngress.Listeners) require.Equal(t, ingress1.Listeners, readIngress.Listeners)
case "bar": case "bar":
@ -168,8 +169,9 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
require.Equal(t, ingress2.Name, readIngress.Name) require.Equal(t, ingress2.Name, readIngress.Name)
require.Len(t, readIngress.Listeners, 1) require.Len(t, readIngress.Listeners, 1)
require.Len(t, readIngress.Listeners[0].Services, 1) require.Len(t, readIngress.Listeners[0].Services, 1)
// Set namespace to blank so that OSS and ent can utilize the same tests // Set namespace and partition to blank so that OSS and ent can utilize the same tests
readIngress.Listeners[0].Services[0].Namespace = "" readIngress.Listeners[0].Services[0].Namespace = ""
readIngress.Listeners[0].Services[0].Partition = ""
require.Equal(t, ingress2.Listeners, readIngress.Listeners) require.Equal(t, ingress2.Listeners, readIngress.Listeners)
} }

View File

@ -964,7 +964,8 @@ func TestDecodeConfigEntry(t *testing.T) {
"Services": [ "Services": [
{ {
"Name": "web", "Name": "web",
"Namespace": "foo" "Namespace": "foo",
"Partition": "bar"
}, },
{ {
"Name": "db" "Name": "db"
@ -1001,6 +1002,7 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
Name: "web", Name: "web",
Namespace: "foo", Namespace: "foo",
Partition: "bar",
}, },
{ {
Name: "db", Name: "db",

View File

@ -30,6 +30,11 @@ type Intention struct {
SourceNS, SourceName string SourceNS, SourceName string
DestinationNS, DestinationName string DestinationNS, DestinationName string
// SourcePartition and DestinationPartition cannot be wildcards "*" and
// are not compatible with legacy intentions.
SourcePartition string
DestinationPartition string
// SourceType is the type of the value for the source. // SourceType is the type of the value for the source.
SourceType IntentionSourceType SourceType IntentionSourceType
@ -363,8 +368,8 @@ func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *
func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
r := c.c.newRequest("PUT", "/v1/connect/intentions/exact") r := c.c.newRequest("PUT", "/v1/connect/intentions/exact")
r.setWriteOptions(q) r.setWriteOptions(q)
r.params.Set("source", maybePrefixNamespace(ixn.SourceNS, ixn.SourceName)) r.params.Set("source", maybePrefixNamespaceAndPartition(ixn.SourcePartition, ixn.SourceNS, ixn.SourceName))
r.params.Set("destination", maybePrefixNamespace(ixn.DestinationNS, ixn.DestinationName)) r.params.Set("destination", maybePrefixNamespaceAndPartition(ixn.DestinationPartition, ixn.DestinationNS, ixn.DestinationName))
r.obj = ixn r.obj = ixn
rtt, resp, err := c.c.doRequest(r) rtt, resp, err := c.c.doRequest(r)
if err != nil { if err != nil {
@ -380,11 +385,17 @@ func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta,
return wm, nil return wm, nil
} }
func maybePrefixNamespace(ns, name string) string { func maybePrefixNamespaceAndPartition(part, ns, name string) string {
if ns == "" { switch {
case part == "" && ns == "":
return name return name
case part == "" && ns != "":
return ns + "/" + name
case part != "" && ns == "":
return part + "/" + IntentionDefaultNamespace + "/" + name
default:
return part + "/" + ns + "/" + name
} }
return ns + "/" + name
} }
// IntentionCreate will create a new intention. The ID in the given // IntentionCreate will create a new intention. The ID in the given

View File

@ -33,6 +33,8 @@ func TestAPI_ConnectIntentionCreateListGetUpdateDelete(t *testing.T) {
ixn.UpdatedAt = actual.UpdatedAt ixn.UpdatedAt = actual.UpdatedAt
ixn.CreateIndex = actual.CreateIndex ixn.CreateIndex = actual.CreateIndex
ixn.ModifyIndex = actual.ModifyIndex ixn.ModifyIndex = actual.ModifyIndex
ixn.SourcePartition = actual.SourcePartition
ixn.DestinationPartition = actual.DestinationPartition
ixn.Hash = actual.Hash ixn.Hash = actual.Hash
require.Equal(t, ixn, actual) require.Equal(t, ixn, actual)

View File

@ -26,6 +26,9 @@ type AdminPartition struct {
ModifyIndex uint64 `json:"ModifyIndex,omitempty"` ModifyIndex uint64 `json:"ModifyIndex,omitempty"`
} }
// PartitionDefaultName is the default partition value.
const PartitionDefaultName = "default"
type AdminPartitions struct { type AdminPartitions struct {
Partitions []*AdminPartition Partitions []*AdminPartition
} }

View File

@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/intention" "github.com/hashicorp/consul/command/intention"
"github.com/mitchellh/cli"
) )
func New(ui cli.Ui) *cmd { func New(ui cli.Ui) *cmd {
@ -36,12 +36,12 @@ type cmd struct {
func (c *cmd) init() { func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.ingressGateway, "ingress-gateway", "", c.flags.StringVar(&c.ingressGateway, "ingress-gateway", "",
"(Required) The name of the ingress gateway service to use. A namespace "+ "(Required) The name of the ingress gateway service to use. Namespace and partition "+
"can optionally be specified as a prefix via the 'namespace/service' format.") "can optionally be specified as a prefix via the 'partition/namespace/service' format.")
c.flags.StringVar(&c.service, "service", "", c.flags.StringVar(&c.service, "service", "",
"(Required) The name of destination service to expose. A namespace "+ "(Required) The name of destination service to expose. Namespace and partition "+
"can optionally be specified as a prefix via the 'namespace/service' format.") "can optionally be specified as a prefix via the 'partition/namespace/service' format.")
c.flags.IntVar(&c.port, "port", 0, c.flags.IntVar(&c.port, "port", 0,
"(Required) The listener port to use for the service on the Ingress gateway.") "(Required) The listener port to use for the service on the Ingress gateway.")
@ -79,7 +79,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error("A service name must be given via the -service flag.") c.UI.Error("A service name must be given via the -service flag.")
return 1 return 1
} }
svc, svcNamespace, err := intention.ParseIntentionTarget(c.service) svc, svcNS, svcPart, err := intention.ParseIntentionTarget(c.service)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Invalid service name: %s", err)) c.UI.Error(fmt.Sprintf("Invalid service name: %s", err))
return 1 return 1
@ -89,7 +89,7 @@ func (c *cmd) Run(args []string) int {
c.UI.Error("An ingress gateway service must be given via the -ingress-gateway flag.") c.UI.Error("An ingress gateway service must be given via the -ingress-gateway flag.")
return 1 return 1
} }
gateway, gatewayNamespace, err := intention.ParseIntentionTarget(c.ingressGateway) gateway, gatewayNS, gatewayPart, err := intention.ParseIntentionTarget(c.ingressGateway)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Invalid ingress gateway name: %s", err)) c.UI.Error(fmt.Sprintf("Invalid ingress gateway name: %s", err))
return 1 return 1
@ -102,7 +102,9 @@ func (c *cmd) Run(args []string) int {
// First get the config entry for the ingress gateway, if it exists. Don't error if it's a 404 as that // First get the config entry for the ingress gateway, if it exists. Don't error if it's a 404 as that
// just means we'll need to create a new config entry. // just means we'll need to create a new config entry.
conf, _, err := client.ConfigEntries().Get(api.IngressGateway, gateway, nil) conf, _, err := client.ConfigEntries().Get(
api.IngressGateway, gateway, &api.QueryOptions{Partition: gatewayPart, Namespace: gatewayNS},
)
if err != nil && !strings.Contains(err.Error(), agent.ConfigEntryNotFoundErr) { if err != nil && !strings.Contains(err.Error(), agent.ConfigEntryNotFoundErr) {
c.UI.Error(fmt.Sprintf("Error fetching existing ingress gateway configuration: %s", err)) c.UI.Error(fmt.Sprintf("Error fetching existing ingress gateway configuration: %s", err))
return 1 return 1
@ -111,7 +113,8 @@ func (c *cmd) Run(args []string) int {
conf = &api.IngressGatewayConfigEntry{ conf = &api.IngressGatewayConfigEntry{
Kind: api.IngressGateway, Kind: api.IngressGateway,
Name: gateway, Name: gateway,
Namespace: gatewayNamespace, Namespace: gatewayNS,
Partition: gatewayPart,
} }
} }
@ -127,7 +130,8 @@ func (c *cmd) Run(args []string) int {
serviceIdx := -1 serviceIdx := -1
newService := api.IngressService{ newService := api.IngressService{
Name: svc, Name: svc,
Namespace: svcNamespace, Namespace: svcNS,
Partition: svcPart,
Hosts: c.hosts, Hosts: c.hosts,
} }
for i, listener := range ingressConf.Listeners { for i, listener := range ingressConf.Listeners {
@ -145,7 +149,7 @@ func (c *cmd) Run(args []string) int {
// Make sure the service isn't already exposed in this gateway // Make sure the service isn't already exposed in this gateway
for j, service := range listener.Services { for j, service := range listener.Services {
if service.Name == svc && namespaceMatch(service.Namespace, svcNamespace) { if service.Name == svc && entMetaMatch(service.Namespace, service.Partition, svcNS, svcPart) {
serviceIdx = j serviceIdx = j
c.UI.Output(fmt.Sprintf("Updating service definition for %q on listener with port %d", c.service, listener.Port)) c.UI.Output(fmt.Sprintf("Updating service definition for %q on listener with port %d", c.service, listener.Port))
break break
@ -170,7 +174,7 @@ func (c *cmd) Run(args []string) int {
// Write the updated config entry using a check-and-set, so it fails if the entry // Write the updated config entry using a check-and-set, so it fails if the entry
// has been changed since we looked it up. // has been changed since we looked it up.
succeeded, _, err := client.ConfigEntries().CAS(ingressConf, ingressConf.GetModifyIndex(), nil) succeeded, _, err := client.ConfigEntries().CAS(ingressConf, ingressConf.GetModifyIndex(), &api.WriteOptions{Partition: gatewayPart, Namespace: gatewayNS})
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error writing ingress config entry: %v", err)) c.UI.Error(fmt.Sprintf("Error writing ingress config entry: %v", err))
return 1 return 1
@ -194,12 +198,14 @@ func (c *cmd) Run(args []string) int {
// Add the intention between the gateway service and the destination. // Add the intention between the gateway service and the destination.
ixn := &api.Intention{ ixn := &api.Intention{
SourceName: gateway, SourceName: gateway,
SourceNS: gatewayNamespace, SourceNS: gatewayNS,
DestinationName: svc, SourcePartition: gatewayPart,
DestinationNS: svcNamespace, DestinationName: svc,
SourceType: api.IntentionSourceConsul, DestinationNS: svcNS,
Action: api.IntentionActionAllow, DestinationPartition: svcPart,
SourceType: api.IntentionSourceConsul,
Action: api.IntentionActionAllow,
} }
if _, err = client.Connect().IntentionUpsert(ixn, nil); err != nil { if _, err = client.Connect().IntentionUpsert(ixn, nil); err != nil {
c.UI.Error(fmt.Sprintf("Error upserting intention: %s", err)) c.UI.Error(fmt.Sprintf("Error upserting intention: %s", err))
@ -210,17 +216,21 @@ func (c *cmd) Run(args []string) int {
return 0 return 0
} }
func namespaceMatch(a, b string) bool { func entMetaMatch(nsA, partitionA, nsB, partitionB string) bool {
namespaceA := a if nsA == "" {
namespaceB := b nsA = api.IntentionDefaultNamespace
if namespaceA == "" {
namespaceA = structs.IntentionDefaultNamespace
} }
if namespaceB == "" { if partitionA == "" {
namespaceB = structs.IntentionDefaultNamespace partitionA = api.PartitionDefaultName
}
if nsB == "" {
nsB = api.IntentionDefaultNamespace
}
if partitionB == "" {
partitionB = api.PartitionDefaultName
} }
return namespaceA == namespaceB return strings.EqualFold(partitionA, partitionB) && strings.EqualFold(nsA, nsB)
} }
func (c *cmd) Synopsis() string { func (c *cmd) Synopsis() string {

View File

@ -43,6 +43,7 @@ func TestConnectExpose(t *testing.T) {
entry, _, err := client.ConfigEntries().Get(api.IngressGateway, "ingress", nil) entry, _, err := client.ConfigEntries().Get(api.IngressGateway, "ingress", nil)
require.NoError(err) require.NoError(err)
ns := entry.(*api.IngressGatewayConfigEntry).Namespace ns := entry.(*api.IngressGatewayConfigEntry).Namespace
ap := entry.(*api.IngressGatewayConfigEntry).Partition
expected := &api.IngressGatewayConfigEntry{ expected := &api.IngressGatewayConfigEntry{
Kind: api.IngressGateway, Kind: api.IngressGateway,
Name: "ingress", Name: "ingress",
@ -55,6 +56,7 @@ func TestConnectExpose(t *testing.T) {
{ {
Name: "foo", Name: "foo",
Namespace: ns, Namespace: ns,
Partition: ap,
}, },
}, },
}, },
@ -95,6 +97,7 @@ func TestConnectExpose(t *testing.T) {
{ {
Name: "foo", Name: "foo",
Namespace: ns, Namespace: ns,
Partition: ap,
}, },
}, },
}) })
@ -283,6 +286,7 @@ func TestConnectExpose_existingConfig(t *testing.T) {
ingressConf.Namespace = entryConf.Namespace ingressConf.Namespace = entryConf.Namespace
for i, listener := range ingressConf.Listeners { for i, listener := range ingressConf.Listeners {
listener.Services[0].Namespace = entryConf.Listeners[i].Services[0].Namespace listener.Services[0].Namespace = entryConf.Listeners[i].Services[0].Namespace
listener.Services[0].Partition = entryConf.Listeners[i].Services[0].Partition
} }
ingressConf.CreateIndex = entry.GetCreateIndex() ingressConf.CreateIndex = entry.GetCreateIndex()
ingressConf.ModifyIndex = entry.GetModifyIndex() ingressConf.ModifyIndex = entry.GetModifyIndex()
@ -317,6 +321,7 @@ func TestConnectExpose_existingConfig(t *testing.T) {
ingressConf.Listeners[1].Services = append(ingressConf.Listeners[1].Services, api.IngressService{ ingressConf.Listeners[1].Services = append(ingressConf.Listeners[1].Services, api.IngressService{
Name: "zoo", Name: "zoo",
Namespace: entryConf.Listeners[1].Services[1].Namespace, Namespace: entryConf.Listeners[1].Services[1].Namespace,
Partition: entryConf.Listeners[1].Services[1].Partition,
Hosts: []string{"foo.com", "foo.net"}, Hosts: []string{"foo.com", "foo.net"},
}) })
ingressConf.CreateIndex = entry.GetCreateIndex() ingressConf.CreateIndex = entry.GetCreateIndex()

View File

@ -153,24 +153,26 @@ func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) {
return nil, fmt.Errorf("Must specify two arguments: source and destination") return nil, fmt.Errorf("Must specify two arguments: source and destination")
} }
srcName, srcNamespace, err := intention.ParseIntentionTarget(args[0]) srcName, srcNS, srcPart, err := intention.ParseIntentionTarget(args[0])
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid intention source: %v", err) return nil, fmt.Errorf("Invalid intention source: %v", err)
} }
dstName, dstNamespace, err := intention.ParseIntentionTarget(args[1]) dstName, dstNS, dstPart, err := intention.ParseIntentionTarget(args[1])
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid intention destination: %v", err) return nil, fmt.Errorf("Invalid intention destination: %v", err)
} }
return []*api.Intention{{ return []*api.Intention{{
SourceNS: srcNamespace, SourcePartition: srcPart,
SourceName: srcName, SourceNS: srcNS,
DestinationNS: dstNamespace, SourceName: srcName,
DestinationName: dstName, DestinationPartition: dstPart,
SourceType: api.IntentionSourceConsul, DestinationNS: dstNS,
Action: c.ixnAction(), DestinationName: dstName,
Meta: c.flagMeta, SourceType: api.IntentionSourceConsul,
Action: c.ixnAction(),
Meta: c.flagMeta,
}}, nil }}, nil
} }

View File

@ -7,25 +7,28 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
) )
// ParseIntentionTarget parses a target of the form <namespace>/<name> and returns // ParseIntentionTarget parses a target of the form <partition>/<namespace>/<name> and returns
// the two distinct parts. In some cases the namespace may be elided and this function // the distinct parts. In some cases the partition and namespace may be elided and this function
// will return the empty string for the namespace then. // will return the empty string for them then.
func ParseIntentionTarget(input string) (name string, namespace string, err error) { // If two parts are present, it is assumed they are namespace/name and not partition/name.
// Get the index to the '/'. If it doesn't exist, we have just a name func ParseIntentionTarget(input string) (name string, ns string, partition string, err error) {
// so just set that and return. ss := strings.Split(input, "/")
idx := strings.IndexByte(input, '/') switch len(ss) {
if idx == -1 { case 1: // Name only
// let the agent do token based defaulting of the namespace name = ss[0]
return input, "", nil return
case 2: // namespace/name
ns = ss[0]
name = ss[1]
return
case 3: // partition/namespace/name
partition = ss[0]
ns = ss[1]
name = ss[2]
return
default:
return "", "", "", fmt.Errorf("input can contain at most two '/'")
} }
namespace = input[:idx]
name = input[idx+1:]
if strings.IndexByte(name, '/') != -1 {
return "", "", fmt.Errorf("target can contain at most one '/'")
}
return name, namespace, nil
} }
func GetFromArgs(client *api.Client, args []string) (*api.Intention, error) { func GetFromArgs(client *api.Client, args []string) (*api.Intention, error) {

View File

@ -2,4 +2,5 @@
export DEFAULT_REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy" export DEFAULT_REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy"
export REQUIRED_SERVICES="${DEFAULT_REQUIRED_SERVICES}" export REQUIRED_SERVICES="${DEFAULT_REQUIRED_SERVICES}"
export REQUIRE_SECONDARY=0 export REQUIRE_SECONDARY=0
export REQUIRE_PARTITIONS=0

View File

@ -115,14 +115,19 @@ function assert_proxy_presents_cert_uri {
local SERVICENAME=$2 local SERVICENAME=$2
local DC=${3:-primary} local DC=${3:-primary}
local NS=${4:-default} local NS=${4:-default}
local PARTITION=${5:default}
CERT=$(retry_default get_cert $HOSTPORT) CERT=$(retry_default get_cert $HOSTPORT)
echo "WANT SERVICE: ${NS}/${SERVICENAME}" echo "WANT SERVICE: ${PARTITION}/${NS}/${SERVICENAME}"
echo "GOT CERT:" echo "GOT CERT:"
echo "$CERT" echo "$CERT"
echo "$CERT" | grep -Eo "URI:spiffe://([a-zA-Z0-9-]+).consul/ns/${NS}/dc/${DC}/svc/$SERVICENAME" if [[ -z $PARTITION ]] || [[ $PARTITION = "default" ]]; then
echo "$CERT" | grep -Eo "URI:spiffe://([a-zA-Z0-9-]+).consul/ns/${NS}/dc/${DC}/svc/$SERVICENAME"
else
echo "$CERT" | grep -Eo "URI:spiffe://([a-zA-Z0-9-]+).consul/ap/${PARTITION}/ns/${NS}/dc/${DC}/svc/$SERVICENAME"
fi
} }
function assert_dnssan_in_cert { function assert_dnssan_in_cert {

View File

@ -40,39 +40,39 @@ function network_snippet {
} }
function init_workdir { function init_workdir {
local DC="$1" local CLUSTER="$1"
if test -z "$DC" if test -z "$CLUSTER"
then then
DC=primary CLUSTER=primary
fi fi
# Note, we use explicit set of dirs so we don't delete .gitignore. Also, # Note, we use explicit set of dirs so we don't delete .gitignore. Also,
# don't wipe logs between runs as they are already split and we need them to # don't wipe logs between runs as they are already split and we need them to
# upload as artifacts later. # upload as artifacts later.
rm -rf workdir/${DC} rm -rf workdir/${CLUSTER}
mkdir -p workdir/${DC}/{consul,register,envoy,bats,statsd,data} mkdir -p workdir/${CLUSTER}/{consul,register,envoy,bats,statsd,data}
# Reload consul config from defaults # Reload consul config from defaults
cp consul-base-cfg/*.hcl workdir/${DC}/consul/ cp consul-base-cfg/*.hcl workdir/${CLUSTER}/consul/
# Add any overrides if there are any (no op if not) # Add any overrides if there are any (no op if not)
find ${CASE_DIR} -maxdepth 1 -name '*.hcl' -type f -exec cp -f {} workdir/${DC}/consul \; find ${CASE_DIR} -maxdepth 1 -name '*.hcl' -type f -exec cp -f {} workdir/${CLUSTER}/consul \;
# Copy all the test files # Copy all the test files
find ${CASE_DIR} -maxdepth 1 -name '*.bats' -type f -exec cp -f {} workdir/${DC}/bats \; find ${CASE_DIR} -maxdepth 1 -name '*.bats' -type f -exec cp -f {} workdir/${CLUSTER}/bats \;
# Copy DC specific bats # Copy CLUSTER specific bats
cp helpers.bash workdir/${DC}/bats cp helpers.bash workdir/${CLUSTER}/bats
# Add any DC overrides # Add any CLUSTER overrides
if test -d "${CASE_DIR}/${DC}" if test -d "${CASE_DIR}/${CLUSTER}"
then then
find ${CASE_DIR}/${DC} -type f -name '*.hcl' -exec cp -f {} workdir/${DC}/consul \; find ${CASE_DIR}/${CLUSTER} -type f -name '*.hcl' -exec cp -f {} workdir/${CLUSTER}/consul \;
find ${CASE_DIR}/${DC} -type f -name '*.bats' -exec cp -f {} workdir/${DC}/bats \; find ${CASE_DIR}/${CLUSTER} -type f -name '*.bats' -exec cp -f {} workdir/${CLUSTER}/bats \;
fi fi
# move all of the registration files OUT of the consul config dir now # move all of the registration files OUT of the consul config dir now
find workdir/${DC}/consul -type f -name 'service_*.hcl' -exec mv -f {} workdir/${DC}/register \; find workdir/${CLUSTER}/consul -type f -name 'service_*.hcl' -exec mv -f {} workdir/${CLUSTER}/register \;
# copy the ca-certs for SDS so we can verify the right ones are served # copy the ca-certs for SDS so we can verify the right ones are served
mkdir -p workdir/test-sds-server/certs mkdir -p workdir/test-sds-server/certs
@ -80,7 +80,7 @@ function init_workdir {
if test -d "${CASE_DIR}/data" if test -d "${CASE_DIR}/data"
then then
cp -r ${CASE_DIR}/data/* workdir/${DC}/data cp -r ${CASE_DIR}/data/* workdir/${CLUSTER}/data
fi fi
return 0 return 0
@ -157,13 +157,48 @@ function start_consul {
-client "0.0.0.0" >/dev/null -client "0.0.0.0" >/dev/null
} }
function start_partitioned_client {
local PARTITION=${1:-ap1}
# Start consul now as setup script needs it up
docker_kill_rm consul-${PARTITION}
license="${CONSUL_LICENSE:-}"
# load the consul license so we can pass it into the consul
# containers as an env var in the case that this is a consul
# enterprise test
if test -z "$license" -a -n "${CONSUL_LICENSE_PATH:-}"
then
license=$(cat $CONSUL_LICENSE_PATH)
fi
sh -c "rm -rf /workdir/${PARTITION}/data"
# Run consul and expose some ports to the host to make debugging locally a
# bit easier.
#
docker run -d --name envoy_consul-${PARTITION}_1 \
--net=envoy-tests \
$WORKDIR_SNIPPET \
--hostname "consul-${PARTITION}" \
--network-alias "consul-${PARTITION}" \
-e "CONSUL_LICENSE=$license" \
consul-dev agent \
-datacenter "primary" \
-retry-join "consul-primary" \
-grpc-port 8502 \
-data-dir "/tmp/consul" \
-config-dir "/workdir/${PARTITION}/consul" \
-client "0.0.0.0" >/dev/null
}
function pre_service_setup { function pre_service_setup {
local DC=${1:-primary} local CLUSTER=${1:-primary}
# Run test case setup (e.g. generating Envoy bootstrap, starting containers) # Run test case setup (e.g. generating Envoy bootstrap, starting containers)
if [ -f "${CASE_DIR}/${DC}/setup.sh" ] if [ -f "${CASE_DIR}/${CLUSTER}/setup.sh" ]
then then
source ${CASE_DIR}/${DC}/setup.sh source ${CASE_DIR}/${CLUSTER}/setup.sh
else else
source ${CASE_DIR}/setup.sh source ${CASE_DIR}/setup.sh
fi fi
@ -184,29 +219,29 @@ function start_services {
} }
function verify { function verify {
local DC=$1 local CLUSTER="$1"
if test -z "$DC"; then if test -z "$CLUSTER"; then
DC=primary CLUSTER="primary"
fi fi
# Execute tests # Execute tests
res=0 res=0
# Nuke any previous case's verify container. # Nuke any previous case's verify container.
docker_kill_rm verify-${DC} docker_kill_rm verify-${CLUSTER}
echo "Running ${DC} verification step for ${CASE_DIR}..." echo "Running ${CLUSTER} verification step for ${CASE_DIR}..."
# need to tell the PID 1 inside of the container that it won't be actual PID # need to tell the PID 1 inside of the container that it won't be actual PID
# 1 because we're using --pid=host so we use TINI_SUBREAPER # 1 because we're using --pid=host so we use TINI_SUBREAPER
if docker run --name envoy_verify-${DC}_1 -t \ if docker run --name envoy_verify-${CLUSTER}_1 -t \
-e TINI_SUBREAPER=1 \ -e TINI_SUBREAPER=1 \
-e ENVOY_VERSION \ -e ENVOY_VERSION \
$WORKDIR_SNIPPET \ $WORKDIR_SNIPPET \
--pid=host \ --pid=host \
$(network_snippet $DC) \ $(network_snippet $CLUSTER) \
bats-verify \ bats-verify \
--pretty /workdir/${DC}/bats ; then --pretty /workdir/${CLUSTER}/bats ; then
echogreen "✓ PASS" echogreen "✓ PASS"
else else
echored " FAIL" echored " FAIL"
@ -228,6 +263,11 @@ function capture_logs {
then then
services="$services consul-secondary" services="$services consul-secondary"
fi fi
if is_set $REQUIRE_PARTITIONS
then
services="$services consul-ap1"
fi
if [ -f "${CASE_DIR}/capture.sh" ] if [ -f "${CASE_DIR}/capture.sh" ]
then then
@ -247,7 +287,7 @@ function stop_services {
# Teardown # Teardown
docker_kill_rm $REQUIRED_SERVICES docker_kill_rm $REQUIRED_SERVICES
docker_kill_rm consul-primary consul-secondary docker_kill_rm consul-primary consul-secondary consul-ap1
} }
function init_vars { function init_vars {
@ -286,6 +326,10 @@ function run_tests {
then then
init_workdir secondary init_workdir secondary
fi fi
if is_set $REQUIRE_PARTITIONS
then
init_workdir ap1
fi
global_setup global_setup
@ -307,6 +351,10 @@ function run_tests {
if is_set $REQUIRE_SECONDARY; then if is_set $REQUIRE_SECONDARY; then
start_consul secondary start_consul secondary
fi fi
if is_set $REQUIRE_PARTITIONS; then
docker_consul "primary" admin-partition create -name ap1 > /dev/null
start_partitioned_client ap1
fi
echo "Setting up the primary datacenter" echo "Setting up the primary datacenter"
pre_service_setup primary pre_service_setup primary
@ -315,14 +363,20 @@ function run_tests {
echo "Setting up the secondary datacenter" echo "Setting up the secondary datacenter"
pre_service_setup secondary pre_service_setup secondary
fi fi
if is_set $REQUIRE_PARTITIONS; then
echo "Setting up the non-default partition"
pre_service_setup ap1
fi
echo "Starting services" echo "Starting services"
start_services start_services
# Run the verify container and report on the output # Run the verify container and report on the output
echo "Verifying the primary datacenter"
verify primary verify primary
if is_set $REQUIRE_SECONDARY; then if is_set $REQUIRE_SECONDARY; then
echo "Verifying the secondary datacenter"
verify secondary verify secondary
fi fi
} }
@ -378,7 +432,7 @@ function suite_teardown {
docker_kill_rm $(grep "^function run_container_" $self_name | \ docker_kill_rm $(grep "^function run_container_" $self_name | \
sed 's/^function run_container_\(.*\) {/\1/g') sed 's/^function run_container_\(.*\) {/\1/g')
docker_kill_rm consul-primary consul-secondary docker_kill_rm consul-primary consul-secondary consul-ap1
if docker network inspect envoy-tests &>/dev/null ; then if docker network inspect envoy-tests &>/dev/null ; then
echo -n "Deleting network 'envoy-tests'..." echo -n "Deleting network 'envoy-tests'..."
@ -402,13 +456,13 @@ function run_container {
function common_run_container_service { function common_run_container_service {
local service="$1" local service="$1"
local DC="$2" local CLUSTER="$2"
local httpPort="$3" local httpPort="$3"
local grpcPort="$4" local grpcPort="$4"
docker run -d --name $(container_name_prev) \ docker run -d --name $(container_name_prev) \
-e "FORTIO_NAME=${service}" \ -e "FORTIO_NAME=${service}" \
$(network_snippet $DC) \ $(network_snippet $CLUSTER) \
"${HASHICORP_DOCKER_PROXY}/fortio/fortio" \ "${HASHICORP_DOCKER_PROXY}/fortio/fortio" \
server \ server \
-http-port ":$httpPort" \ -http-port ":$httpPort" \
@ -420,6 +474,10 @@ function run_container_s1 {
common_run_container_service s1 primary 8080 8079 common_run_container_service s1 primary 8080 8079
} }
function run_container_s1-ap1 {
common_run_container_service s1 ap1 8080 8079
}
function run_container_s2 { function run_container_s2 {
common_run_container_service s2 primary 8181 8179 common_run_container_service s2 primary 8181 8179
} }
@ -457,7 +515,7 @@ function run_container_s2-secondary {
function common_run_container_sidecar_proxy { function common_run_container_sidecar_proxy {
local service="$1" local service="$1"
local DC="$2" local CLUSTER="$2"
# Hot restart breaks since both envoys seem to interact with each other # Hot restart breaks since both envoys seem to interact with each other
# despite separate containers that don't share IPC namespace. Not quite # despite separate containers that don't share IPC namespace. Not quite
@ -465,10 +523,10 @@ function common_run_container_sidecar_proxy {
# location? # location?
docker run -d --name $(container_name_prev) \ docker run -d --name $(container_name_prev) \
$WORKDIR_SNIPPET \ $WORKDIR_SNIPPET \
$(network_snippet $DC) \ $(network_snippet $CLUSTER) \
"${HASHICORP_DOCKER_PROXY}/envoyproxy/envoy:v${ENVOY_VERSION}" \ "${HASHICORP_DOCKER_PROXY}/envoyproxy/envoy:v${ENVOY_VERSION}" \
envoy \ envoy \
-c /workdir/${DC}/envoy/${service}-bootstrap.json \ -c /workdir/${CLUSTER}/envoy/${service}-bootstrap.json \
-l debug \ -l debug \
--disable-hot-restart \ --disable-hot-restart \
--drain-time-s 1 >/dev/null --drain-time-s 1 >/dev/null
@ -477,6 +535,11 @@ function common_run_container_sidecar_proxy {
function run_container_s1-sidecar-proxy { function run_container_s1-sidecar-proxy {
common_run_container_sidecar_proxy s1 primary common_run_container_sidecar_proxy s1 primary
} }
function run_container_s1-ap1-sidecar-proxy {
common_run_container_sidecar_proxy s1 ap1
}
function run_container_s1-sidecar-proxy-consul-exec { function run_container_s1-sidecar-proxy-consul-exec {
docker run -d --name $(container_name) \ docker run -d --name $(container_name) \
$(network_snippet primary) \ $(network_snippet primary) \