Kubernetes の controller-runtime で Namespace == "" が発生するワケ ―― “空ネームスペース事件” を防ぐための整理＆ベストプラクティス ――

# Kubernetes Controllerにおける

## 対象読者
- Kubernetes Controller を Go (controller-runtime) で実装している方
- tombstone（DeletedFinalStateUnknown）にハマったことがある／テストで妙な NULL を踏んだことがある方

---

## TL;DR

| 起因 | どんな時に起こる？ | 見分け方 | 取るべき対策 |
|------|-------------------|---------|-------------|
| DeletedFinalStateUnknown | オブジェクトが**キャッシュに載る前に削除**された | Event の型が `cache.DeletedFinalStateUnknown` | tombstone 専用ハンドラで Key 解析 or 無視 |
| フェイククライアント | テスト用 `fake.NewClientBuilder()` に渡したオブジェクトに `metadata.namespace` を書き忘れた | テストだけで再現する／APIServerを経由していない | `obj.SetNamespace(...)` or `client.Create()` で検証を通す |
| スコープ不一致 | Cluster-scoped 型を誤って Watch している | CRD の `scope: Namespaced/Cluster` を確認 | スコープ専用 RBAC & unit test で早期検知 |
| YAML の namespace 抜け | POST /apis/... を直叩き or GitOps で namespace 省略 | kubectl 経由では再現しない | CI でスキーマ検証 or admission webhook |

---

## 1. DeletedFinalStateUnknown ―― "無縁仏" Tombstone イベント

コントローラが Informer から受け取る削除イベントには２種類あります。

| 種類 | キャッシュに実体がある？ | 典型的な流れ |
|------|------------------------|-------------|
| DeleteEvent | ある | Add → Update → Delete |
| DeletedFinalStateUnknown | ない | Add が届く前に物理削除 → キャッシュに無いまま Delete だけ届く |

```mermaid
sequenceDiagram
  participant API
  participant Informer
  participant Controller
  API-->>Informer: create X<br/>(すぐに delete)
  Note over API: キャッシュ反映前に削除
  Informer-->>Controller: DeletedFinalStateUnknown(tombstone)
```

DeletedFinalStateUnknown.Obj には「ゼロ値」のオブジェクトが挿入されるため、

```go
if tomb, ok := ev.Object.(cache.DeletedFinalStateUnknown); ok {
    key := tomb.Key            // => "default/foo"
    // → key から namespace/name をパースする
}
```

のように Key 文字列 から情報を復元するか、"存在しない前提"で早期 return するのが安全です。

---

## 2. フェイククライアントで namespace を入れ忘れた

`fake.NewClientBuilder()` に渡すオブジェクトは APIServer バリデーションが走りません。
テストデータを手書きする際に `metadata.namespace` を忘れると、そのまま `""` が保存されます。

```go
obj := &ipamv1.Subnet{
  ObjectMeta: metav1.ObjectMeta{
    Name: "foo",
    // Namespace: "default", ← 忘れると空に！
  },
}
client := fake.NewClientBuilder().
  WithScheme(scheme).
  WithObjects(obj).           // → ここで検証されない
  Build()
```

### ベストプラクティス
1. 必ず `obj.SetNamespace("default")` などで埋める
2. もしくは `client.Create(ctx, obj)` 経由で登録し、fakeClient 内部のスキーマ検証を利用する

---

## 3. スコープ不一致（Cluster-scoped 型を誤って扱う）
- CRD を Namespaced → Cluster に変更した
- Watch 対象の型と RBAC が食い違った

……といった場合、Informer から届くオブジェクトは当然 Namespace == "" です。
CI で スキーマ＆RBAC テスト を走らせておくと早期に検知できます。

```bash
# controller-gen の lint
controller-gen paths=./... crd:crdVersions=v1 output:stdout \
  | kubeconform -strict -
```

---

## 4. YAML の metadata.namespace が抜けたまま直 API コール

`kubectl -n default apply -f ...` なら補完されますが、
GitOps で Raw Manifest を API に流し込む場合は 空のまま登録 されることがあります。
- Admission Webhook (CEL / kyverno) で必須フィールドチェック
- CI で kubeconform + OpenAPI 検証を推奨

---

## 5. 実装例：安全なイベントハンドリングの雛形

```go
func (r *Reconciler) handleEvent(obj client.Object) {
    // tombstone を区別
    if tomb, ok := obj.(cache.DeletedFinalStateUnknown); ok {
        // (a) 無視する
        return

        // --- または (b) Key をパースする ---
        // ns, name := cache.SplitMetaNamespaceKey(tomb.Key)
        // ...
    }

    // Namespace を必ずチェック
    if obj.GetNamespace() == "" {
        // cluster-scoped? それとも異常？
        log.Info("empty namespace; skip", "name", obj.GetName())
        return
    }

    // 以降は安心して Reconcile
}
```

---

## まとめ
- "空 namespace" はバグではなく "例外イベント or テスト環境" が主犯
- tombstone かどうかを判定し、Key 解析 or 無視で安全に捌く
- フェイククライアント・GitOps マニフェストの namespace 欠落は自分で防御
- スコープ不一致は CI と RBAC テストで早期発見

この４点を押さえておけば、controller-runtime の "空ネームスペース事件" でハマる確率はほぼゼロになります。