mobyを使ってgolangからDockerコンテナを操作する

はじめに
--------------------
- golangから動的にDockerコンテナを動的に扱いたくて方法を調べた。
- ソースコードから部分的に抜粋してきてるので、もしかしたらそのまま動かないかもしれません（ごめんなさい）


参考リンク集
--------------------
- [golangからDocker Remote APIを使ってイメージ・コンテナを操作する方法まとめ](http://kuroeveryday.blogspot.jp/2017/09/docker-remote-api-with-golang.html)
- [GolangでDocker Remote APIを使ってDockerfileからイメージ作成する方法](https://kuroeveryday.blogspot.jp/2017/09/golang-build-image-with-dockerfile.html)
- [GoのコードからDockerコンテナの起動を実装する](http://otiai10.hatenablog.com/entry/2017/05/24/163701)


パッケージのインポートでハマる
--------------------
- 2017/09/23時点では、 `github.com/moby/moby` および `github.com/docker/docker` がほぼ同一のAPIを提供している
- 使用する構造体は、それぞれ適切なパッケージからインポートしなければエラーが起きる
- 現状、下記のようにインポート先を分ければ動くらしい
    - Docker APIへの接続クライアントは `github.com/moby/moby` からインポートする
    - それ以外は `github.com/docker/docker` からインポートする

```go
import (
	"log"

	"github.com/moby/moby/client"
)

func main() {
	cl, err := client.NewEnvClient()
	if err != nil {
		log.Fatalf("%v", err)
	}
}
```




イメージ関係
--------------------

### イメージのプル

```go
import (
	"log"
	"context"

	"github.com/docker/docker/api/types"
)

func main() {
	opts := types.ImagePullOptions{}
	_, err := cl.ImagePull(context.TODO(), "<image_name>", opts)
	if err != nil {
		log.Fatalf("%v", err)
	}
}
```

### Dockerfileからイメージビルド
- Dockerfile は tarファイル として固めておく
    - gzip してもしなくてもOK
- ビルド開始後、処理完了前に抜けると、ビルドが完了しなかった
    - `ctx.Done()` 待ってみたけど、返ってこなかった
    - こういうときどうするのが正解か分かんなかったので、とりあえずsleep

```go
import (
	"os"
	"log"
	"context"
	"bufio"

	"github.com/docker/docker/api/types"
)

func main() {

	// tarファイル取得
	tarfile, _ := os.Open("<tarfile_path>")
	defer tarfile.Close()

	// イメージのビルド
	opts := types.ImageBuildOptions{
		Tags:        []string{"<repository_name>"},
		ForceRemove: true,
	}
	resp, err := cl.ImageBuild(context.TODO(), bufio.NewReader(tarfile), opts)
	if err != nil {
		log.Fatalf("%v", err)
	}
	defer resp.Body.Close()

	// ちょっと待たないと先に死んじゃう
	time.Sleep(100 * time.Millisecond)
}
```


#### （おまけ） tarファイルの作成

```go
import (
	"os"
	"ioutil"

	"archive/tar"
	"compress/gzip"
)

func main() {

	// tarファイル生成
	tarfile, _ := os.Create("<tarfile_path>")
	defer tarfile.Close()

	// gzip
	gzw := gzip.NewWriter(tarfile)
	defer gzw.Close()

	// tar
	tw := tar.NewWriter(gzw)
	defer tw.Close()

	// contentの取得
	body, _ := ioutil.ReadFile("<contentfile_path>")

	header := &tar.Header{
		Name: "<dockerfile_path_in_tarfile>",
		Size: int64(len(body)),
	}
	if err := tw.WriteHeader(header); err != nil {
		log.Fatalf("%v", err)
	}
	if _, err := tw.Write(body); err != nil {
		log.Fatalf("%v", err)
	}
}
```


コンテナ関係
--------------------

### コンテナ作成
- 前述の通り、パッケージのインポート先が `github.com/docker/docker` になっていることに注意

```go
import (
	"context"
	"log"

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

func main() {
	config := &container.Config{
		Image: "<image_name>",
	}
	hostConfig := &container.HostConfig{}
	netConfig := &network.NetworkingConfig{}
	resp, err := cl.ContainerCreate(context.TODO(), config, hostConfig, netConfig, "<container_name>")
	if err != nil {
		log.Fatalf("%v", err)
	}

	log.Printf("%v", resp.ID) // ContainerID
}
```

- ゲストのポートをホストにバインドする
- Dockerfileで `EXPOSE` していても、 `config` に `ExposedPorts` は必要らしい（これで小一時間ハマった）

```go
config := &container.Config{
	Image:        "<image_name>",
	ExposedPorts: nat.PortSet{nat.Port("<guest_port>"): struct{}{}},
}

hostConfig := &container.HostConfig{
	PortBindings: nat.PortMap{
		nat.Port("<guest_port>"): []nat.PortBinding{{HostPort: "<host_port>"}},
	},
}
```

- 特定のネットワークに所属させる

```go
netConfig := &network.NetworkingConfig{
	EndpointsConfig: map[string]*network.EndpointSettings{
		"<network_name>": {
			IPAMConfig: &network.EndpointIPAMConfig{
				IPv4Address: "<guest_ip_address>",
			},
		},
	},
}
```

- AutoRemove と AutoRestart
- どちらかのみ設定可能（同時に設定すると起動時にエラーする）

```go
hostConfig := &container.HostConfig{
	RestartPolicy: container.RestartPolicy{
		Name: "always",
	},
	AutoRemove: false,
}
```

### コンテナへのファイルコピー
- コピーするファイルの渡し方でちょっと迷った
- tarファイル をオープンして、 `bufio.NewReader()` でラップするとうまくいった

```go
import (
	"os"
	"log"
	"bufio"

	"github.com/docker/docker/api/types"
)

func main() {

	// archive の オープン
	archive, _ := os.Open("<archive_path>")
	defer archive.Close()

	// コンテナへコピー
	opts := types.CopyToContainerOptions{}
	if err := cl.CopyToContainer(context.TODO(), "<container_id>", "<guest_destination_path>", bufio.NewReader(archive), opts); err != nil {
		log.Fatalf("%v", err)
	}
}
```

ネットワーク関係
--------------------
### ネットワーク作成

```go
import (
	"context"
	"log"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/network"
)

func main() {
	opts := types.NetworkCreate{
		IPAM: &network.IPAM{
			Config: []network.IPAMConfig{
				{Subnet: "<subnet>"},
			},
		},
	}
	if _, err = cl.NetworkCreate(context.TODO(), "<network_name>", opts); err != nil {
		log.Fatalf("%v", err)
	}
}
```

一覧取得関係
--------------------
- フィルタ条件の渡し方でちょっと迷う
    - 基本的には、 `map[string][]string` をJSON形式になおしてあげるとOK

### イメージ一覧の取得

```go
import (
	"context"
	"log"
	"encoding/json"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
)

func main() {

	// フィルタ条件
	filtMap := map[string][]string{"reference": {"<repository_name>:<version>"}}
	filtBytes, _ := json.Marshal(filtMap)
	filt, err := filters.FromParam(string(filtBytes))
	if err != nil {
		log.Fatalf("%v", err)
	}

	opts := types.ImageListOptions{Filters: filt}
	images, err := cl.ImageList(context.TODO(), opts)
	if err != nil {
		log.Fatalf("%v", err)
	}
}
```

### コンテナ一覧の取得
- `All` にすると、停止中コンテナも含める
- `Quiet` にすると、コンテナIDだけ返ってくる

```go
import (
	"context"
	"log"
	"encoding/json"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
)

func main() {

	// フィルタ条件
	filtMap := map[string][]string{"name": {"<container_name>"}}
	filtBytes, _ := json.Marshal(filtMap)
	filt, _ := filters.FromParam(string(filtBytes))

	opts := types.ContainerListOptions{
		All:     true,
		Quiet:   true,
		Filters: filt,
	}
	resp, err := cl.ContainerList(context.TODO(), opts)
	if err != nil {
		log.Fatalf("%v", err)
	}
}
```

### ネットワーク一覧取得

```go
import (
	"context"
	"log"
	"encoding/json"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
)

func main() {

	// フィルタ条件
	filtMap := map[string][]string{"name": {"<network_name>"}}
	filtBytes, _ := json.Marshal(filtMap)
	filt, err := filters.FromParam(string(filtBytes))
	if err != nil {
		log.Fatalf("%v", err)
	}

	opts := types.NetworkListOptions{Filters: filt}
	nets, err := cl.NetworkList(context.TODO(), opts)
	if err != nil {
		log.Fatalf("%v", err)
	}
}
```