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 }