From b39ee78bf96055f9ad77b8244009a7d12b8e00cb Mon Sep 17 00:00:00 2001 From: nanofi Date: Sun, 21 Dec 2014 18:47:42 +0900 Subject: [PATCH] Dynamic configure --- .dockerignore | 6 + .gitignore | 2 + .travis.yml | 24 ++++ Dockerfile | 22 +++- Makefile | 30 +++++ Procfile | 2 + README.md | 18 ++- client.go | 141 ++++++++++++++++++++++ docker-mysql.go | 197 +++++++++++++++++++++++++++++++ docker-entrypoint.sh => start.sh | 22 ---- 10 files changed, 437 insertions(+), 27 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 Procfile create mode 100644 client.go create mode 100644 docker-mysql.go rename docker-entrypoint.sh => start.sh (58%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2450b09 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +README.md +LICENSE +Makefile +GLOCKFILE +*.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..230deef --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +docker-mysql diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ecd1cce --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: go +go: + - 1.4 +install: + - make deps +script: + - make test +before_deploy: + - make tool + - make release +deploy: + provider: releases + api_key: + secure: i4LFQ1kyX97abC925LrvK4rVO7zzaaqsZzWBCHSjxUBunKVtTe7jc8XHHG0Mh2qJfdog+sCh57+AmZsDy3B6fw3ZSrpFJh2miHPRWWdpfjCEVqQFHzQrfVm7hfqSYg+p7n4yxyY0FCNZ7T6/fNef/W8TUdvhc/RyZvgZ9eqePJw= + skip_cleanup: true + file: + - docker-mysql-linux-386-0.1.0.tar.gz + - docker-mysql-linux-amd64-0.1.0.tar.gz + - docker-mysql-linux-arm-0.1.0.tar.gz + - docker-mysql-darwin-386-0.1.0.tar.gz + - docker-mysql-darwin-amd64-0.1.0.tar.gz + on: + tags: true + all_branches: true diff --git a/Dockerfile b/Dockerfile index 65132d6..83b31fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,27 @@ FROM mysql:5.7 MAINTAINER nanofi +RUN \ + apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates wget && \ + apt-get clean && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* + +RUN \ + wget -P /usr/local/bin https://godist.herokuapp.com/projects/ddollar/forego/releases/current/linux-amd64/forego && \ + chmod u+x /usr/local/bin/forego + +ENV CONTAINER_VERSION 0.1.0 +RUN \ + wget https://github.com/nanofi/docker-mysql/releases/download/$CONTAINER_VERSION/docker-mysql-linux-amd64-$CONTAINER_VERSION.tar.gz && \ + tar -xvzf docker-mysql-linux-amd64-$CONTAINER_VERSION.tar.gz && \ + mv dist/docker-mysql-linux-amd64 /usr/local/bin/docker-mysql && \ + rm -rf dist + RUN usermod -u 1000 mysql -COPY docker-entrypoint.sh /entrypoint.sh +COPY . /app/ +WORKDIR /app/ + +ENTRYPOINT [] CMD ["mysqld_safe", "--datadir=/var/lib/mysql", "--user=mysql"] +CMD ["forego", "start", "-r"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..484325a --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +.SILENT : +.PHONY : docker-mysql clean fmt + +TAG:=`git describe --abbrev=0 --tags` +LDFLAGS:=-X main.buildVersion $(TAG) + +all: docker-mysql + +deps: + go get github.com/mitchellh/gox + go get github.com/fsouza/go-dockerclient + +tool: + gox -build-toolchain -os "darwin linux" + +test: + go test -v + +docker-mysql: + echo "Building docker-mysql" + go build -ldflags "$(LDFLAGS)" + +dist-clean: + rm -rf dist + +dist: dist-clean + gox -os "darwin linux" -output "dist/{{.Dir}}-{{.OS}}-{{.Arch}}" + +release: dist + ls dist | xargs -I {} tar -cvzf {}-$(TAG).tar.gz dist/{} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..7a4d917 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +mysql: /app/start.sh mysqld_safe --datadir=/var/lib/mysql --user=mysql +config: docker-mysql diff --git a/README.md b/README.md index 80e6735..66fc316 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ docker-mysql ============ -A Dockerfile that installs a mysql server with multiple databases. +[![Build Status](https://travis-ci.org/nanofi/docker-mysql.svg?branch=master)](https://travis-ci.org/nanofi/docker-mysql) +A Dockerfile that installs a mysql server with multiple databases. databases and users will generate dynamically with starting or stopping or killing a container. -## Formatss of Parameters -- `MYSQL_DATABASES=database1,database2,...,databaseN` -- `MYSQL_USERS=user1:pass1:database1/database3,user2:pass2:database2/database6,...` +## Usage +To run it: +``` +$ docker run -d -v /var/run/docker.sock:/var/run/docker.sock nanofi/mysql +``` +Start any containers with env vars: +- `DB_NAME`; database name +- `DB_USER`; username +- `DB_PASS`; password + +If `DB_NAME` presences, the container creates a database. If `DB_NAME`, `DB_USER` and `DB_PASS` presence, the container creates an user which has all privilege to the database. + diff --git a/client.go b/client.go new file mode 100644 index 0000000..3b5b4a0 --- /dev/null +++ b/client.go @@ -0,0 +1,141 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "strconv" + "strings" + + docker "github.com/fsouza/go-dockerclient" +) + +type DockerContainer struct { +} + +func exists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func getEndpoint() (string, error) { + defaultEndpoint := "unix:///var/run/docker.sock" + if os.Getenv("DOCKER_HOST") != "" { + defaultEndpoint = os.Getenv("DOCKER_HOST") + } + if endpoint != "" { + defaultEndpoint = endpoint + } + proto, host, err := parseHost(defaultEndpoint) + if err != nil { + return "", err + } + if proto == "unix" { + exist, err := exists(host) + if err != nil { + return "", err + } + if !exist { + return "", errors.New(host + " does not exist") + } + } + return defaultEndpoint, nil +} + +// based off of https://github.com/dotcloud/docker/blob/2a711d16e05b69328f2636f88f8eac035477f7e4/utils/utils.go +func parseHost(addr string) (string, string, error) { + var ( + proto string + host string + port int + ) + addr = strings.TrimSpace(addr) + switch { + case addr == "tcp://": + return "", "", fmt.Errorf("Invalid bind address format: %s", addr) + case strings.HasPrefix(addr, "unix://"): + proto = "unix" + addr = strings.TrimPrefix(addr, "unix://") + if addr == "" { + addr = "/var/run/docker.sock" + } + case strings.HasPrefix(addr, "tcp://"): + proto = "tcp" + addr = strings.TrimPrefix(addr, "tcp://") + case strings.HasPrefix(addr, "fd://"): + return "fd", addr, nil + case addr == "": + proto = "unix" + addr = "/var/run/docker.sock" + default: + if strings.Contains(addr, "://") { + return "", "", fmt.Errorf("Invalid bind address protocol: %s", addr) + } + proto = "tcp" + } + + if proto != "unix" && strings.Contains(addr, ":") { + hostParts := strings.Split(addr, ":") + if len(hostParts) != 2 { + return "", "", fmt.Errorf("Invalid bind address format: %s", addr) + } + if hostParts[0] != "" { + host = hostParts[0] + } else { + host = "127.0.0.1" + } + + if p, err := strconv.Atoi(hostParts[1]); err == nil && p != 0 { + port = p + } else { + return "", "", fmt.Errorf("Invalid bind address format: %s", addr) + } + + } else if proto == "tcp" && !strings.Contains(addr, ":") { + return "", "", fmt.Errorf("Invalid bind address format: %s", addr) + } else { + host = addr + } + if proto == "unix" { + return proto, host, nil + + } + return proto, fmt.Sprintf("%s:%d", host, port), nil +} + +func getContainers(client *docker.Client) ([]*RuntimeContainer, error) { + apiContainers, err := client.ListContainers(docker.ListContainersOptions{ + All: false, + Size: false, + }) + if err != nil { + return nil, err + } + containers := []*RuntimeContainer{} + for _, apiContainer := range apiContainers { + container, err := client.InspectContainer(apiContainer.ID) + if err != nil { + log.Printf("error inspecting container: %s: %s\n", apiContainer.ID, err) + continue + } + runtimeContainer := &RuntimeContainer{ + ID: container.ID, + Name: strings.TrimLeft(container.Name, "/"), + Address: container.NetworkSettings.IPAddress, + Env: make(map[string]string), + } + for _, entry := range container.Config.Env { + parts := strings.Split(entry, "=") + runtimeContainer.Env[parts[0]] = parts[1] + } + containers = append(containers, runtimeContainer) + } + return containers, nil +} diff --git a/docker-mysql.go b/docker-mysql.go new file mode 100644 index 0000000..ac15f08 --- /dev/null +++ b/docker-mysql.go @@ -0,0 +1,197 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "sync" + "time" + + docker "github.com/fsouza/go-dockerclient" +) + +var ( + buildVersion string + version bool + endpoint string + wg sync.WaitGroup +) + +type Event struct { + ContainerID string `json:"id"` + Status string `json:"status"` + Image string `json:"image"` +} + +type RuntimeContainer struct { + ID string + Name string + Address string + Env map[string]string +} + +func NewDockerClient(endpoint string) (*docker.Client, error) { + return docker.NewClient(endpoint) +} + +func update(client *docker.Client) { + containers, err := getContainers(client) + if err != nil { + log.Printf("error listing containers: %s\n", err) + return + } + dest, err := ioutil.TempFile("/tmp", "docker-mysql") + defer func() { + dest.Close() + os.Remove(dest.Name()) + }() + if err != nil { + log.Fatalf("unable to create temp file: %s\n", err) + } + for _, container := range containers { + if val, ok := container.Env["DB_NAME"]; ok { + fmt.Fprintf(dest, "CREATE DATABASE IF NOT EXISTS %s ;\n", val) + } + } + for _, container := range containers { + user, okUser := container.Env["DB_USER"] + pass, okPass := container.Env["DB_PASS"] + db, okDb := container.Env["DB_NAME"] + address := container.Address + if okUser && okPass && okDb { + fmt.Fprintf(dest, "GRANT ALL ON %s.* TO '%s'@'%s' identified by '%s' ;\n", db, user, address, pass) + } + } + fmt.Fprintln(dest, "FLUSH PRIVILEGES ;") + log.Printf("Configure database") + cmdStr := fmt.Sprintf("mysql -u root -p$MYSQL_ROOT_PASSWORD < %s", dest.Name()) + cmd := exec.Command("/bin/sh", "-c", cmdStr) + out, err := cmd.CombinedOutput() + log.Println(string(out)) + if err != nil { + log.Printf("Error running notify command: %s, %s\n", cmdStr, err) + log.Print(string(out)) + } +} + +func listen(client *docker.Client) { + wg.Add(1) + defer wg.Done() + + for { + if client == nil { + var err error + endpoint, err := getEndpoint() + if err != nil { + log.Printf("Bad endpoint: %s", err) + time.Sleep(10 * time.Second) + continue + } + client, err = NewDockerClient(endpoint) + if err != nil { + log.Printf("Unable to connect to docker daemon: %s", err) + time.Sleep(10 * time.Second) + continue + } + update(client) + } + eventChan := make(chan *docker.APIEvents, 100) + defer close(eventChan) + + watching := false + for { + if client == nil { + break + } + err := client.Ping() + if err != nil { + log.Printf("Unable to ping docker daemon: %s", err) + if watching { + client.RemoveEventListener(eventChan) + watching = false + client = nil + } + time.Sleep(10 * time.Second) + break + } + if !watching { + err = client.AddEventListener(eventChan) + if err != nil && err != docker.ErrListenerAlreadyExists { + log.Printf("Error registering docker event listener: %s", err) + time.Sleep(10 * time.Second) + continue + } + watching = true + log.Println("Watching docker events") + } + select { + case event := <-eventChan: + if event == nil { + if watching { + client.RemoveEventListener(eventChan) + watching = false + client = nil + } + break + } + if event.Status == "start" || event.Status == "stop" || event.Status == "die" { + log.Printf("Received event %s for container %s", event.Status, event.ID[:12]) + update(client) + } + case <-time.After(10 * time.Second): + // check for docker liveness + } + } + } +} + +func start(client *docker.Client) { + update(client) + listen(client) +} + +func waitDbUp() { + log.Printf("Waiting for starting database") + cmdStr := "mysql -u root -p$MYSQL_ROOT_PASSWORD -e 'show databases;'" + for { + cmd := exec.Command("/bin/sh", "-c", cmdStr) + _, err := cmd.CombinedOutput() + if err == nil { + break + } + log.Printf(".") + time.Sleep(10 * time.Second) + } + log.Println("Alive database") +} + +func initFlags() { + flag.BoolVar(&version, "version", false, "show version") + flag.Parse() +} + +func main() { + initFlags() + + if version { + fmt.Println(buildVersion) + return + } + + endpoint, err := getEndpoint() + if err != nil { + log.Fatalf("Bad endpoint: %s", err) + } + + client, err := NewDockerClient(endpoint) + if err != nil { + log.Fatalf("Unable to create docker client: %s", err) + } + + waitDbUp() + start(client) + wg.Wait() +} diff --git a/docker-entrypoint.sh b/start.sh similarity index 58% rename from docker-entrypoint.sh rename to start.sh index 2c7460c..d0d38b1 100755 --- a/docker-entrypoint.sh +++ b/start.sh @@ -20,28 +20,6 @@ if [ ! -d '/var/lib/mysql/mysql' -a "${1%_safe}" = 'mysqld' ]; then GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; DROP DATABASE IF EXISTS test ; EOSQL - - if [ "$MYSQL_DATABASES" ]; then - for DATABASE in $(echo $MYSQL_DATABASES | sed 's/,/ /g'); do - echo "CREATE DATABASE IF NOT EXISTS $DATABASE ;" >> "$TEMP_FILE" - done - fi - - if [ "$MYSQL_USERS" ]; then - for USER in $(echo $MYSQL_USERS | sed 's/,/ /g'); do - ARRY=($(echo $USER | sed 's/:/ /g')) - NAME=${ARRY[0]} - PASS=${ARRY[1]} - GRANTS="${ARRY[2]}" - echo "CREATE USER '$NAME'@'%' IDENTIFIED BY '$PASS' ;" >> "$TEMP_FILE" - - if [ "$GRANTS" ]; then - for GRANT in $(echo $GRANTS | sed 's/\// /g'); do - echo "GRANT ALL ON $GRANT.* TO '$NAME'@'%' ;" >> "$TEMP_FILE" - done - fi - done - fi echo 'FLUSH PRIVILEGES ;' >> "$TEMP_FILE"