1package api
2
3import (
4	"fmt"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9)
10
11func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
12	t.Parallel()
13	c, s := makeClient(t)
14	defer s.Stop()
15
16	config_entries := c.ConfigEntries()
17
18	verifyResolver := func(t *testing.T, initial ConfigEntry) {
19		t.Helper()
20		require.IsType(t, &ServiceResolverConfigEntry{}, initial)
21		testEntry := initial.(*ServiceResolverConfigEntry)
22
23		// set it
24		_, wm, err := config_entries.Set(testEntry, nil)
25		require.NoError(t, err)
26		require.NotNil(t, wm)
27		require.NotEqual(t, 0, wm.RequestTime)
28
29		// get it
30		entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
31		require.NoError(t, err)
32		require.NotNil(t, qm)
33		require.NotEqual(t, 0, qm.RequestTime)
34
35		// generic verification
36		require.Equal(t, testEntry.Meta, entry.GetMeta())
37
38		// verify it
39		readResolver, ok := entry.(*ServiceResolverConfigEntry)
40		require.True(t, ok)
41		readResolver.ModifyIndex = 0 // reset for Equals()
42		readResolver.CreateIndex = 0 // reset for Equals()
43
44		require.Equal(t, testEntry, readResolver)
45
46		// TODO(rb): cas?
47		// TODO(rb): list?
48	}
49
50	verifySplitter := func(t *testing.T, initial ConfigEntry) {
51		t.Helper()
52		require.IsType(t, &ServiceSplitterConfigEntry{}, initial)
53		testEntry := initial.(*ServiceSplitterConfigEntry)
54
55		// set it
56		_, wm, err := config_entries.Set(testEntry, nil)
57		require.NoError(t, err)
58		require.NotNil(t, wm)
59		require.NotEqual(t, 0, wm.RequestTime)
60
61		// get it
62		entry, qm, err := config_entries.Get(ServiceSplitter, testEntry.Name, nil)
63		require.NoError(t, err)
64		require.NotNil(t, qm)
65		require.NotEqual(t, 0, qm.RequestTime)
66
67		// generic verification
68		require.Equal(t, testEntry.Meta, entry.GetMeta())
69
70		// verify it
71		readSplitter, ok := entry.(*ServiceSplitterConfigEntry)
72		require.True(t, ok)
73		readSplitter.ModifyIndex = 0 // reset for Equals()
74		readSplitter.CreateIndex = 0 // reset for Equals()
75
76		require.Equal(t, testEntry, readSplitter)
77
78		// TODO(rb): cas?
79		// TODO(rb): list?
80	}
81
82	verifyRouter := func(t *testing.T, initial ConfigEntry) {
83		t.Helper()
84		require.IsType(t, &ServiceRouterConfigEntry{}, initial)
85		testEntry := initial.(*ServiceRouterConfigEntry)
86
87		// set it
88		_, wm, err := config_entries.Set(testEntry, nil)
89		require.NoError(t, err)
90		require.NotNil(t, wm)
91		require.NotEqual(t, 0, wm.RequestTime)
92
93		// get it
94		entry, qm, err := config_entries.Get(ServiceRouter, testEntry.Name, nil)
95		require.NoError(t, err)
96		require.NotNil(t, qm)
97		require.NotEqual(t, 0, qm.RequestTime)
98
99		// generic verification
100		require.Equal(t, testEntry.Meta, entry.GetMeta())
101
102		// verify it
103		readRouter, ok := entry.(*ServiceRouterConfigEntry)
104		require.True(t, ok)
105		readRouter.ModifyIndex = 0 // reset for Equals()
106		readRouter.CreateIndex = 0 // reset for Equals()
107
108		require.Equal(t, testEntry, readRouter)
109
110		// TODO(rb): cas?
111		// TODO(rb): list?
112	}
113
114	// First set the necessary protocols to allow advanced routing features.
115	for _, service := range []string{
116		"test-failover",
117		"test-redirect",
118		"alternate",
119		"test-split",
120		"test-route",
121	} {
122		serviceDefaults := &ServiceConfigEntry{
123			Kind:     ServiceDefaults,
124			Name:     service,
125			Protocol: "http",
126		}
127		_, _, err := config_entries.Set(serviceDefaults, nil)
128		require.NoError(t, err)
129	}
130
131	// NOTE: Due to service graph validation, these have to happen in a specific order.
132	for _, tc := range []struct {
133		name   string
134		entry  ConfigEntry
135		verify func(t *testing.T, initial ConfigEntry)
136	}{
137		{
138			name: "failover",
139			entry: &ServiceResolverConfigEntry{
140				Kind:          ServiceResolver,
141				Name:          "test-failover",
142				Namespace:     defaultNamespace,
143				DefaultSubset: "v1",
144				Subsets: map[string]ServiceResolverSubset{
145					"v1": {
146						Filter: "Service.Meta.version == v1",
147					},
148					"v2": {
149						Filter: "Service.Meta.version == v2",
150					},
151				},
152				Failover: map[string]ServiceResolverFailover{
153					"*": {
154						Datacenters: []string{"dc2"},
155					},
156					"v1": {
157						Service:   "alternate",
158						Namespace: defaultNamespace,
159					},
160				},
161				ConnectTimeout: 5 * time.Second,
162				Meta: map[string]string{
163					"foo": "bar",
164					"gir": "zim",
165				},
166			},
167			verify: verifyResolver,
168		},
169		{
170			name: "redirect",
171			entry: &ServiceResolverConfigEntry{
172				Kind:      ServiceResolver,
173				Name:      "test-redirect",
174				Namespace: defaultNamespace,
175				Redirect: &ServiceResolverRedirect{
176					Service:       "test-failover",
177					ServiceSubset: "v2",
178					Namespace:     defaultNamespace,
179					Datacenter:    "d",
180				},
181			},
182			verify: verifyResolver,
183		},
184		{
185			name: "mega splitter", // use one mega object to avoid multiple trips
186			entry: &ServiceSplitterConfigEntry{
187				Kind:      ServiceSplitter,
188				Name:      "test-split",
189				Namespace: defaultNamespace,
190				Splits: []ServiceSplit{
191					{
192						Weight:        90,
193						Service:       "test-failover",
194						ServiceSubset: "v1",
195						Namespace:     defaultNamespace,
196					},
197					{
198						Weight:    10,
199						Service:   "test-redirect",
200						Namespace: defaultNamespace,
201					},
202				},
203				Meta: map[string]string{
204					"foo": "bar",
205					"gir": "zim",
206				},
207			},
208			verify: verifySplitter,
209		},
210		{
211			name: "mega router", // use one mega object to avoid multiple trips
212			entry: &ServiceRouterConfigEntry{
213				Kind:      ServiceRouter,
214				Name:      "test-route",
215				Namespace: defaultNamespace,
216				Routes: []ServiceRoute{
217					{
218						Match: &ServiceRouteMatch{
219							HTTP: &ServiceRouteHTTPMatch{
220								PathPrefix: "/prefix",
221								Header: []ServiceRouteHTTPMatchHeader{
222									{Name: "x-debug", Exact: "1"},
223								},
224								QueryParam: []ServiceRouteHTTPMatchQueryParam{
225									{Name: "debug", Exact: "1"},
226								},
227							},
228						},
229						Destination: &ServiceRouteDestination{
230							Service:               "test-failover",
231							ServiceSubset:         "v2",
232							Namespace:             defaultNamespace,
233							PrefixRewrite:         "/",
234							RequestTimeout:        5 * time.Second,
235							NumRetries:            5,
236							RetryOnConnectFailure: true,
237							RetryOnStatusCodes:    []uint32{500, 503, 401},
238						},
239					},
240				},
241				Meta: map[string]string{
242					"foo": "bar",
243					"gir": "zim",
244				},
245			},
246			verify: verifyRouter,
247		},
248	} {
249		tc := tc
250		name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
251		ok := t.Run(name, func(t *testing.T) {
252			tc.verify(t, tc.entry)
253		})
254		require.True(t, ok, "subtest %q failed so aborting remainder", name)
255	}
256}
257
258func TestAPI_ConfigEntry_ServiceResolver_LoadBalancer(t *testing.T) {
259	t.Parallel()
260	c, s := makeClient(t)
261	defer s.Stop()
262
263	config_entries := c.ConfigEntries()
264
265	verifyResolver := func(t *testing.T, initial ConfigEntry) {
266		t.Helper()
267		require.IsType(t, &ServiceResolverConfigEntry{}, initial)
268		testEntry := initial.(*ServiceResolverConfigEntry)
269
270		// set it
271		_, wm, err := config_entries.Set(testEntry, nil)
272		require.NoError(t, err)
273		require.NotNil(t, wm)
274		require.NotEqual(t, 0, wm.RequestTime)
275
276		// get it
277		entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
278		require.NoError(t, err)
279		require.NotNil(t, qm)
280		require.NotEqual(t, 0, qm.RequestTime)
281
282		// verify it
283		readResolver, ok := entry.(*ServiceResolverConfigEntry)
284		require.True(t, ok)
285		readResolver.ModifyIndex = 0 // reset for Equals()
286		readResolver.CreateIndex = 0 // reset for Equals()
287
288		require.Equal(t, testEntry, readResolver)
289	}
290
291	// First set the necessary protocols to allow advanced routing features.
292	for _, service := range []string{
293		"test-least-req",
294		"test-ring-hash",
295	} {
296		serviceDefaults := &ServiceConfigEntry{
297			Kind:     ServiceDefaults,
298			Name:     service,
299			Protocol: "http",
300		}
301		_, _, err := config_entries.Set(serviceDefaults, nil)
302		require.NoError(t, err)
303	}
304
305	// NOTE: Due to service graph validation, these have to happen in a specific order.
306	for _, tc := range []struct {
307		name   string
308		entry  ConfigEntry
309		verify func(t *testing.T, initial ConfigEntry)
310	}{
311		{
312			name: "least-req",
313			entry: &ServiceResolverConfigEntry{
314				Kind:      ServiceResolver,
315				Name:      "test-least-req",
316				Namespace: defaultNamespace,
317				LoadBalancer: &LoadBalancer{
318					Policy:             "least_request",
319					LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10},
320				},
321			},
322			verify: verifyResolver,
323		},
324		{
325			name: "ring-hash-with-policies",
326			entry: &ServiceResolverConfigEntry{
327				Kind:      ServiceResolver,
328				Name:      "test-ring-hash",
329				Namespace: defaultNamespace,
330				LoadBalancer: &LoadBalancer{
331					Policy: "ring_hash",
332					RingHashConfig: &RingHashConfig{
333						MinimumRingSize: 1024 * 2,
334						MaximumRingSize: 1024 * 4,
335					},
336					HashPolicies: []HashPolicy{
337						{
338							Field:      "header",
339							FieldValue: "my-session-header",
340							Terminal:   true,
341						},
342						{
343							Field:      "cookie",
344							FieldValue: "oreo",
345							CookieConfig: &CookieConfig{
346								Path: "/tray",
347								TTL:  20 * time.Millisecond,
348							},
349						},
350						{
351							Field:      "cookie",
352							FieldValue: "sugar",
353							CookieConfig: &CookieConfig{
354								Session: true,
355								Path:    "/tin",
356							},
357						},
358						{
359							SourceIP: true,
360						},
361					},
362				},
363			},
364			verify: verifyResolver,
365		},
366	} {
367		tc := tc
368		name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
369		ok := t.Run(name, func(t *testing.T) {
370			tc.verify(t, tc.entry)
371		})
372		require.True(t, ok, "subtest %q failed so aborting remainder", name)
373	}
374}
375