Kubernetes 上での API 実行を自動化する CommandBuilder + Utility 実装解説【TypeScript】

# はじめに

Kubernetes 上の Pod で `curl` を実行して API 通信を行うためのコマンドを自動生成し、ログを記録する TypeScript 実装を紹介します。

Remoteの実装については下記の記事をご参照ください。

https://qiita.com/oharu121/items/770808ec6d5fc0b55f97

### 解決する課題
- 複雑な curl コマンドを毎回書くのが大変
- Pod に対しての exec 実行をスクリプト化したい
- 実行結果をログとして残したい

### 使用技術
- TypeScript
- ssh2（Remote exec）
- Kubernetes (`kubectl exec`)

# コマンド生成クラス `CommandBuilder`

### 目的
Kubernetes 上の Pod に対して `kubectl exec` 経由で `curl` コマンドを組み立てるためのビルダークラス `CommandBuilder` を実装したものです。各メソッドが `curl` コマンドの一部を動的に構築します。

以下、分割して解説します。

---

### 🔹 クラス定義と初期化部分
```ts:CommandBuilder.ts
class CommandBuilder {
  private config: string[] = [];
  private command: string[] = [];
```
- `config`：`kubectl exec` などのベース部分を格納。
- `command`：`curl` コマンドのパラメータ部分を構築。

### 🔹 `reset()` メソッド
```ts:CommandBuilder.ts
public reset(): void {
  this.command = [];
}
```
- `command` をリセット。`config` はそのまま保持する。

---

### 🔹 `pod(podName: string)` メソッド
```ts:CommandBuilder.ts
public pod(podName: string): void {
  this.reset();
  this.config.push(`kubectl exec`);
  this.config.push(podName);
  this.config.push(`-- curl -Lgkv`);
}
```
- Pod 名を指定して `kubectl exec` + `curl` のコマンド基盤を構成。
- `curl` は `-L`（リダイレクト追従）, `-g`（glob無効）, `-k`（SSL検証無効）, `-v`（verbose）を指定。

---

### 🔹 `patch()` メソッド
```ts:CommandBuilder.ts
public patch(): CommandBuilder {
  this.command.push(`-X PATCH`);
  return this;
}
```
- `PATCH` リクエストを指定するオプション。

---

### 🔹 `token(token: string)` メソッド
```ts:CommandBuilder.ts
public token(token: string): CommandBuilder {
  this.command.push(`-H "Authorization: Bearer ${token}"`);
  return this;
}
```
- Bearer トークンによる認証ヘッダーを追加。

---

### 🔹 `header(header: string)` メソッド
```ts:CommandBuilder.ts
public header(header: string): CommandBuilder {
  this.command.push(`-H "${header}"`);
  return this;
}
```
- 任意のヘッダーを追加。

---

### 🔹 `content(content: string = "json")` メソッド
```ts:CommandBuilder.ts
public content(content: string = "json"): CommandBuilder {
  switch(content) {
    case "form":
      this.command.push(`-H 'Content-Type: multipart/form-data'`);
      break;
    case "patch":
      this.command.push(`-H 'Content-Type: application/json-patch+json'`);
      break;
    default:
      this.command.push(`-H 'Content-Type: application/json'`);
  }
  return this;
}
```
- `Content-Type` ヘッダーを用途に応じて追加。
  - `"form"`：ファイルアップロード用途。
  - `"patch"`：JSON Patch。
  - それ以外：JSON 通信。

---

### 🔹 `payload(payload: object)` メソッド
```ts:CommandBuilder.ts
public payload(payload: object): CommandBuilder {
  this.command.push(`-d '${JSON.stringify(payload)}'`);
  return this;
}
```
- リクエストボディを JSON 文字列として追加。

---

### 🔹 `api(api: string)` メソッド
```ts:CommandBuilder.ts
public api(api: string): CommandBuilder {
  this.command.push(`${api}`);
  return this;
}
```
- 実行する API のエンドポイント URL を追加。

---

### 🔹 `formMeta(meta: string)` メソッド
```ts:CommandBuilder.ts
public formMeta(meta: string): CommandBuilder {
  this.command.push(`-F "${meta};type=application/json"`);
  return this;
}
```
- multipart フォームの一部として JSON メタ情報を送る。

---

### 🔹 `formFile(file: string)` メソッド
```ts
public formFile(file: string): CommandBuilder {
  this.command.push(`-F "${file};type=application/pdf"`);
  return this;
}
```
- multipart フォームの一部としてファイル（PDF）を送信。

---

### 🔹 `create()` メソッド
```ts
public create(): string {
  return [...this.config, ...this.command].join(" ");
}
```
- `kubectl exec ... curl ...` の全体コマンド文字列を組み立てて返す。

---

### 🔹 最後の export
```ts
export default new CommandBuilder();
```
- インスタンスをシングルトンとしてエクスポート。

---

### ✅ 使用例（イメージ）

```ts
const cmd = CommandBuilder
  .pod("my-pod")
  .patch()
  .token("abc123")
  .content("json")
  .payload({ foo: "bar" })
  .api("http://localhost/api")
  .create();

console.log(cmd);
// kubectl exec my-pod -- curl -Lgkv -X PATCH -H "Authorization: Bearer abc123" -H 'Content-Type: application/json' -d '{"foo":"bar"}' http://localhost/api
```

