@@ -11,10 +11,12 @@ import (
1111 "fmt"
1212 "io"
1313 "log"
14+ "net"
1415 "net/url"
1516 "os"
1617 "os/exec"
1718 "path"
19+ "strconv"
1820 "strings"
1921 "time"
2022
@@ -50,9 +52,11 @@ type (
5052 }
5153 // A Container is an instance of a created container.
5254 Container struct {
53- Config // Config used to create this container
54- ID string
55- HostPort string // Host port to connect to
55+ Config // Config used to create this container
56+ // ID of the container.
57+ ID string
58+ // Port on the host this containers service is bound to.
59+ Port string
5660 }
5761 // ConnOptions allows configuring the underlying connection pool.
5862 ConnOptions struct {
@@ -384,54 +388,40 @@ func Conn(s *ConnOptions) ConfigOption {
384388
385389// Run pulls and starts a new docker container from the Config.
386390func (c * Config ) Run (ctx context.Context ) (* Container , error ) {
387- if c == nil || c .Image == "" || c .Port == "" || c .Out == nil {
388- return nil , fmt .Errorf ("docker: invalid configuration %#v" , c )
389- }
390- args := []string {"run" , "--rm" , "--detach" , "--publish" , c .Port }
391- for _ , e := range c .Env {
392- args = append (args , "--env" , e )
393- }
394- args = append (args , c .Image )
395- id , err := c .docker (ctx , args ... )
396- if err != nil {
397- return nil , fmt .Errorf ("docker: run container: %w" , err )
391+ // Make sure the configuration is not missing critical values.
392+ if err := c .validate (); err != nil {
393+ return nil , err
398394 }
399- hostPort , err := c .docker (ctx , "port" , id , c .Port )
395+ // Get a free host TCP port the container can bind its exposed service port on.
396+ p , err := freePort ()
400397 if err != nil {
401- return nil , fmt .Errorf ("docker: get container port: %w" , err )
398+ return nil , fmt .Errorf ("getting open port: %w" , err )
402399 }
403- return & Container {
404- Config : * c ,
405- ID : id ,
406- HostPort : strings .SplitN (hostPort , "\n " , 2 )[0 ],
407- }, nil
408- }
409-
410- // StopAndRemove stops the container with the given id.
411- func (c * Config ) StopAndRemove (ctx context.Context , id string ) error {
412- _ , err := c .docker (ctx , "kill" , id )
413- if err != nil {
414- return fmt .Errorf ("docker: kill container: %w" , err )
400+ // Run the container.
401+ args := []string {"docker" , "run" , "--rm" , "--detach" }
402+ for _ , e := range c .Env {
403+ args = append (args , "-e" , e )
415404 }
416- return nil
417- }
418-
419- func (c * Config ) docker (ctx context.Context , args ... string ) (string , error ) {
405+ args = append (args , "-p" , fmt .Sprintf ("%s:%s" , p , c .Port ), c .Image )
406+ cmd := exec .CommandContext (ctx , args [0 ], args [1 :]... ) //nolint:gosec
420407 stdout , stderr := & bytes.Buffer {}, & bytes.Buffer {}
421- cmd := exec .CommandContext (ctx , "docker" , args ... ) //nolint:gosec
422408 cmd .Stdout , cmd .Stderr = io .MultiWriter (c .Out , stdout ), stderr
423409 if err := cmd .Run (); err != nil {
424410 if stderr .Len () > 0 {
425411 err = errors .New (strings .TrimSpace (stderr .String ()))
426412 }
427- return "" , err
413+ return nil , err
428414 }
429- return strings .TrimSpace (stdout .String ()), nil
415+ return & Container {
416+ Config : * c ,
417+ ID : strings .TrimSpace (stdout .String ()),
418+ Port : p ,
419+ }, nil
430420}
431421
432422// Close stops and removes this container.
433423func (c * Container ) Close () error {
434- return c . Config . StopAndRemove ( context . Background (), c .ID )
424+ return exec . Command ( "docker" , "kill" , c .ID ). Run () //nolint:gosec
435425}
436426
437427// Wait waits for this container to be ready.
@@ -442,16 +432,20 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
442432 if timeout > time .Minute {
443433 timeout = time .Minute
444434 }
445- u , err := c .PingURL ()
435+ var (
436+ done = time .After (timeout )
437+ u , err = c .URL ()
438+ )
446439 if err != nil {
447440 return err
448441 }
449- for done := time .After (timeout ); ; {
442+ pingURL := c .PingURL (* u )
443+ for {
450444 select {
451445 case <- time .After (100 * time .Millisecond ):
452446 var client * sqlclient.Client
453447 // Ping against the root connection.
454- client , err = sqlclient .Open (ctx , u )
448+ client , err = sqlclient .Open (ctx , pingURL )
455449 if err != nil {
456450 continue
457451 }
@@ -479,39 +473,22 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
479473 }
480474}
481475
482- // PingURL returns a URL to ping the Container.
483- func (c * Container ) PingURL () (string , error ) {
484- u , err := c .URL ()
485- if err != nil {
486- return "" , err
487- }
488- switch c .driver {
489- case DriverSQLServer :
490- q := u .Query ()
491- q .Del ("database" )
492- u .RawQuery = q .Encode ()
493- return u .String (), nil
494- default :
495- u .Path = "/"
496- return u .String (), nil
497- }
498- }
499-
500476// URL returns a URL to connect to the Container.
501477func (c * Container ) URL () (* url.URL , error ) {
502- u := & url.URL {
503- Scheme : c .driver ,
504- User : c .User ,
505- Host : c .HostPort ,
506- }
478+ host := "localhost"
507479 // Check if the DOCKER_HOST env var is set.
508480 // If it is, use the host from the URL.
509481 if h := os .Getenv ("DOCKER_HOST" ); h != "" {
510- dh , err := url .Parse (h )
482+ u , err := url .Parse (h )
511483 if err != nil {
512484 return nil , err
513485 }
514- u .Host = fmt .Sprintf ("%s:%s" , dh .Hostname (), u .Port ())
486+ host = u .Hostname ()
487+ }
488+ u := & url.URL {
489+ Scheme : c .driver ,
490+ User : c .User ,
491+ Host : fmt .Sprintf ("%s:%s" , host , c .Port ),
515492 }
516493 switch c .driver {
517494 case DriverSQLServer :
@@ -533,6 +510,43 @@ func (c *Container) URL() (*url.URL, error) {
533510 return u , nil
534511}
535512
513+ // PingURL returns a URL to ping the Container.
514+ func (c * Container ) PingURL (u url.URL ) string {
515+ switch c .driver {
516+ case DriverSQLServer :
517+ q := u .Query ()
518+ q .Del ("database" )
519+ u .RawQuery = q .Encode ()
520+ return u .String ()
521+ default :
522+ u .Path = "/"
523+ return u .String ()
524+ }
525+ }
526+
527+ // validate that no empty values are given.
528+ func (c * Config ) validate () error {
529+ if c == nil || c .Image == "" || c .Port == "" || c .Out == nil {
530+ return fmt .Errorf ("invalid configuration %#v" , c )
531+ }
532+ return nil
533+ }
534+
535+ func freePort () (string , error ) {
536+ a , err := net .ResolveTCPAddr ("tcp" , ":0" )
537+ if err != nil {
538+ return "" , err
539+ }
540+ l , err := net .ListenTCP ("tcp" , a )
541+ if err != nil {
542+ return "" , err
543+ }
544+ if err := l .Close (); err != nil {
545+ return "" , err
546+ }
547+ return strconv .Itoa (l .Addr ().(* net.TCPAddr ).Port ), nil
548+ }
549+
536550func init () {
537551 sqlclient .Register (
538552 "docker" ,
0 commit comments