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