github.com/hyperledger-labs/bdls@v2.1.1+incompatible/integration/runner/couchdb.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package runner 8 9 import ( 10 "context" 11 "fmt" 12 "io" 13 "net" 14 "net/http" 15 "os" 16 "runtime/debug" 17 "strconv" 18 "sync" 19 "time" 20 21 docker "github.com/fsouza/go-dockerclient" 22 "github.com/pkg/errors" 23 "github.com/tedsuo/ifrit" 24 ) 25 26 const CouchDBDefaultImage = "couchdb:2.3" 27 28 // CouchDB manages the execution of an instance of a dockerized CounchDB 29 // for tests. 30 type CouchDB struct { 31 Client *docker.Client 32 Image string 33 HostIP string 34 HostPort int 35 ContainerPort docker.Port 36 Name string 37 StartTimeout time.Duration 38 Binds []string 39 40 ErrorStream io.Writer 41 OutputStream io.Writer 42 43 creator string 44 containerID string 45 hostAddress string 46 containerAddress string 47 address string 48 49 mutex sync.Mutex 50 stopped bool 51 } 52 53 // Run runs a CouchDB container. It implements the ifrit.Runner interface 54 func (c *CouchDB) Run(sigCh <-chan os.Signal, ready chan<- struct{}) error { 55 if c.Image == "" { 56 c.Image = CouchDBDefaultImage 57 } 58 59 if c.Name == "" { 60 c.Name = DefaultNamer() 61 } 62 63 if c.HostIP == "" { 64 c.HostIP = "127.0.0.1" 65 } 66 67 if c.ContainerPort == docker.Port("") { 68 c.ContainerPort = docker.Port("5984/tcp") 69 } 70 71 if c.StartTimeout == 0 { 72 c.StartTimeout = DefaultStartTimeout 73 } 74 75 if c.Client == nil { 76 client, err := docker.NewClientFromEnv() 77 if err != nil { 78 return err 79 } 80 c.Client = client 81 } 82 83 hostConfig := &docker.HostConfig{ 84 AutoRemove: true, 85 PortBindings: map[docker.Port][]docker.PortBinding{ 86 c.ContainerPort: {{ 87 HostIP: c.HostIP, 88 HostPort: strconv.Itoa(c.HostPort), 89 }}, 90 }, 91 Binds: c.Binds, 92 } 93 94 container, err := c.Client.CreateContainer( 95 docker.CreateContainerOptions{ 96 Name: c.Name, 97 Config: &docker.Config{ 98 Image: c.Image, 99 Env: []string{"_creator=" + c.creator}, 100 }, 101 HostConfig: hostConfig, 102 }, 103 ) 104 if err != nil { 105 return err 106 } 107 c.containerID = container.ID 108 109 err = c.Client.StartContainer(container.ID, nil) 110 if err != nil { 111 return err 112 } 113 defer c.Stop() 114 115 container, err = c.Client.InspectContainer(container.ID) 116 if err != nil { 117 return err 118 } 119 c.hostAddress = net.JoinHostPort( 120 container.NetworkSettings.Ports[c.ContainerPort][0].HostIP, 121 container.NetworkSettings.Ports[c.ContainerPort][0].HostPort, 122 ) 123 c.containerAddress = net.JoinHostPort( 124 container.NetworkSettings.IPAddress, 125 c.ContainerPort.Port(), 126 ) 127 128 streamCtx, streamCancel := context.WithCancel(context.Background()) 129 defer streamCancel() 130 go c.streamLogs(streamCtx) 131 132 containerExit := c.wait() 133 ctx, cancel := context.WithTimeout(context.Background(), c.StartTimeout) 134 defer cancel() 135 136 select { 137 case <-ctx.Done(): 138 return errors.Wrapf(ctx.Err(), "database in container %s did not start", c.containerID) 139 case <-containerExit: 140 return errors.New("container exited before ready") 141 case <-c.ready(ctx, c.hostAddress): 142 c.address = c.hostAddress 143 case <-c.ready(ctx, c.containerAddress): 144 c.address = c.containerAddress 145 } 146 147 cancel() 148 close(ready) 149 150 for { 151 select { 152 case err := <-containerExit: 153 return err 154 case <-sigCh: 155 if err := c.Stop(); err != nil { 156 return err 157 } 158 } 159 } 160 } 161 162 func endpointReady(ctx context.Context, url string) bool { 163 ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) 164 defer cancel() 165 166 req, err := http.NewRequest(http.MethodGet, url, nil) 167 if err != nil { 168 return false 169 } 170 171 resp, err := http.DefaultClient.Do(req.WithContext(ctx)) 172 return err == nil && resp.StatusCode == http.StatusOK 173 } 174 175 func (c *CouchDB) ready(ctx context.Context, addr string) <-chan struct{} { 176 readyCh := make(chan struct{}) 177 url := fmt.Sprintf("http://%s/", addr) 178 go func() { 179 ticker := time.NewTicker(100 * time.Millisecond) 180 defer ticker.Stop() 181 for { 182 if endpointReady(ctx, url) { 183 close(readyCh) 184 return 185 } 186 select { 187 case <-ticker.C: 188 case <-ctx.Done(): 189 return 190 } 191 } 192 }() 193 194 return readyCh 195 } 196 197 func (c *CouchDB) wait() <-chan error { 198 exitCh := make(chan error) 199 go func() { 200 exitCode, err := c.Client.WaitContainer(c.containerID) 201 if err == nil { 202 err = fmt.Errorf("couchdb: process exited with %d", exitCode) 203 } 204 exitCh <- err 205 }() 206 207 return exitCh 208 } 209 210 func (c *CouchDB) streamLogs(ctx context.Context) { 211 if c.ErrorStream == nil && c.OutputStream == nil { 212 return 213 } 214 215 logOptions := docker.LogsOptions{ 216 Context: ctx, 217 Container: c.containerID, 218 Follow: true, 219 ErrorStream: c.ErrorStream, 220 OutputStream: c.OutputStream, 221 Stderr: c.ErrorStream != nil, 222 Stdout: c.OutputStream != nil, 223 } 224 225 err := c.Client.Logs(logOptions) 226 if err != nil { 227 fmt.Fprintf(c.ErrorStream, "log stream ended with error: %s", err) 228 } 229 } 230 231 // Address returns the address successfully used by the readiness check. 232 func (c *CouchDB) Address() string { 233 return c.address 234 } 235 236 // HostAddress returns the host address where this CouchDB instance is available. 237 func (c *CouchDB) HostAddress() string { 238 return c.hostAddress 239 } 240 241 // ContainerAddress returns the container address where this CouchDB instance 242 // is available. 243 func (c *CouchDB) ContainerAddress() string { 244 return c.containerAddress 245 } 246 247 // ContainerID returns the container ID of this CouchDB 248 func (c *CouchDB) ContainerID() string { 249 return c.containerID 250 } 251 252 // Start starts the CouchDB container using an ifrit runner 253 func (c *CouchDB) Start() error { 254 c.creator = string(debug.Stack()) 255 p := ifrit.Invoke(c) 256 257 select { 258 case <-p.Ready(): 259 return nil 260 case err := <-p.Wait(): 261 return err 262 } 263 } 264 265 // Stop stops and removes the CouchDB container 266 func (c *CouchDB) Stop() error { 267 c.mutex.Lock() 268 if c.stopped { 269 c.mutex.Unlock() 270 return errors.Errorf("container %s already stopped", c.containerID) 271 } 272 c.stopped = true 273 c.mutex.Unlock() 274 275 err := c.Client.StopContainer(c.containerID, 0) 276 if err != nil { 277 return err 278 } 279 280 return nil 281 }