github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/sqlcon/dockertest/test_helper.go (about)

     1  package dockertest
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/api/types/filters"
    17  	"github.com/docker/docker/client"
    18  	"github.com/gobuffalo/pop/v6"
    19  	"github.com/jmoiron/sqlx"
    20  	"github.com/pkg/errors"
    21  	"github.com/stretchr/testify/require"
    22  
    23  	"github.com/ory/dockertest/v3"
    24  	dc "github.com/ory/dockertest/v3/docker"
    25  	"github.com/ory/x/logrusx"
    26  	"github.com/ory/x/resilience"
    27  	"github.com/ory/x/stringsx"
    28  )
    29  
    30  // atexit := atexit.NewOnExit()
    31  // atexit.Add(func() {
    32  //	dockertest.KillAll()
    33  // })
    34  // atexit.Exit(testMain(m))
    35  
    36  // func WrapCleanup
    37  
    38  type dockerPool interface {
    39  	Purge(r *dockertest.Resource) error
    40  	Run(repository, tag string, env []string) (*dockertest.Resource, error)
    41  	RunWithOptions(opts *dockertest.RunOptions, hcOpts ...func(*dc.HostConfig)) (*dockertest.Resource, error)
    42  }
    43  
    44  var resources = []*dockertest.Resource{}
    45  var pool dockerPool
    46  
    47  func getPool() (dockerPool, error) {
    48  	if pool != nil {
    49  		return pool, nil
    50  	}
    51  	var err error
    52  	pool, err = dockertest.NewPool("")
    53  	return pool, err
    54  }
    55  
    56  // KillAllTestDatabases deletes all test databases.
    57  func KillAllTestDatabases() {
    58  	pool, err := getPool()
    59  	if err != nil {
    60  		panic(err)
    61  	}
    62  
    63  	for _, r := range resources {
    64  		if err := pool.Purge(r); err != nil {
    65  			log.Printf("Failed to purge resource: %s", err)
    66  		}
    67  	}
    68  
    69  	resources = []*dockertest.Resource{}
    70  }
    71  
    72  // Register sets up OnExit.
    73  func Register() *OnExit {
    74  	onexit := NewOnExit()
    75  	onexit.Add(func() {
    76  		KillAllTestDatabases()
    77  	})
    78  	return onexit
    79  }
    80  
    81  // Parallel runs tasks in parallel.
    82  func Parallel(fs []func()) {
    83  	wg := sync.WaitGroup{}
    84  
    85  	wg.Add(len(fs))
    86  	for _, f := range fs {
    87  		go func(ff func()) {
    88  			defer wg.Done()
    89  			ff()
    90  		}(f)
    91  	}
    92  
    93  	wg.Wait()
    94  }
    95  
    96  func connect(dialect, driver, dsn string) (db *sqlx.DB, err error) {
    97  	if scheme := strings.Split(dsn, "://")[0]; scheme == "mysql" {
    98  		dsn = strings.Replace(dsn, "mysql://", "", -1)
    99  	} else if scheme == "cockroach" {
   100  		dsn = strings.Replace(dsn, "cockroach://", "postgres://", 1)
   101  	}
   102  	err = resilience.Retry(
   103  		logrusx.New("", ""),
   104  		time.Second*5,
   105  		time.Minute*5,
   106  		func() (err error) {
   107  			db, err = sqlx.Open(dialect, dsn)
   108  			if err != nil {
   109  				log.Printf("Connecting to database %s failed: %s", driver, err)
   110  				return err
   111  			}
   112  
   113  			if err := db.Ping(); err != nil {
   114  				log.Printf("Pinging database %s failed: %s", driver, err)
   115  				return err
   116  			}
   117  
   118  			return nil
   119  		},
   120  	)
   121  	if err != nil {
   122  		return nil, errors.Errorf("Unable to connect to %s (%s): %s", driver, dsn, err)
   123  	}
   124  	log.Printf("Connected to database %s", driver)
   125  	return db, nil
   126  }
   127  
   128  func connectPop(t require.TestingT, url string) (c *pop.Connection) {
   129  	require.NoError(t, resilience.Retry(logrusx.New("", ""), time.Second*5, time.Minute*5, func() error {
   130  		var err error
   131  		c, err = pop.NewConnection(&pop.ConnectionDetails{
   132  			URL: url,
   133  		})
   134  		if err != nil {
   135  			log.Printf("could not create pop connection")
   136  			return err
   137  		}
   138  		if err := c.Open(); err != nil {
   139  			// an Open error probably means we have a problem with the connections config
   140  			log.Printf("could not open pop connection: %+v", err)
   141  			return err
   142  		}
   143  		return c.RawQuery("select version()").Exec()
   144  	}))
   145  	return
   146  }
   147  
   148  // ## PostgreSQL ##
   149  
   150  func startPostgreSQL() (*dockertest.Resource, error) {
   151  	pool, err := getPool()
   152  	if err != nil {
   153  		return nil, errors.Wrap(err, "Could not connect to docker")
   154  	}
   155  
   156  	resource, err := pool.Run("postgres", "11.8", []string{"POSTGRES_PASSWORD=secret", "POSTGRES_DB=postgres"})
   157  	if err == nil {
   158  		resources = append(resources, resource)
   159  	}
   160  	return resource, err
   161  }
   162  
   163  // RunTestPostgreSQL runs a PostgreSQL database and returns the URL to it.
   164  // If a docker container is started for the database, the container be removed
   165  // at the end of the test.
   166  func RunTestPostgreSQL(t testing.TB) string {
   167  	if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" {
   168  		t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_POSTGRESQL is set to: %s", dsn)
   169  		return dsn
   170  	}
   171  
   172  	u, cleanup, err := runPosgreSQLCleanup()
   173  	require.NoError(t, err)
   174  	t.Cleanup(cleanup)
   175  
   176  	return u
   177  }
   178  
   179  // RunPostgreSQL runs a PostgreSQL database and returns the URL to it.
   180  func RunPostgreSQL() (string, error) {
   181  	dsn, _, err := runPosgreSQLCleanup()
   182  	return dsn, err
   183  }
   184  
   185  func runPosgreSQLCleanup() (string, func(), error) {
   186  	resource, err := startPostgreSQL()
   187  	if err != nil {
   188  		return "", func() {}, err
   189  	}
   190  
   191  	return fmt.Sprintf("postgres://postgres:secret@127.0.0.1:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp")),
   192  		func() { pool.Purge(resource) }, nil
   193  }
   194  
   195  // ConnectToTestPostgreSQL connects to a PostgreSQL database.
   196  func ConnectToTestPostgreSQL() (*sqlx.DB, error) {
   197  	if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" {
   198  		return connect("pgx", "postgres", dsn)
   199  	}
   200  
   201  	resource, err := startPostgreSQL()
   202  	if err != nil {
   203  		return nil, errors.Wrap(err, "Could not start resource")
   204  	}
   205  
   206  	db := bootstrap("postgres://postgres:secret@localhost:%s/postgres?sslmode=disable", "5432/tcp", "pgx", pool, resource)
   207  	return db, nil
   208  }
   209  
   210  // ConnectToTestPostgreSQLPop connects to a test PostgreSQL database.
   211  // If a docker container is started for the database, the container be removed
   212  // at the end of the test.
   213  func ConnectToTestPostgreSQLPop(t testing.TB) *pop.Connection {
   214  	url := RunTestPostgreSQL(t)
   215  	return connectPop(t, url)
   216  }
   217  
   218  // ## MySQL ##
   219  
   220  func startMySQL() (*dockertest.Resource, error) {
   221  	pool, err := getPool()
   222  	if err != nil {
   223  		return nil, errors.Wrap(err, "Could not connect to docker")
   224  	}
   225  
   226  	resource, err := pool.Run(
   227  		"mysql",
   228  		"8.0",
   229  		[]string{
   230  			"MYSQL_ROOT_PASSWORD=secret",
   231  			"MYSQL_ROOT_HOST=%",
   232  		},
   233  	)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	resources = append(resources, resource)
   238  	return resource, nil
   239  }
   240  
   241  // RunMySQL runs a RunMySQL database and returns the URL to it.
   242  func RunMySQL() (string, error) {
   243  	dsn, _, err := runMySQLCleanup()
   244  	return dsn, err
   245  }
   246  
   247  func runMySQLCleanup() (string, func(), error) {
   248  	resource, err := startMySQL()
   249  	if err != nil {
   250  		return "", func() {}, err
   251  	}
   252  
   253  	return fmt.Sprintf("mysql://root:secret@(localhost:%s)/mysql?parseTime=true&multiStatements=true", resource.GetPort("3306/tcp")),
   254  		func() { pool.Purge(resource) }, nil
   255  }
   256  
   257  // RunTestMySQL runs a MySQL database and returns the URL to it.
   258  // If a docker container is started for the database, the container be removed
   259  // at the end of the test.
   260  func RunTestMySQL(t testing.TB) string {
   261  	if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" {
   262  		t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_MYSQL is set to: %s", dsn)
   263  		return dsn
   264  	}
   265  
   266  	u, cleanup, err := runMySQLCleanup()
   267  	require.NoError(t, err)
   268  	t.Cleanup(cleanup)
   269  
   270  	return u
   271  }
   272  
   273  // ConnectToTestMySQL connects to a MySQL database.
   274  func ConnectToTestMySQL() (*sqlx.DB, error) {
   275  	if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" {
   276  		log.Println("Found mysql test database config, skipping dockertest...")
   277  		return connect("mysql", "mysql", dsn)
   278  	}
   279  
   280  	resource, err := startMySQL()
   281  	if err != nil {
   282  		return nil, errors.Wrap(err, "Could not start resource")
   283  	}
   284  
   285  	db := bootstrap("root:secret@(localhost:%s)/mysql?parseTime=true", "3306/tcp", "mysql", pool, resource)
   286  	return db, nil
   287  }
   288  
   289  func ConnectToTestMySQLPop(t testing.TB) *pop.Connection {
   290  	url := RunTestMySQL(t)
   291  	return connectPop(t, url)
   292  }
   293  
   294  // ## CockroachDB
   295  
   296  func startCockroachDB(version string) (*dockertest.Resource, error) {
   297  	pool, err := getPool()
   298  	if err != nil {
   299  		return nil, errors.Wrap(err, "Could not connect to docker")
   300  	}
   301  
   302  	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
   303  		Repository: "cockroachdb/cockroach",
   304  		Tag:        stringsx.Coalesce(version, "v20.2.5"),
   305  		Cmd:        []string{"start-single-node", "--insecure"},
   306  	})
   307  	if err == nil {
   308  		resources = append(resources, resource)
   309  	}
   310  	return resource, err
   311  }
   312  
   313  // RunCockroachDB runs a CockroachDB database and returns the URL to it.
   314  func RunCockroachDB() (string, error) {
   315  	return RunCockroachDBWithVersion("")
   316  }
   317  
   318  // RunCockroachDB runs a CockroachDB database and returns the URL to it.
   319  func RunCockroachDBWithVersion(version string) (string, error) {
   320  	resource, err := startCockroachDB(version)
   321  	if err != nil {
   322  		return "", err
   323  	}
   324  
   325  	return fmt.Sprintf("cockroach://root@localhost:%s/defaultdb?sslmode=disable", resource.GetPort("26257/tcp")), nil
   326  }
   327  
   328  func runCockroachDBWithVersionCleanup(version string) (string, func(), error) {
   329  	resource, err := startCockroachDB(version)
   330  	if err != nil {
   331  		return "", func() {}, err
   332  	}
   333  
   334  	return fmt.Sprintf("cockroach://root@localhost:%s/defaultdb?sslmode=disable", resource.GetPort("26257/tcp")),
   335  		func() { pool.Purge(resource) },
   336  		nil
   337  }
   338  
   339  // RunTestCockroachDB runs a CockroachDB database and returns the URL to it.
   340  // If a docker container is started for the database, the container be removed
   341  // at the end of the test.
   342  func RunTestCockroachDB(t testing.TB) string {
   343  	return RunTestCockroachDBWithVersion(t, "")
   344  }
   345  
   346  // RunTestCockroachDB runs a CockroachDB database and returns the URL to it.
   347  // If a docker container is started for the database, the container be removed
   348  // at the end of the test.
   349  func RunTestCockroachDBWithVersion(t testing.TB, version string) string {
   350  	if dsn := os.Getenv("TEST_DATABASE_COCKROACHDB"); dsn != "" {
   351  		t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_COCKROACHDB is set to: %s", dsn)
   352  		return dsn
   353  	}
   354  
   355  	u, cleanup, err := runCockroachDBWithVersionCleanup(version)
   356  	require.NoError(t, err)
   357  	t.Cleanup(cleanup)
   358  
   359  	return u
   360  }
   361  
   362  // ConnectToTestCockroachDB connects to a CockroachDB database.
   363  func ConnectToTestCockroachDB() (*sqlx.DB, error) {
   364  	if dsn := os.Getenv("TEST_DATABASE_COCKROACHDB"); dsn != "" {
   365  		log.Println("Found cockroachdb test database config, skipping dockertest...")
   366  		return connect("pgx", "cockroach", dsn)
   367  	}
   368  
   369  	resource, err := startCockroachDB("")
   370  	if err != nil {
   371  		return nil, errors.Wrap(err, "Could not start resource")
   372  	}
   373  
   374  	db := bootstrap("postgres://root@localhost:%s/defaultdb?sslmode=disable", "26257/tcp", "pgx", pool, resource)
   375  	return db, nil
   376  }
   377  
   378  // ConnectToTestCockroachDBPop connects to a test CockroachDB database.
   379  // If a docker container is started for the database, the container be removed
   380  // at the end of the test.
   381  func ConnectToTestCockroachDBPop(t testing.TB) *pop.Connection {
   382  	url := RunTestCockroachDB(t)
   383  	return connectPop(t, url)
   384  }
   385  
   386  func bootstrap(u, port, d string, pool dockerPool, resource *dockertest.Resource) (db *sqlx.DB) {
   387  	if err := resilience.Retry(logrusx.New("", ""), time.Second*5, time.Minute*5, func() error {
   388  		var err error
   389  		db, err = sqlx.Open(d, fmt.Sprintf(u, resource.GetPort(port)))
   390  		if err != nil {
   391  			return err
   392  		}
   393  
   394  		return db.Ping()
   395  	}); err != nil {
   396  		if pErr := pool.Purge(resource); pErr != nil {
   397  			log.Fatalf("Could not connect to docker and unable to remove image: %s - %s", err, pErr)
   398  		}
   399  		log.Fatalf("Could not connect to docker: %s", err)
   400  	}
   401  	return
   402  }
   403  
   404  var comments = regexp.MustCompile("(--[^\n]*\n)|(?s:/\\*.+\\*/)")
   405  
   406  func StripDump(d string) string {
   407  	d = comments.ReplaceAllLiteralString(d, "")
   408  	d = strings.TrimPrefix(d, "Command \"dump\" is deprecated, cockroach dump will be removed in a subsequent release.\r\nFor details, see: https://github.com/cockroachdb/cockroach/issues/54040\r\n")
   409  	d = strings.ReplaceAll(d, "\r\n", "")
   410  	d = strings.ReplaceAll(d, "\t", " ")
   411  	d = strings.ReplaceAll(d, "\n", " ")
   412  	return d
   413  }
   414  
   415  func DumpSchema(ctx context.Context, t *testing.T, db string) string {
   416  	var containerPort string
   417  	var cmd []string
   418  
   419  	switch c := stringsx.SwitchExact(db); {
   420  	case c.AddCase("postgres"):
   421  		containerPort = "5432"
   422  		cmd = []string{"pg_dump", "-U", "postgres", "-s", "-T", "hydra_*_migration", "-T", "schema_migration"}
   423  	case c.AddCase("mysql"):
   424  		containerPort = "3306"
   425  		cmd = []string{"/usr/bin/mysqldump", "-u", "root", "--password=secret", "mysql"}
   426  	case c.AddCase("cockroach"):
   427  		containerPort = "26257"
   428  		cmd = []string{"./cockroach", "dump", "defaultdb", "--insecure", "--dump-mode=schema"}
   429  	default:
   430  		t.Log(c.ToUnknownCaseErr())
   431  		t.FailNow()
   432  		return ""
   433  	}
   434  
   435  	cli, err := client.NewClientWithOpts(client.FromEnv)
   436  	require.NoError(t, err)
   437  	containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
   438  		Quiet:   true,
   439  		Filters: filters.NewArgs(filters.Arg("expose", containerPort)),
   440  	})
   441  	require.NoError(t, err)
   442  
   443  	if len(containers) != 1 {
   444  		t.Logf("Ambiguous amount of %s containers: %d", db, len(containers))
   445  		t.FailNow()
   446  	}
   447  
   448  	process, err := cli.ContainerExecCreate(ctx, containers[0].ID, types.ExecConfig{
   449  		Tty:          true,
   450  		AttachStdout: true,
   451  		Cmd:          cmd,
   452  	})
   453  	require.NoError(t, err)
   454  
   455  	resp, err := cli.ContainerExecAttach(ctx, process.ID, types.ExecStartCheck{
   456  		Tty: true,
   457  	})
   458  	require.NoError(t, err)
   459  	dump, err := ioutil.ReadAll(resp.Reader)
   460  	require.NoError(t, err, "%s", dump)
   461  
   462  	return StripDump(string(dump))
   463  }