1package api
2
3import (
4	"encoding/json"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9)
10
11func TestAPI_ConfigEntries(t *testing.T) {
12	t.Parallel()
13	c, s := makeClient(t)
14	defer s.Stop()
15
16	config_entries := c.ConfigEntries()
17
18	t.Run("Proxy Defaults", func(t *testing.T) {
19		global_proxy := &ProxyConfigEntry{
20			Kind: ProxyDefaults,
21			Name: ProxyConfigGlobal,
22			Config: map[string]interface{}{
23				"foo": "bar",
24				"bar": 1.0,
25			},
26			Meta: map[string]string{
27				"foo": "bar",
28				"gir": "zim",
29			},
30		}
31
32		// set it
33		_, wm, err := config_entries.Set(global_proxy, nil)
34		require.NoError(t, err)
35		require.NotNil(t, wm)
36		require.NotEqual(t, 0, wm.RequestTime)
37
38		// get it
39		entry, qm, err := config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil)
40		require.NoError(t, err)
41		require.NotNil(t, qm)
42		require.NotEqual(t, 0, qm.RequestTime)
43
44		// verify it
45		readProxy, ok := entry.(*ProxyConfigEntry)
46		require.True(t, ok)
47		require.Equal(t, global_proxy.Kind, readProxy.Kind)
48		require.Equal(t, global_proxy.Name, readProxy.Name)
49		require.Equal(t, global_proxy.Config, readProxy.Config)
50		require.Equal(t, global_proxy.Meta, readProxy.Meta)
51		require.Equal(t, global_proxy.Meta, readProxy.GetMeta())
52
53		global_proxy.Config["baz"] = true
54		// CAS update fail
55		written, _, err := config_entries.CAS(global_proxy, 0, nil)
56		require.NoError(t, err)
57		require.False(t, written)
58
59		// CAS update success
60		written, wm, err = config_entries.CAS(global_proxy, readProxy.ModifyIndex, nil)
61		require.NoError(t, err)
62		require.NotNil(t, wm)
63		require.NotEqual(t, 0, wm.RequestTime)
64		require.NoError(t, err)
65		require.True(t, written)
66
67		// Non CAS update
68		global_proxy.Config["baz"] = "baz"
69		_, wm, err = config_entries.Set(global_proxy, nil)
70		require.NoError(t, err)
71		require.NotNil(t, wm)
72		require.NotEqual(t, 0, wm.RequestTime)
73
74		// list it
75		entries, qm, err := config_entries.List(ProxyDefaults, nil)
76		require.NoError(t, err)
77		require.NotNil(t, qm)
78		require.NotEqual(t, 0, qm.RequestTime)
79		require.Len(t, entries, 1)
80		readProxy, ok = entries[0].(*ProxyConfigEntry)
81		require.True(t, ok)
82		require.Equal(t, global_proxy.Kind, readProxy.Kind)
83		require.Equal(t, global_proxy.Name, readProxy.Name)
84		require.Equal(t, global_proxy.Config, readProxy.Config)
85
86		// delete it
87		wm, err = config_entries.Delete(ProxyDefaults, ProxyConfigGlobal, nil)
88		require.NoError(t, err)
89		require.NotNil(t, wm)
90		require.NotEqual(t, 0, wm.RequestTime)
91
92		_, _, err = config_entries.Get(ProxyDefaults, ProxyConfigGlobal, nil)
93		require.Error(t, err)
94	})
95
96	t.Run("Service Defaults", func(t *testing.T) {
97		service := &ServiceConfigEntry{
98			Kind:     ServiceDefaults,
99			Name:     "foo",
100			Protocol: "udp",
101			Meta: map[string]string{
102				"foo": "bar",
103				"gir": "zim",
104			},
105		}
106
107		service2 := &ServiceConfigEntry{
108			Kind:     ServiceDefaults,
109			Name:     "bar",
110			Protocol: "tcp",
111		}
112
113		// set it
114		_, wm, err := config_entries.Set(service, nil)
115		require.NoError(t, err)
116		require.NotNil(t, wm)
117		require.NotEqual(t, 0, wm.RequestTime)
118
119		// also set the second one
120		_, wm, err = config_entries.Set(service2, nil)
121		require.NoError(t, err)
122		require.NotNil(t, wm)
123		require.NotEqual(t, 0, wm.RequestTime)
124
125		// get it
126		entry, qm, err := config_entries.Get(ServiceDefaults, "foo", nil)
127		require.NoError(t, err)
128		require.NotNil(t, qm)
129		require.NotEqual(t, 0, qm.RequestTime)
130
131		// verify it
132		readService, ok := entry.(*ServiceConfigEntry)
133		require.True(t, ok)
134		require.Equal(t, service.Kind, readService.Kind)
135		require.Equal(t, service.Name, readService.Name)
136		require.Equal(t, service.Protocol, readService.Protocol)
137		require.Equal(t, service.Meta, readService.Meta)
138		require.Equal(t, service.Meta, readService.GetMeta())
139
140		// update it
141		service.Protocol = "tcp"
142
143		// CAS fail
144		written, _, err := config_entries.CAS(service, 0, nil)
145		require.NoError(t, err)
146		require.False(t, written)
147
148		// CAS success
149		written, wm, err = config_entries.CAS(service, readService.ModifyIndex, nil)
150		require.NoError(t, err)
151		require.NotNil(t, wm)
152		require.NotEqual(t, 0, wm.RequestTime)
153		require.True(t, written)
154
155		// update no cas
156		service.Protocol = "http"
157
158		_, wm, err = config_entries.Set(service, nil)
159		require.NoError(t, err)
160		require.NotNil(t, wm)
161		require.NotEqual(t, 0, wm.RequestTime)
162
163		// list them
164		entries, qm, err := config_entries.List(ServiceDefaults, nil)
165		require.NoError(t, err)
166		require.NotNil(t, qm)
167		require.NotEqual(t, 0, qm.RequestTime)
168		require.Len(t, entries, 2)
169
170		for _, entry = range entries {
171			switch entry.GetName() {
172			case "foo":
173				// this also verifies that the update value was persisted and
174				// the updated values are seen
175				readService, ok = entry.(*ServiceConfigEntry)
176				require.True(t, ok)
177				require.Equal(t, service.Kind, readService.Kind)
178				require.Equal(t, service.Name, readService.Name)
179				require.Equal(t, service.Protocol, readService.Protocol)
180			case "bar":
181				readService, ok = entry.(*ServiceConfigEntry)
182				require.True(t, ok)
183				require.Equal(t, service2.Kind, readService.Kind)
184				require.Equal(t, service2.Name, readService.Name)
185				require.Equal(t, service2.Protocol, readService.Protocol)
186			}
187		}
188
189		// delete it
190		wm, err = config_entries.Delete(ServiceDefaults, "foo", nil)
191		require.NoError(t, err)
192		require.NotNil(t, wm)
193		require.NotEqual(t, 0, wm.RequestTime)
194
195		// verify deletion
196		_, _, err = config_entries.Get(ServiceDefaults, "foo", nil)
197		require.Error(t, err)
198	})
199
200	t.Run("Mesh", func(t *testing.T) {
201		mesh := &MeshConfigEntry{
202			TransparentProxy: TransparentProxyMeshConfig{MeshDestinationsOnly: true},
203			Meta: map[string]string{
204				"foo": "bar",
205				"gir": "zim",
206			},
207			Namespace: defaultNamespace,
208		}
209		ce := c.ConfigEntries()
210
211		runStep(t, "set and get", func(t *testing.T) {
212			_, wm, err := ce.Set(mesh, nil)
213			require.NoError(t, err)
214			require.NotNil(t, wm)
215			require.NotEqual(t, 0, wm.RequestTime)
216
217			entry, qm, err := ce.Get(MeshConfig, MeshConfigMesh, nil)
218			require.NoError(t, err)
219			require.NotNil(t, qm)
220			require.NotEqual(t, 0, qm.RequestTime)
221
222			result, ok := entry.(*MeshConfigEntry)
223			require.True(t, ok)
224
225			// ignore indexes
226			result.CreateIndex = 0
227			result.ModifyIndex = 0
228			require.Equal(t, mesh, result)
229		})
230
231		runStep(t, "list", func(t *testing.T) {
232			entries, qm, err := ce.List(MeshConfig, nil)
233			require.NoError(t, err)
234			require.NotNil(t, qm)
235			require.NotEqual(t, 0, qm.RequestTime)
236			require.Len(t, entries, 1)
237		})
238
239		runStep(t, "delete", func(t *testing.T) {
240			wm, err := ce.Delete(MeshConfig, MeshConfigMesh, nil)
241			require.NoError(t, err)
242			require.NotNil(t, wm)
243			require.NotEqual(t, 0, wm.RequestTime)
244
245			// verify deletion
246			_, _, err = ce.Get(MeshConfig, MeshConfigMesh, nil)
247			require.Error(t, err)
248		})
249	})
250
251}
252
253func runStep(t *testing.T, name string, fn func(t *testing.T)) {
254	t.Helper()
255	if !t.Run(name, fn) {
256		t.FailNow()
257	}
258}
259
260func TestDecodeConfigEntry(t *testing.T) {
261	t.Parallel()
262
263	for _, tc := range []struct {
264		name      string
265		body      string
266		expect    ConfigEntry
267		expectErr string
268	}{
269		{
270			name: "expose-paths: kitchen sink proxy",
271			body: `
272			{
273				"Kind": "proxy-defaults",
274				"Name": "global",
275				"Expose": {
276					"Checks": true,
277					"Paths": [
278						{
279							"LocalPathPort": 8080,
280							"ListenerPort": 21500,
281							"Path": "/healthz",
282							"Protocol": "http2"
283						}
284					]
285				}
286			}
287			`,
288			expect: &ProxyConfigEntry{
289				Kind: "proxy-defaults",
290				Name: "global",
291				Expose: ExposeConfig{
292					Checks: true,
293					Paths: []ExposePath{
294						{
295							LocalPathPort: 8080,
296							ListenerPort:  21500,
297							Path:          "/healthz",
298							Protocol:      "http2",
299						},
300					},
301				},
302			},
303		},
304		{
305			name: "expose-paths: kitchen sink service default",
306			body: `
307			{
308				"Kind": "service-defaults",
309				"Name": "global",
310				"Expose": {
311					"Checks": true,
312					"Paths": [
313						{
314							"LocalPathPort": 8080,
315							"ListenerPort": 21500,
316							"Path": "/healthz",
317							"Protocol": "http2"
318						}
319					]
320				}
321			}
322			`,
323			expect: &ServiceConfigEntry{
324				Kind: "service-defaults",
325				Name: "global",
326				Expose: ExposeConfig{
327					Checks: true,
328					Paths: []ExposePath{
329						{
330							LocalPathPort: 8080,
331							ListenerPort:  21500,
332							Path:          "/healthz",
333							Protocol:      "http2",
334						},
335					},
336				},
337			},
338		},
339		{
340			name: "proxy-defaults",
341			body: `
342			{
343				"Kind": "proxy-defaults",
344				"Name": "main",
345				"Meta" : {
346					"foo": "bar",
347					"gir": "zim"
348				},
349				"Config": {
350				  "foo": 19,
351				  "bar": "abc",
352				  "moreconfig": {
353					"moar": "config"
354				  }
355				},
356				"MeshGateway": {
357					"Mode": "remote"
358				},
359				"Mode": "transparent",
360				"TransparentProxy": {
361					"OutboundListenerPort": 808,
362					"DialedDirectly": true
363				}
364			}
365			`,
366			expect: &ProxyConfigEntry{
367				Kind: "proxy-defaults",
368				Name: "main",
369				Meta: map[string]string{
370					"foo": "bar",
371					"gir": "zim",
372				},
373				Config: map[string]interface{}{
374					"foo": float64(19),
375					"bar": "abc",
376					"moreconfig": map[string]interface{}{
377						"moar": "config",
378					},
379				},
380				MeshGateway: MeshGatewayConfig{
381					Mode: MeshGatewayModeRemote,
382				},
383				Mode: ProxyModeTransparent,
384				TransparentProxy: &TransparentProxyConfig{
385					OutboundListenerPort: 808,
386					DialedDirectly:       true,
387				},
388			},
389		},
390		{
391			name: "service-defaults",
392			body: `
393			{
394				"Kind": "service-defaults",
395				"Name": "main",
396				"Meta" : {
397					"foo": "bar",
398					"gir": "zim"
399				},
400				"Protocol": "http",
401				"ExternalSNI": "abc-123",
402				"MeshGateway": {
403					"Mode": "remote"
404				},
405				"Mode": "transparent",
406				"TransparentProxy": {
407					"OutboundListenerPort": 808,
408					"DialedDirectly": true
409				},
410				"UpstreamConfig": {
411					"Overrides": [
412						{
413							"Name": "redis",
414							"PassiveHealthCheck": {
415								"MaxFailures": 3,
416								"Interval": "2s"
417							}
418						},
419						{
420							"Name": "finance--billing",
421							"MeshGateway": {
422								"Mode": "remote"
423							}
424						}
425					],
426					"Defaults": {
427						"EnvoyClusterJSON": "zip",
428						"EnvoyListenerJSON": "zop",
429						"ConnectTimeoutMs": 5000,
430						"Protocol": "http",
431						"Limits": {
432							"MaxConnections": 3,
433							"MaxPendingRequests": 4,
434							"MaxConcurrentRequests": 5
435						},
436						"PassiveHealthCheck": {
437								"MaxFailures": 5,
438								"Interval": "4s"
439						}
440					}
441				}
442			}
443			`,
444			expect: &ServiceConfigEntry{
445				Kind: "service-defaults",
446				Name: "main",
447				Meta: map[string]string{
448					"foo": "bar",
449					"gir": "zim",
450				},
451				Protocol:    "http",
452				ExternalSNI: "abc-123",
453				MeshGateway: MeshGatewayConfig{
454					Mode: MeshGatewayModeRemote,
455				},
456				Mode: ProxyModeTransparent,
457				TransparentProxy: &TransparentProxyConfig{
458					OutboundListenerPort: 808,
459					DialedDirectly:       true,
460				},
461				UpstreamConfig: &UpstreamConfiguration{
462					Overrides: []*UpstreamConfig{
463						{
464							Name: "redis",
465							PassiveHealthCheck: &PassiveHealthCheck{
466								MaxFailures: 3,
467								Interval:    2 * time.Second,
468							},
469						},
470						{
471							Name:        "finance--billing",
472							MeshGateway: MeshGatewayConfig{Mode: "remote"},
473						},
474					},
475					Defaults: &UpstreamConfig{
476						EnvoyClusterJSON:  "zip",
477						EnvoyListenerJSON: "zop",
478						Protocol:          "http",
479						ConnectTimeoutMs:  5000,
480						Limits: &UpstreamLimits{
481							MaxConnections:        intPointer(3),
482							MaxPendingRequests:    intPointer(4),
483							MaxConcurrentRequests: intPointer(5),
484						},
485						PassiveHealthCheck: &PassiveHealthCheck{
486							MaxFailures: 5,
487							Interval:    4 * time.Second,
488						},
489					},
490				},
491			},
492		},
493		{
494			name: "service-router: kitchen sink",
495			body: `
496			{
497				"Kind": "service-router",
498				"Name": "main",
499				"Meta" : {
500					"foo": "bar",
501					"gir": "zim"
502				},
503				"Routes": [
504					{
505						"Match": {
506							"HTTP": {
507								"PathExact": "/foo",
508								"Header": [
509									{
510										"Name": "debug1",
511										"Present": true
512									},
513									{
514										"Name": "debug2",
515										"Present": false,
516										"Invert": true
517									},
518									{
519										"Name": "debug3",
520										"Exact": "1"
521									},
522									{
523										"Name": "debug4",
524										"Prefix": "aaa"
525									},
526									{
527										"Name": "debug5",
528										"Suffix": "bbb"
529									},
530									{
531										"Name": "debug6",
532										"Regex": "a.*z"
533									}
534								]
535							}
536						},
537						"Destination": {
538						  "Service": "carrot",
539						  "ServiceSubset": "kale",
540						  "Namespace": "leek",
541						  "PrefixRewrite": "/alternate",
542						  "RequestTimeout": "99s",
543						  "NumRetries": 12345,
544						  "RetryOnConnectFailure": true,
545						  "RetryOnStatusCodes": [401, 209]
546						}
547					},
548					{
549						"Match": {
550							"HTTP": {
551								"PathPrefix": "/foo",
552								"Methods": [ "GET", "DELETE" ],
553								"QueryParam": [
554									{
555										"Name": "hack1",
556										"Present": true
557									},
558									{
559										"Name": "hack2",
560										"Exact": "1"
561									},
562									{
563										"Name": "hack3",
564										"Regex": "a.*z"
565									}
566								]
567							}
568						}
569					},
570					{
571						"Match": {
572							"HTTP": {
573								"PathRegex": "/foo"
574							}
575						}
576					}
577				]
578			}
579			`,
580			expect: &ServiceRouterConfigEntry{
581				Kind: "service-router",
582				Name: "main",
583				Meta: map[string]string{
584					"foo": "bar",
585					"gir": "zim",
586				},
587				Routes: []ServiceRoute{
588					{
589						Match: &ServiceRouteMatch{
590							HTTP: &ServiceRouteHTTPMatch{
591								PathExact: "/foo",
592								Header: []ServiceRouteHTTPMatchHeader{
593									{
594										Name:    "debug1",
595										Present: true,
596									},
597									{
598										Name:    "debug2",
599										Present: false,
600										Invert:  true,
601									},
602									{
603										Name:  "debug3",
604										Exact: "1",
605									},
606									{
607										Name:   "debug4",
608										Prefix: "aaa",
609									},
610									{
611										Name:   "debug5",
612										Suffix: "bbb",
613									},
614									{
615										Name:  "debug6",
616										Regex: "a.*z",
617									},
618								},
619							},
620						},
621						Destination: &ServiceRouteDestination{
622							Service:               "carrot",
623							ServiceSubset:         "kale",
624							Namespace:             "leek",
625							PrefixRewrite:         "/alternate",
626							RequestTimeout:        99 * time.Second,
627							NumRetries:            12345,
628							RetryOnConnectFailure: true,
629							RetryOnStatusCodes:    []uint32{401, 209},
630						},
631					},
632					{
633						Match: &ServiceRouteMatch{
634							HTTP: &ServiceRouteHTTPMatch{
635								PathPrefix: "/foo",
636								Methods:    []string{"GET", "DELETE"},
637								QueryParam: []ServiceRouteHTTPMatchQueryParam{
638									{
639										Name:    "hack1",
640										Present: true,
641									},
642									{
643										Name:  "hack2",
644										Exact: "1",
645									},
646									{
647										Name:  "hack3",
648										Regex: "a.*z",
649									},
650								},
651							},
652						},
653					},
654					{
655						Match: &ServiceRouteMatch{
656							HTTP: &ServiceRouteHTTPMatch{
657								PathRegex: "/foo",
658							},
659						},
660					},
661				},
662			},
663		},
664		{
665			name: "service-splitter: kitchen sink",
666			body: `
667			{
668				"Kind": "service-splitter",
669				"Name": "main",
670				"Meta" : {
671					"foo": "bar",
672					"gir": "zim"
673				},
674				"Splits": [
675				  {
676					"Weight": 99.1,
677					"ServiceSubset": "v1"
678				  },
679				  {
680					"Weight": 0.9,
681					"Service": "other",
682					"Namespace": "alt"
683				  }
684				]
685			}
686			`,
687			expect: &ServiceSplitterConfigEntry{
688				Kind: ServiceSplitter,
689				Name: "main",
690				Meta: map[string]string{
691					"foo": "bar",
692					"gir": "zim",
693				},
694				Splits: []ServiceSplit{
695					{
696						Weight:        99.1,
697						ServiceSubset: "v1",
698					},
699					{
700						Weight:    0.9,
701						Service:   "other",
702						Namespace: "alt",
703					},
704				},
705			},
706		},
707		{
708			name: "service-resolver: subsets with failover",
709			body: `
710			{
711				"Kind": "service-resolver",
712				"Name": "main",
713				"Meta" : {
714					"foo": "bar",
715					"gir": "zim"
716				},
717				"DefaultSubset": "v1",
718				"ConnectTimeout": "15s",
719				"Subsets": {
720					"v1": {
721						"Filter": "Service.Meta.version == v1"
722					},
723					"v2": {
724						"Filter": "Service.Meta.version == v2",
725						"OnlyPassing": true
726					}
727				},
728				"Failover": {
729					"v2": {
730						"Service": "failcopy",
731						"ServiceSubset": "sure",
732						"Namespace": "neighbor",
733						"Datacenters": ["dc5", "dc14"]
734					},
735					"*": {
736						"Datacenters": ["dc7"]
737					}
738				}
739			}`,
740			expect: &ServiceResolverConfigEntry{
741				Kind: "service-resolver",
742				Name: "main",
743				Meta: map[string]string{
744					"foo": "bar",
745					"gir": "zim",
746				},
747				DefaultSubset:  "v1",
748				ConnectTimeout: 15 * time.Second,
749				Subsets: map[string]ServiceResolverSubset{
750					"v1": {
751						Filter: "Service.Meta.version == v1",
752					},
753					"v2": {
754						Filter:      "Service.Meta.version == v2",
755						OnlyPassing: true,
756					},
757				},
758				Failover: map[string]ServiceResolverFailover{
759					"v2": {
760						Service:       "failcopy",
761						ServiceSubset: "sure",
762						Namespace:     "neighbor",
763						Datacenters:   []string{"dc5", "dc14"},
764					},
765					"*": {
766						Datacenters: []string{"dc7"},
767					},
768				},
769			},
770		},
771		{
772			name: "service-resolver: redirect",
773			body: `
774			{
775				"Kind": "service-resolver",
776				"Name": "main",
777				"Redirect": {
778					"Service": "other",
779					"ServiceSubset": "backup",
780					"Namespace": "alt",
781					"Datacenter": "dc9"
782				}
783			}
784			`,
785			expect: &ServiceResolverConfigEntry{
786				Kind: "service-resolver",
787				Name: "main",
788				Redirect: &ServiceResolverRedirect{
789					Service:       "other",
790					ServiceSubset: "backup",
791					Namespace:     "alt",
792					Datacenter:    "dc9",
793				},
794			},
795		},
796		{
797			name: "service-resolver: default",
798			body: `
799			{
800				"Kind": "service-resolver",
801				"Name": "main"
802			}
803			`,
804			expect: &ServiceResolverConfigEntry{
805				Kind: "service-resolver",
806				Name: "main",
807			},
808		},
809		{
810			name: "service-resolver: envoy hash lb kitchen sink",
811			body: `
812			{
813				"Kind": "service-resolver",
814				"Name": "main",
815				"LoadBalancer": {
816					"Policy": "ring_hash",
817					"RingHashConfig": {
818						"MinimumRingSize": 1,
819						"MaximumRingSize": 2
820					},
821					"HashPolicies": [
822						{
823							"Field": "cookie",
824							"FieldValue": "good-cookie",
825							"CookieConfig": {
826								"TTL": "1s",
827								"Path": "/oven"
828							},
829							"Terminal": true
830						},
831						{
832							"Field": "cookie",
833							"FieldValue": "less-good-cookie",
834							"CookieConfig": {
835								"Session": true,
836								"Path": "/toaster"
837							},
838							"Terminal": true
839						},
840						{
841							"Field": "header",
842							"FieldValue": "x-user-id"
843						},
844						{
845							"SourceIP": true
846						}
847					]
848				}
849			}
850			`,
851			expect: &ServiceResolverConfigEntry{
852				Kind: "service-resolver",
853				Name: "main",
854				LoadBalancer: &LoadBalancer{
855					Policy: "ring_hash",
856					RingHashConfig: &RingHashConfig{
857						MinimumRingSize: 1,
858						MaximumRingSize: 2,
859					},
860					HashPolicies: []HashPolicy{
861						{
862							Field:      "cookie",
863							FieldValue: "good-cookie",
864							CookieConfig: &CookieConfig{
865								TTL:  1 * time.Second,
866								Path: "/oven",
867							},
868							Terminal: true,
869						},
870						{
871							Field:      "cookie",
872							FieldValue: "less-good-cookie",
873							CookieConfig: &CookieConfig{
874								Session: true,
875								Path:    "/toaster",
876							},
877							Terminal: true,
878						},
879						{
880							Field:      "header",
881							FieldValue: "x-user-id",
882						},
883						{
884							SourceIP: true,
885						},
886					},
887				},
888			},
889		},
890		{
891			name: "service-resolver: envoy least request kitchen sink",
892			body: `
893			{
894				"Kind": "service-resolver",
895				"Name": "main",
896				"LoadBalancer": {
897					"Policy": "least_request",
898					"LeastRequestConfig": {
899						"ChoiceCount": 2
900					}
901				}
902			}
903			`,
904			expect: &ServiceResolverConfigEntry{
905				Kind: "service-resolver",
906				Name: "main",
907				LoadBalancer: &LoadBalancer{
908					Policy: "least_request",
909					LeastRequestConfig: &LeastRequestConfig{
910						ChoiceCount: 2,
911					},
912				},
913			},
914		},
915		{
916			name: "ingress-gateway",
917			body: `
918			{
919				"Kind": "ingress-gateway",
920				"Name": "ingress-web",
921				"Meta" : {
922					"foo": "bar",
923					"gir": "zim"
924				},
925				"Tls": {
926					"Enabled": true
927				},
928				"Listeners": [
929					{
930						"Port": 8080,
931						"Protocol": "http",
932						"Services": [
933							{
934								"Name": "web",
935								"Namespace": "foo"
936							},
937							{
938								"Name": "db"
939							}
940						]
941					},
942					{
943						"Port": 9999,
944						"Protocol": "tcp",
945						"Services": [
946							{
947								"Name": "mysql"
948							}
949						]
950					}
951				]
952			}
953			`,
954			expect: &IngressGatewayConfigEntry{
955				Kind: "ingress-gateway",
956				Name: "ingress-web",
957				Meta: map[string]string{
958					"foo": "bar",
959					"gir": "zim",
960				},
961				TLS: GatewayTLSConfig{
962					Enabled: true,
963				},
964				Listeners: []IngressListener{
965					{
966						Port:     8080,
967						Protocol: "http",
968						Services: []IngressService{
969							{
970								Name:      "web",
971								Namespace: "foo",
972							},
973							{
974								Name: "db",
975							},
976						},
977					},
978					{
979						Port:     9999,
980						Protocol: "tcp",
981						Services: []IngressService{
982							{
983								Name: "mysql",
984							},
985						},
986					},
987				},
988			},
989		},
990		{
991			name: "terminating-gateway",
992			body: `
993			{
994				"Kind": "terminating-gateway",
995				"Name": "terminating-west",
996				"Meta" : {
997					"foo": "bar",
998					"gir": "zim"
999				},
1000				"Services": [
1001					{
1002						"Namespace": "foo",
1003						"Name": "web",
1004						"CAFile": "/etc/ca.pem",
1005						"CertFile": "/etc/cert.pem",
1006						"KeyFile": "/etc/tls.key",
1007						"SNI": "mydomain"
1008					},
1009					{
1010						"Name": "api"
1011					},
1012					{
1013						"Namespace": "bar",
1014						"Name": "*"
1015					}
1016				]
1017			}`,
1018			expect: &TerminatingGatewayConfigEntry{
1019				Kind: "terminating-gateway",
1020				Name: "terminating-west",
1021				Meta: map[string]string{
1022					"foo": "bar",
1023					"gir": "zim",
1024				},
1025				Services: []LinkedService{
1026					{
1027						Namespace: "foo",
1028						Name:      "web",
1029						CAFile:    "/etc/ca.pem",
1030						CertFile:  "/etc/cert.pem",
1031						KeyFile:   "/etc/tls.key",
1032						SNI:       "mydomain",
1033					},
1034					{
1035						Name: "api",
1036					},
1037					{
1038						Namespace: "bar",
1039						Name:      "*",
1040					},
1041				},
1042			},
1043		},
1044		{
1045			name: "service-intentions: kitchen sink",
1046			body: `
1047			{
1048				"Kind": "service-intentions",
1049				"Name": "web",
1050				"Meta" : {
1051					"foo": "bar",
1052					"gir": "zim"
1053				},
1054				"Sources": [
1055					{
1056						"Name": "foo",
1057						"Action": "deny",
1058						"Type": "consul",
1059						"Description": "foo desc"
1060					},
1061					{
1062						"Name": "bar",
1063						"Action": "allow",
1064						"Description": "bar desc"
1065					},
1066					{
1067						"Name": "l7",
1068						"Permissions": [
1069							{
1070								"Action": "deny",
1071								"HTTP": {
1072									"PathExact": "/admin",
1073									"Header": [
1074										{
1075											"Name": "hdr-present",
1076											"Present": true
1077										},
1078										{
1079											"Name": "hdr-exact",
1080											"Exact": "exact"
1081										},
1082										{
1083											"Name": "hdr-prefix",
1084											"Prefix": "prefix"
1085										},
1086										{
1087											"Name": "hdr-suffix",
1088											"Suffix": "suffix"
1089										},
1090										{
1091											"Name": "hdr-regex",
1092											"Regex": "regex"
1093										},
1094										{
1095											"Name": "hdr-absent",
1096											"Present": true,
1097											"Invert": true
1098										}
1099									]
1100								}
1101							},
1102							{
1103								"Action": "allow",
1104								"HTTP": {
1105									"PathPrefix": "/v3/"
1106								}
1107							},
1108							{
1109								"Action": "allow",
1110								"HTTP": {
1111									"PathRegex": "/v[12]/.*",
1112									"Methods": [
1113										"GET",
1114										"POST"
1115									]
1116								}
1117							}
1118						]
1119					},
1120					{
1121						"Name": "*",
1122						"Action": "deny",
1123						"Description": "wild desc"
1124					}
1125				]
1126			}
1127			`,
1128			expect: &ServiceIntentionsConfigEntry{
1129				Kind: "service-intentions",
1130				Name: "web",
1131				Meta: map[string]string{
1132					"foo": "bar",
1133					"gir": "zim",
1134				},
1135				Sources: []*SourceIntention{
1136					{
1137						Name:        "foo",
1138						Action:      "deny",
1139						Type:        "consul",
1140						Description: "foo desc",
1141					},
1142					{
1143						Name:        "bar",
1144						Action:      "allow",
1145						Description: "bar desc",
1146					},
1147					{
1148						Name: "l7",
1149						Permissions: []*IntentionPermission{
1150							{
1151								Action: "deny",
1152								HTTP: &IntentionHTTPPermission{
1153									PathExact: "/admin",
1154									Header: []IntentionHTTPHeaderPermission{
1155										{
1156											Name:    "hdr-present",
1157											Present: true,
1158										},
1159										{
1160											Name:  "hdr-exact",
1161											Exact: "exact",
1162										},
1163										{
1164											Name:   "hdr-prefix",
1165											Prefix: "prefix",
1166										},
1167										{
1168											Name:   "hdr-suffix",
1169											Suffix: "suffix",
1170										},
1171										{
1172											Name:  "hdr-regex",
1173											Regex: "regex",
1174										},
1175										{
1176											Name:    "hdr-absent",
1177											Present: true,
1178											Invert:  true,
1179										},
1180									},
1181								},
1182							},
1183							{
1184								Action: "allow",
1185								HTTP: &IntentionHTTPPermission{
1186									PathPrefix: "/v3/",
1187								},
1188							},
1189							{
1190								Action: "allow",
1191								HTTP: &IntentionHTTPPermission{
1192									PathRegex: "/v[12]/.*",
1193									Methods:   []string{"GET", "POST"},
1194								},
1195							},
1196						},
1197					},
1198					{
1199						Name:        "*",
1200						Action:      "deny",
1201						Description: "wild desc",
1202					},
1203				},
1204			},
1205		},
1206		{
1207			name: "mesh",
1208			body: `
1209			{
1210				"Kind": "mesh",
1211				"Meta" : {
1212					"foo": "bar",
1213					"gir": "zim"
1214				},
1215				"TransparentProxy": {
1216					"MeshDestinationsOnly": true
1217				}
1218			}
1219			`,
1220			expect: &MeshConfigEntry{
1221				Meta: map[string]string{
1222					"foo": "bar",
1223					"gir": "zim",
1224				},
1225				TransparentProxy: TransparentProxyMeshConfig{
1226					MeshDestinationsOnly: true,
1227				},
1228			},
1229		},
1230	} {
1231		tc := tc
1232
1233		t.Run(tc.name+": DecodeConfigEntry", func(t *testing.T) {
1234			var raw map[string]interface{}
1235			require.NoError(t, json.Unmarshal([]byte(tc.body), &raw))
1236
1237			got, err := DecodeConfigEntry(raw)
1238			require.NoError(t, err)
1239			require.Equal(t, tc.expect, got)
1240		})
1241
1242		t.Run(tc.name+": DecodeConfigEntryFromJSON", func(t *testing.T) {
1243			got, err := DecodeConfigEntryFromJSON([]byte(tc.body))
1244			require.NoError(t, err)
1245			require.Equal(t, tc.expect, got)
1246		})
1247
1248		t.Run(tc.name+": DecodeConfigEntrySlice", func(t *testing.T) {
1249			var raw []map[string]interface{}
1250			require.NoError(t, json.Unmarshal([]byte("["+tc.body+"]"), &raw))
1251
1252			got, err := decodeConfigEntrySlice(raw)
1253			require.NoError(t, err)
1254			require.Len(t, got, 1)
1255			require.Equal(t, tc.expect, got[0])
1256		})
1257	}
1258}
1259
1260func intPointer(v int) *int {
1261	return &v
1262}
1263