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