github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/dockertest/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 dockertest
    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  
    37  // Resource is an object that provides a handle
    38  // to a service being spun up via docker.
    39  type Resource struct {
    40  	resource *dockertest.Resource
    41  	closed   bool
    42  
    43  	logger *zap.Logger
    44  
    45  	pool *dockertest.Pool
    46  }
    47  
    48  // NewDockerResource creates a new DockerResource.
    49  // If resourceOpts.Image is empty, it will attempt to connect to an existing container.
    50  // Otherwise, it will start the container with the specified image.
    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  	// TODO: this seems hard to use; a different method might be more appropriate.
    69  	if image.Name == "" {
    70  		logger.Info("connecting to existing container", zap.String("container", containerName))
    71  		var ok bool
    72  		resource, ok := pool.ContainerByName(containerName)
    73  		if !ok {
    74  			logger.Error("could not find container")
    75  			return nil, fmt.Errorf("could not find container %v", containerName)
    76  		}
    77  
    78  		return &Resource{
    79  			logger:   logger,
    80  			resource: resource,
    81  			pool:     nil,
    82  		}, nil
    83  	}
    84  
    85  	opts := newOptions(containerName)
    86  	if !resourceOpts.NoNetworkOverlay {
    87  		opts.NetworkID = networkName
    88  	}
    89  	opts, err := exposePorts(opts, portList, resourceOpts.PortMappings)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	hostConfigOpts := func(c *dc.HostConfig) {
    95  		if !resourceOpts.NoNetworkOverlay {
    96  			c.NetworkMode = networkName
    97  		}
    98  		// Allow the docker container to call services on the host machine.
    99  		// Docker for OS X and Windows support the host.docker.internal hostname
   100  		// natively, but Docker for Linux requires us to register host.docker.internal
   101  		// as an extra host before the hostname works.
   102  		if runtime.GOOS == "linux" {
   103  			c.ExtraHosts = []string{"host.docker.internal:172.17.0.1"}
   104  		}
   105  		mounts := make([]dc.HostMount, 0, len(resourceOpts.TmpfsMounts))
   106  		for _, m := range resourceOpts.TmpfsMounts {
   107  			mounts = append(mounts, dc.HostMount{
   108  				Target: m,
   109  				Type:   string(mount.TypeTmpfs),
   110  			})
   111  		}
   112  
   113  		c.Mounts = mounts
   114  	}
   115  
   116  	opts = useImage(opts, image)
   117  	opts.Mounts = resourceOpts.Mounts
   118  	opts.Env = resourceOpts.Env
   119  	opts.Cmd = resourceOpts.Cmd
   120  
   121  	imageWithTag := fmt.Sprintf("%v:%v", image.Name, image.Tag)
   122  	logger.Info("running container with options",
   123  		zap.String("image", imageWithTag), zap.Any("options", opts))
   124  	resource, err := pool.RunWithOptions(opts, hostConfigOpts)
   125  
   126  	if err != nil {
   127  		logger.Error("could not run container", zap.Error(err))
   128  		return nil, err
   129  	}
   130  
   131  	return &Resource{
   132  		logger:   logger,
   133  		resource: resource,
   134  		pool:     pool,
   135  	}, nil
   136  }
   137  
   138  // GetPort retrieves the port for accessing this resource.
   139  func (c *Resource) GetPort(bindPort int) (int, error) {
   140  	port := c.resource.GetPort(fmt.Sprintf("%d/tcp", bindPort))
   141  	return strconv.Atoi(port)
   142  }
   143  
   144  // GetURL retrieves the URL for accessing this resource.
   145  func (c *Resource) GetURL(port int, path string) string {
   146  	tcpPort := fmt.Sprintf("%d/tcp", port)
   147  	return fmt.Sprintf("http://%s:%s/%s",
   148  		c.resource.GetBoundIP(tcpPort), c.resource.GetPort(tcpPort), path)
   149  }
   150  
   151  // Exec runs commands within a docker container.
   152  func (c *Resource) Exec(commands ...string) (string, error) {
   153  	if c.closed {
   154  		return "", ErrClosed
   155  	}
   156  
   157  	// NB: this is prefixed with a `/` that should be trimmed off.
   158  	name := strings.TrimLeft(c.resource.Container.Name, "/")
   159  	logger := c.logger.With(zap.String("method", "exec"))
   160  	client := c.pool.Client
   161  	exec, err := client.CreateExec(dc.CreateExecOptions{
   162  		AttachStdout: true,
   163  		AttachStderr: true,
   164  		Container:    name,
   165  		Cmd:          commands,
   166  	})
   167  	if err != nil {
   168  		logger.Error("failed generating exec", zap.Error(err))
   169  		return "", err
   170  	}
   171  
   172  	var outBuf, errBuf bytes.Buffer
   173  	logger.Info("starting exec",
   174  		zap.Strings("commands", commands),
   175  		zap.String("execID", exec.ID))
   176  	err = client.StartExec(exec.ID, dc.StartExecOptions{
   177  		OutputStream: &outBuf,
   178  		ErrorStream:  &errBuf,
   179  	})
   180  
   181  	output, bufferErr := outBuf.String(), errBuf.String()
   182  	logger = logger.With(zap.String("stdout", output),
   183  		zap.String("stderr", bufferErr))
   184  
   185  	if err != nil {
   186  		logger.Error("failed starting exec",
   187  			zap.Error(err))
   188  		return "", err
   189  	}
   190  
   191  	if len(bufferErr) != 0 {
   192  		err = errors.New(bufferErr)
   193  		logger.Error("exec failed", zap.Error(err))
   194  		return "", err
   195  	}
   196  
   197  	logger.Info("succeeded exec")
   198  	return output, nil
   199  }
   200  
   201  // GoalStateExec runs commands within a container until
   202  // a specified goal state is met.
   203  func (c *Resource) GoalStateExec(
   204  	verifier GoalStateVerifier,
   205  	commands ...string,
   206  ) error {
   207  	if c.closed {
   208  		return ErrClosed
   209  	}
   210  
   211  	logger := c.logger.With(zap.String("method", "GoalStateExec"))
   212  	return c.pool.Retry(func() error {
   213  		err := verifier(c.Exec(commands...))
   214  		if err != nil {
   215  			logger.Error("rerunning goal state verification", zap.Error(err))
   216  			return err
   217  		}
   218  
   219  		logger.Info("goal state verification succeeded")
   220  		return nil
   221  	})
   222  }
   223  
   224  // Close closes and cleans up the resource.
   225  func (c *Resource) Close() error {
   226  	if c.closed {
   227  		c.logger.Error("closing closed resource", zap.Error(ErrClosed))
   228  		return ErrClosed
   229  	}
   230  
   231  	c.closed = true
   232  	c.logger.Info("closing resource")
   233  	return c.pool.Purge(c.Resource())
   234  }
   235  
   236  // Closed returns true if the resource has been closed.
   237  func (c *Resource) Closed() bool {
   238  	return c.closed
   239  }
   240  
   241  // Resource is the underlying dockertest resource used by this Resource. It can be used to perform more advanced
   242  // operations not exposed by this class.
   243  func (c *Resource) Resource() *dockertest.Resource {
   244  	return c.resource
   245  }