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