1// This file and its contents are licensed under the Apache License 2.0. 2// Please see the included NOTICE for copyright information and 3// LICENSE for a copy of the license 4 5package testhelpers 6 7import ( 8 "context" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "runtime" 15 "testing" 16 "time" 17 18 "github.com/docker/go-connections/nat" 19 "github.com/jackc/pgconn" 20 "github.com/jackc/pgx/v4" 21 "github.com/jackc/pgx/v4/pgxpool" 22 _ "github.com/jackc/pgx/v4/stdlib" 23 "github.com/stretchr/testify/require" 24 "github.com/testcontainers/testcontainers-go" 25 "github.com/testcontainers/testcontainers-go/wait" 26 "github.com/timescale/promscale/pkg/pgmodel/common/schema" 27) 28 29const ( 30 defaultDB = "postgres" 31 connectTemplate = "postgres://%s:password@%s:%d/%s" 32 33 postgresUser = "postgres" 34 promUser = "prom" 35 emptyPromConfig = "global:\n scrape_interval: 10s\nstorage:\n exemplars:\n max_exemplars: 100000" 36 37 Superuser = true 38 NoSuperuser = false 39 40 LatestDBWithPromscaleImageBase = "timescaledev/promscale-extension" 41 LatestDBHAPromscaleImageBase = "timescale/timescaledb-ha" 42) 43 44var ( 45 users = []string{ 46 promUser, 47 "prom_reader_user", 48 "prom_writer_user", 49 "prom_modifier_user", 50 "prom_admin_user", 51 "prom_maintenance_user", 52 } 53) 54 55type ExtensionState int 56 57const ( 58 timescaleBit = 1 << iota 59 promscaleBit = 1 << iota 60 timescale2Bit = 1 << iota 61 multinodeBit = 1 << iota 62 postgres12Bit = 1 << iota 63 timescaleOSSBit = 1 << iota 64 timescaleNightlyBit = 1 << iota 65) 66 67const ( 68 VanillaPostgres ExtensionState = 0 69 Timescale1 ExtensionState = timescaleBit 70 Timescale1AndPromscale ExtensionState = timescaleBit | promscaleBit 71 Timescale2 ExtensionState = timescaleBit | timescale2Bit 72 Timescale2AndPromscale ExtensionState = timescaleBit | timescale2Bit | promscaleBit 73 Multinode ExtensionState = timescaleBit | timescale2Bit | multinodeBit 74 MultinodeAndPromscale ExtensionState = timescaleBit | timescale2Bit | multinodeBit | promscaleBit 75 TimescaleOSS ExtensionState = timescaleBit | timescale2Bit | timescaleOSSBit 76 77 TimescaleNightly ExtensionState = timescaleBit | timescale2Bit | timescaleNightlyBit 78 TimescaleNightlyMultinode ExtensionState = timescaleBit | timescale2Bit | multinodeBit | timescaleNightlyBit 79) 80 81func (e *ExtensionState) UsePromscale() { 82 *e |= timescaleBit | promscaleBit 83} 84 85func (e *ExtensionState) UseTimescaleDB() { 86 *e |= timescaleBit 87} 88 89func (e *ExtensionState) UseTimescaleDB2() { 90 *e |= timescaleBit | timescale2Bit 91} 92 93func (e *ExtensionState) UseTimescaleNightly() { 94 *e |= timescaleBit | timescale2Bit | timescaleNightlyBit 95} 96 97func (e *ExtensionState) UseTimescaleNightlyMultinode() { 98 *e |= timescaleBit | timescale2Bit | multinodeBit | timescaleNightlyBit 99} 100 101func (e *ExtensionState) UseMultinode() { 102 *e |= timescaleBit | timescale2Bit | multinodeBit 103} 104 105func (e *ExtensionState) UsePG12() { 106 *e |= postgres12Bit 107} 108 109func (e *ExtensionState) UseTimescaleDBOSS() { 110 *e |= timescaleBit | timescale2Bit | timescaleOSSBit 111} 112 113func (e ExtensionState) UsesTimescaleDB() bool { 114 return (e & timescaleBit) != 0 115} 116 117func (e ExtensionState) UsesTimescale2() bool { 118 return (e & timescale2Bit) != 0 119} 120 121func (e ExtensionState) UsesTimescaleNightly() bool { 122 return (e & timescaleNightlyBit) != 0 123} 124 125func (e ExtensionState) UsesMultinode() bool { 126 return (e & multinodeBit) != 0 127} 128 129func (e ExtensionState) usesPromscale() bool { 130 return (e & promscaleBit) != 0 131} 132 133func (e ExtensionState) UsesPG12() bool { 134 return (e & postgres12Bit) != 0 135} 136 137func (e ExtensionState) UsesTimescaleDBOSS() bool { 138 return (e & timescaleOSSBit) != 0 139} 140 141var ( 142 pgHost = "localhost" 143 pgPort nat.Port = "5432/tcp" 144) 145 146type SuperuserStatus = bool 147 148func PgConnectURL(dbName string, superuser SuperuserStatus) string { 149 user := postgresUser 150 if !superuser { 151 user = promUser 152 } 153 return PgConnectURLUser(dbName, user) 154} 155 156func PgConnectURLUser(dbName string, user string) string { 157 return fmt.Sprintf(connectTemplate, user, pgHost, pgPort.Int(), dbName) 158} 159 160func getRoleUser(role string) string { 161 return role + "_user" 162} 163func setupRole(t testing.TB, dbName string, role string) { 164 user := getRoleUser(role) 165 dbOwner, err := pgx.Connect(context.Background(), PgConnectURL(dbName, NoSuperuser)) 166 require.NoError(t, err) 167 defer dbOwner.Close(context.Background()) 168 169 _, err = dbOwner.Exec(context.Background(), fmt.Sprintf("CALL "+schema.Catalog+".execute_everywhere(NULL, $$ GRANT %s TO %s $$);", role, user)) 170 require.NoError(t, err) 171} 172 173func PgxPoolWithRole(t testing.TB, dbName string, role string) *pgxpool.Pool { 174 user := getRoleUser(role) 175 setupRole(t, dbName, role) 176 pool, err := pgxpool.Connect(context.Background(), PgConnectURLUser(dbName, user)) 177 require.NoError(t, err) 178 return pool 179} 180 181// WithDB establishes a database for testing and calls the callback 182func WithDB(t testing.TB, DBName string, superuser SuperuserStatus, deferNode2Setup bool, extensionState ExtensionState, f func(db *pgxpool.Pool, t testing.TB, connectString string)) { 183 db, err := DbSetup(DBName, superuser, deferNode2Setup, extensionState) 184 if err != nil { 185 t.Fatal(err) 186 return 187 } 188 defer db.Close() 189 f(db, t, PgConnectURL(DBName, superuser)) 190} 191 192func GetReadOnlyConnection(t testing.TB, DBName string) *pgxpool.Pool { 193 role := "prom_reader" 194 user := getRoleUser(role) 195 setupRole(t, DBName, role) 196 197 pgConfig, err := pgxpool.ParseConfig(PgConnectURLUser(DBName, user)) 198 require.NoError(t, err) 199 200 pgConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { 201 _, err := conn.Exec(context.Background(), "SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY") 202 return err 203 } 204 205 dbPool, err := pgxpool.ConnectConfig(context.Background(), pgConfig) 206 if err != nil { 207 t.Fatal(err) 208 } 209 210 return dbPool 211} 212 213func DbSetup(DBName string, superuser SuperuserStatus, deferNode2Setup bool, extensionState ExtensionState) (*pgxpool.Pool, error) { 214 defaultDb, err := pgx.Connect(context.Background(), PgConnectURL(defaultDB, Superuser)) 215 if err != nil { 216 return nil, err 217 } 218 err = func() error { 219 _, err = defaultDb.Exec(context.Background(), fmt.Sprintf("DROP DATABASE IF EXISTS %s", DBName)) 220 if err != nil { 221 return err 222 } 223 224 if extensionState.UsesMultinode() { 225 dropDistributed := "CALL distributed_exec($$ DROP DATABASE IF EXISTS %s $$, transactional => false)" 226 _, err = defaultDb.Exec(context.Background(), fmt.Sprintf(dropDistributed, DBName)) 227 if err != nil { 228 return err 229 } 230 } 231 232 _, err = defaultDb.Exec(context.Background(), fmt.Sprintf("CREATE DATABASE %s OWNER %s", DBName, promUser)) 233 if err != nil { 234 return err 235 } 236 237 return nil 238 }() 239 240 if err != nil { 241 _ = defaultDb.Close(context.Background()) 242 return nil, err 243 } 244 245 err = defaultDb.Close(context.Background()) 246 if err != nil { 247 return nil, err 248 } 249 250 ourDb, err := pgx.Connect(context.Background(), PgConnectURL(DBName, Superuser)) 251 if err != nil { 252 return nil, err 253 } 254 255 if extensionState.UsesMultinode() { 256 // Multinode requires the administrator to set up data nodes, so in 257 // that case timescaledb should always be installed by the time the 258 // connector runs. We just set up the data nodes here. 259 err = AddDataNode1(ourDb, DBName) 260 if err == nil && !deferNode2Setup { 261 err = AddDataNode2(ourDb, DBName) 262 } 263 if err != nil { 264 _ = ourDb.Close(context.Background()) 265 return nil, err 266 } 267 } else { 268 // some docker images may have timescaledb installed in a template, 269 // but we want to test our own timescale installation. 270 // (Multinode requires the administrator to set up data nodes, so in 271 // that case timescaledb should always be installed by the time the 272 // connector runs) 273 _, err = ourDb.Exec(context.Background(), "DROP EXTENSION IF EXISTS timescaledb") 274 if err != nil { 275 _ = ourDb.Close(context.Background()) 276 return nil, err 277 } 278 } 279 280 if err = ourDb.Close(context.Background()); err != nil { 281 return nil, err 282 } 283 284 dbPool, err := pgxpool.Connect(context.Background(), PgConnectURL(DBName, superuser)) 285 if err != nil { 286 return nil, err 287 } 288 return dbPool, nil 289} 290 291type stdoutLogConsumer struct { 292} 293 294func (s stdoutLogConsumer) Accept(l testcontainers.Log) { 295 if l.LogType == testcontainers.StderrLog { 296 fmt.Print(l.LogType, " ", string(l.Content)) 297 } else { 298 fmt.Print(string(l.Content)) 299 } 300} 301 302// StartPGContainer starts a postgreSQL container for use in testing 303func StartPGContainer( 304 ctx context.Context, 305 extensionState ExtensionState, 306 testDataDir string, 307 printLogs bool, 308) (testcontainers.Container, io.Closer, error) { 309 var image string 310 PGMajor := "13" 311 if extensionState.UsesPG12() { 312 PGMajor = "12" 313 } 314 PGTag := "pg" + PGMajor 315 316 switch extensionState &^ postgres12Bit { 317 case Timescale1: 318 if PGMajor != "12" { 319 return nil, nil, fmt.Errorf("timescaledb 1.x requires pg12") 320 } 321 image = "timescale/timescaledb:1.7.4-pg12" 322 case Timescale1AndPromscale: 323 if PGMajor != "12" { 324 return nil, nil, fmt.Errorf("timescaledb 1.x requires pg12") 325 } 326 image = LatestDBWithPromscaleImageBase + ":latest-ts1-pg12" 327 case Timescale2, Multinode: 328 image = "timescale/timescaledb:latest-" + PGTag 329 case Timescale2AndPromscale, MultinodeAndPromscale: 330 image = LatestDBHAPromscaleImageBase + ":" + PGTag + "-latest" 331 case VanillaPostgres: 332 image = "postgres:" + PGMajor 333 case TimescaleOSS: 334 image = "timescale/timescaledb:latest-" + PGTag + "-oss" 335 case TimescaleNightly, TimescaleNightlyMultinode: 336 image = "timescaledev/timescaledb:nightly-" + PGTag 337 } 338 339 return StartDatabaseImage(ctx, image, testDataDir, "", printLogs, extensionState) 340} 341 342func StartDatabaseImage(ctx context.Context, 343 image string, 344 testDataDir string, 345 dataDir string, 346 printLogs bool, 347 extensionState ExtensionState, 348) (testcontainers.Container, io.Closer, error) { 349 c := CloseAll{} 350 var networks []string 351 if extensionState.UsesMultinode() { 352 networkName, closer, err := createMultinodeNetwork() 353 if err != nil { 354 return nil, c, err 355 } 356 c.Append(closer) 357 networks = []string{networkName} 358 } 359 360 startContainer := func(containerPort nat.Port) (testcontainers.Container, error) { 361 container, closer, err := startPGInstance( 362 ctx, 363 image, 364 testDataDir, 365 dataDir, 366 extensionState, 367 printLogs, 368 networks, 369 containerPort, 370 ) 371 if closer != nil { 372 c.Append(closer) 373 } 374 return container, err 375 } 376 377 if extensionState.UsesMultinode() { 378 containerPort := nat.Port("5433/tcp") 379 _, err := startContainer(containerPort) 380 if err != nil { 381 _ = c.Close() 382 return nil, nil, err 383 } 384 containerPort = nat.Port("5434/tcp") 385 _, err = startContainer(containerPort) 386 if err != nil { 387 _ = c.Close() 388 return nil, nil, err 389 } 390 } 391 392 containerPort := nat.Port("5432/tcp") 393 container, err := startContainer(containerPort) 394 if err != nil { 395 _ = c.Close() 396 return nil, nil, err 397 } 398 if extensionState.UsesMultinode() { 399 /* Setting up the cluster on the default db allows us to run distributed_exec commands on all nodes. 400 * Currently, it's used for dropping databases, although may be used for other things in future. */ 401 db, err := pgx.Connect(context.Background(), PgConnectURL(defaultDB, Superuser)) 402 if err != nil { 403 _ = c.Close() 404 return nil, nil, err 405 } 406 407 err = AddDataNode1(db, defaultDB) 408 if err == nil { 409 err = AddDataNode2(db, defaultDB) 410 } 411 if err != nil { 412 _ = c.Close() 413 return nil, nil, fmt.Errorf("error adding nodes: %w", err) 414 } 415 416 err = db.Close(context.Background()) 417 if err != nil { 418 _ = c.Close() 419 return nil, nil, err 420 } 421 } 422 423 return container, c, nil 424} 425 426func createMultinodeNetwork() (networkName string, closer func(), err error) { 427 networkName = "promscale-network" 428 networkRequest := testcontainers.GenericNetworkRequest{ 429 NetworkRequest: testcontainers.NetworkRequest{ 430 Driver: "bridge", 431 Name: networkName, 432 Attachable: true, 433 }, 434 } 435 network, err := testcontainers.GenericNetwork(context.Background(), networkRequest) 436 if err != nil { 437 return "", nil, err 438 } 439 closer = func() { _ = network.Remove(context.Background()) } 440 return networkName, closer, nil 441} 442 443func startPGInstance( 444 ctx context.Context, 445 image string, 446 testDataDir string, 447 dataDir string, 448 extensionState ExtensionState, 449 printLogs bool, 450 networks []string, 451 containerPort nat.Port, 452) (testcontainers.Container, func(), error) { 453 // we map the access node to port 5432 and the others to other ports 454 isDataNode := containerPort.Port() != "5432" 455 nodeType := "AN" 456 if isDataNode { 457 nodeType = "DN" + containerPort.Port() 458 } 459 460 req := testcontainers.ContainerRequest{ 461 Image: image, 462 ExposedPorts: []string{string(containerPort)}, 463 WaitingFor: wait.ForSQL(containerPort, "pgx", func(port nat.Port) string { 464 return "dbname=postgres password=password user=postgres host=127.0.0.1 port=" + port.Port() 465 }).Timeout(120 * time.Second), 466 Env: map[string]string{ 467 "POSTGRES_PASSWORD": "password", 468 "PGDATA": "/var/lib/postgresql/data", 469 }, 470 Networks: networks, 471 NetworkAliases: map[string][]string{"promscale-network": {"db" + containerPort.Port()}}, 472 SkipReaper: false, /* switch to true not to kill docker container */ 473 } 474 req.Cmd = []string{ 475 "-c", "max_connections=100", 476 "-c", "port=" + containerPort.Port(), 477 "-c", "max_prepared_transactions=150", 478 "-c", "log_line_prefix=" + nodeType + " %m [%d]", 479 "-i", 480 } 481 482 if extensionState.UsesTimescaleDB() { 483 req.Cmd = append(req.Cmd, 484 "-c", "shared_preload_libraries=timescaledb", 485 "-c", "timescaledb.max_background_workers=0", 486 ) 487 } 488 489 if extensionState.usesPromscale() { 490 req.Cmd = append(req.Cmd, 491 "-c", "local_preload_libraries=pgextwlist", 492 //timescale_prometheus_extra included for upgrade tests with old extension name 493 "-c", "extwlist.extensions=promscale,timescaledb,timescale_prometheus_extra", 494 ) 495 } 496 497 req.BindMounts = make(map[string]string) 498 if testDataDir != "" { 499 req.BindMounts[testDataDir] = "/testdata" 500 } 501 if dataDir != "" { 502 var bindDir string 503 if isDataNode { 504 bindDir = dataDir + "/dn" + containerPort.Port() 505 } else { 506 bindDir = dataDir + "/an" 507 } 508 if err := os.Mkdir(bindDir, 0700); err != nil && !os.IsExist(err) { 509 return nil, nil, err 510 } 511 req.BindMounts[bindDir] = "/var/lib/postgresql/data" 512 } 513 514 container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 515 ContainerRequest: req, 516 Started: false, 517 }) 518 if err != nil { 519 return nil, nil, err 520 } 521 522 if printLogs { 523 container.FollowOutput(stdoutLogConsumer{}) 524 } 525 526 err = container.Start(context.Background()) 527 if err != nil { 528 return nil, nil, err 529 } 530 531 if printLogs { 532 err = container.StartLogProducer(context.Background()) 533 if err != nil { 534 fmt.Println("Error setting up logger", err) 535 os.Exit(1) 536 } 537 } 538 539 closer := func() { 540 StopContainer(ctx, container, printLogs) 541 } 542 543 if isDataNode { 544 err = setDataNodePermissions(container) 545 if err != nil { 546 return nil, closer, err 547 } 548 } 549 550 pgHost, err = container.Host(ctx) 551 if err != nil { 552 return nil, closer, err 553 } 554 555 pgPort, err = container.MappedPort(ctx, containerPort) 556 if err != nil { 557 return nil, closer, err 558 } 559 560 db, err := pgx.Connect(context.Background(), PgConnectURL(defaultDB, Superuser)) 561 if err != nil { 562 return nil, closer, err 563 } 564 565 for _, username := range users { 566 _, err = db.Exec(context.Background(), fmt.Sprintf("CREATE USER %s WITH NOSUPERUSER CREATEROLE PASSWORD 'password'", username)) 567 if err != nil { 568 //ignore duplicate errors 569 pgErr, ok := err.(*pgconn.PgError) 570 if !ok || pgErr.Code != "42710" { 571 return nil, closer, err 572 } 573 } 574 } 575 576 if isDataNode { 577 // reload the pg_hba.conf settings we changed in setDataNodePermissions 578 _, err = db.Exec(context.Background(), "SELECT pg_reload_conf()") 579 if err != nil { 580 return nil, closer, err 581 } 582 } 583 584 err = db.Close(context.Background()) 585 if err != nil { 586 return nil, closer, err 587 } 588 589 return container, closer, nil 590} 591 592func AddDataNode1(db *pgx.Conn, database string) error { 593 return addDataNode(db, database, "dn0", "5433") 594} 595 596func AddDataNode2(db *pgx.Conn, database string) error { 597 return addDataNode(db, database, "dn1", "5434") 598} 599 600func addDataNode(db *pgx.Conn, database string, nodeName string, nodePort string) error { 601 _, err := db.Exec(context.Background(), "SELECT add_data_node('"+nodeName+"', host => 'db"+nodePort+"', port => "+nodePort+", if_not_exists=>true);") 602 if err != nil { 603 return err 604 } 605 606 for _, username := range users { 607 _, err = db.Exec(context.Background(), "GRANT USAGE ON FOREIGN SERVER "+nodeName+" TO "+username) 608 if err != nil { 609 return err 610 } 611 } 612 grantDistributed := "CALL distributed_exec($$ GRANT ALL ON DATABASE %s TO %s $$, transactional => false)" 613 _, err = db.Exec(context.Background(), fmt.Sprintf(grantDistributed, database, promUser)) 614 if err != nil { 615 return err 616 } 617 return nil 618} 619 620func setDataNodePermissions(container testcontainers.Container) error { 621 // trust all incoming connections on the datanodes so we can add them to 622 // the cluster without too much hassle. We'll reload the pg_hba.conf later 623 // NOTE: the setting must be prepended to pg_hba.conf so it overrides 624 // the latter, stricter, settings 625 code, err := container.Exec(context.Background(), []string{ 626 "bash", 627 "-c", 628 "echo -e \"host all all 0.0.0.0/0 trust\n$(cat /var/lib/postgresql/data/pg_hba.conf)\"" + 629 "> /var/lib/postgresql/data/pg_hba.conf"}) 630 if err != nil { 631 return err 632 } 633 if code != 0 { 634 return fmt.Errorf("docker exec error. exit code: %d", code) 635 } 636 return nil 637} 638 639type CloseAll struct { 640 toClose []func() 641} 642 643func (c *CloseAll) Append(closer func()) { 644 c.toClose = append(c.toClose, closer) 645} 646 647func (c CloseAll) Close() error { 648 for i := len(c.toClose) - 1; i >= 0; i-- { 649 c.toClose[i]() 650 } 651 return nil 652} 653 654// StartPromContainer starts a Prometheus container for use in testing 655// #nosec 656func StartPromContainer(storagePath string, ctx context.Context) (testcontainers.Container, string, nat.Port, error) { 657 // Set the storage directories permissions so Prometheus can write to them. 658 err := os.Chmod(storagePath, 0777) 659 if err != nil { 660 return nil, "", "", err 661 } 662 if err := os.Chmod(filepath.Join(storagePath, "wal"), 0777); err != nil { 663 return nil, "", "", err 664 } 665 666 promConfigFile := filepath.Join(storagePath, "prometheus.yml") 667 err = ioutil.WriteFile(promConfigFile, []byte(emptyPromConfig), 0777) 668 if err != nil { 669 return nil, "", "", err 670 } 671 prometheusPort := nat.Port("9090/tcp") 672 req := testcontainers.ContainerRequest{ 673 Image: "prom/prometheus:main", 674 ExposedPorts: []string{string(prometheusPort)}, 675 WaitingFor: wait.ForListeningPort(prometheusPort), 676 BindMounts: map[string]string{ 677 storagePath: "/prometheus", 678 promConfigFile: "/etc/prometheus/prometheus.yml", 679 }, 680 Cmd: []string{ 681 // Default configuration. 682 "--config.file=/etc/prometheus/prometheus.yml", 683 "--storage.tsdb.path=/prometheus", 684 "--web.console.libraries=/usr/share/prometheus/console_libraries", 685 "--web.console.templates=/usr/share/prometheus/consoles", 686 "--log.level=debug", 687 688 // Enable features. 689 "--enable-feature=promql-at-modifier,promql-negative-offset,exemplar-storage", 690 691 // This is to stop Prometheus from messing with the data. 692 "--storage.tsdb.retention.time=30y", 693 "--storage.tsdb.min-block-duration=30y", 694 "--storage.tsdb.max-block-duration=30y", 695 }, 696 } 697 container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 698 ContainerRequest: req, 699 Started: true, 700 }) 701 if err != nil { 702 return nil, "", "", err 703 } 704 host, err := container.Host(ctx) 705 if err != nil { 706 return nil, "", "", err 707 } 708 709 port, err := container.MappedPort(ctx, prometheusPort) 710 if err != nil { 711 return nil, "", "", err 712 } 713 714 return container, host, port, nil 715} 716 717var ConnectorPort = nat.Port("9201/tcp") 718 719func StartConnectorWithImage(ctx context.Context, dbContainer testcontainers.Container, image string, printLogs bool, cmds []string, dbname string) (testcontainers.Container, error) { 720 dbUser := promUser 721 if dbname == "postgres" { 722 dbUser = "postgres" 723 } 724 dbHost := "172.17.0.1" 725 if runtime.GOOS == "darwin" { 726 dbHost = "host.docker.internal" 727 } 728 729 req := testcontainers.ContainerRequest{ 730 Image: image, 731 ExposedPorts: []string{string(ConnectorPort)}, 732 WaitingFor: wait.ForHTTP("/metrics").WithPort(ConnectorPort).WithAllowInsecure(true), 733 SkipReaper: false, /* switch to true not to kill docker container */ 734 Cmd: []string{ 735 "-db-host", dbHost, 736 "-db-port", pgPort.Port(), 737 "-db-user", dbUser, 738 "-db-password", "password", 739 "-db-name", dbname, 740 "-db-ssl-mode", "prefer", 741 }, 742 } 743 744 req.Cmd = append(req.Cmd, cmds...) 745 746 container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 747 ContainerRequest: req, 748 Started: false, 749 }) 750 if err != nil { 751 return nil, err 752 } 753 754 if printLogs { 755 container.FollowOutput(stdoutLogConsumer{}) 756 } 757 758 err = container.Start(context.Background()) 759 if err != nil { 760 return nil, err 761 } 762 763 if printLogs { 764 err = container.StartLogProducer(context.Background()) 765 if err != nil { 766 fmt.Println("Error setting up logger", err) 767 os.Exit(1) 768 } 769 } 770 771 return container, nil 772} 773 774func StopContainer(ctx context.Context, container testcontainers.Container, printLogs bool) { 775 if printLogs { 776 err := container.StopLogProducer() 777 if err != nil { 778 fmt.Fprintln(os.Stderr, "couldn't stop log producer", err) 779 } 780 } 781 782 err := container.Terminate(ctx) 783 if err != nil { 784 fmt.Fprintln(os.Stderr, "couldn't terminate container", err) 785 } 786} 787 788// TempDir returns a temp directory for tests 789func TempDir(name string) (string, error) { 790 tmpDir := "" 791 792 if runtime.GOOS == "darwin" { 793 // Docker on Mac lacks access to default os tmp dir - "/var/folders/random_number" 794 // so switch to cross-user tmp dir 795 tmpDir = "/tmp" 796 } 797 return ioutil.TempDir(tmpDir, name) 798} 799