Last active
November 24, 2017 13:54
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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