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  }