1package consul
2
3import (
4	"context"
5	"fmt"
6	"math/rand"
7	"os"
8	"reflect"
9	"strings"
10	"sync"
11	"testing"
12	"time"
13
14	"github.com/hashicorp/consul/api"
15	log "github.com/hashicorp/go-hclog"
16	"github.com/hashicorp/vault/helper/testhelpers/consul"
17	"github.com/hashicorp/vault/sdk/helper/logging"
18	"github.com/hashicorp/vault/sdk/helper/strutil"
19	"github.com/hashicorp/vault/sdk/physical"
20)
21
22type consulConf map[string]string
23
24var (
25	addrCount int = 0
26)
27
28func testConsulBackend(t *testing.T) *ConsulBackend {
29	return testConsulBackendConfig(t, &consulConf{})
30}
31
32func testConsulBackendConfig(t *testing.T, conf *consulConf) *ConsulBackend {
33	logger := logging.NewVaultLogger(log.Debug)
34
35	be, err := NewConsulBackend(*conf, logger)
36	if err != nil {
37		t.Fatalf("Expected Consul to initialize: %v", err)
38	}
39
40	c, ok := be.(*ConsulBackend)
41	if !ok {
42		t.Fatalf("Expected ConsulBackend")
43	}
44
45	return c
46}
47
48func testConsul_testConsulBackend(t *testing.T) {
49	c := testConsulBackend(t)
50	if c == nil {
51		t.Fatalf("bad")
52	}
53}
54
55func testActiveFunc(activePct float64) physical.ActiveFunction {
56	return func() bool {
57		var active bool
58		standbyProb := rand.Float64()
59		if standbyProb > activePct {
60			active = true
61		}
62		return active
63	}
64}
65
66func testSealedFunc(sealedPct float64) physical.SealedFunction {
67	return func() bool {
68		var sealed bool
69		unsealedProb := rand.Float64()
70		if unsealedProb > sealedPct {
71			sealed = true
72		}
73		return sealed
74	}
75}
76
77func testPerformanceStandbyFunc(perfPct float64) physical.PerformanceStandbyFunction {
78	return func() bool {
79		var ps bool
80		unsealedProb := rand.Float64()
81		if unsealedProb > perfPct {
82			ps = true
83		}
84		return ps
85	}
86}
87
88func TestConsul_ServiceTags(t *testing.T) {
89	consulConfig := map[string]string{
90		"path":                 "seaTech/",
91		"service":              "astronomy",
92		"service_tags":         "deadbeef, cafeefac, deadc0de, feedface",
93		"redirect_addr":        "http://127.0.0.2:8200",
94		"check_timeout":        "6s",
95		"address":              "127.0.0.2",
96		"scheme":               "https",
97		"token":                "deadbeef-cafeefac-deadc0de-feedface",
98		"max_parallel":         "4",
99		"disable_registration": "false",
100	}
101	logger := logging.NewVaultLogger(log.Debug)
102
103	be, err := NewConsulBackend(consulConfig, logger)
104	if err != nil {
105		t.Fatal(err)
106	}
107
108	c, ok := be.(*ConsulBackend)
109	if !ok {
110		t.Fatalf("failed to create physical Consul backend")
111	}
112
113	expected := []string{"deadbeef", "cafeefac", "deadc0de", "feedface"}
114	actual := c.fetchServiceTags(false, false)
115	if !strutil.EquivalentSlices(actual, append(expected, "standby")) {
116		t.Fatalf("bad: expected:%s actual:%s", append(expected, "standby"), actual)
117	}
118
119	actual = c.fetchServiceTags(true, false)
120	if !strutil.EquivalentSlices(actual, append(expected, "active")) {
121		t.Fatalf("bad: expected:%s actual:%s", append(expected, "active"), actual)
122	}
123
124	actual = c.fetchServiceTags(false, true)
125	if !strutil.EquivalentSlices(actual, append(expected, "performance-standby")) {
126		t.Fatalf("bad: expected:%s actual:%s", append(expected, "performance-standby"), actual)
127	}
128
129	actual = c.fetchServiceTags(true, true)
130	if !strutil.EquivalentSlices(actual, append(expected, "performance-standby")) {
131		t.Fatalf("bad: expected:%s actual:%s", append(expected, "performance-standby"), actual)
132	}
133}
134
135func TestConsul_ServiceAddress(t *testing.T) {
136	tests := []struct {
137		consulConfig   map[string]string
138		serviceAddrNil bool
139	}{
140		{
141			consulConfig: map[string]string{
142				"service_address": "",
143			},
144		},
145		{
146			consulConfig: map[string]string{
147				"service_address": "vault.example.com",
148			},
149		},
150		{
151			serviceAddrNil: true,
152		},
153	}
154
155	for _, test := range tests {
156		logger := logging.NewVaultLogger(log.Debug)
157
158		be, err := NewConsulBackend(test.consulConfig, logger)
159		if err != nil {
160			t.Fatalf("expected Consul to initialize: %v", err)
161		}
162
163		c, ok := be.(*ConsulBackend)
164		if !ok {
165			t.Fatalf("Expected ConsulBackend")
166		}
167
168		if test.serviceAddrNil {
169			if c.serviceAddress != nil {
170				t.Fatalf("expected service address to be nil")
171			}
172		} else {
173			if c.serviceAddress == nil {
174				t.Fatalf("did not expect service address to be nil")
175			}
176		}
177	}
178}
179
180func TestConsul_newConsulBackend(t *testing.T) {
181	tests := []struct {
182		name            string
183		consulConfig    map[string]string
184		fail            bool
185		redirectAddr    string
186		checkTimeout    time.Duration
187		path            string
188		service         string
189		address         string
190		scheme          string
191		token           string
192		max_parallel    int
193		disableReg      bool
194		consistencyMode string
195	}{
196		{
197			name:            "Valid default config",
198			consulConfig:    map[string]string{},
199			checkTimeout:    5 * time.Second,
200			redirectAddr:    "http://127.0.0.1:8200",
201			path:            "vault/",
202			service:         "vault",
203			address:         "127.0.0.1:8500",
204			scheme:          "http",
205			token:           "",
206			max_parallel:    4,
207			disableReg:      false,
208			consistencyMode: "default",
209		},
210		{
211			name: "Valid modified config",
212			consulConfig: map[string]string{
213				"path":                 "seaTech/",
214				"service":              "astronomy",
215				"redirect_addr":        "http://127.0.0.2:8200",
216				"check_timeout":        "6s",
217				"address":              "127.0.0.2",
218				"scheme":               "https",
219				"token":                "deadbeef-cafeefac-deadc0de-feedface",
220				"max_parallel":         "4",
221				"disable_registration": "false",
222				"consistency_mode":     "strong",
223			},
224			checkTimeout:    6 * time.Second,
225			path:            "seaTech/",
226			service:         "astronomy",
227			redirectAddr:    "http://127.0.0.2:8200",
228			address:         "127.0.0.2",
229			scheme:          "https",
230			token:           "deadbeef-cafeefac-deadc0de-feedface",
231			max_parallel:    4,
232			consistencyMode: "strong",
233		},
234		{
235			name: "Unix socket",
236			consulConfig: map[string]string{
237				"address": "unix:///tmp/.consul.http.sock",
238			},
239			address: "/tmp/.consul.http.sock",
240			scheme:  "http", // Default, not overridden?
241
242			// Defaults
243			checkTimeout:    5 * time.Second,
244			redirectAddr:    "http://127.0.0.1:8200",
245			path:            "vault/",
246			service:         "vault",
247			token:           "",
248			max_parallel:    4,
249			disableReg:      false,
250			consistencyMode: "default",
251		},
252		{
253			name: "Scheme in address",
254			consulConfig: map[string]string{
255				"address": "https://127.0.0.2:5000",
256			},
257			address: "127.0.0.2:5000",
258			scheme:  "https",
259
260			// Defaults
261			checkTimeout:    5 * time.Second,
262			redirectAddr:    "http://127.0.0.1:8200",
263			path:            "vault/",
264			service:         "vault",
265			token:           "",
266			max_parallel:    4,
267			disableReg:      false,
268			consistencyMode: "default",
269		},
270		{
271			name: "check timeout too short",
272			fail: true,
273			consulConfig: map[string]string{
274				"check_timeout": "99ms",
275			},
276		},
277	}
278
279	for _, test := range tests {
280		logger := logging.NewVaultLogger(log.Debug)
281
282		be, err := NewConsulBackend(test.consulConfig, logger)
283		if test.fail {
284			if err == nil {
285				t.Fatalf(`Expected config "%s" to fail`, test.name)
286			} else {
287				continue
288			}
289		} else if !test.fail && err != nil {
290			t.Fatalf("Expected config %s to not fail: %v", test.name, err)
291		}
292
293		c, ok := be.(*ConsulBackend)
294		if !ok {
295			t.Fatalf("Expected ConsulBackend: %s", test.name)
296		}
297		c.disableRegistration = true
298
299		if c.disableRegistration == false {
300			addr := os.Getenv("CONSUL_HTTP_ADDR")
301			if addr == "" {
302				continue
303			}
304		}
305
306		var shutdownCh physical.ShutdownChannel
307		waitGroup := &sync.WaitGroup{}
308		if err := c.RunServiceDiscovery(waitGroup, shutdownCh, test.redirectAddr, testActiveFunc(0.5), testSealedFunc(0.5), testPerformanceStandbyFunc(0.5)); err != nil {
309			t.Fatalf("bad: %v", err)
310		}
311
312		if test.checkTimeout != c.checkTimeout {
313			t.Errorf("bad: %v != %v", test.checkTimeout, c.checkTimeout)
314		}
315
316		if test.path != c.path {
317			t.Errorf("bad: %s %v != %v", test.name, test.path, c.path)
318		}
319
320		if test.service != c.serviceName {
321			t.Errorf("bad: %v != %v", test.service, c.serviceName)
322		}
323
324		if test.consistencyMode != c.consistencyMode {
325			t.Errorf("bad consistency_mode value: %v != %v", test.consistencyMode, c.consistencyMode)
326		}
327
328		// The configuration stored in the Consul "client" object is not exported, so
329		// we either have to skip validating it, or add a method to export it, or use reflection.
330		consulConfig := reflect.Indirect(reflect.ValueOf(c.client)).FieldByName("config")
331		consulConfigScheme := consulConfig.FieldByName("Scheme").String()
332		consulConfigAddress := consulConfig.FieldByName("Address").String()
333
334		if test.scheme != consulConfigScheme {
335			t.Errorf("bad scheme value: %v != %v", test.scheme, consulConfigScheme)
336		}
337
338		if test.address != consulConfigAddress {
339			t.Errorf("bad address value: %v != %v", test.address, consulConfigAddress)
340		}
341
342		// FIXME(sean@): Unable to test max_parallel
343		// if test.max_parallel != cap(c.permitPool) {
344		// 	t.Errorf("bad: %v != %v", test.max_parallel, cap(c.permitPool))
345		// }
346	}
347}
348
349func TestConsul_serviceTags(t *testing.T) {
350	tests := []struct {
351		active      bool
352		perfStandby bool
353		tags        []string
354	}{
355		{
356			active:      true,
357			perfStandby: false,
358			tags:        []string{"active"},
359		},
360		{
361			active:      false,
362			perfStandby: false,
363			tags:        []string{"standby"},
364		},
365		{
366			active:      false,
367			perfStandby: true,
368			tags:        []string{"performance-standby"},
369		},
370		{
371			active:      true,
372			perfStandby: true,
373			tags:        []string{"performance-standby"},
374		},
375	}
376
377	c := testConsulBackend(t)
378
379	for _, test := range tests {
380		tags := c.fetchServiceTags(test.active, test.perfStandby)
381		if !reflect.DeepEqual(tags[:], test.tags[:]) {
382			t.Errorf("Bad %v: %v %v", test.active, tags, test.tags)
383		}
384	}
385}
386
387func TestConsul_setRedirectAddr(t *testing.T) {
388	tests := []struct {
389		addr string
390		host string
391		port int64
392		pass bool
393	}{
394		{
395			addr: "http://127.0.0.1:8200/",
396			host: "127.0.0.1",
397			port: 8200,
398			pass: true,
399		},
400		{
401			addr: "http://127.0.0.1:8200",
402			host: "127.0.0.1",
403			port: 8200,
404			pass: true,
405		},
406		{
407			addr: "https://127.0.0.1:8200",
408			host: "127.0.0.1",
409			port: 8200,
410			pass: true,
411		},
412		{
413			addr: "unix:///tmp/.vault.addr.sock",
414			host: "/tmp/.vault.addr.sock",
415			port: -1,
416			pass: true,
417		},
418		{
419			addr: "127.0.0.1:8200",
420			pass: false,
421		},
422		{
423			addr: "127.0.0.1",
424			pass: false,
425		},
426	}
427	for _, test := range tests {
428		c := testConsulBackend(t)
429		err := c.setRedirectAddr(test.addr)
430		if test.pass {
431			if err != nil {
432				t.Fatalf("bad: %v", err)
433			}
434		} else {
435			if err == nil {
436				t.Fatalf("bad, expected fail")
437			} else {
438				continue
439			}
440		}
441
442		if c.redirectHost != test.host {
443			t.Fatalf("bad: %v != %v", c.redirectHost, test.host)
444		}
445
446		if c.redirectPort != test.port {
447			t.Fatalf("bad: %v != %v", c.redirectPort, test.port)
448		}
449	}
450}
451
452func TestConsul_NotifyActiveStateChange(t *testing.T) {
453	c := testConsulBackend(t)
454
455	if err := c.NotifyActiveStateChange(); err != nil {
456		t.Fatalf("bad: %v", err)
457	}
458}
459
460func TestConsul_NotifySealedStateChange(t *testing.T) {
461	c := testConsulBackend(t)
462
463	if err := c.NotifySealedStateChange(); err != nil {
464		t.Fatalf("bad: %v", err)
465	}
466}
467
468func TestConsul_serviceID(t *testing.T) {
469	tests := []struct {
470		name         string
471		redirectAddr string
472		serviceName  string
473		expected     string
474		valid        bool
475	}{
476		{
477			name:         "valid host w/o slash",
478			redirectAddr: "http://127.0.0.1:8200",
479			serviceName:  "sea-tech-astronomy",
480			expected:     "sea-tech-astronomy:127.0.0.1:8200",
481			valid:        true,
482		},
483		{
484			name:         "valid host w/ slash",
485			redirectAddr: "http://127.0.0.1:8200/",
486			serviceName:  "sea-tech-astronomy",
487			expected:     "sea-tech-astronomy:127.0.0.1:8200",
488			valid:        true,
489		},
490		{
491			name:         "valid https host w/ slash",
492			redirectAddr: "https://127.0.0.1:8200/",
493			serviceName:  "sea-tech-astronomy",
494			expected:     "sea-tech-astronomy:127.0.0.1:8200",
495			valid:        true,
496		},
497		{
498			name:         "invalid host name",
499			redirectAddr: "https://127.0.0.1:8200/",
500			serviceName:  "sea_tech_astronomy",
501			expected:     "",
502			valid:        false,
503		},
504	}
505
506	logger := logging.NewVaultLogger(log.Debug)
507
508	for _, test := range tests {
509		be, err := NewConsulBackend(consulConf{
510			"service": test.serviceName,
511		}, logger)
512		if !test.valid {
513			if err == nil {
514				t.Fatalf("expected an error initializing for name %q", test.serviceName)
515			}
516			continue
517		}
518		if test.valid && err != nil {
519			t.Fatalf("expected Consul to initialize: %v", err)
520		}
521
522		c, ok := be.(*ConsulBackend)
523		if !ok {
524			t.Fatalf("Expected ConsulBackend")
525		}
526
527		if err := c.setRedirectAddr(test.redirectAddr); err != nil {
528			t.Fatalf("bad: %s %v", test.name, err)
529		}
530
531		serviceID := c.serviceID()
532		if serviceID != test.expected {
533			t.Fatalf("bad: %v != %v", serviceID, test.expected)
534		}
535	}
536}
537
538func TestConsulBackend(t *testing.T) {
539	consulToken := os.Getenv("CONSUL_HTTP_TOKEN")
540	addr := os.Getenv("CONSUL_HTTP_ADDR")
541	if addr == "" {
542		cleanup, connURL, token := consul.PrepareTestContainer(t, "1.4.4")
543		defer cleanup()
544		addr, consulToken = connURL, token
545	}
546
547	conf := api.DefaultConfig()
548	conf.Address = addr
549	conf.Token = consulToken
550	client, err := api.NewClient(conf)
551	if err != nil {
552		t.Fatalf("err: %v", err)
553	}
554
555	randPath := fmt.Sprintf("vault-%d/", time.Now().Unix())
556	defer func() {
557		client.KV().DeleteTree(randPath, nil)
558	}()
559
560	logger := logging.NewVaultLogger(log.Debug)
561
562	b, err := NewConsulBackend(map[string]string{
563		"address":      conf.Address,
564		"path":         randPath,
565		"max_parallel": "256",
566		"token":        conf.Token,
567	}, logger)
568	if err != nil {
569		t.Fatalf("err: %s", err)
570	}
571
572	physical.ExerciseBackend(t, b)
573	physical.ExerciseBackend_ListPrefix(t, b)
574}
575
576func TestConsul_TooLarge(t *testing.T) {
577	consulToken := os.Getenv("CONSUL_HTTP_TOKEN")
578	addr := os.Getenv("CONSUL_HTTP_ADDR")
579	if addr == "" {
580		cleanup, connURL, token := consul.PrepareTestContainer(t, "1.4.4")
581		defer cleanup()
582		addr, consulToken = connURL, token
583	}
584
585	conf := api.DefaultConfig()
586	conf.Address = addr
587	conf.Token = consulToken
588	client, err := api.NewClient(conf)
589	if err != nil {
590		t.Fatalf("err: %v", err)
591	}
592
593	randPath := fmt.Sprintf("vault-%d/", time.Now().Unix())
594	defer func() {
595		client.KV().DeleteTree(randPath, nil)
596	}()
597
598	logger := logging.NewVaultLogger(log.Debug)
599
600	b, err := NewConsulBackend(map[string]string{
601		"address":      conf.Address,
602		"path":         randPath,
603		"max_parallel": "256",
604		"token":        conf.Token,
605	}, logger)
606	if err != nil {
607		t.Fatalf("err: %s", err)
608	}
609
610	zeros := make([]byte, 600000, 600000)
611	n, err := rand.Read(zeros)
612	if n != 600000 {
613		t.Fatalf("expected 500k zeros, read %d", n)
614	}
615	if err != nil {
616		t.Fatal(err)
617	}
618
619	err = b.Put(context.Background(), &physical.Entry{
620		Key:   "foo",
621		Value: zeros,
622	})
623	if err == nil {
624		t.Fatal("expected error")
625	}
626	if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
627		t.Fatalf("expected value too large error, got %v", err)
628	}
629
630	err = b.(physical.Transactional).Transaction(context.Background(), []*physical.TxnEntry{
631		{
632			Operation: physical.PutOperation,
633			Entry: &physical.Entry{
634				Key:   "foo",
635				Value: zeros,
636			},
637		},
638	})
639	if err == nil {
640		t.Fatal("expected error")
641	}
642	if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
643		t.Fatalf("expected value too large error, got %v", err)
644	}
645}
646
647func TestConsulHABackend(t *testing.T) {
648	consulToken := os.Getenv("CONSUL_HTTP_TOKEN")
649	addr := os.Getenv("CONSUL_HTTP_ADDR")
650	if addr == "" {
651		cleanup, connURL, token := consul.PrepareTestContainer(t, "1.4.4")
652		defer cleanup()
653		addr, consulToken = connURL, token
654	}
655
656	conf := api.DefaultConfig()
657	conf.Address = addr
658	conf.Token = consulToken
659	client, err := api.NewClient(conf)
660	if err != nil {
661		t.Fatalf("err: %v", err)
662	}
663
664	randPath := fmt.Sprintf("vault-%d/", time.Now().Unix())
665	defer func() {
666		client.KV().DeleteTree(randPath, nil)
667	}()
668
669	logger := logging.NewVaultLogger(log.Debug)
670	config := map[string]string{
671		"address":      conf.Address,
672		"path":         randPath,
673		"max_parallel": "-1",
674		"token":        conf.Token,
675	}
676
677	b, err := NewConsulBackend(config, logger)
678	if err != nil {
679		t.Fatalf("err: %s", err)
680	}
681
682	b2, err := NewConsulBackend(config, logger)
683	if err != nil {
684		t.Fatalf("err: %s", err)
685	}
686
687	physical.ExerciseHABackend(t, b.(physical.HABackend), b2.(physical.HABackend))
688
689	detect, ok := b.(physical.RedirectDetect)
690	if !ok {
691		t.Fatalf("consul does not implement RedirectDetect")
692	}
693	host, err := detect.DetectHostAddr()
694	if err != nil {
695		t.Fatalf("err: %s", err)
696	}
697	if host == "" {
698		t.Fatalf("bad addr: %v", host)
699	}
700}
701