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 }