github.com/extrame/fabric-ca@v2.0.0-alpha+incompatible/integration/runner/mysql.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  	"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/go-sql-driver/mysql" //Driver passed to the sqlx package
    25  	"github.com/jmoiron/sqlx"
    26  	"github.com/pkg/errors"
    27  	"github.com/tedsuo/ifrit"
    28  )
    29  
    30  // MySQLDefaultImage is used if none is specified
    31  const MySQLDefaultImage = "mysql:5.7"
    32  
    33  // MySQL defines a containerized MySQL Server
    34  type MySQL 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 *MySQL) Run(sigCh <-chan os.Signal, ready chan<- struct{}) error {
    57  	if c.Image == "" {
    58  		c.Image = MySQLDefaultImage
    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 = 3306
    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  			"3306/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  		Env: []string{
   109  			"MYSQL_ALLOW_EMPTY_PASSWORD=yes",
   110  		},
   111  	}
   112  
   113  	containerResp, err := c.Client.ContainerCreate(context.Background(), containerConfig, hostConfig, nil, c.Name)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	c.containerID = containerResp.ID
   118  
   119  	err = c.Client.ContainerStart(context.Background(), c.containerID, types.ContainerStartOptions{})
   120  	if err != nil {
   121  		return err
   122  	}
   123  	defer c.Stop()
   124  
   125  	response, err := c.Client.ContainerInspect(context.Background(), c.containerID)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	if c.HostPort == 0 {
   131  		port, err := strconv.Atoi(response.NetworkSettings.Ports[port][0].HostPort)
   132  		if err != nil {
   133  			return err
   134  		}
   135  		c.HostPort = port
   136  	}
   137  
   138  	c.hostAddress = net.JoinHostPort(
   139  		response.NetworkSettings.Ports[port][0].HostIP,
   140  		response.NetworkSettings.Ports[port][0].HostPort,
   141  	)
   142  	c.containerAddress = net.JoinHostPort(
   143  		response.NetworkSettings.IPAddress,
   144  		port.Port(),
   145  	)
   146  
   147  	streamCtx, streamCancel := context.WithCancel(context.Background())
   148  	defer streamCancel()
   149  	go c.streamLogs(streamCtx)
   150  
   151  	containerExit := c.wait()
   152  	ctx, cancel := context.WithTimeout(context.Background(), c.StartTimeout)
   153  	defer cancel()
   154  
   155  	select {
   156  	case <-ctx.Done():
   157  		return errors.Wrapf(ctx.Err(), "database in container %s did not start", c.containerID)
   158  	case <-containerExit:
   159  		return errors.New("container exited before ready")
   160  	case <-c.ready(ctx):
   161  		break
   162  	}
   163  
   164  	cancel()
   165  	close(ready)
   166  
   167  	for {
   168  		select {
   169  		case err := <-containerExit:
   170  			return err
   171  		case <-sigCh:
   172  			err := c.Stop()
   173  			if err != nil {
   174  				return err
   175  			}
   176  			return nil
   177  		}
   178  	}
   179  }
   180  
   181  func (c *MySQL) endpointReady(ctx context.Context, db *sqlx.DB) bool {
   182  	conn, err := db.Conn(ctx)
   183  	if err != nil {
   184  		return false
   185  	}
   186  
   187  	conn.QueryContext(ctx, "SET GLOBAL sql_mode = '';")
   188  	db.Close()
   189  
   190  	return true
   191  }
   192  
   193  func (c *MySQL) ready(ctx context.Context) <-chan struct{} {
   194  	readyCh := make(chan struct{})
   195  
   196  	str := fmt.Sprintf("root:@(%s:%d)/mysql", c.HostIP, c.HostPort)
   197  	db, err := sqlx.Open("mysql", str)
   198  	if err != nil {
   199  		ctx.Done()
   200  	}
   201  
   202  	go func() {
   203  		ticker := time.NewTicker(time.Second)
   204  		defer ticker.Stop()
   205  		for {
   206  			if c.endpointReady(ctx, db) {
   207  				close(readyCh)
   208  				return
   209  			}
   210  			select {
   211  			case <-ticker.C:
   212  			case <-ctx.Done():
   213  				return
   214  			}
   215  		}
   216  	}()
   217  
   218  	return readyCh
   219  }
   220  
   221  func (c *MySQL) wait() <-chan error {
   222  	exitCh := make(chan error, 1)
   223  	go func() {
   224  		exitCode, errCh := c.Client.ContainerWait(context.Background(), c.containerID, container.WaitConditionNotRunning)
   225  		select {
   226  		case exit := <-exitCode:
   227  			if exit.StatusCode != 0 {
   228  				err := fmt.Errorf("mysql: process exited with %d", exit.StatusCode)
   229  				exitCh <- err
   230  			} else {
   231  				exitCh <- nil
   232  			}
   233  		case err := <-errCh:
   234  			exitCh <- err
   235  		}
   236  	}()
   237  
   238  	return exitCh
   239  }
   240  
   241  func (c *MySQL) streamLogs(ctx context.Context) {
   242  	if c.ErrorStream == nil && c.OutputStream == nil {
   243  		return
   244  	}
   245  
   246  	logOptions := types.ContainerLogsOptions{
   247  		Follow:     true,
   248  		ShowStderr: c.ErrorStream != nil,
   249  		ShowStdout: c.OutputStream != nil,
   250  	}
   251  
   252  	out, err := c.Client.ContainerLogs(ctx, c.containerID, logOptions)
   253  	if err != nil {
   254  		fmt.Fprintf(c.ErrorStream, "log stream ended with error: %s", out)
   255  	}
   256  	stdcopy.StdCopy(c.OutputStream, c.ErrorStream, out)
   257  }
   258  
   259  // HostAddress returns the host address where this MySQL instance is available.
   260  func (c *MySQL) HostAddress() string {
   261  	return c.hostAddress
   262  }
   263  
   264  // ContainerAddress returns the container address where this MySQL instance
   265  // is available.
   266  func (c *MySQL) ContainerAddress() string {
   267  	return c.containerAddress
   268  }
   269  
   270  // ContainerID returns the container ID of this MySQL instance
   271  func (c *MySQL) ContainerID() string {
   272  	return c.containerID
   273  }
   274  
   275  // Start starts the MySQL container using an ifrit runner
   276  func (c *MySQL) Start() error {
   277  	p := ifrit.Invoke(c)
   278  
   279  	select {
   280  	case <-p.Ready():
   281  		return nil
   282  	case err := <-p.Wait():
   283  		return err
   284  	}
   285  }
   286  
   287  // Stop stops and removes the MySQL container
   288  func (c *MySQL) Stop() error {
   289  	c.mutex.Lock()
   290  	if c.stopped {
   291  		c.mutex.Unlock()
   292  		return errors.Errorf("container %s already stopped", c.containerID)
   293  	}
   294  	c.stopped = true
   295  	c.mutex.Unlock()
   296  
   297  	err := c.Client.ContainerStop(context.Background(), c.containerID, &c.ShutdownTimeout)
   298  	if err != nil {
   299  		return err
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  // GetConnectionString returns the sql connection string for connecting to the DB
   306  func (c *MySQL) GetConnectionString() (string, error) {
   307  	if c.HostIP != "" && c.HostPort != 0 {
   308  		return fmt.Sprintf("root:@(%s:%d)/mysql", c.HostIP, c.HostPort), nil
   309  	}
   310  	return "", fmt.Errorf("mysql db not initialized")
   311  }