mirror of https://github.com/status-im/consul.git
500 lines
13 KiB
Go
500 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClient(t)
|
|
defer s.Stop()
|
|
|
|
config_entries := c.ConfigEntries()
|
|
|
|
verifyResolver := func(t *testing.T, initial ConfigEntry) {
|
|
t.Helper()
|
|
require.IsType(t, &ServiceResolverConfigEntry{}, initial)
|
|
testEntry := initial.(*ServiceResolverConfigEntry)
|
|
|
|
// set it
|
|
_, wm, err := config_entries.Set(testEntry, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, wm)
|
|
require.NotEqual(t, 0, wm.RequestTime)
|
|
|
|
// get it
|
|
entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qm)
|
|
require.NotEqual(t, 0, qm.RequestTime)
|
|
|
|
// generic verification
|
|
require.Equal(t, testEntry.Meta, entry.GetMeta())
|
|
|
|
// verify it
|
|
readResolver, ok := entry.(*ServiceResolverConfigEntry)
|
|
require.True(t, ok)
|
|
readResolver.ModifyIndex = 0 // reset for Equals()
|
|
readResolver.CreateIndex = 0 // reset for Equals()
|
|
|
|
require.Equal(t, testEntry, readResolver)
|
|
|
|
// TODO(rb): cas?
|
|
// TODO(rb): list?
|
|
}
|
|
|
|
verifySplitter := func(t *testing.T, initial ConfigEntry) {
|
|
t.Helper()
|
|
require.IsType(t, &ServiceSplitterConfigEntry{}, initial)
|
|
testEntry := initial.(*ServiceSplitterConfigEntry)
|
|
|
|
// set it
|
|
_, wm, err := config_entries.Set(testEntry, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, wm)
|
|
require.NotEqual(t, 0, wm.RequestTime)
|
|
|
|
// get it
|
|
entry, qm, err := config_entries.Get(ServiceSplitter, testEntry.Name, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qm)
|
|
require.NotEqual(t, 0, qm.RequestTime)
|
|
|
|
// generic verification
|
|
require.Equal(t, testEntry.Meta, entry.GetMeta())
|
|
|
|
// verify it
|
|
readSplitter, ok := entry.(*ServiceSplitterConfigEntry)
|
|
require.True(t, ok)
|
|
readSplitter.ModifyIndex = 0 // reset for Equals()
|
|
readSplitter.CreateIndex = 0 // reset for Equals()
|
|
|
|
require.Equal(t, testEntry, readSplitter)
|
|
|
|
// TODO(rb): cas?
|
|
// TODO(rb): list?
|
|
}
|
|
|
|
verifyRouter := func(t *testing.T, initial ConfigEntry) {
|
|
t.Helper()
|
|
require.IsType(t, &ServiceRouterConfigEntry{}, initial)
|
|
testEntry := initial.(*ServiceRouterConfigEntry)
|
|
|
|
// set it
|
|
_, wm, err := config_entries.Set(testEntry, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, wm)
|
|
require.NotEqual(t, 0, wm.RequestTime)
|
|
|
|
// get it
|
|
entry, qm, err := config_entries.Get(ServiceRouter, testEntry.Name, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qm)
|
|
require.NotEqual(t, 0, qm.RequestTime)
|
|
|
|
// generic verification
|
|
require.Equal(t, testEntry.Meta, entry.GetMeta())
|
|
|
|
// verify it
|
|
readRouter, ok := entry.(*ServiceRouterConfigEntry)
|
|
require.True(t, ok)
|
|
readRouter.ModifyIndex = 0 // reset for Equals()
|
|
readRouter.CreateIndex = 0 // reset for Equals()
|
|
|
|
require.Equal(t, testEntry, readRouter)
|
|
|
|
// TODO(rb): cas?
|
|
// TODO(rb): list?
|
|
}
|
|
|
|
// First set the necessary protocols to allow advanced routing features.
|
|
for _, service := range []string{
|
|
"test-failover",
|
|
"test-redirect",
|
|
"alternate",
|
|
"test-split",
|
|
"test-route",
|
|
"test-route-case-insensitive",
|
|
} {
|
|
serviceDefaults := &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: service,
|
|
Protocol: "http",
|
|
}
|
|
_, _, err := config_entries.Set(serviceDefaults, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// NOTE: Due to service graph validation, these have to happen in a specific order.
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry ConfigEntry
|
|
verify func(t *testing.T, initial ConfigEntry)
|
|
}{
|
|
{
|
|
name: "failover",
|
|
entry: &ServiceResolverConfigEntry{
|
|
Kind: ServiceResolver,
|
|
Name: "test-failover",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
DefaultSubset: "v1",
|
|
Subsets: map[string]ServiceResolverSubset{
|
|
"v1": {
|
|
Filter: "Service.Meta.version == v1",
|
|
},
|
|
"v2": {
|
|
Filter: "Service.Meta.version == v2",
|
|
},
|
|
"v3": {
|
|
Filter: "Service.Meta.version == v3",
|
|
},
|
|
},
|
|
Failover: map[string]ServiceResolverFailover{
|
|
"*": {
|
|
Datacenters: []string{"dc2"},
|
|
},
|
|
"v1": {
|
|
Service: "alternate",
|
|
Namespace: defaultNamespace,
|
|
},
|
|
"v3": {
|
|
Targets: []ServiceResolverFailoverTarget{
|
|
{Peer: "cluster-01"},
|
|
{Datacenter: "dc1"},
|
|
{Service: "another-service", ServiceSubset: "v1"},
|
|
},
|
|
},
|
|
},
|
|
ConnectTimeout: 5 * time.Second,
|
|
RequestTimeout: 10 * time.Second,
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
verify: verifyResolver,
|
|
},
|
|
{
|
|
name: "redirect",
|
|
entry: &ServiceResolverConfigEntry{
|
|
Kind: ServiceResolver,
|
|
Name: "test-redirect",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
Redirect: &ServiceResolverRedirect{
|
|
Service: "test-failover",
|
|
ServiceSubset: "v2",
|
|
Namespace: defaultNamespace,
|
|
Datacenter: "d",
|
|
},
|
|
},
|
|
verify: verifyResolver,
|
|
},
|
|
{
|
|
name: "redirect to peer",
|
|
entry: &ServiceResolverConfigEntry{
|
|
Kind: ServiceResolver,
|
|
Name: "test-redirect",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
Redirect: &ServiceResolverRedirect{
|
|
Service: "test-failover",
|
|
Peer: "cluster-01",
|
|
},
|
|
},
|
|
verify: verifyResolver,
|
|
},
|
|
{
|
|
name: "mega splitter", // use one mega object to avoid multiple trips
|
|
entry: &ServiceSplitterConfigEntry{
|
|
Kind: ServiceSplitter,
|
|
Name: "test-split",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
Splits: []ServiceSplit{
|
|
{
|
|
Weight: 90,
|
|
Service: "test-failover",
|
|
ServiceSubset: "v1",
|
|
Namespace: defaultNamespace,
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Set: map[string]string{
|
|
"x-foo": "bar",
|
|
},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Remove: []string{"x-foo"},
|
|
},
|
|
},
|
|
{
|
|
Weight: 10,
|
|
Service: "test-redirect",
|
|
Namespace: defaultNamespace,
|
|
},
|
|
},
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
verify: verifySplitter,
|
|
},
|
|
{
|
|
name: "mega router", // use one mega object to avoid multiple trips
|
|
entry: &ServiceRouterConfigEntry{
|
|
Kind: ServiceRouter,
|
|
Name: "test-route",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
Routes: []ServiceRoute{
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathPrefix: "/prefix",
|
|
Header: []ServiceRouteHTTPMatchHeader{
|
|
{Name: "x-debug", Exact: "1"},
|
|
},
|
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
{Name: "debug", Exact: "1"},
|
|
},
|
|
},
|
|
},
|
|
Destination: &ServiceRouteDestination{
|
|
Service: "test-failover",
|
|
ServiceSubset: "v2",
|
|
Namespace: defaultNamespace,
|
|
Partition: defaultPartition,
|
|
PrefixRewrite: "/",
|
|
RequestTimeout: 5 * time.Second,
|
|
NumRetries: 5,
|
|
RetryOnConnectFailure: true,
|
|
RetryOnStatusCodes: []uint32{500, 503, 401},
|
|
RetryOn: []string{
|
|
"gateway-error",
|
|
"reset",
|
|
"envoy-ratelimited",
|
|
"retriable-4xx",
|
|
"refused-stream",
|
|
"cancelled",
|
|
"deadline-exceeded",
|
|
"internal",
|
|
"resource-exhausted",
|
|
"unavailable",
|
|
},
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Set: map[string]string{
|
|
"x-foo": "bar",
|
|
},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Remove: []string{"x-foo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
verify: verifyRouter,
|
|
},
|
|
{
|
|
name: "mega router case insensitive", // use one mega object to avoid multiple trips
|
|
entry: &ServiceRouterConfigEntry{
|
|
Kind: ServiceRouter,
|
|
Name: "test-route-case-insensitive",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
Routes: []ServiceRoute{
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathPrefix: "/prEfix",
|
|
CaseInsensitive: true,
|
|
Header: []ServiceRouteHTTPMatchHeader{
|
|
{Name: "x-debug", Exact: "1"},
|
|
},
|
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
{Name: "debug", Exact: "1"},
|
|
},
|
|
},
|
|
},
|
|
Destination: &ServiceRouteDestination{
|
|
Service: "test-failover",
|
|
ServiceSubset: "v2",
|
|
Namespace: defaultNamespace,
|
|
Partition: defaultPartition,
|
|
PrefixRewrite: "/",
|
|
RequestTimeout: 5 * time.Second,
|
|
NumRetries: 5,
|
|
RetryOnConnectFailure: true,
|
|
RetryOnStatusCodes: []uint32{500, 503, 401},
|
|
RetryOn: []string{
|
|
"gateway-error",
|
|
"reset",
|
|
"envoy-ratelimited",
|
|
"retriable-4xx",
|
|
"refused-stream",
|
|
"cancelled",
|
|
"deadline-exceeded",
|
|
"internal",
|
|
"resource-exhausted",
|
|
"unavailable",
|
|
},
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Set: map[string]string{
|
|
"x-foo": "bar",
|
|
},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Remove: []string{"x-foo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
verify: verifyRouter,
|
|
},
|
|
} {
|
|
tc := tc
|
|
name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
|
|
ok := t.Run(name, func(t *testing.T) {
|
|
tc.verify(t, tc.entry)
|
|
})
|
|
require.True(t, ok, "subtest %q failed so aborting remainder", name)
|
|
}
|
|
}
|
|
|
|
func TestAPI_ConfigEntry_ServiceResolver_LoadBalancer(t *testing.T) {
|
|
t.Parallel()
|
|
c, s := makeClient(t)
|
|
defer s.Stop()
|
|
|
|
config_entries := c.ConfigEntries()
|
|
|
|
verifyResolver := func(t *testing.T, initial ConfigEntry) {
|
|
t.Helper()
|
|
require.IsType(t, &ServiceResolverConfigEntry{}, initial)
|
|
testEntry := initial.(*ServiceResolverConfigEntry)
|
|
|
|
// set it
|
|
_, wm, err := config_entries.Set(testEntry, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, wm)
|
|
require.NotEqual(t, 0, wm.RequestTime)
|
|
|
|
// get it
|
|
entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qm)
|
|
require.NotEqual(t, 0, qm.RequestTime)
|
|
|
|
// verify it
|
|
readResolver, ok := entry.(*ServiceResolverConfigEntry)
|
|
require.True(t, ok)
|
|
readResolver.ModifyIndex = 0 // reset for Equals()
|
|
readResolver.CreateIndex = 0 // reset for Equals()
|
|
|
|
require.Equal(t, testEntry, readResolver)
|
|
}
|
|
|
|
// First set the necessary protocols to allow advanced routing features.
|
|
for _, service := range []string{
|
|
"test-least-req",
|
|
"test-ring-hash",
|
|
} {
|
|
serviceDefaults := &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: service,
|
|
Protocol: "http",
|
|
}
|
|
_, _, err := config_entries.Set(serviceDefaults, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// NOTE: Due to service graph validation, these have to happen in a specific order.
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry ConfigEntry
|
|
verify func(t *testing.T, initial ConfigEntry)
|
|
}{
|
|
{
|
|
name: "least-req",
|
|
entry: &ServiceResolverConfigEntry{
|
|
Kind: ServiceResolver,
|
|
Name: "test-least-req",
|
|
Partition: defaultPartition,
|
|
Namespace: defaultNamespace,
|
|
LoadBalancer: &LoadBalancer{
|
|
Policy: "least_request",
|
|
LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10},
|
|
},
|
|
},
|
|
verify: verifyResolver,
|
|
},
|
|
{
|
|
name: "ring-hash-with-policies",
|
|
entry: &ServiceResolverConfigEntry{
|
|
Kind: ServiceResolver,
|
|
Name: "test-ring-hash",
|
|
Namespace: defaultNamespace,
|
|
Partition: defaultPartition,
|
|
LoadBalancer: &LoadBalancer{
|
|
Policy: "ring_hash",
|
|
RingHashConfig: &RingHashConfig{
|
|
MinimumRingSize: 1024 * 2,
|
|
MaximumRingSize: 1024 * 4,
|
|
},
|
|
HashPolicies: []HashPolicy{
|
|
{
|
|
Field: "header",
|
|
FieldValue: "my-session-header",
|
|
Terminal: true,
|
|
},
|
|
{
|
|
Field: "cookie",
|
|
FieldValue: "oreo",
|
|
CookieConfig: &CookieConfig{
|
|
Path: "/tray",
|
|
TTL: 20 * time.Millisecond,
|
|
},
|
|
},
|
|
{
|
|
Field: "cookie",
|
|
FieldValue: "sugar",
|
|
CookieConfig: &CookieConfig{
|
|
Session: true,
|
|
Path: "/tin",
|
|
},
|
|
},
|
|
{
|
|
SourceIP: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
verify: verifyResolver,
|
|
},
|
|
} {
|
|
tc := tc
|
|
name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
|
|
ok := t.Run(name, func(t *testing.T) {
|
|
tc.verify(t, tc.entry)
|
|
})
|
|
require.True(t, ok, "subtest %q failed so aborting remainder", name)
|
|
}
|
|
}
|