github.com/getsynq/migrate/v4@v4.15.3-0.20220615182648-8e72daaa5ed9/testing/docker.go (about)

     1  // Package testing is used in driver tests and should only be used by migrate tests.
     2  //
     3  // Deprecated: If you'd like to test using Docker images, use package github.com/dhui/dktest instead
     4  package testing
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	dockertypes "github.com/docker/docker/api/types"
    13  	dockercontainer "github.com/docker/docker/api/types/container"
    14  	dockernetwork "github.com/docker/docker/api/types/network"
    15  	dockerclient "github.com/docker/docker/client"
    16  	"github.com/hashicorp/go-multierror"
    17  	"io"
    18  	"math/rand"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  )
    24  
    25  func NewDockerContainer(t testing.TB, image string, env []string, cmd []string) (*DockerContainer, error) {
    26  	c, err := dockerclient.NewClientWithOpts(
    27  		dockerclient.FromEnv,
    28  		dockerclient.WithAPIVersionNegotiation(),
    29  	)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	if cmd == nil {
    35  		cmd = make([]string, 0)
    36  	}
    37  
    38  	contr := &DockerContainer{
    39  		t:         t,
    40  		client:    c,
    41  		ImageName: image,
    42  		ENV:       env,
    43  		Cmd:       cmd,
    44  	}
    45  
    46  	if err := contr.PullImage(); err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	if err := contr.Start(); err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	return contr, nil
    55  }
    56  
    57  // DockerContainer implements Instance interface
    58  type DockerContainer struct {
    59  	t                  testing.TB
    60  	client             *dockerclient.Client
    61  	ImageName          string
    62  	ENV                []string
    63  	Cmd                []string
    64  	ContainerId        string
    65  	ContainerName      string
    66  	ContainerJSON      dockertypes.ContainerJSON
    67  	containerInspected bool
    68  	keepForDebugging   bool
    69  }
    70  
    71  func (d *DockerContainer) PullImage() (err error) {
    72  	if d == nil {
    73  		return errors.New("Cannot pull image on a nil *DockerContainer")
    74  	}
    75  	d.t.Logf("Docker: Pull image %v", d.ImageName)
    76  	r, err := d.client.ImagePull(context.Background(), d.ImageName, dockertypes.ImagePullOptions{})
    77  	if err != nil {
    78  		return err
    79  	}
    80  	defer func() {
    81  		if errClose := r.Close(); errClose != nil {
    82  			err = multierror.Append(errClose)
    83  		}
    84  	}()
    85  
    86  	// read output and log relevant lines
    87  	bf := bufio.NewScanner(r)
    88  	for bf.Scan() {
    89  		var resp dockerImagePullOutput
    90  		if err := json.Unmarshal(bf.Bytes(), &resp); err != nil {
    91  			return err
    92  		}
    93  		if strings.HasPrefix(resp.Status, "Status: ") {
    94  			d.t.Logf("Docker: %v", resp.Status)
    95  		}
    96  	}
    97  	return bf.Err()
    98  }
    99  
   100  func (d *DockerContainer) Start() error {
   101  	if d == nil {
   102  		return errors.New("Cannot start a nil *DockerContainer")
   103  	}
   104  
   105  	containerName := fmt.Sprintf("migrate_test_%s", pseudoRandStr(10))
   106  
   107  	// create container first
   108  	resp, err := d.client.ContainerCreate(context.Background(),
   109  		&dockercontainer.Config{
   110  			Image:  d.ImageName,
   111  			Labels: map[string]string{"migrate_test": "true"},
   112  			Env:    d.ENV,
   113  			Cmd:    d.Cmd,
   114  		},
   115  		&dockercontainer.HostConfig{
   116  			PublishAllPorts: true,
   117  		},
   118  		&dockernetwork.NetworkingConfig{},
   119  		nil,
   120  		containerName)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	d.ContainerId = resp.ID
   126  	d.ContainerName = containerName
   127  
   128  	// then start it
   129  	if err := d.client.ContainerStart(context.Background(), resp.ID, dockertypes.ContainerStartOptions{}); err != nil {
   130  		return err
   131  	}
   132  
   133  	d.t.Logf("Docker: Started container %v (%v) for image %v listening at %v:%v", resp.ID[0:12], containerName, d.ImageName, d.Host(), d.Port())
   134  	for _, v := range resp.Warnings {
   135  		d.t.Logf("Docker: Warning: %v", v)
   136  	}
   137  	return nil
   138  }
   139  
   140  func (d *DockerContainer) KeepForDebugging() {
   141  	if d == nil {
   142  		return
   143  	}
   144  
   145  	d.keepForDebugging = true
   146  }
   147  
   148  func (d *DockerContainer) Remove() error {
   149  	if d == nil {
   150  		return errors.New("Cannot remove a nil *DockerContainer")
   151  	}
   152  
   153  	if d.keepForDebugging {
   154  		return nil
   155  	}
   156  
   157  	if len(d.ContainerId) == 0 {
   158  		return errors.New("missing containerId")
   159  	}
   160  	if err := d.client.ContainerRemove(context.Background(), d.ContainerId,
   161  		dockertypes.ContainerRemoveOptions{
   162  			Force: true,
   163  		}); err != nil {
   164  		d.t.Log(err)
   165  		return err
   166  	}
   167  	d.t.Logf("Docker: Removed %v", d.ContainerName)
   168  	return nil
   169  }
   170  
   171  func (d *DockerContainer) Inspect() error {
   172  	if d == nil {
   173  		return errors.New("Cannot inspect a nil *DockerContainer")
   174  	}
   175  
   176  	if len(d.ContainerId) == 0 {
   177  		return errors.New("missing containerId")
   178  	}
   179  	resp, err := d.client.ContainerInspect(context.Background(), d.ContainerId)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	d.ContainerJSON = resp
   185  	d.containerInspected = true
   186  	return nil
   187  }
   188  
   189  func (d *DockerContainer) Logs() (io.ReadCloser, error) {
   190  	if d == nil {
   191  		return nil, errors.New("Cannot view logs for a nil *DockerContainer")
   192  	}
   193  	if len(d.ContainerId) == 0 {
   194  		return nil, errors.New("missing containerId")
   195  	}
   196  
   197  	return d.client.ContainerLogs(context.Background(), d.ContainerId, dockertypes.ContainerLogsOptions{
   198  		ShowStdout: true,
   199  		ShowStderr: true,
   200  	})
   201  }
   202  
   203  func (d *DockerContainer) portMapping(selectFirst bool, cPort int) (containerPort uint, hostIP string, hostPort uint, err error) { // nolint:unparam
   204  	if !d.containerInspected {
   205  		if err := d.Inspect(); err != nil {
   206  			d.t.Fatal(err)
   207  		}
   208  	}
   209  
   210  	for port, bindings := range d.ContainerJSON.NetworkSettings.Ports {
   211  		if !selectFirst && port.Int() != cPort {
   212  			// Skip ahead until we find the port we want
   213  			continue
   214  		}
   215  		for _, binding := range bindings {
   216  
   217  			hostPortUint, err := strconv.ParseUint(binding.HostPort, 10, 64)
   218  			if err != nil {
   219  				return 0, "", 0, err
   220  			}
   221  
   222  			return uint(port.Int()), binding.HostIP, uint(hostPortUint), nil // nolint: staticcheck
   223  		}
   224  	}
   225  
   226  	if selectFirst {
   227  		return 0, "", 0, errors.New("no port binding")
   228  	} else {
   229  		return 0, "", 0, errors.New("specified port not bound")
   230  	}
   231  }
   232  
   233  func (d *DockerContainer) Host() string {
   234  	if d == nil {
   235  		panic("Cannot get host for a nil *DockerContainer")
   236  	}
   237  	_, hostIP, _, err := d.portMapping(true, -1)
   238  	if err != nil {
   239  		d.t.Fatal(err)
   240  	}
   241  
   242  	if hostIP == "0.0.0.0" {
   243  		return "127.0.0.1"
   244  	} else {
   245  		return hostIP
   246  	}
   247  }
   248  
   249  func (d *DockerContainer) Port() uint {
   250  	if d == nil {
   251  		panic("Cannot get port for a nil *DockerContainer")
   252  	}
   253  	_, _, port, err := d.portMapping(true, -1)
   254  	if err != nil {
   255  		d.t.Fatal(err)
   256  	}
   257  	return port
   258  }
   259  
   260  func (d *DockerContainer) PortFor(cPort int) uint {
   261  	if d == nil {
   262  		panic("Cannot get port for a nil *DockerContainer")
   263  	}
   264  	_, _, port, err := d.portMapping(false, cPort)
   265  	if err != nil {
   266  		d.t.Fatal(err)
   267  	}
   268  	return port
   269  }
   270  
   271  func (d *DockerContainer) NetworkSettings() dockertypes.NetworkSettings {
   272  	if d == nil {
   273  		panic("Cannot get network settings for a nil *DockerContainer")
   274  	}
   275  	netSettings := d.ContainerJSON.NetworkSettings
   276  	return *netSettings
   277  }
   278  
   279  type dockerImagePullOutput struct {
   280  	Status          string `json:"status"`
   281  	ProgressDetails struct {
   282  		Current int `json:"current"`
   283  		Total   int `json:"total"`
   284  	} `json:"progressDetail"`
   285  	Id       string `json:"id"`
   286  	Progress string `json:"progress"`
   287  }
   288  
   289  func init() {
   290  	rand.Seed(time.Now().UnixNano())
   291  }
   292  
   293  func pseudoRandStr(n int) string {
   294  	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
   295  	b := make([]rune, n)
   296  	for i := range b {
   297  		b[i] = letterRunes[rand.Intn(len(letterRunes))]
   298  	}
   299  	return string(b)
   300  }