github.com/canhui/fabric_ca2_2@v2.0.0-alpha+incompatible/integration/runner/postgres.go (about)

     1  package runner
     2  
     3  /*
     4  Copyright IBM Corp. All Rights Reserved.
     5  
     6  SPDX-License-Identifier: Apache-2.0
     7  */
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"os"
    15  	"strconv"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/docker/docker/api/types"
    20  	"github.com/docker/docker/api/types/container"
    21  	docker "github.com/docker/docker/client"
    22  	"github.com/docker/docker/pkg/stdcopy"
    23  	"github.com/docker/go-connections/nat"
    24  	"github.com/jmoiron/sqlx"
    25  	_ "github.com/lib/pq" //Driver passed to the sqlx package
    26  	"github.com/pkg/errors"
    27  	"github.com/tedsuo/ifrit"
    28  )
    29  
    30  // PostgresDBDefaultImage is used if none is specified
    31  const PostgresDBDefaultImage = "postgres:9.6"
    32  
    33  // PostgresDB defines a containerized Postgres Server
    34  type PostgresDB struct {
    35  	Client          *docker.Client
    36  	Image           string
    37  	HostIP          string
    38  	HostPort        int
    39  	Name            string
    40  	ContainerPort   int
    41  	StartTimeout    time.Duration
    42  	ShutdownTimeout time.Duration
    43  
    44  	ErrorStream  io.Writer
    45  	OutputStream io.Writer
    46  
    47  	containerID      string
    48  	hostAddress      string
    49  	containerAddress string
    50  
    51  	mutex   sync.Mutex
    52  	stopped bool
    53  }
    54  
    55  // Run is called by the ifrit runner to start a process
    56  func (c *PostgresDB) Run(sigCh <-chan os.Signal, ready chan<- struct{}) error {
    57  	if c.Image == "" {
    58  		c.Image = PostgresDBDefaultImage
    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.StartTimeout == 0 {
    70  		c.StartTimeout = DefaultStartTimeout
    71  	}
    72  
    73  	if c.ShutdownTimeout == 0 {
    74  		c.ShutdownTimeout = time.Duration(DefaultShutdownTimeout)
    75  	}
    76  
    77  	if c.ContainerPort == 0 {
    78  		c.ContainerPort = 5432
    79  	}
    80  
    81  	port, err := nat.NewPort("tcp", strconv.Itoa(c.ContainerPort))
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	if c.Client == nil {
    87  		client, err := docker.NewClientWithOpts(docker.FromEnv)
    88  		if err != nil {
    89  			return err
    90  		}
    91  		client.NegotiateAPIVersion(context.Background())
    92  		c.Client = client
    93  	}
    94  
    95  	hostConfig := &container.HostConfig{
    96  		AutoRemove: true,
    97  		PortBindings: nat.PortMap{
    98  			"5432/tcp": []nat.PortBinding{
    99  				{
   100  					HostIP:   c.HostIP,
   101  					HostPort: strconv.Itoa(c.HostPort),
   102  				},
   103  			},
   104  		},
   105  	}
   106  	containerConfig := &container.Config{
   107  		Image: c.Image,
   108  	}
   109  
   110  	containerResp, err := c.Client.ContainerCreate(context.Background(), containerConfig, hostConfig, nil, c.Name)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	c.containerID = containerResp.ID
   115  
   116  	err = c.Client.ContainerStart(context.Background(), c.containerID, types.ContainerStartOptions{})
   117  	if err != nil {
   118  		return err
   119  	}
   120  	defer c.Stop()
   121  
   122  	response, err := c.Client.ContainerInspect(context.Background(), c.containerID)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	if c.HostPort == 0 {
   128  		port, err := strconv.Atoi(response.NetworkSettings.Ports[port][0].HostPort)
   129  		if err != nil {
   130  			return err
   131  		}
   132  		c.HostPort = port
   133  	}
   134  
   135  	c.hostAddress = net.JoinHostPort(
   136  		response.NetworkSettings.Ports[port][0].HostIP,
   137  		response.NetworkSettings.Ports[port][0].HostPort,
   138  	)
   139  	c.containerAddress = net.JoinHostPort(
   140  		response.NetworkSettings.IPAddress,
   141  		port.Port(),
   142  	)
   143  
   144  	streamCtx, streamCancel := context.WithCancel(context.Background())
   145  	defer streamCancel()
   146  	go c.streamLogs(streamCtx)
   147  
   148  	containerExit := c.wait()
   149  	ctx, cancel := context.WithTimeout(context.Background(), c.StartTimeout)
   150  	defer cancel()
   151  
   152  	select {
   153  	case <-ctx.Done():
   154  		return errors.Wrapf(ctx.Err(), "database in container %s did not start", c.containerID)
   155  	case <-containerExit:
   156  		return errors.New("container exited before ready")
   157  	case <-c.ready(ctx):
   158  		break
   159  	}
   160  
   161  	cancel()
   162  	close(ready)
   163  
   164  	for {
   165  		select {
   166  		case err := <-containerExit:
   167  			return err
   168  		case <-sigCh:
   169  			err := c.Stop()
   170  			if err != nil {
   171  				return err
   172  			}
   173  			return nil
   174  		}
   175  	}
   176  }
   177  
   178  func (c *PostgresDB) endpointReady(ctx context.Context, db *sqlx.DB) bool {
   179  	_, err := db.Conn(ctx)
   180  	if err != nil {
   181  		return false
   182  	}
   183  
   184  	db.Close()
   185  	return true
   186  }
   187  
   188  func (c *PostgresDB) ready(ctx context.Context) <-chan struct{} {
   189  	readyCh := make(chan struct{})
   190  
   191  	connStr, _ := c.GetConnectionString()
   192  	db, err := sqlx.Open("postgres", connStr)
   193  	if err != nil {
   194  		ctx.Done()
   195  	}
   196  
   197  	go func() {
   198  		ticker := time.NewTicker(100 * time.Millisecond)
   199  		defer ticker.Stop()
   200  		for {
   201  			if c.endpointReady(ctx, db) {
   202  				close(readyCh)
   203  				return
   204  			}
   205  			select {
   206  			case <-ticker.C:
   207  			case <-ctx.Done():
   208  				return
   209  			}
   210  		}
   211  	}()
   212  
   213  	return readyCh
   214  }
   215  
   216  func (c *PostgresDB) wait() <-chan error {
   217  	exitCh := make(chan error, 1)
   218  	go func() {
   219  		exitCode, errCh := c.Client.ContainerWait(context.Background(), c.containerID, container.WaitConditionNotRunning)
   220  		select {
   221  		case exit := <-exitCode:
   222  			if exit.StatusCode != 0 {
   223  				err := fmt.Errorf("postgres: process exited with %d", exit.StatusCode)
   224  				exitCh <- err
   225  			} else {
   226  				exitCh <- nil
   227  			}
   228  		case err := <-errCh:
   229  			exitCh <- err
   230  		}
   231  	}()
   232  
   233  	return exitCh
   234  }
   235  
   236  func (c *PostgresDB) streamLogs(ctx context.Context) {
   237  	if c.ErrorStream == nil && c.OutputStream == nil {
   238  		return
   239  	}
   240  
   241  	logOptions := types.ContainerLogsOptions{
   242  		Follow:     true,
   243  		ShowStderr: c.ErrorStream != nil,
   244  		ShowStdout: c.OutputStream != nil,
   245  	}
   246  
   247  	out, err := c.Client.ContainerLogs(ctx, c.containerID, logOptions)
   248  	if err != nil {
   249  		fmt.Fprintf(c.ErrorStream, "log stream ended with error: %s", out)
   250  	}
   251  	stdcopy.StdCopy(c.OutputStream, c.ErrorStream, out)
   252  }
   253  
   254  // HostAddress returns the host address where this PostgresDB instance is available.
   255  func (c *PostgresDB) HostAddress() string {
   256  	return c.hostAddress
   257  }
   258  
   259  // ContainerAddress returns the container address where this PostgresDB instance
   260  // is available.
   261  func (c *PostgresDB) ContainerAddress() string {
   262  	return c.containerAddress
   263  }
   264  
   265  // ContainerID returns the container ID of this PostgresDB
   266  func (c *PostgresDB) ContainerID() string {
   267  	return c.containerID
   268  }
   269  
   270  // Start starts the PostgresDB container using an ifrit runner
   271  func (c *PostgresDB) Start() error {
   272  	p := ifrit.Invoke(c)
   273  
   274  	select {
   275  	case <-p.Ready():
   276  		return nil
   277  	case err := <-p.Wait():
   278  		return err
   279  	}
   280  }
   281  
   282  // Stop stops and removes the PostgresDB container
   283  func (c *PostgresDB) Stop() error {
   284  	c.mutex.Lock()
   285  	if c.stopped {
   286  		c.mutex.Unlock()
   287  		return errors.Errorf("container %s already stopped", c.containerID)
   288  	}
   289  	c.stopped = true
   290  	c.mutex.Unlock()
   291  
   292  	err := c.Client.ContainerStop(context.Background(), c.containerID, &c.ShutdownTimeout)
   293  	if err != nil {
   294  		return err
   295  	}
   296  
   297  	return nil
   298  }
   299  
   300  // GetConnectionString returns the sql connection string for connecting to the DB
   301  func (c *PostgresDB) GetConnectionString() (string, error) {
   302  	if c.HostIP != "" && c.HostPort != 0 {
   303  		return fmt.Sprintf("host=%s port=%d user=postgres dbname=postgres sslmode=disable",
   304  			c.HostIP, c.HostPort), nil
   305  	}
   306  	return "", fmt.Errorf("DB not initialized")
   307  }