Vimでk8sを操作するプラグインを作った
初めに
こんにちは、森に帰省中のゴリラです。
最近、完全ガイドでkubernetesの勉強をしていますが、覚えること、理解することがなかなか多くてたいへんです。
さて、kubernetesを使ったことがある人なら、一度くらいk9sを使ったことがあるかと思います。
これはとても便利なもので、podを見たりコンテナにアタッチしたりと、いろいろできます。
ただ、自分は普段Vimをメインに使っているので、できればVim上で同じことをやりたいと思って、k8s.vimというプラグインを作りました。
 k8s.vimの概要
UIはこんな感じになっています。
基本的にk8s://*という仮想バッファを開くとリソースを見れたり、操作できたりします。

現時点では次のことができます。
- Pod関連
- 一覧
 - コンテナ一覧
 - describeの出力
 - yaml出力
 
 - Node関連
- 一覧
 - describeの出力
 
 
たとえば、podのコンテナでシェルを実行したい場合は、こんな感じになります。

実装
k8s.vimはdenops.vimで動くプラグインなので、Denoで動いています。
一応、公式製のclientライブラリをDenoで使えないか検証したんですが、
esm.shとskypack.dev経由で動かなかったので断念しました。
そして、PoC時点ではサードパーティ製のライブラリを使おうとしていました。
しかし、次のメリット・デメリットから、シンプルにkubectlをラップする形で実装しました。
サードパーティのライブラリを使う場合
- デメリット
- 認証とコンテキスト周りの管理が面倒そう
 - 公式のライブラリではないため、メンテナンス継続性と信頼性が低い
 
 - メリット
- APIと直接おしゃべりするので、処理のオーバーヘッドが少ない
 
 
kubectlをラップする場合
- デメリット
- 外部プロセス起動のオーバーヘッドがある
 
 - メリット
- 公式CLIなので安心・信頼がある
 - kubernetesを使っている人で
kubectlを使っていない人は居ない(はず)ので追加の導入コストがない - 認証やコンテキスト周りの管理をしなくて済む
 - 
kubectlの機能を把握できる(学習観点) 
 
実際にこんな感じでkubectlを叩いて、その出力を取っています。
ありがたいことにkubectlは-o jsonで出力フォーマットを指定できるので、データの加工は簡単でした。
cli.ts
export async function getResourceAsObject<T>(
  resource: string,
  opts: ResourceOptions,
): Promise<T> {
  opts.format = "json";
  const output = await getResourceAsText(resource, opts);
  return JSON.parse(output);
}
export async function getResourceAsText(
  resource: string,
  opts: ResourceOptions,
): Promise<string> {
  const cmd = [
    "kubectl",
    "get",
    resource,
  ];
  if (opts.all) {
    cmd.push("-A");
  }
  if (opts.namespace) {
    cmd.push("-n", opts.namespace);
  }
  if (opts.format) {
    cmd.push("-o", opts.format);
  }
  const output = await run(cmd);
  return output;
}
export async function run(cmd: string[]): Promise<string> {
  const opt: Deno.RunOptions = {
    cmd: cmd,
    stdin: "null",
    stdout: "piped",
    stderr: "piped",
  };
  const p = Deno.run(opt);
  const result = dec.decode(await p.output());
  const status = await p.status();
  if (!status.success) {
    const error = dec.decode(await p.stderrOutput());
    throw new Error(
      `failed to execute command: ${cmd.join(" ")}, error: ${error}`,
    );
  }
  p.stderr!.close();
  p.stdout!.close();
  p.close();
  return result;
}
実装を見てPodなどの型情報はどうするの?って思う方がいるかも知れません。
実は型情報は自動生成してそれを使っています。
型情報の生成について
kubernetesのAPIからJSON形式のAPI定義情報を抜き出すことができます。
$ kubectl get --raw /openapi/v2
この定義はOpenAPIなので、openapi-generator-cliを使ってコードを自動生成できます。
docker run --rm -v $PWD:/local -w /local openapitools/openapi-generator-cli generate \
  --skip-operation-example \
  --additional-properties=platform=deno \
  --global-property models \
  -g typescript \
  -i denops/k8s/swagger.json \
  -o /local/denops/k8s
ちなみに、データの型定義だけあればいいので--global-property modelsを付けています。
これを付けないと、クライアントのコードまで生成されてしまいます。
またDenoで動くように--additional-properties=platform=denoも使っています。
他のオプションはこちらを参照してください。
これで、データの構造が型レベルで分かるようになるので、こんな感じでimportして使っています。
import { IoK8sApiCoreV1Pod } from "./models/IoK8sApiCoreV1Pod.ts";
export function renderPodList(pods: IoK8sApiCoreV1Pod[]): string[] {
  const body = pods.map((pod) => {
    const podIPs = pod.status?.podIPs?.map((podip) => podip.ip ?? "");
    return [
      pod.metadata?.namespace ?? "<unknown>",
      pod.metadata?.name ?? "<unknown>",
      pod.status?.phase ?? "<unknown>",
      podIPs?.join(" ") ?? "<unknown>",
      pod.spec?.nodeName ?? "<unknown>",
      pod.status?.startTime?.toLocaleString() ?? "<unknown>",
    ];
  });
  const header = ["NAMESPACE", "NAME", "STATUS", "IP", "NODE", "START TIME"];
  const table = new Table();
  table.header(header)
    .body(body);
  return table.toString().split("\n");
}
最後に
kubernetesをはじめて数日のド素人レベルですが、とりあえず作ってみました。
これから学習しつつ、便利な機能を追加していく予定です。
興味ある方は、ぜひ使ってみてください。
機能の要望など受け付けています。
Discussion