1package config
2
3import (
4	"errors"
5	"os"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/require"
10	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config"
11	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config/log"
12	gitaly_prometheus "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config/prometheus"
13	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config/sentry"
14)
15
16func TestConfigValidation(t *testing.T) {
17	vs1Nodes := []*Node{
18		{Storage: "internal-1.0", Address: "localhost:23456", Token: "secret-token-1"},
19		{Storage: "internal-2.0", Address: "localhost:23457", Token: "secret-token-1"},
20		{Storage: "internal-3.0", Address: "localhost:23458", Token: "secret-token-1"},
21	}
22
23	vs2Nodes := []*Node{
24		// storage can have same name as storage in another virtual storage, but all addresses must be unique
25		{Storage: "internal-1.0", Address: "localhost:33456", Token: "secret-token-2"},
26		{Storage: "internal-2.1", Address: "localhost:33457", Token: "secret-token-2"},
27		{Storage: "internal-3.1", Address: "localhost:33458", Token: "secret-token-2"},
28	}
29
30	testCases := []struct {
31		desc         string
32		changeConfig func(*Config)
33		errMsg       string
34	}{
35		{
36			desc:         "Valid config with ListenAddr",
37			changeConfig: func(*Config) {},
38		},
39		{
40			desc: "Valid config with local elector",
41			changeConfig: func(cfg *Config) {
42				cfg.Failover.ElectionStrategy = ElectionStrategyLocal
43			},
44		},
45		{
46			desc: "Valid config with per repository elector",
47			changeConfig: func(cfg *Config) {
48				cfg.Failover.ElectionStrategy = ElectionStrategyPerRepository
49			},
50		},
51		{
52			desc: "Invalid election strategy",
53			changeConfig: func(cfg *Config) {
54				cfg.Failover.ElectionStrategy = "invalid-strategy"
55			},
56			errMsg: `invalid election strategy: "invalid-strategy"`,
57		},
58		{
59			desc: "Valid config with TLSListenAddr",
60			changeConfig: func(cfg *Config) {
61				cfg.ListenAddr = ""
62				cfg.TLSListenAddr = "tls://localhost:4321"
63			},
64		},
65		{
66			desc: "Valid config with SocketPath",
67			changeConfig: func(cfg *Config) {
68				cfg.ListenAddr = ""
69				cfg.SocketPath = "/tmp/praefect.socket"
70			},
71		},
72		{
73			desc: "Invalid replication batch size",
74			changeConfig: func(cfg *Config) {
75				cfg.Replication = Replication{BatchSize: 0}
76			},
77			errMsg: "replication batch size was 0 but must be >=1",
78		},
79		{
80			desc: "No ListenAddr or SocketPath or TLSListenAddr",
81			changeConfig: func(cfg *Config) {
82				cfg.ListenAddr = ""
83			},
84			errMsg: "no listen address or socket path configured",
85		},
86		{
87			desc: "No virtual storages",
88			changeConfig: func(cfg *Config) {
89				cfg.VirtualStorages = nil
90			},
91			errMsg: "no virtual storages configured",
92		},
93		{
94			desc: "duplicate storage",
95			changeConfig: func(cfg *Config) {
96				cfg.VirtualStorages = []*VirtualStorage{
97					{
98						Name: "default",
99						Nodes: append(vs1Nodes, &Node{
100							Storage: vs1Nodes[0].Storage,
101							Address: vs1Nodes[1].Address,
102						}),
103					},
104				}
105			},
106			errMsg: `virtual storage "default": internal gitaly storages are not unique`,
107		},
108		{
109			desc: "Node storage has no name",
110			changeConfig: func(cfg *Config) {
111				cfg.VirtualStorages = []*VirtualStorage{
112					{
113						Name: "default",
114						Nodes: []*Node{
115							{
116								Storage: "",
117								Address: "localhost:23456",
118								Token:   "secret-token-1",
119							},
120						},
121					},
122				}
123			},
124			errMsg: `virtual storage "default": all gitaly nodes must have a storage`,
125		},
126		{
127			desc: "Node storage has no address",
128			changeConfig: func(cfg *Config) {
129				cfg.VirtualStorages = []*VirtualStorage{
130					{
131						Name: "default",
132						Nodes: []*Node{
133							{
134								Storage: "internal",
135								Address: "",
136								Token:   "secret-token-1",
137							},
138						},
139					},
140				}
141			},
142			errMsg: `virtual storage "default": all gitaly nodes must have an address`,
143		},
144		{
145			desc: "Virtual storage has no name",
146			changeConfig: func(cfg *Config) {
147				cfg.VirtualStorages = []*VirtualStorage{
148					{Name: "", Nodes: vs1Nodes},
149				}
150			},
151			errMsg: `virtual storages must have a name`,
152		},
153		{
154			desc: "Virtual storage not unique",
155			changeConfig: func(cfg *Config) {
156				cfg.VirtualStorages = []*VirtualStorage{
157					{Name: "default", Nodes: vs1Nodes},
158					{Name: "default", Nodes: vs2Nodes},
159				}
160			},
161			errMsg: `virtual storage "default": virtual storages must have unique names`,
162		},
163		{
164			desc: "Virtual storage has no nodes",
165			changeConfig: func(cfg *Config) {
166				cfg.VirtualStorages = []*VirtualStorage{
167					{Name: "default", Nodes: vs1Nodes},
168					{Name: "secondary", Nodes: nil},
169				}
170			},
171			errMsg: `virtual storage "secondary": no primary gitaly backends configured`,
172		},
173		{
174			desc: "Node storage has address duplicate",
175			changeConfig: func(cfg *Config) {
176				cfg.VirtualStorages = []*VirtualStorage{
177					{Name: "default", Nodes: vs1Nodes},
178					{Name: "secondary", Nodes: append(vs2Nodes, vs1Nodes[1])},
179				}
180			},
181			errMsg: `multiple storages have the same address`,
182		},
183		{
184			desc: "default replication factor too high",
185			changeConfig: func(cfg *Config) {
186				cfg.VirtualStorages = []*VirtualStorage{
187					{
188						Name:                     "default",
189						DefaultReplicationFactor: 2,
190						Nodes: []*Node{
191							{
192								Storage: "storage-1",
193								Address: "localhost:23456",
194							},
195						},
196					},
197				}
198			},
199			errMsg: `virtual storage "default" has a default replication factor (2) which is higher than the number of storages (1)`,
200		},
201	}
202
203	for _, tc := range testCases {
204		t.Run(tc.desc, func(t *testing.T) {
205			config := Config{
206				ListenAddr:  "localhost:1234",
207				Replication: DefaultReplicationConfig(),
208				VirtualStorages: []*VirtualStorage{
209					{Name: "default", Nodes: vs1Nodes},
210					{Name: "secondary", Nodes: vs2Nodes},
211				},
212				Failover: Failover{ElectionStrategy: ElectionStrategySQL},
213			}
214
215			tc.changeConfig(&config)
216
217			err := config.Validate()
218			if tc.errMsg == "" {
219				require.NoError(t, err)
220				return
221			}
222
223			require.Error(t, err)
224			require.Contains(t, err.Error(), tc.errMsg)
225		})
226	}
227}
228
229func TestConfigParsing(t *testing.T) {
230	testCases := []struct {
231		desc        string
232		filePath    string
233		expected    Config
234		expectedErr error
235	}{
236		{
237			desc:     "check all configuration values",
238			filePath: "testdata/config.toml",
239			expected: Config{
240				TLSListenAddr: "0.0.0.0:2306",
241				TLS: config.TLS{
242					CertPath: "/home/git/cert.cert",
243					KeyPath:  "/home/git/key.pem",
244				},
245				Logging: log.Config{
246					Level:  "info",
247					Format: "json",
248				},
249				Sentry: sentry.Config{
250					DSN:         "abcd123",
251					Environment: "production",
252				},
253				VirtualStorages: []*VirtualStorage{
254					&VirtualStorage{
255						Name:                     "praefect",
256						DefaultReplicationFactor: 2,
257						Nodes: []*Node{
258							&Node{
259								Address: "tcp://gitaly-internal-1.example.com",
260								Storage: "praefect-internal-1",
261							},
262							{
263								Address: "tcp://gitaly-internal-2.example.com",
264								Storage: "praefect-internal-2",
265							},
266							{
267								Address: "tcp://gitaly-internal-3.example.com",
268								Storage: "praefect-internal-3",
269							},
270						},
271					},
272				},
273				Prometheus: gitaly_prometheus.Config{
274					GRPCLatencyBuckets: []float64{0.1, 0.2, 0.3},
275				},
276				DB: DB{
277					Host:        "1.2.3.4",
278					Port:        5432,
279					User:        "praefect",
280					Password:    "db-secret",
281					DBName:      "praefect_production",
282					SSLMode:     "require",
283					SSLCert:     "/path/to/cert",
284					SSLKey:      "/path/to/key",
285					SSLRootCert: "/path/to/root-cert",
286					HostNoProxy: "2.3.4.5",
287					PortNoProxy: 6432,
288				},
289				MemoryQueueEnabled:  true,
290				GracefulStopTimeout: config.Duration(30 * time.Second),
291				Reconciliation: Reconciliation{
292					SchedulingInterval: config.Duration(time.Minute),
293					HistogramBuckets:   []float64{1, 2, 3, 4, 5},
294				},
295				Replication: Replication{BatchSize: 1},
296				Failover: Failover{
297					Enabled:                  true,
298					ElectionStrategy:         ElectionStrategyPerRepository,
299					ErrorThresholdWindow:     config.Duration(20 * time.Second),
300					WriteErrorThresholdCount: 1500,
301					ReadErrorThresholdCount:  100,
302					BootstrapInterval:        config.Duration(1 * time.Second),
303					MonitorInterval:          config.Duration(3 * time.Second),
304				},
305			},
306		},
307		{
308			desc:     "overwriting default values in the config",
309			filePath: "testdata/config.overwritedefaults.toml",
310			expected: Config{
311				GracefulStopTimeout: config.Duration(time.Minute),
312				Reconciliation: Reconciliation{
313					SchedulingInterval: 0,
314					HistogramBuckets:   []float64{1, 2, 3, 4, 5},
315				},
316				Replication: Replication{BatchSize: 1},
317				Failover: Failover{
318					Enabled:           false,
319					ElectionStrategy:  "local",
320					BootstrapInterval: config.Duration(5 * time.Second),
321					MonitorInterval:   config.Duration(10 * time.Second),
322				},
323			},
324		},
325		{
326			desc:     "empty config yields default values",
327			filePath: "testdata/config.empty.toml",
328			expected: Config{
329				GracefulStopTimeout: config.Duration(time.Minute),
330				Reconciliation:      DefaultReconciliationConfig(),
331				Replication:         DefaultReplicationConfig(),
332				Failover: Failover{
333					Enabled:           true,
334					ElectionStrategy:  ElectionStrategyPerRepository,
335					BootstrapInterval: config.Duration(time.Second),
336					MonitorInterval:   config.Duration(3 * time.Second),
337				},
338			},
339		},
340		{
341			desc:        "config file does not exist",
342			filePath:    "testdata/config.invalid-path.toml",
343			expectedErr: os.ErrNotExist,
344		},
345	}
346
347	for _, tc := range testCases {
348		t.Run(tc.desc, func(t *testing.T) {
349			cfg, err := FromFile(tc.filePath)
350			require.True(t, errors.Is(err, tc.expectedErr), "actual error: %v", err)
351			require.Equal(t, tc.expected, cfg)
352		})
353	}
354}
355
356func TestVirtualStorageNames(t *testing.T) {
357	conf := Config{VirtualStorages: []*VirtualStorage{{Name: "praefect-1"}, {Name: "praefect-2"}}}
358	require.Equal(t, []string{"praefect-1", "praefect-2"}, conf.VirtualStorageNames())
359}
360
361func TestStorageNames(t *testing.T) {
362	conf := Config{
363		VirtualStorages: []*VirtualStorage{
364			{Name: "virtual-storage-1", Nodes: []*Node{{Storage: "gitaly-1"}, {Storage: "gitaly-2"}}},
365			{Name: "virtual-storage-2", Nodes: []*Node{{Storage: "gitaly-3"}, {Storage: "gitaly-4"}}},
366		}}
367	require.Equal(t, map[string][]string{
368		"virtual-storage-1": {"gitaly-1", "gitaly-2"},
369		"virtual-storage-2": {"gitaly-3", "gitaly-4"},
370	}, conf.StorageNames())
371}
372
373func TestToPQString(t *testing.T) {
374	testCases := []struct {
375		desc   string
376		in     DB
377		direct bool
378		out    string
379	}{
380		{desc: "empty", in: DB{}, out: "binary_parameters=yes"},
381		{
382			desc: "proxy connection",
383			in: DB{
384				Host:        "1.2.3.4",
385				Port:        2345,
386				User:        "praefect-user",
387				Password:    "secret",
388				DBName:      "praefect_production",
389				SSLMode:     "require",
390				SSLCert:     "/path/to/cert",
391				SSLKey:      "/path/to/key",
392				SSLRootCert: "/path/to/root-cert",
393			},
394			direct: false,
395			out:    `port=2345 host=1.2.3.4 user=praefect-user password=secret dbname=praefect_production sslmode=require sslcert=/path/to/cert sslkey=/path/to/key sslrootcert=/path/to/root-cert binary_parameters=yes`,
396		},
397		{
398			desc: "direct connection",
399			in: DB{
400				HostNoProxy: "1.2.3.4",
401				PortNoProxy: 2345,
402				User:        "praefect-user",
403				Password:    "secret",
404				DBName:      "praefect_production",
405				SSLMode:     "require",
406				SSLCert:     "/path/to/cert",
407				SSLKey:      "/path/to/key",
408				SSLRootCert: "/path/to/root-cert",
409			},
410			direct: true,
411			out:    `port=2345 host=1.2.3.4 user=praefect-user password=secret dbname=praefect_production sslmode=require sslcert=/path/to/cert sslkey=/path/to/key sslrootcert=/path/to/root-cert binary_parameters=yes`,
412		},
413		{
414			desc: "direct connection host not set",
415			in: DB{
416				HostNoProxy: "",
417				PortNoProxy: 2345,
418			},
419			direct: true,
420			out:    "",
421		},
422		{
423			desc: "direct connection port not set",
424			in: DB{
425				HostNoProxy: "localhost",
426				PortNoProxy: 0,
427			},
428			direct: true,
429			out:    "",
430		},
431		{
432			desc: "with spaces and quotes",
433			in: DB{
434				Password: "secret foo'bar",
435			},
436			out: `password=secret\ foo\'bar binary_parameters=yes`,
437		},
438	}
439
440	for _, tc := range testCases {
441		t.Run(tc.desc, func(t *testing.T) {
442			require.Equal(t, tc.out, tc.in.ToPQString(tc.direct))
443		})
444	}
445}
446
447func TestDefaultReplicationFactors(t *testing.T) {
448	for _, tc := range []struct {
449		desc                      string
450		virtualStorages           []*VirtualStorage
451		defaultReplicationFactors map[string]int
452	}{
453		{
454			desc: "replication factors set on some",
455			virtualStorages: []*VirtualStorage{
456				{Name: "virtual-storage-1", DefaultReplicationFactor: 0},
457				{Name: "virtual-storage-2", DefaultReplicationFactor: 1},
458			},
459			defaultReplicationFactors: map[string]int{
460				"virtual-storage-1": 0,
461				"virtual-storage-2": 1,
462			},
463		},
464		{
465			desc:                      "returns always initialized map",
466			virtualStorages:           []*VirtualStorage{},
467			defaultReplicationFactors: map[string]int{},
468		},
469	} {
470		t.Run(tc.desc, func(t *testing.T) {
471			require.Equal(t,
472				tc.defaultReplicationFactors,
473				Config{VirtualStorages: tc.virtualStorages}.DefaultReplicationFactors(),
474			)
475		})
476	}
477}
478
479func TestNeedsSQL(t *testing.T) {
480	testCases := []struct {
481		desc     string
482		config   Config
483		expected bool
484	}{
485		{
486			desc:     "default",
487			config:   Config{},
488			expected: true,
489		},
490		{
491			desc:     "Memory queue enabled",
492			config:   Config{MemoryQueueEnabled: true},
493			expected: false,
494		},
495		{
496			desc:     "Failover enabled with default election strategy",
497			config:   Config{Failover: Failover{Enabled: true}},
498			expected: true,
499		},
500		{
501			desc:     "Failover enabled with SQL election strategy",
502			config:   Config{Failover: Failover{Enabled: true, ElectionStrategy: ElectionStrategyPerRepository}},
503			expected: true,
504		},
505		{
506			desc:     "Both PostgresQL and SQL election strategy enabled",
507			config:   Config{Failover: Failover{Enabled: true, ElectionStrategy: ElectionStrategyPerRepository}},
508			expected: true,
509		},
510		{
511			desc:     "Both PostgresQL and SQL election strategy enabled but failover disabled",
512			config:   Config{Failover: Failover{Enabled: false, ElectionStrategy: ElectionStrategyPerRepository}},
513			expected: true,
514		},
515		{
516			desc:     "Both PostgresQL and per_repository election strategy enabled but failover disabled",
517			config:   Config{Failover: Failover{Enabled: false, ElectionStrategy: ElectionStrategyPerRepository}},
518			expected: true,
519		},
520	}
521
522	for _, tc := range testCases {
523		t.Run(tc.desc, func(t *testing.T) {
524			require.Equal(t, tc.expected, tc.config.NeedsSQL())
525		})
526	}
527}
528