2017年2月17日金曜日

golang 製の docker ライブラリで docker API をコールしてみた

概要

わりとサンプルが Web 上に少ないので試したのを紹介します
今回試したのは

  • docker run alpine echo hello
  • docker run -d -p 8080:80 nginx

になります
また使用したライブラリは docker が公開している公式のライブラリになります
https://github.com/docker/docker/tree/master/client

このライブラリの使い方にも結構苦労したのでその辺も紹介します

環境

  • Mac OS X 10.12.3
  • golang 1.7.5
  • docker client 946787e85e7c747c78354c5ca8d7a512b8766969

docker client ライブラリのインストール

go get でインストールします
が、vendor ディレクトリの関係で少し作業が必要になります

  • export GOPATH=”/Users/hawksnowlog/go”
  • go get github.com/docker/docker/client
  • cd /Users/hawksnowlog/go/src
  • mv /Users/hawksnowlog/go/src/github.com/docker/docker/vendor .

という vendor ディレクトリの移動が必要になります
これをやらないで go build を実行すると

  • (type "vendor/github.com/docker/go-connections/nat".PortSet) as type "github.com/docker/docker/vendor/github.com/docker/go-connections/nat".PortSet in field value

というエラーとなるビルドに失敗します
執筆時に以下の issue があったのでこれが解決されればやる必要はなくなるかもしれません
https://github.com/docker/docker/issues/28269

alpine のコード

ではコーディングしていきます

  • mkdir -p /Users/hawksnowlog/go/src/github.com/hawksnowlog/docker-golang-test
  • cd /Users/hawksnowlog/go/src/github.com/hawksnowlog/docker-golang-test
  • touch main.go
package main

import (
    "fmt"
    "io"
    "os"
    "time"

    "context"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/client"
)

func main() {
    ctx := context.Background()

    cli, err := client.NewEnvClient()
    if err != nil {
        panic(err)
    }

    imageName := "alpine:latest"
    _, err = cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }

    count := 0
    max := 30
    downloaded := false
    for {
        images, err := cli.ImageList(ctx, types.ImageListOptions{})
        if err != nil {
            panic(err)
        }
        for _, image := range images {
            for _, tag := range image.RepoTags {
                fmt.Println(tag)
                if tag == imageName {
                    downloaded = true
                }
            }
        }
        if downloaded == true {
            break
        }
        if count >= max {
            break
        }
        count++
        time.Sleep(5 * time.Second)
    }
    resp, err := cli.ContainerCreate(ctx,
        &container.Config{
            Image: imageName,
            Cmd:   []string{"echo", "hello world"},
        }, nil, nil, "")
    if err != nil {
        panic(err)
    }

    if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
        panic(err)
    }

    if _, err = cli.ContainerWait(ctx, resp.ID); err != nil {
        panic(err)
    }

    out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
    if err != nil {
        panic(err)
    }

    io.Copy(os.Stdout, out)
}
  • go fmt github.com/hawksnowlog/docker-golang-test
  • go build github.com/hawksnowlog/docker-golang-test
  • go install github.com/hawksnowlog/docker-golang-test
  • ./docker-golang-test

で動作します
成功すれば hello world と標準出力に表示されると思います

ポイントは ImagePull 後に ImageList してイメージがダウンロードされるのを待っている部分です
これがないと次の ContainerCreate を行ったときに「そんなイメージありません」と言われてエラーとなってしまいます
公式のチュートリアルを見るとこの処理が完全に抜け落ちているため、手動で docker pull をしないと必ずエラーになってしまいます

あと docker 公式の golang ライブラリには docker run が実装されていません
なので、create -> start と順番に実行する必要があります

nginx のコード

次に nginx を動作させるコードです
作業ディレクトリやファイルは先ほど同じです

  • vim main.go
package main

import (
    "fmt"
    "time"

    "context"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/client"
    "github.com/docker/go-connections/nat"
)

func Nginx() {
    ctx := context.Background()

    cli, err := client.NewEnvClient()
    if err != nil {
        panic(err)
    }

    imageName := "nginx:latest"
    _, err = cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }

    count := 0
    max := 30
    downloaded := false
    for {
        images, err := cli.ImageList(ctx, types.ImageListOptions{})
        if err != nil {
            panic(err)
        }
        for _, image := range images {
            for _, tag := range image.RepoTags {
                fmt.Println(tag)
                if tag == imageName {
                    downloaded = true
                }
            }
        }
        if downloaded == true {
            break
        }
        if count >= max {
            break
        }
        count++
        time.Sleep(5 * time.Second)
    }
    p1, _ := nat.NewPort("tcp", "80")
    p2, _ := nat.NewPort("tcp", "80")
    ports1 := make(nat.PortSet)
    ports2 := make(nat.PortMap)
    ports1[p1] = struct{}{}
    ports2[p2] = []nat.PortBinding{nat.PortBinding{"0.0.0.0", "8080"}}
    resp, err := cli.ContainerCreate(ctx,
        &container.Config{
            Image:        imageName,
            ExposedPorts: ports1,
        },
        &container.HostConfig{
            PortBindings: ports2,
        }, nil, "")
    if err != nil {
        panic(err)
    }

    if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
        panic(err)
    }
}

こっちのポイントは ContainerCreate 時にコンテナとホストでポートバインディングするパラメータが追加になっている点です
コマンドで言うところの -p オプションです
また、nginx イメージは Cmd オプションが不要なので削除しています

(ここで nat を参照しているのですが、この参照が vendor ディレクトリを移動しないとできないようです)

あとは処理の最後で ContainerWait しないようにしています
これがあると実行したときにコンテナが stop するまでプロンプトが返ってこなくなります

最後に

alpine と nginx イメージをコマンドではなく golang 製のライブラリから API をコールして run してみました
正直コード的には長すぎるしかなり面倒でした

他の言語のライブラリを見ると run が実装されているものが多いので素直にそれを使ったほうが良いかもしれません
ただ、docker 公式なのでバージョン追従やサポートは手厚いと思います

参考サイト

0 件のコメント:

コメントを投稿