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  }