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  }