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