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 }