Skip to content

Instantly share code, notes, and snippets.

@ymgyt
Last active November 24, 2017 13:54
Show Gist options
  • Save ymgyt/0c3b3711f49753746d80f7b30c4181f4 to your computer and use it in GitHub Desktop.
Save ymgyt/0c3b3711f49753746d80f7b30c4181f4 to your computer and use it in GitHub Desktop.
Goからlocalのtest用DB(MySQL)をdockerで起動する ref: https://qiita.com/YmgchiYt/items/cc97142614f5b61a69e9
# containerは存在しない
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 起動
go run main.go
2017-11-23T02:30:38.881+0900 INFO workspace/main.go:52 local_docker_db {"status": "starting"}
2017-11-23T02:30:38.934+0900 DEBUG workspace/main.go:77 local_docker_db {"container_info": "not_found", "container": "myproject-mysql-db"}
2017-11-23T02:30:39.428+0900 DEBUG workspace/main.go:107 local_docker_db {"exec docker cmd": "docker container run --detach --name myproject-mysql-db --publish 3307:3306 --mount type=bind,source=/tmp/docker_db,target=/var/lib/mysql --env MYSQL_USER=gopher --env MYSQL_PASSWORD=golangorgohome --env MYSQL_INITDB_SKIP_TZINFO=yes --env MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:5.6", "stdout": "4a7336c9364b14df0813463161335d8690d9c6e777f5c631bb406b5f55d2e181\n", "stderr": ""}
# 確認
mysql -h0.0.0.0 -ugopher -pgolangorgohome --port=3307 -e "show databases" --batch information_schema
mysql: [Warning] Using a password on the command line interface can be insecure.
Database
information_schema
# 既に起動中に、再度起動
go run main.go
2017-11-23T02:32:34.746+0900 INFO workspace/main.go:52 local_docker_db {"status": "starting"}
2017-11-23T02:32:34.830+0900 DEBUG workspace/main.go:66 local_docker_db {"container_info": "already running", "container": "myproject-mysql-db"}
# 一度、停止
docker container stop myproject-mysql-db
myproject-mysql-db
# 停止中のcontainerを起動
go run main.go
2017-11-23T02:35:03.844+0900 INFO workspace/main.go:52 local_docker_db {"status": "starting"}
2017-11-23T02:35:03.899+0900 DEBUG workspace/main.go:94 local_docker_db {"container_info": "exited", "container": "myproject-mysql-db"}
2017-11-23T02:35:04.289+0900 DEBUG workspace/main.go:107 local_docker_db {"exec docker cmd": "docker container start 4a7336c9364b", "stdout": "4a7336c9364b\n", "stderr": ""}
# 確認
mysql -h0.0.0.0 -ugopher -pgolangorgohome --port=3307 -e "show databases" --batch information_schema
mysql: [Warning] Using a password on the command line interface can be insecure.
Database
information_schema
package main
import (
"bytes"
"context"
"log"
"os"
"os/exec"
"strings"
"time"
"github.com/juju/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
dockerStatusExited = "exited"
dockerStatusRunning = "running"
)
type LocalDockerDB struct {
Image string
Container string
MountDir string
IPAddr string
Port string
User string
Password string
Logger *zap.Logger
}
type LocalDockerDBOption func(*LocalDockerDB)
func NewLocalDockerDB(options ...LocalDockerDBOption) *LocalDockerDB {
d := &LocalDockerDB{
Image: "mysql:5.6",
Container: "myproject-mysql-db",
MountDir: "/tmp/docker_db",
Port: "3306",
User: "gopher",
Password: "golangorgohome",
Logger: zap.NewNop(),
}
for _, option := range options {
option(d)
}
return d
}
func (d *LocalDockerDB) Start(ctx context.Context) error {
d.Logger.Info("local_docker_db", zap.String("status", "starting"))
// docker daemon must be running
if err := exec.Command("docker", "container", "ls").Run(); err != nil {
return errors.Trace(err)
}
if err := os.MkdirAll(d.MountDir, 0755); err != nil {
return errors.Annotatef(err, "failed to create mount dir %q", d.MountDir)
}
info, err := dockerContainerInfo(d.Container)
if info != nil && info.status == dockerStatusRunning {
d.Logger.Debug("local_docker_db",
zap.String("container_info", "already running"),
zap.String("container", d.Container),
)
d.IPAddr, err = decodeIPPort(info.mappings)
return errors.Trace(err)
}
var cmd *exec.Cmd
// start or resume container
if info == nil && errors.IsNotFound(err) {
d.Logger.Debug("local_docker_db",
zap.String("container_info", "not_found"),
zap.String("container", d.Container),
)
volumeMapping := "type=bind,source=" + d.MountDir + ",target=/var/lib/mysql"
portMapping := d.Port + ":3306"
cmd = exec.Command("docker", "container", "run", "--detach",
"--name", d.Container,
"--publish", portMapping,
"--mount", volumeMapping,
"--env", "MYSQL_USER="+d.User,
"--env", "MYSQL_PASSWORD="+d.Password,
"--env", "MYSQL_INITDB_SKIP_TZINFO=yes",
"--env", "MYSQL_ALLOW_EMPTY_PASSWORD=yes",
d.Image,
)
} else if info != nil && info.status != "" {
d.Logger.Debug("local_docker_db",
zap.String("container_info", info.status),
zap.String("container", d.Container),
)
cmd = exec.Command("docker", "container", "start", info.id)
} else {
return errors.New("failed to start container")
}
var stdOut, stdErr bytes.Buffer
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err = cmd.Run()
d.Logger.Debug("local_docker_db",
zap.String("exec docker cmd", strings.Join(cmd.Args, " ")),
zap.String("stdout", stdOut.String()),
zap.String("stderr", stdErr.String()),
)
if err != nil {
return errors.Trace(err)
}
loop:
for {
select {
case <-ctx.Done():
err = ctx.Err()
break loop
default:
info, err := dockerContainerInfo(d.Container)
if err != nil {
err = errors.Trace(err)
break loop
}
if info != nil && info.status == dockerStatusRunning {
d.IPAddr, err = decodeIPPort(info.mappings)
break loop
}
time.Sleep(time.Second)
}
}
return errors.Trace(err)
}
type containerInfo struct {
id string
name string
mappings string
status string
}
func decodeContainerStatus(status string) string {
// convert "Exited(0) 2 days ago" into statusExited
if strings.HasPrefix(status, "Exited") {
return dockerStatusExited
}
// convert "Up <time>" into statusRunning
if strings.HasPrefix(status, "Up") {
return dockerStatusRunning
}
return strings.ToLower(status)
}
func dockerContainerInfo(containerName string) (*containerInfo, error) {
cmd := exec.Command("docker", "container", "ls", "-a", "--format", "{{.ID}}|{{.Status}}|{{.Ports}}|{{.Names}}")
stdOutErr, err := cmd.CombinedOutput()
if err != nil {
return nil, errors.Annotate(err, string(stdOutErr))
}
s := string(stdOutErr)
s = strings.TrimSpace(s)
lines := strings.Split(s, "\n")
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, "|")
if len(parts) != 4 {
return nil, errors.Errorf("unexpected output from docker container ls %s. expected 4 parts, got %d (%v)", line, len(parts), parts)
}
id, status, mappings, name := parts[0], parts[1], parts[2], parts[3]
if containerName == name {
return &containerInfo{
id: id,
name: name,
mappings: mappings,
status: decodeContainerStatus(status),
}, nil
}
}
return nil, errors.NotFoundf(containerName)
}
// given:
// 0.0.0.0:3307->3306/tcp
func decodeIPPort(mappings string) (string, error) {
parts := strings.Split(mappings, "->")
if len(parts) != 2 {
return "", errors.Errorf("invalid mappings string: %q", mappings)
}
parts = strings.Split(parts[0], ":")
if len(parts) != 2 {
return "", errors.Errorf("invalid mappings string: %q", mappings)
}
return parts[0], nil
}
func NewLogger(level int) (*zap.Logger, error) {
cfg := &zap.Config{
Level: zap.NewAtomicLevelAt(zapcore.Level(int8(level))),
Development: true,
Encoding: "console", // or json
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
EncoderConfig: zapcore.EncoderConfig{
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
MessageKey: "M",
StacktraceKey: "S",
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
},
}
zapOption := zap.AddStacktrace(zapcore.ErrorLevel)
return cfg.Build(zapOption)
}
func main() {
logger, _ := NewLogger(-1)
d := NewLocalDockerDB(func(d *LocalDockerDB) {
d.Port = "3307"
d.Logger = logger
})
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*10))
defer cancel()
if err := d.Start(ctx); err != nil {
log.Fatal(errors.ErrorStack(err))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment