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 }