2019-06-27 17:37:43 +00:00
|
|
|
package structs
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestServiceResolverConfigEntry(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
name string
|
|
|
|
entry *ServiceResolverConfigEntry
|
|
|
|
normalizeErr string
|
|
|
|
validateErr string
|
|
|
|
// check is called between normalize and validate
|
|
|
|
check func(t *testing.T, entry *ServiceResolverConfigEntry)
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "nil",
|
|
|
|
entry: nil,
|
|
|
|
normalizeErr: "config entry is nil",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "no name",
|
|
|
|
entry: &ServiceResolverConfigEntry{},
|
|
|
|
validateErr: "Name is required",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "empty",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "empty subset name",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
|
|
|
"": {OnlyPassing: true},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: "Subset defined with empty name",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "default subset does not exist",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
DefaultSubset: "gone",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `DefaultSubset "gone" is not a valid subset`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "default subset does exist",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
DefaultSubset: "v1",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "empty redirect",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Redirect: &ServiceResolverRedirect{},
|
|
|
|
},
|
|
|
|
validateErr: "Redirect is empty",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "redirect subset with no service",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Redirect: &ServiceResolverRedirect{
|
|
|
|
ServiceSubset: "next",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: "Redirect.ServiceSubset defined without Redirect.Service",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "redirect namespace with no service",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Redirect: &ServiceResolverRedirect{
|
|
|
|
Namespace: "alternate",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: "Redirect.Namespace defined without Redirect.Service",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "self redirect with invalid subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Redirect: &ServiceResolverRedirect{
|
|
|
|
Service: "test",
|
|
|
|
ServiceSubset: "gone",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `Redirect.ServiceSubset "gone" is not a valid subset of "test"`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "self redirect with valid subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Redirect: &ServiceResolverRedirect{
|
|
|
|
Service: "test",
|
|
|
|
ServiceSubset: "v1",
|
|
|
|
},
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "simple wildcard failover",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"*": ServiceResolverFailover{
|
|
|
|
Datacenters: []string{"dc2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover for missing subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"gone": ServiceResolverFailover{
|
|
|
|
Datacenters: []string{"dc2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `Bad Failover["gone"]: not a valid subset`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover for present subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"v1": ServiceResolverFailover{
|
|
|
|
Datacenters: []string{"dc2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover empty",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"v1": ServiceResolverFailover{},
|
|
|
|
},
|
|
|
|
},
|
2019-07-01 20:23:36 +00:00
|
|
|
validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, or Datacenters is required`,
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover to self using invalid subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"v1": ServiceResolverFailover{
|
|
|
|
Service: "test",
|
|
|
|
ServiceSubset: "gone",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `Bad Failover["v1"].ServiceSubset "gone" is not a valid subset of "test"`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover to self using valid subset",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Subsets: map[string]ServiceResolverSubset{
|
2019-07-01 20:23:36 +00:00
|
|
|
"v1": {Filter: "Service.Meta.version == v1"},
|
|
|
|
"v2": {Filter: "Service.Meta.version == v2"},
|
2019-06-27 17:37:43 +00:00
|
|
|
},
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"v1": ServiceResolverFailover{
|
|
|
|
Service: "test",
|
|
|
|
ServiceSubset: "v2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover with invalid overprovisioning factor",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"*": ServiceResolverFailover{
|
|
|
|
Service: "backup",
|
|
|
|
OverprovisioningFactor: -1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `Bad Failover["*"].OverprovisioningFactor '-1', must be >= 0`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "failover with empty datacenters in list",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
Failover: map[string]ServiceResolverFailover{
|
|
|
|
"*": ServiceResolverFailover{
|
|
|
|
Service: "backup",
|
|
|
|
Datacenters: []string{"", "dc2", "dc3"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
validateErr: `Bad Failover["*"].Datacenters: found empty datacenter`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "bad connect timeout",
|
|
|
|
entry: &ServiceResolverConfigEntry{
|
|
|
|
Kind: ServiceResolver,
|
|
|
|
Name: "test",
|
|
|
|
ConnectTimeout: -1 * time.Second,
|
|
|
|
},
|
|
|
|
validateErr: "Bad ConnectTimeout",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
err := tc.entry.Normalize()
|
|
|
|
if tc.normalizeErr != "" {
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), tc.normalizeErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
if tc.check != nil {
|
|
|
|
tc.check(t, tc.entry)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tc.entry.Validate()
|
|
|
|
if tc.validateErr != "" {
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), tc.validateErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestServiceSplitterConfigEntry(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
makesplitter := func(splits ...ServiceSplit) *ServiceSplitterConfigEntry {
|
|
|
|
return &ServiceSplitterConfigEntry{
|
|
|
|
Kind: ServiceSplitter,
|
|
|
|
Name: "test",
|
|
|
|
Splits: splits,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
makesplit := func(weight float32, service, serviceSubset, namespace string) ServiceSplit {
|
|
|
|
return ServiceSplit{
|
|
|
|
Weight: weight,
|
|
|
|
Service: service,
|
|
|
|
ServiceSubset: serviceSubset,
|
|
|
|
Namespace: namespace,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
name string
|
|
|
|
entry *ServiceSplitterConfigEntry
|
|
|
|
normalizeErr string
|
|
|
|
validateErr string
|
|
|
|
// check is called between normalize and validate
|
|
|
|
check func(t *testing.T, entry *ServiceSplitterConfigEntry)
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "nil",
|
|
|
|
entry: nil,
|
|
|
|
normalizeErr: "config entry is nil",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "no name",
|
|
|
|
entry: &ServiceSplitterConfigEntry{},
|
|
|
|
validateErr: "Name is required",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "empty",
|
|
|
|
entry: makesplitter(),
|
|
|
|
validateErr: "no splits configured",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "1 split",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(100, "test", "", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(100), entry.Splits[0].Weight)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "1 split not enough weight",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(99.99, "test", "", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(99.99), entry.Splits[0].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "the sum of all split weights must be 100",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "1 split too much weight",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(100.01, "test", "", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(100.01), entry.Splits[0].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "the sum of all split weights must be 100",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "2 splits",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(99, "test", "v1", ""),
|
|
|
|
makesplit(1, "test", "v2", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(99), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(1), entry.Splits[1].Weight)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "2 splits - rounded up to smallest units",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(99.999, "test", "v1", ""),
|
|
|
|
makesplit(0.001, "test", "v2", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(100), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(0), entry.Splits[1].Weight)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "2 splits not enough weight",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(99.98, "test", "v1", ""),
|
|
|
|
makesplit(0.01, "test", "v2", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(99.98), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(0.01), entry.Splits[1].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "the sum of all split weights must be 100",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "2 splits too much weight",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(100, "test", "v1", ""),
|
|
|
|
makesplit(0.01, "test", "v2", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(100), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(0.01), entry.Splits[1].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "the sum of all split weights must be 100",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "3 splits",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(34, "test", "v1", ""),
|
|
|
|
makesplit(33, "test", "v2", ""),
|
|
|
|
makesplit(33, "test", "v3", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(34), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[1].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[2].Weight)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "3 splits one duplicated same weights",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(34, "test", "v1", ""),
|
|
|
|
makesplit(33, "test", "v2", ""),
|
|
|
|
makesplit(33, "test", "v2", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(34), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[1].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[2].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "split destination occurs more than once",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "3 splits one duplicated diff weights",
|
|
|
|
entry: makesplitter(
|
|
|
|
makesplit(34, "test", "v1", ""),
|
|
|
|
makesplit(33, "test", "v2", ""),
|
|
|
|
makesplit(33, "test", "v1", ""),
|
|
|
|
),
|
|
|
|
check: func(t *testing.T, entry *ServiceSplitterConfigEntry) {
|
|
|
|
require.Equal(t, float32(34), entry.Splits[0].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[1].Weight)
|
|
|
|
require.Equal(t, float32(33), entry.Splits[2].Weight)
|
|
|
|
},
|
|
|
|
validateErr: "split destination occurs more than once",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
err := tc.entry.Normalize()
|
|
|
|
if tc.normalizeErr != "" {
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), tc.normalizeErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
if tc.check != nil {
|
|
|
|
tc.check(t, tc.entry)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tc.entry.Validate()
|
|
|
|
if tc.validateErr != "" {
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Contains(t, err.Error(), tc.validateErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|