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 "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 desc: "repositories_cleanup minimal duration is too low", 203 changeConfig: func(cfg *Config) { 204 cfg.RepositoriesCleanup.CheckInterval = config.Duration(minimalSyncCheckInterval - time.Nanosecond) 205 }, 206 errMsg: `repositories_cleanup.check_interval is less then 1m0s, which could lead to a database performance problem`, 207 }, 208 { 209 desc: "repositories_cleanup minimal duration is too low", 210 changeConfig: func(cfg *Config) { 211 cfg.RepositoriesCleanup.RunInterval = config.Duration(minimalSyncRunInterval - time.Nanosecond) 212 }, 213 errMsg: `repositories_cleanup.run_interval is less then 1m0s, which could lead to a database performance problem`, 214 }, 215 } 216 217 for _, tc := range testCases { 218 t.Run(tc.desc, func(t *testing.T) { 219 config := Config{ 220 ListenAddr: "localhost:1234", 221 Replication: DefaultReplicationConfig(), 222 VirtualStorages: []*VirtualStorage{ 223 {Name: "default", Nodes: vs1Nodes}, 224 {Name: "secondary", Nodes: vs2Nodes}, 225 }, 226 Failover: Failover{ElectionStrategy: ElectionStrategySQL}, 227 RepositoriesCleanup: DefaultRepositoriesCleanup(), 228 } 229 230 tc.changeConfig(&config) 231 232 err := config.Validate() 233 if tc.errMsg == "" { 234 require.NoError(t, err) 235 return 236 } 237 238 require.Error(t, err) 239 require.Contains(t, err.Error(), tc.errMsg) 240 }) 241 } 242} 243 244func TestConfigParsing(t *testing.T) { 245 testCases := []struct { 246 desc string 247 filePath string 248 expected Config 249 expectedErr error 250 }{ 251 { 252 desc: "check all configuration values", 253 filePath: "testdata/config.toml", 254 expected: Config{ 255 TLSListenAddr: "0.0.0.0:2306", 256 TLS: config.TLS{ 257 CertPath: "/home/git/cert.cert", 258 KeyPath: "/home/git/key.pem", 259 }, 260 Logging: log.Config{ 261 Level: "info", 262 Format: "json", 263 }, 264 Sentry: sentry.Config{ 265 DSN: "abcd123", 266 Environment: "production", 267 }, 268 VirtualStorages: []*VirtualStorage{ 269 { 270 Name: "praefect", 271 DefaultReplicationFactor: 2, 272 Nodes: []*Node{ 273 { 274 Address: "tcp://gitaly-internal-1.example.com", 275 Storage: "praefect-internal-1", 276 }, 277 { 278 Address: "tcp://gitaly-internal-2.example.com", 279 Storage: "praefect-internal-2", 280 }, 281 { 282 Address: "tcp://gitaly-internal-3.example.com", 283 Storage: "praefect-internal-3", 284 }, 285 }, 286 }, 287 }, 288 Prometheus: prometheus.Config{ 289 ScrapeTimeout: time.Second, 290 GRPCLatencyBuckets: []float64{0.1, 0.2, 0.3}, 291 }, 292 DB: DB{ 293 Host: "1.2.3.4", 294 Port: 5432, 295 User: "praefect", 296 Password: "db-secret", 297 DBName: "praefect_production", 298 SSLMode: "require", 299 SSLCert: "/path/to/cert", 300 SSLKey: "/path/to/key", 301 SSLRootCert: "/path/to/root-cert", 302 SessionPooled: DBConnection{ 303 Host: "2.3.4.5", 304 Port: 6432, 305 User: "praefect_sp", 306 Password: "db-secret-sp", 307 DBName: "praefect_production_sp", 308 SSLMode: "prefer", 309 SSLCert: "/path/to/sp/cert", 310 SSLKey: "/path/to/sp/key", 311 SSLRootCert: "/path/to/sp/root-cert", 312 }, 313 }, 314 MemoryQueueEnabled: true, 315 GracefulStopTimeout: config.Duration(30 * time.Second), 316 Reconciliation: Reconciliation{ 317 SchedulingInterval: config.Duration(time.Minute), 318 HistogramBuckets: []float64{1, 2, 3, 4, 5}, 319 }, 320 Replication: Replication{BatchSize: 1, ParallelStorageProcessingWorkers: 2}, 321 Failover: Failover{ 322 Enabled: true, 323 ElectionStrategy: ElectionStrategyPerRepository, 324 ErrorThresholdWindow: config.Duration(20 * time.Second), 325 WriteErrorThresholdCount: 1500, 326 ReadErrorThresholdCount: 100, 327 BootstrapInterval: config.Duration(1 * time.Second), 328 MonitorInterval: config.Duration(3 * time.Second), 329 }, 330 RepositoriesCleanup: RepositoriesCleanup{ 331 CheckInterval: config.Duration(time.Second), 332 RunInterval: config.Duration(3 * time.Second), 333 RepositoriesInBatch: 10, 334 }, 335 }, 336 }, 337 { 338 desc: "overwriting default values in the config", 339 filePath: "testdata/config.overwritedefaults.toml", 340 expected: Config{ 341 GracefulStopTimeout: config.Duration(time.Minute), 342 Reconciliation: Reconciliation{ 343 SchedulingInterval: 0, 344 HistogramBuckets: []float64{1, 2, 3, 4, 5}, 345 }, 346 Prometheus: prometheus.DefaultConfig(), 347 Replication: Replication{BatchSize: 1, ParallelStorageProcessingWorkers: 2}, 348 Failover: Failover{ 349 Enabled: false, 350 ElectionStrategy: "local", 351 BootstrapInterval: config.Duration(5 * time.Second), 352 MonitorInterval: config.Duration(10 * time.Second), 353 }, 354 RepositoriesCleanup: RepositoriesCleanup{ 355 CheckInterval: config.Duration(time.Second), 356 RunInterval: config.Duration(4 * time.Second), 357 RepositoriesInBatch: 11, 358 }, 359 }, 360 }, 361 { 362 desc: "empty config yields default values", 363 filePath: "testdata/config.empty.toml", 364 expected: Config{ 365 GracefulStopTimeout: config.Duration(time.Minute), 366 Prometheus: prometheus.DefaultConfig(), 367 Reconciliation: DefaultReconciliationConfig(), 368 Replication: DefaultReplicationConfig(), 369 Failover: Failover{ 370 Enabled: true, 371 ElectionStrategy: ElectionStrategyPerRepository, 372 BootstrapInterval: config.Duration(time.Second), 373 MonitorInterval: config.Duration(3 * time.Second), 374 }, 375 RepositoriesCleanup: RepositoriesCleanup{ 376 CheckInterval: config.Duration(30 * time.Minute), 377 RunInterval: config.Duration(24 * time.Hour), 378 RepositoriesInBatch: 16, 379 }, 380 }, 381 }, 382 { 383 desc: "config file does not exist", 384 filePath: "testdata/config.invalid-path.toml", 385 expectedErr: os.ErrNotExist, 386 }, 387 } 388 389 for _, tc := range testCases { 390 t.Run(tc.desc, func(t *testing.T) { 391 cfg, err := FromFile(tc.filePath) 392 require.True(t, errors.Is(err, tc.expectedErr), "actual error: %v", err) 393 require.Equal(t, tc.expected, cfg) 394 }) 395 } 396} 397 398func TestVirtualStorageNames(t *testing.T) { 399 conf := Config{VirtualStorages: []*VirtualStorage{{Name: "praefect-1"}, {Name: "praefect-2"}}} 400 require.Equal(t, []string{"praefect-1", "praefect-2"}, conf.VirtualStorageNames()) 401} 402 403func TestStorageNames(t *testing.T) { 404 conf := Config{ 405 VirtualStorages: []*VirtualStorage{ 406 {Name: "virtual-storage-1", Nodes: []*Node{{Storage: "gitaly-1"}, {Storage: "gitaly-2"}}}, 407 {Name: "virtual-storage-2", Nodes: []*Node{{Storage: "gitaly-3"}, {Storage: "gitaly-4"}}}, 408 }, 409 } 410 require.Equal(t, map[string][]string{ 411 "virtual-storage-1": {"gitaly-1", "gitaly-2"}, 412 "virtual-storage-2": {"gitaly-3", "gitaly-4"}, 413 }, conf.StorageNames()) 414} 415 416func TestToPQString(t *testing.T) { 417 testCases := []struct { 418 desc string 419 in DB 420 direct bool 421 out string 422 }{ 423 {desc: "empty", in: DB{}, out: "binary_parameters=yes"}, 424 { 425 desc: "proxy connection", 426 in: DB{ 427 Host: "1.2.3.4", 428 Port: 2345, 429 User: "praefect-user", 430 Password: "secret", 431 DBName: "praefect_production", 432 SSLMode: "require", 433 SSLCert: "/path/to/cert", 434 SSLKey: "/path/to/key", 435 SSLRootCert: "/path/to/root-cert", 436 }, 437 direct: false, 438 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`, 439 }, 440 { 441 desc: "direct connection with different host and port", 442 in: DB{ 443 User: "praefect-user", 444 Password: "secret", 445 DBName: "praefect_production", 446 SSLMode: "require", 447 SSLCert: "/path/to/cert", 448 SSLKey: "/path/to/key", 449 SSLRootCert: "/path/to/root-cert", 450 SessionPooled: DBConnection{ 451 Host: "1.2.3.4", 452 Port: 2345, 453 }, 454 }, 455 direct: true, 456 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`, 457 }, 458 { 459 desc: "direct connection with dbname", 460 in: DB{ 461 Host: "1.2.3.4", 462 Port: 2345, 463 User: "praefect-user", 464 Password: "secret", 465 DBName: "praefect_production", 466 SSLMode: "require", 467 SSLCert: "/path/to/cert", 468 SSLKey: "/path/to/key", 469 SSLRootCert: "/path/to/root-cert", 470 SessionPooled: DBConnection{ 471 DBName: "praefect_production_sp", 472 }, 473 }, 474 direct: true, 475 out: `port=2345 host=1.2.3.4 user=praefect-user password=secret dbname=praefect_production_sp sslmode=require sslcert=/path/to/cert sslkey=/path/to/key sslrootcert=/path/to/root-cert binary_parameters=yes`, 476 }, 477 { 478 desc: "direct connection with exactly the same parameters", 479 in: DB{ 480 Host: "1.2.3.4", 481 Port: 2345, 482 User: "praefect-user", 483 Password: "secret", 484 DBName: "praefect_production", 485 SSLMode: "require", 486 SSLCert: "/path/to/cert", 487 SSLKey: "/path/to/key", 488 SSLRootCert: "/path/to/root-cert", 489 SessionPooled: DBConnection{}, 490 }, 491 direct: true, 492 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`, 493 }, 494 { 495 desc: "direct connection with completely different parameters", 496 in: DB{ 497 Host: "1.2.3.4", 498 Port: 2345, 499 User: "praefect-user", 500 Password: "secret", 501 DBName: "praefect_production", 502 SSLMode: "require", 503 SSLCert: "/path/to/cert", 504 SSLKey: "/path/to/key", 505 SSLRootCert: "/path/to/root-cert", 506 SessionPooled: DBConnection{ 507 Host: "2.3.4.5", 508 Port: 6432, 509 User: "praefect_sp", 510 Password: "secret-sp", 511 DBName: "praefect_production_sp", 512 SSLMode: "prefer", 513 SSLCert: "/path/to/sp/cert", 514 SSLKey: "/path/to/sp/key", 515 SSLRootCert: "/path/to/sp/root-cert", 516 }, 517 }, 518 direct: true, 519 out: `port=6432 host=2.3.4.5 user=praefect_sp password=secret-sp dbname=praefect_production_sp sslmode=prefer sslcert=/path/to/sp/cert sslkey=/path/to/sp/key sslrootcert=/path/to/sp/root-cert binary_parameters=yes`, 520 }, 521 { 522 desc: "with spaces and quotes", 523 in: DB{ 524 Password: "secret foo'bar", 525 }, 526 out: `password=secret\ foo\'bar binary_parameters=yes`, 527 }, 528 } 529 530 for _, tc := range testCases { 531 t.Run(tc.desc, func(t *testing.T) { 532 require.Equal(t, tc.out, tc.in.ToPQString(tc.direct)) 533 }) 534 } 535} 536 537func TestDefaultReplicationFactors(t *testing.T) { 538 for _, tc := range []struct { 539 desc string 540 virtualStorages []*VirtualStorage 541 defaultReplicationFactors map[string]int 542 }{ 543 { 544 desc: "replication factors set on some", 545 virtualStorages: []*VirtualStorage{ 546 {Name: "virtual-storage-1", DefaultReplicationFactor: 0}, 547 {Name: "virtual-storage-2", DefaultReplicationFactor: 1}, 548 }, 549 defaultReplicationFactors: map[string]int{ 550 "virtual-storage-1": 0, 551 "virtual-storage-2": 1, 552 }, 553 }, 554 { 555 desc: "returns always initialized map", 556 virtualStorages: []*VirtualStorage{}, 557 defaultReplicationFactors: map[string]int{}, 558 }, 559 } { 560 t.Run(tc.desc, func(t *testing.T) { 561 require.Equal(t, 562 tc.defaultReplicationFactors, 563 Config{VirtualStorages: tc.virtualStorages}.DefaultReplicationFactors(), 564 ) 565 }) 566 } 567} 568 569func TestNeedsSQL(t *testing.T) { 570 testCases := []struct { 571 desc string 572 config Config 573 expected bool 574 }{ 575 { 576 desc: "default", 577 config: Config{}, 578 expected: true, 579 }, 580 { 581 desc: "Memory queue enabled", 582 config: Config{MemoryQueueEnabled: true}, 583 expected: false, 584 }, 585 { 586 desc: "Failover enabled with default election strategy", 587 config: Config{Failover: Failover{Enabled: true}}, 588 expected: true, 589 }, 590 { 591 desc: "Failover enabled with SQL election strategy", 592 config: Config{Failover: Failover{Enabled: true, ElectionStrategy: ElectionStrategyPerRepository}}, 593 expected: true, 594 }, 595 { 596 desc: "Both PostgresQL and SQL election strategy enabled", 597 config: Config{Failover: Failover{Enabled: true, ElectionStrategy: ElectionStrategyPerRepository}}, 598 expected: true, 599 }, 600 { 601 desc: "Both PostgresQL and SQL election strategy enabled but failover disabled", 602 config: Config{Failover: Failover{Enabled: false, ElectionStrategy: ElectionStrategyPerRepository}}, 603 expected: true, 604 }, 605 { 606 desc: "Both PostgresQL and per_repository election strategy enabled but failover disabled", 607 config: Config{Failover: Failover{Enabled: false, ElectionStrategy: ElectionStrategyPerRepository}}, 608 expected: true, 609 }, 610 } 611 612 for _, tc := range testCases { 613 t.Run(tc.desc, func(t *testing.T) { 614 require.Equal(t, tc.expected, tc.config.NeedsSQL()) 615 }) 616 } 617} 618