Docker SDK for Goを使ってリモートのDockerデーモン上でイメージをビルドする際、Dockerfileと関連ファイルをそのまま読み込ませると「Cannot locate specified Dockerfile」エラーが発生する。これはDockerビルドコンテキストがtarアーカイブとしてパッケージ化されている必要があるためだ。標準ライブラリのarchive/tarを用いて手動でtarアーカイブを作成する方法を示す。
ディレクトリ全体をtarにパッケージ化
package main
import (
"archive/tar"
"errors"
"io"
"os"
"path/filepath"
)
func archiveDirectory(baseDir, outPath string) error {
outFile, err := os.Create(outPath)
if err != nil {
return err
}
defer outFile.Close()
tw := tar.NewWriter(outFile)
defer tw.Close()
return filepath.Walk(baseDir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(fi, rel)
if err != nil {
return err
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
}
func main() {
if err := archiveDirectory("./buildctx", "context.tar"); err != nil {
panic(err)
}
}
特定ファイルのみをtarにパッケージ化
package main
import (
"archive/tar"
"io"
"os"
)
func archiveFiles(paths []string, outPath string) error {
outFile, err := os.Create(outPath)
if err != nil {
return err
}
defer outFile.Close()
tw := tar.NewWriter(outFile)
defer tw.Close()
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
stat, err := f.Stat()
if err != nil {
f.Close()
return err
}
hdr := &tar.Header{
Name: p,
Mode: int64(stat.Mode().Perm()),
Size: stat.Size(),
}
if err := tw.WriteHeader(hdr); err != nil {
f.Close()
return err
}
if _, err := io.Copy(tw, f); err != nil {
f.Close()
return err
}
f.Close()
}
return nil
}
func main() {
if err := archiveFiles([]string{"Dockerfile", "app.py"}, "build.tar"); err != nil {
panic(err)
}
}
アーカイブ作成からイメージビルドまで統合
package main
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func createTarStream(baseDir string) (io.Reader, error) {
buf := new(bytes.Buffer)
tw := tar.NewWriter(buf)
defer tw.Close()
err := filepath.Walk(baseDir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
hdr, err := tar.FileInfoHeader(fi, rel)
if err != nil {
return err
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
return err
})
if err != nil {
return nil, err
}
return buf, nil
}
func main() {
cli, err := client.NewClientWithOpts(
client.WithHost("tcp://10.0.0.12:2376"),
client.WithAPIVersionNegotiation(),
)
if err != nil {
panic(err)
}
// メモリ上でtarストリームを生成(ファイルに書き出さない)
buildCtx, err := createTarStream("./buildctx")
if err != nil {
panic(err)
}
opts := types.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{"myapp:latest"},
}
resp, err := cli.ImageBuild(context.Background(), buildCtx, opts)
if err != nil {
panic(err)
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
for {
var msg map[string]interface{}
if err := dec.Decode(&msg); err != nil {
break
}
if stream, ok := msg["stream"]; ok {
fmt.Print(stream)
}
}
}
上記の統合例では、tarアーカイブをファイルに保存せずにbytes.Bufferでメモリ上に生成し、直接ImageBuildに渡している。これにより一時ファイルの作成・削除が不要になり、よりクリーンな実装となる。