このようにして、Kubernetes 上の Pod に curl を通して API を実行するためのコマンドを柔軟に構築できます。

# 実行ロジックを絞り込む `Utility` クラス

### 目的
Kubernetes 上の Pod で `curl` コマンドを実行し、API 呼び出しとログ記録を行うユーティリティクラス `Utility` を定義しています。以下、機能ごとに分断して詳しく解説します。

### 実行フロー一覧
1. Pod 名取得：`checkPodname()`
2. Token 取得：`Postman.getToken()`
3. curl 組み立て：`CommandBuilder.xxx()`
4. Pod 上で実行：`Remote.execRemote()`
5. 結果解析とログ記録：`handleApiResponse()` & `PodLogger`

このコードは、Kubernetes 上の Pod で `curl` コマンドを実行し、API 呼び出しとログ記録を行うユーティリティクラス `Utility` を定義しています。以下、機能ごとに分断して詳しく解説します。

---

### 🔹 インポート & 初期設定
```ts:Utility.ts
import Remote from "@class/ssh/Remote";
import CommandBuilder from "@class/builder/CommandBuilder";
import PodLogger from "@class/dto/PodLogger";
import PocketBase from "@class/db/PocketBase";
import Pod from "@class/ssh/Pod";
```
- 外部 API 型やユーティリティクラスを読み込む。

---

### 🔹 `@reset()` デコレータの定義
```ts:Utility.ts
function reset() {
  return function (_target, _key, descriptor) {
    const original = descriptor.value;
    descriptor.value = function (...args) {
      CommandBuilder.reset();
      return original.apply(this, args);
    };
    return descriptor;
  };
}
```
- メソッド実行前に `CommandBuilder.reset()` を自動的に挿入するデコレータ。
- コマンドビルダーの使い回しによる副作用を防止。

---

### 🔹 `Utility` クラスの本体
```ts:Utility.ts
class Utility {
  public podName?: string;
```
- Kubernetes の Pod 名を保持。

---

### 🔹 Pod 名取得メソッド
```ts:Utility.ts
private async checkPodname() {
  if (!this.podName) {
    const podName = (await Pod.getPodName())[0] || "";
    this.podName = podName;
    CommandBuilder.pod(podName);
  }
}
```
- Pod 名が未取得の場合に取得し、CommandBuilder に設定。

---

### 🔹 `updatePaymentStatus` メソッド
```ts:Utility.ts
@reset()
public async updatePaymentStatus(): Promise<void> {
  this.checkPodname();

  const api = apis.UPDATE_PAYMENT_STATUS;
  const token = await Postman.getToken();
  const cmd = CommandBuilder.content("json")
    .header("correlationId:12345")
    .header("requestorUserId:random_user")
    .token(token)
    .api(api)
    .create();

  const result = await Remote.execRemote(cmd);
  const analyze = (data: { numberOfScanned: number }) => {
    if (data.numberOfScanned > 0) PodLogger.success();
  };
  await this.handleApiResponse(result, analyze);

  PodLogger.api(api);
  await PocketBase.writePodLog();
}
```
- `支払ステータス更新API` を呼び出すためのコマンドを組み立てて実行。
- 成功判定後に Pod ログを保存。

---

### 🔹 `uploadFile` メソッド
```ts:Utility.ts
@reset()
public async uploadFile(fileName: string): Promise<string> {
  this.checkPodname();

  const api = apis.UPLOAD_FILE_TO_DMS;
  const payload = PodLogger.getPayload();
  const jsonString = JSON.stringify(payload).replace(/"/g, '\\"');
  const token = await Postman.getToken();
  const cmd = CommandBuilder.content("form")
    .token(token)
    .header("requestorUserId:123")
    .formMeta(`listOfFile=${jsonString}`)
    .formFile(`fileStream=@/tmp/${fileName}`)
    .api(api)
    .create();

  const result = await Remote.execRemote(cmd);
  let fileId = "";
  const analyze = async (res: FileResponse) => {
    if (res.responseCode === "200") {
      PodLogger.success();
      fileId = res.listOfFiles[0]!.documentIdentifier;
    } else {
      await this.uploadOrderKYC(fileName); // 再帰的リトライ
    }
  };
  await this.handleApiResponse(result, analyze);

  PodLogger.api(api);
  await PocketBase.writePodLog();
  return fileId;
}
```
- ファイルを DMS にアップロードし、`documentIdentifier` を取得。
- 成功時に ID を取得、失敗時はリトライ。

---

### 🔹 `handleApiResponse` メソッド
```ts:Utility.ts
private async handleApiResponse(
  raw: string,
  callback: (data: any) => void
): Promise<void> {
  let response;
  try {
    response = JSON.parse(raw);
    await callback(response);
  } catch {
    response = { msg: response || "" };
    PodLogger.failed();
  }
  PodLogger.response(response);
}
```
- JSON をパースして結果を渡す。
- パース失敗時は `failed()` を記録。

---

### 🔹 エクスポート
```ts:Utility.ts
export default new Utility();
```
- シングルトンとしてクラスインスタンスを外部に提供。

# おまとめ
本記事では、Pod 上での curl 実行を簡素化・共通化する方法として、`CommandBuilder` と `Utility` クラスを組み合わせた実装を紹介しました。

今後の拡張としては：

- CommandBuilder に GET, PUT などのメソッド追加
- PodLogger の標準化
- CLIツールとしての切り出し

などが考えられます。