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 }