Vim/Neovimでdockerを操作できるdenops-docker.vimを作った
初めに
最近denops.vimを使ってプラグインを作るのが自分の中で流行っています。
denops.vim
はTypeScriptメインでVim/Neovim対応のプラグインを書けるところが魅力的で、型システムがあることでデータ構造が明確に分かるためとても書きやすいので、
今後プラグインを作るときは基本denops.vim
で書こうと考えています。
denops.vim
の詳細に関してこれ以上触れないので、気になる方はこちらの記事を参照してください。
本題ですが、Vimを初めたころにdocker.vimというプラグインを作りました。
本記事はdocker.vim
をdenops.vim
で書き直した話しになります。
docker.vimとは
docker.vim
はVim
上でdockerのコンテナやイメージなどを操作できるプラグインになります。Vim script
で書かれていて、UIにVim
のpopup windowを使っていました。
こちらは自分自身も愛用していて、仕事では外せないプラグインとなっていますが、Neovim
に対応していないという問題がありました。
実際issueでもNeovim
対応してほしいと要望が上がってきていますが、Neovim
対応するには残念ながら次の問題点があり、対応するには根本的に作り直す必要ありました。
-
Neovim
にはFloating window
というのがあるが、Vimと完全にIFが異なるため、差分吸収が難しい -
docker engine
のAPIをVim
のjobを使って非同期にcurlで叩いているが、Vim
とNeovim
のjobのIFが異なるため、差分吸収が難しい
また、個人的な話ですが最近はもっぱらNeovim
を使っているので、dockerの操作をするのにいちいちターミナルを起動するのが面倒というのもあってずっと対応したいなと考えていました。
そこで、ちょうどdenops.vim
が登場してきたので、このビッグウェーブに乗り、denops-docker.vim
を作りました。
denops-docker.vimについて
denops-docker.vimではpopup windowを廃止して、バッファベースのUIにしました。
docker.vim
でUIをpopup windowにしたのは単に当時のVim
の新機能で「おもしろいし、かっこいい」という理由だけだったので、よりシンプルで保守しやすい形に倒しました。
また、機能も最低限必要なものだけにして、必要に応じて足していくという形にしました。
たとえばdocker.vim
ではDockerfile
で選択した範囲のみイメージをビルドしたり、CPU/MEM使用率をグラフで表示できたりしますが、ほとんど使わない機能だったのでこれらはすべて廃止しました。
現在denops-docker.vim
の機能はREADMEに書いてありますが、以下となっています。これがあれば自分は仕事困らないというくらいですが、今後は要望があれば機能が増えるかもしれません。
denops-docker.vimの実装話
denops.vim
のエコシステムに乗っかっているので、実装はだいぶ楽でしたが、API通信の部分で実装の課題がありました。
denops-docker.vim
ではunix domain socketを通してdocker engine
のAPIを叩いています。
unix domain socket
は簡単にいうとプロセス間の通信をソケットファイルを通して行うというものです。
docker engine
はデフォルト、ソケットファイルを生成して、docker cli
はそこを通してdocker engine
のAPIをたたくようになっています。
https://www.vermasachin.com/posts/7-running-docker-mac/ より
上図はMac版ではありますが、Linuxも同様です。
簡易に説明すると、docker engine
のAPIとのやりとりは次のようになっています。
denops-docker.vim <--HTTP--> socket file <--HTTP--> docker engine
deno
はunix domain socket
に対応していて、次のようにオプションで指定するだけでconnection
を得られます。
このconnection
に対してread
またはwrite
することで通信できます。
const defaultOptions = <Deno.UnixConnectOptions> {
transport: "unix",
path: "/var/run/docker.sock",
};
export async function connect(
options?: Deno.UnixConnectOptions | Deno.ConnectOptions,
): Promise<Deno.Conn> {
const conn = await Deno.connect(options ?? defaultOptions);
return conn;
}
しかし、ソケットからHTTPのデータを読み取ったり、POSTしたりするライブラリは存在しないため、自分で実装する必要があります。
一部ではありますが、実際HTTPのリクエストを組み立てる実装は次のようになっています。
static newRequest(req: Request): string {
req.method = req.method ?? "GET";
let header = `${req.method} ${req.url}`;
if (req.params && Object.keys(req.params).length) {
const params = new URLSearchParams(req.params);
header += `?${params.toString()}`;
}
header += ` HTTP/1.1\r\nHost: localhost\r\n`;
for (
const [k, v] of Object.entries(req.header ?? [])
) {
header += `${k}: ${v}\r\n`;
}
let reqStr = `${header}\r\n`;
if ("data" in req) {
let data = req.data;
if (isObject(data)) {
if (Object.entries(data).length) {
data = JSON.stringify(data);
} else {
data = "";
}
}
reqStr = `${header}\r\n${data}\r\n`;
}
return reqStr;
}
また、レスポンスはchunkですが、こちらも自前実装しなければいけませんでした。
自分には荷が重かったんですが、serveio.tsというライブラリを作者から教えていただき、読み取りの処理をほぼそのまま使わせていただきました。
こんな感じで、ちょっと低レイヤの部分について勉強しつつ、なんとか通信部分の実装しました。
denops-docker.vimの今後
いつもとおり、テストが全然書けていないので、まずテストを書いていきたいと思います。
機能に関しては基本的にあまり増やそうと考えていないんですが、リクエストは受け付けるのでほしい機能があればぜひissueをください。
Discussion