🦍

Vim/Neovimでdockerを操作できるdenops-docker.vimを作った

4 min read

初めに

最近denops.vimを使ってプラグインを作るのが自分の中で流行っています。
denops.vimはTypeScriptメインでVim/Neovim対応のプラグインを書けるところが魅力的で、型システムがあることでデータ構造が明確にわかるためとても書きやすいので、
今後プラグインを作るときは基本denops.vimで書こうと考えています。
denops.vimの詳細に関してこれ以上触れないので、気になる方はこちらの記事を参照してください。

本題ですが、Vimを初めたころにdocker.vimというプラグインを作りました。
本記事はdocker.vimdenops.vimで書き直した話しになります。

docker.vimとは

docker.vimVim上でdockerのコンテナやイメージなどを操作できるプラグインになります。Vim scriptで書かれていて、UIにVimのpopup windowを使っていました。
こちらは自分自身も愛用していて、仕事では外せないプラグインとなっていますが、Neovimに対応していないという問題がありました。
実際issueでもNeovim対応してほしいと要望が上がってきていますが、Neovim対応するには残念ながら以下の問題点があり、対応するには根本的に作り直す必要ありました。

  • NeovimにはFloating windowというのがあるが、Vimと完全にIFが異なるため、差分吸収が難しい
  • docker engineのAPIをVimのjobを使って非同期にcurlで叩いているが、VimNeovimの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

denounix 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をください。