DockerコマンドのGo言語ラッパーを作った
この記事はSecHack365 アドベントカレンダー2022の2日目です
25日の夜にコーディングから始めて日付が変わってしまいましたが、空いていたのでねじ込ませてもらいます。 qiita.com
はじめに
SecHack365 2019年度の修了生で、2021年度・2022年度のアシスタントを担当している灰原です。
この記事では、思い付きで作ったDockerコマンドのGo言語ラッパーを紹介します。
github.com
下記のような形式で、指定したDockerコマンドを実行するためのexec.Cmd
を生成してくれるものです。
cmd := docker.DockerVersionCmd(docker.DockerVersionOption{}, []string{}) out, err := cmd.CombinedOutput() if err != nil { log.Fatal(err) } fmt.Println(string(out))
開発の背景
このライブラリはGoプログラムからDockerを楽に使うために作成しました。プログラムからDockerを操作する他の方法に、Docker Engine SDKを使う手もあります。これについては過去のいくつかブログで触れました。
GoでDockerを操る [Docker Engine SDK] - はいばらのブログ
Docker Engine SDKでリモートのDocker daemonにSSH経由で接続する - はいばらのブログ
DockerコマンドはこのDocker Engine SDKを叩いているため、理論的にはDcokerコマンドでできることはDocker Engine SDKを使えばできるはずです。しかし、これが結構厳しそうなことがわかりました。
例えばdocker pull
コマンドのコードを追ってみましょう。GoのCLIツールを作る際の定番のライブラリであるspf13/cobra
が使われています。cobraを使ったことがある人にとってはスッと読めるコードだと思います。
https://github.com/docker/cli/blob/master/cli/command/image/pull.go
メインの処理をしているRunPull
メソッドを見ると、いろいろと前処理をした後、trustedPull
メソッドかimagePullPrivileaged
メソッドのどちらかを叩いているようです。
truestePull
メソッドの実装を見ると、前後にまたいろいろと処理がありますが、imagePullPrivileged
メソッドを呼び出しています。
imagePullPrivileged
メソッドの実装を見ると、ここでDocker Engine SDKのImagePullメソッドを叩いていました。
実際にコードを追ってみると、Dockerコマンドが持っているロジックが意外と多いことに気が付きます。Docker Engine SDKが低レベルのAPI呼び出しを行う層であるのに対して、Dockerコマンドはより高位の抽象化層である訳です。
こんな便利なDockerコマンドをプログラムからも使いたい、でもexec.Cmdの引数を文字列で組み立てるのは大変だからどうにかしたい、というモチベーションでこのライブラリは開発されました。
実装
Dockerコマンドはサブコマンドも、各コマンドのフラグ・引数もたくさんあります。これを一つ一つ型定義していくのは大変です。しかもDockerコマンドのアップデートで仕様が変わった場合、該当箇所を探し出して型定義を修正しなければならず、これもまた大変そうです。
Web APIのクライアントを作る場合、SwaggerやProtocol BuffersなどでAPI仕様が定義されていれば、そこからコードの自動生成をすることで実装/管理コストがかなり抑えられます。しかし今回のようにCLIツールのラッパーを作る場合はどうすれば良いでしょうか。
先ほども紹介したように、Dockerコマンドはcobraを使ったCLIツールです。cobraを使ったCLIツールでは、1つのcobra.Command
構造体に対してツリー状にサブコマンドの情報を紐付けて、その木構造の根にあたる構造体をmain関数から呼び出すパターンが使われることが多く、Dockerコマンドもこれに従っていました。つまり、大元のcobra.Command
構造体を探索すれば、全てのサブコマンドのフラグの情報を得られるはずです。
下記のコードは、Dockerコマンドのソースコードに挿入してビルド・実行することで、Dockerkコマンドのラッパーとなるコードを生成するものです。
https://github.com/w-haibara/docker-wrapper/blob/main/_generate/generate.go
Dockerコマンドのレポジトリにcmd/docker/generate.go
としてこのコードをコピーして、下記の辺りでGenerateCode
メソッドを呼び出すようにします。
$ git diff diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 90945f0c47..5e78c52d9d 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -260,6 +260,8 @@ func runDocker(dockerCli *command.DockerCli) error { return err } + GenerateCode(cmd) + if err := tcmd.Initialize(); err != nil { return err }
そのうえでdocker buildx bake
によってDockerコマンドをビルドし、docker
を実行すると生成されたコードが標準出力に流れます。そのコードがこちらです。12185行あります。自分で書いたコード生成のプログラムが大量のコードを吐いてくれると、なんか気持ちいいですよね。
https://github.com/w-haibara/docker-wrapper/blob/main/docker/docker.go
生成されたコードの中から、先ほど実装を追ってみたdocker pull
に相当するコードを見てみましょう。exec.Cmd
を生成するDockerImagePullCmd
メソッドと、そのフラグが定義されたDockerImagePullOption
構造体です。ここに登場する変数名や型、コメントなどはcobra.Command
に詰まっていた情報です。
type DockerImagePullOption struct { /* Download all tagged images in the repository */ AllTags *bool /* Skip image verification */ DisableContentTrust *bool /* Set platform if server is multi-platform capable */ Platform *string /* Suppress verbose output */ Quiet *bool } /* DockerImagePullCmd is wrapper of 'docker image pull' ------------------------------ pull [OPTIONS] NAME[:TAG|@DIGEST] Pull an image or a repository from a registry ------------------------------ */ func DockerImagePullCmd(opt DockerImagePullOption, args []string) *exec.Cmd { cargs := []string{"image", "pull"} if opt.AllTags != nil { cargs = append(cargs, "--all-tags="+fmt.Sprint(*opt.AllTags)) } if opt.DisableContentTrust != nil { cargs = append(cargs, "--disable-content-trust="+fmt.Sprint(*opt.DisableContentTrust)) } if opt.Platform != nil { cargs = append(cargs, "--platform="+fmt.Sprint(*opt.Platform)) } if opt.Quiet != nil { cargs = append(cargs, "--quiet="+fmt.Sprint(*opt.Quiet)) } cargs = append(cargs, args...) return exec.Command("docker", cargs...) }
コメントも引っ張ってこれたのは思わぬ収穫でした。IDEでコーディングすると良い感じに表示してくれて捗りますね。
おわりに
Dockerコマンドをラッパーという形で、Dockerをプログラムから操作するためのライブラリを作りました。もしプログラムからDockerを使いたいな〜と思うことがあればお試しください。