github.com/m3db/m3@v1.5.0/src/integration/resources/docker/docker_resource.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package docker
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"runtime"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"github.com/ory/dockertest/v3"
    32  	dc "github.com/ory/dockertest/v3/docker"
    33  	"github.com/ory/dockertest/v3/docker/types/mount"
    34  	"go.uber.org/zap"
    35  
    36  	"github.com/m3db/m3/src/integration/resources"
    37  )
    38  
    39  // Resource is an object that provides a handle
    40  // to a service being spun up via docker.
    41  type Resource struct {
    42  	closed bool
    43  
    44  	logger *zap.Logger
    45  
    46  	resource *dockertest.Resource
    47  	pool     *dockertest.Pool
    48  }
    49  
    50  // NewDockerResource creates a new DockerResource.
    51  func NewDockerResource(
    52  	pool *dockertest.Pool,
    53  	resourceOpts ResourceOptions,
    54  ) (*Resource, error) {
    55  	var (
    56  		source        = resourceOpts.Source
    57  		image         = resourceOpts.Image
    58  		containerName = resourceOpts.ContainerName
    59  		iOpts         = resourceOpts.InstrumentOpts
    60  		portList      = resourceOpts.PortList
    61  
    62  		logger = iOpts.Logger().With(
    63  			zap.String("source", source),
    64  			zap.String("container", containerName),
    65  		)
    66  	)
    67  
    68  	opts := exposePorts(newOptions(containerName), portList)
    69  
    70  	hostConfigOpts := func(c *dc.HostConfig) {
    71  		c.AutoRemove = true
    72  		c.NetworkMode = networkName
    73  		// Allow the docker container to call services on the host machine.
    74  		// Docker for OS X and Windows support the host.docker.internal hostname
    75  		// natively, but Docker for Linux requires us to register host.docker.internal
    76  		// as an extra host before the hostname works.
    77  		if runtime.GOOS == "linux" {
    78  			c.ExtraHosts = []string{"host.docker.internal:172.17.0.1"}
    79  		}
    80  		mounts := make([]dc.HostMount, 0, len(resourceOpts.TmpfsMounts))
    81  		for _, m := range resourceOpts.TmpfsMounts {
    82  			mounts = append(mounts, dc.HostMount{
    83  				Target: m,
    84  				Type:   string(mount.TypeTmpfs),
    85  			})
    86  		}
    87  
    88  		c.Mounts = mounts
    89  	}
    90  
    91  	var resource *dockertest.Resource
    92  	var err error
    93  	if image.Name == "" {
    94  		logger.Info("connecting to existing container", zap.String("container", containerName))
    95  		var ok bool
    96  		resource, ok = pool.ContainerByName(containerName)
    97  		if !ok {
    98  			logger.Error("could not find container", zap.Error(err))
    99  			return nil, fmt.Errorf("could not find container %v", containerName)
   100  		}
   101  	} else {
   102  		opts = useImage(opts, image)
   103  		opts.Mounts = resourceOpts.Mounts
   104  		imageWithTag := fmt.Sprintf("%v:%v", image.Name, image.Tag)
   105  		logger.Info("running container with options",
   106  			zap.String("image", imageWithTag), zap.Any("options", opts))
   107  		resource, err = pool.RunWithOptions(opts, hostConfigOpts)
   108  	}
   109  
   110  	if err != nil {
   111  		logger.Error("could not run container", zap.Error(err))
   112  		return nil, err
   113  	}
   114  
   115  	return &Resource{
   116  		logger:   logger,
   117  		resource: resource,
   118  		pool:     pool,
   119  	}, nil
   120  }
   121  
   122  // GetPort retrieves the port for accessing this resource.
   123  func (c *Resource) GetPort(bindPort int) (int, error) {
   124  	port := c.resource.GetPort(fmt.Sprintf("%d/tcp", bindPort))
   125  	return strconv.Atoi(port)
   126  }
   127  
   128  // GetURL retrieves the URL for accessing this resource.
   129  func (c *Resource) GetURL(port int, path string) string {
   130  	tcpPort := fmt.Sprintf("%d/tcp", port)
   131  	return fmt.Sprintf("http://%s:%s/%s",
   132  		c.resource.GetBoundIP(tcpPort), c.resource.GetPort(tcpPort), path)
   133  }
   134  
   135  // Exec runs commands within a docker container.
   136  func (c *Resource) Exec(commands ...string) (string, error) {
   137  	if c.closed {
   138  		return "", errClosed
   139  	}
   140  
   141  	// NB: this is prefixed with a `/` that should be trimmed off.
   142  	name := strings.TrimLeft(c.resource.Container.Name, "/")
   143  	logger := c.logger.With(resources.ZapMethod("exec"))
   144  	client := c.pool.Client
   145  	exec, err := client.CreateExec(dc.CreateExecOptions{
   146  		AttachStdout: true,
   147  		AttachStderr: true,
   148  		Container:    name,
   149  		Cmd:          commands,
   150  	})
   151  	if err != nil {
   152  		logger.Error("failed generating exec", zap.Error(err))
   153  		return "", err
   154  	}
   155  
   156  	var outBuf, errBuf bytes.Buffer
   157  	logger.Info("starting exec",
   158  		zap.Strings("commands", commands),
   159  		zap.String("execID", exec.ID))
   160  	err = client.StartExec(exec.ID, dc.StartExecOptions{
   161  		OutputStream: &outBuf,
   162  		ErrorStream:  &errBuf,
   163  	})
   164  
   165  	output, bufferErr := outBuf.String(), errBuf.String()
   166  	logger = logger.With(zap.String("stdout", output),
   167  		zap.String("stderr", bufferErr))
   168  
   169  	if err != nil {
   170  		logger.Error("failed starting exec",
   171  			zap.Error(err))
   172  		return "", err
   173  	}
   174  
   175  	if len(bufferErr) != 0 {
   176  		err = errors.New(bufferErr)
   177  		logger.Error("exec failed", zap.Error(err))
   178  		return "", err
   179  	}
   180  
   181  	logger.Info("succeeded exec")
   182  	return output, nil
   183  }
   184  
   185  // GoalStateExec runs commands within a container until
   186  // a specified goal state is met.
   187  func (c *Resource) GoalStateExec(
   188  	verifier resources.GoalStateVerifier,
   189  	commands ...string,
   190  ) error {
   191  	if c.closed {
   192  		return errClosed
   193  	}
   194  
   195  	logger := c.logger.With(resources.ZapMethod("GoalStateExec"))
   196  	return c.pool.Retry(func() error {
   197  		err := verifier(c.Exec(commands...))
   198  		if err != nil {
   199  			logger.Error("rerunning goal state verification", zap.Error(err))
   200  			return err
   201  		}
   202  
   203  		logger.Info("goal state verification succeeded")
   204  		return nil
   205  	})
   206  }
   207  
   208  // Close closes and cleans up the resource.
   209  func (c *Resource) Close() error {
   210  	if c.closed {
   211  		c.logger.Error("closing closed resource", zap.Error(errClosed))
   212  		return errClosed
   213  	}
   214  
   215  	c.closed = true
   216  	c.logger.Info("closing resource")
   217  	return c.pool.Purge(c.resource)
   218  }
   219  
   220  // Closed returns true if the resource has been closed.
   221  func (c *Resource) Closed() bool {
   222  	return c.closed
   223  }