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