🦍

Vimでk8sを操作するプラグインを作った

2022/06/22に公開

初めに

こんにちは、森に帰省中のゴリラです。
最近、完全ガイドでkubernetesの勉強をしていますが、覚えること、理解することがなかなか多くてたいへんです。

さて、kubernetesを使ったことがある人なら、一度くらいk9sを使ったことがあるかと思います。
これはとても便利なもので、podを見たりコンテナにアタッチしたりと、いろいろできます。

ただ、自分は普段Vimをメインに使っているので、できればVim上で同じことをやりたいと思って、k8s.vimというプラグインを作りました。

https://github.com/skanehira/k8s.vim

k8s.vimの概要

UIはこんな感じになっています。
基本的にk8s://*という仮想バッファを開くとリソースを見れたり、操作できたりします。

現時点では次のことができます。

  • Pod関連
    • 一覧
    • コンテナ一覧
    • describeの出力
    • yaml出力
  • Node関連
    • 一覧
    • describeの出力

たとえば、podのコンテナでシェルを実行したい場合は、こんな感じになります。

実装

k8s.vimdenops.vimで動くプラグインなので、Denoで動いています。
一応、公式製のclientライブラリをDenoで使えないか検証したんですが、
esm.shskypack.dev経由で動かなかったので断念しました。
そして、PoC時点ではサードパーティ製のライブラリを使おうとしていました。

https://twitter.com/gorilla0513/status/1538765433158873088

しかし、次のメリット・デメリットから、シンプルに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