k8s-rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37dcb752eb0c0b10dd35af27b071e5dad054674e111cff16a60e49b78e7de64b
4
- data.tar.gz: '0867da799bf1c8914940bccae887f22d976e67826271d05313d965bf6aec8f15'
3
+ metadata.gz: 708bfa5afaf656a7fb64a0ab781ef429eaa3f643d049d209941260178d6859d8
4
+ data.tar.gz: e9a0184c46ad0e5afd2f66bfdb1857d7f170c9a573825363a6910087ff4b8995
5
5
  SHA512:
6
- metadata.gz: '039460cf0b175fbb51f160ce454cb9abe54e0001fb5d974f5b9da24c4c55750aa9e39326157d7306c319e08c64556284dfe994618642b374ab00e03c258465ec'
7
- data.tar.gz: 124d5016d3e90068fe6ca03241c6cba6364bf956ee564324cf6d0329426f824b1a27e916e9234f279aacc216ae8450178e3f4ec437a5ef311c8f2cce199b9f28
6
+ metadata.gz: e54a31eee779538219001aec14d679ca11ff2ec2334f7f97a4f6bcb77d274b4b477a032e092daa85333f318656dc2f00feb45d1f163aeb76e36196dfd985ba20
7
+ data.tar.gz: 77ccd48603ee30f6145855fb6808001042bc5dd0cd74f6172a247fa6e2acef6bcde07a1ce21f497b9d823bf9b50f036ceb83aaa36daaabe5c85c07175e6b70ef
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  `k8s-rails` の全 notable な変更はこのファイルに記録する。
4
4
 
5
+ ## 0.2.0
6
+
7
+ - **cluster-scoped CRD サポート**(#17): `K8sRails.crd` に `scope: :namespaced`
8
+ (既定)/ `:cluster` を追加。cluster-scoped CRD(ClusterIssuer 等)は
9
+ `scope: :cluster`(`namespace:` 併用不可)で宣言し、`list_cluster` /
10
+ `find_cluster` / `find_or_nil_cluster` / `create_cluster` /
11
+ `patch_cluster` を使う。スコープと非対称な呼び出し(namespaced 宣言で
12
+ `*_cluster` / cluster 宣言で素のメソッド)は `ArgumentError`。
13
+ transport は kruby の `*_cluster_custom_object` 4 メソッドを新たに利用。
14
+ - **`configure` のアトミック契約**(#16): 「一度だけ」はブロックが
15
+ 正常終了した場合のみ成立。ブロックが異常終了した(任意の例外 —
16
+ `LoadError` / `ScriptError` を含む — / `throw` / non-local return 等)
17
+ 場合は設定済みフラグがリセットされ、後続の `configure` は通常どおり
18
+ 実行される。異常終了前に書き込まれた属性は残存する(再実行ブロックは
19
+ 依存する属性を全て設定する責務を負う。ロールバックはしない)。
20
+ - **`connected?` の注入契約**(#18): `config.api_client` 注入時は
21
+ I/O なしで `true`(注入トランスポートが接続面そのもの)。
22
+ 非注入時は従来どおり VersionApi プローブ。
23
+ - **接続設定探索順序の修正**(#19): README / 設計書 / 設定コメントの
24
+ 自動検出順序を kruby 1.36.x の loader 実装順
25
+ (**KUBECONFIG → `~/.kube/config` → in-cluster**、in-cluster は最後)
26
+ に修正(従来の「in-cluster → KUBECONFIG」記述は誤り)。kruby 上げ替え
27
+ 時の再確認手順を設計書 §7 に追加。
28
+ - テスト注入スタブ(`config.api_client`)の契約: namespaced 4 メソッド +
29
+ cluster 4 メソッド(namespaced 宣言のみ使う場合は前者 4 メソッドで足りる)。
30
+ - 設計書 KBR-DESIGN-001 v0.1.12(案)へ更新(§5.1 / §5.2 / §5.3 / §6 /
31
+ §7 / §9 / §14)。
32
+
5
33
  ## 0.1.0
6
34
 
7
35
  - **M0**: gem 骨子(gemspec / Gemfile / Rakefile / version / require 構造)
data/README.md CHANGED
@@ -22,12 +22,12 @@ For the design rationale, see the [design document (docs/design.md)](docs/design
22
22
 
23
23
  ```ruby
24
24
  # Gemfile
25
- gem "k8s-rails", "~> 0.1"
25
+ gem "k8s-rails", "~> 0.2"
26
26
  ```
27
27
 
28
28
  ```ruby
29
29
  require "k8s-rails"
30
- K8sRails::VERSION # => "0.1.0"
30
+ K8sRails::VERSION # => "0.2.0"
31
31
  ```
32
32
 
33
33
  `require` is side-effect-free and needs no cluster. kruby itself is loaded
@@ -74,12 +74,21 @@ K8sRails.configure do |config|
74
74
  end
75
75
  ```
76
76
 
77
- - `configure` is effective **only once**. A second call prints a warning and is
78
- ignored (use `K8sRails.reset!` to reset the configuration, the cached
79
- transport, and declared CRDs primarily for tests).
77
+ - `configure` is effective **only once** but only when the block returns
78
+ normally. If the block exits abnormally (any exception, including
79
+ `LoadError`; `throw`; non-local return), the "configured" flag is reset,
80
+ so a later `configure` runs normally. Note that attribute writes made
81
+ before the abnormal exit **remain** on the shared configuration (a
82
+ partially applied state is possible); a re-run block should set every
83
+ attribute it depends on.
84
+ A second call on an already-configured gem prints a warning and is ignored
85
+ (use `K8sRails.reset!` to reset the configuration, the cached transport, and
86
+ declared CRDs — primarily for tests).
80
87
  - Connection resolution order: `config.api_client` (test injection) →
81
88
  `config.connection` → `Kubernetes::Configuration.default_config`
82
- (automatic in-cluster KUBECONFIG detection).
89
+ (automatic detection in kruby 1.36.x: `KUBECONFIG` → `~/.kube/config` →
90
+ in-cluster. Note in-cluster is tried **last**, after the file-based
91
+ sources — re-verify `kruby`'s loader when upgrading kruby).
83
92
 
84
93
  ## CRD declaration and access
85
94
 
@@ -89,13 +98,44 @@ naming convention). Re-declaring the same kind raises
89
98
 
90
99
  Return values are **always string-keyed hashes**. kruby returns symbol keys,
91
100
  but Rails-side JSON/views work with string keys, so the gem normalizes
92
- internally in pure Ruby (no ActiveSupport dependency). Every method accepts a
93
- `namespace:` argument to override the namespace from the declaration.
101
+ internally in pure Ruby (no ActiveSupport dependency). Every namespaced
102
+ method accepts a `namespace:` argument to override the namespace from the
103
+ declaration (the `*_cluster` methods take no `namespace:` — see
104
+ [Cluster-scoped CRDs](#cluster-scoped-crds)).
94
105
 
95
106
  `readonly` must be an **explicit boolean** (`nil` or other values raise
96
107
  `ArgumentError`). Writes are enabled **only** by `readonly: false`, so a
97
108
  missing flag can never fail open.
98
109
 
110
+ ### Cluster-scoped CRDs
111
+
112
+ Both namespaced and cluster-scoped CRDs are supported. Declare a
113
+ cluster-scoped CRD (ClusterIssuer, ClusterWorkflowTemplate, ...) with
114
+ `scope: :cluster` and **without** `namespace:` (combining the two raises
115
+ `ArgumentError`):
116
+
117
+ ```ruby
118
+ ClusterIssuer = K8sRails.crd(
119
+ group: "cert-manager.io",
120
+ version: "v1",
121
+ plural: "clusterissuers",
122
+ kind: "ClusterIssuer",
123
+ scope: :cluster, # cluster-scoped endpoints (no namespace)
124
+ )
125
+
126
+ ClusterIssuer.list_cluster # all objects cluster-wide
127
+ ClusterIssuer.find_cluster("letsencrypt")
128
+ ClusterIssuer.find_or_nil_cluster("x")
129
+ ClusterIssuer.create_cluster({ ... }) # readonly: false only
130
+ ClusterIssuer.patch_cluster("letsencrypt", [{ ... }]) # readonly: false only
131
+ ```
132
+
133
+ On a `scope: :namespaced` declaration (the default) the `*_cluster` methods
134
+ raise `ArgumentError` — the CRD is namespaced, so the cluster endpoints would
135
+ 404 anyway. For a cluster-scoped declaration, use the `*_cluster` methods
136
+ (the plain `list`/`find`/... would call the namespaced endpoints and 404, so
137
+ they raise `ArgumentError` there as well).
138
+
99
139
  ## Connectivity check
100
140
 
101
141
  ```ruby
@@ -107,6 +147,11 @@ It never returns `false` — a connection failure surfaces as an exception
107
147
  (handle it with `rescue`). The actual API connection is established lazily on
108
148
  the first API call.
109
149
 
150
+ When a test transport is injected via `config.api_client`, `connected?`
151
+ returns `true` without any network I/O — the injected transport **is** the
152
+ connection surface, so probing a real endpoint would contradict the Resource
153
+ operations that the same injection serves.
154
+
110
155
  ## Exception hierarchy
111
156
 
112
157
  ```
@@ -154,7 +199,9 @@ end
154
199
 
155
200
  The test suite needs **no cluster**. Tests inject a transport stub via
156
201
  `config.api_client`. The stub is wrapped internally by an adapter, so it just
157
- implements the same four methods as kruby's `CustomObjectsApi`:
202
+ implements the same methods as kruby's `CustomObjectsApi` — the four
203
+ `*_namespaced_custom_object` methods, plus (for cluster-scoped declarations)
204
+ the four `*_cluster_custom_object` methods:
158
205
 
159
206
  ```ruby
160
207
  class StubTransport
@@ -162,11 +209,19 @@ class StubTransport
162
209
  def get_namespaced_custom_object(group, version, namespace, plural, name) = {}
163
210
  def create_namespaced_custom_object(group, version, namespace, plural, body) = {}
164
211
  def patch_namespaced_custom_object(group, version, namespace, plural, name, body) = {}
212
+
213
+ def list_cluster_custom_object(group, version, plural) = { items: [] }
214
+ def get_cluster_custom_object(group, version, plural, name) = {}
215
+ def create_cluster_custom_object(group, version, plural, body) = {}
216
+ def patch_cluster_custom_object(group, version, plural, name, body) = {}
165
217
  end
166
218
 
167
219
  K8sRails.configure { |c| c.api_client = StubTransport.new }
168
220
  ```
169
221
 
222
+ With a transport injected, `K8sRails.connected?` returns `true` without
223
+ network I/O (see [Connectivity check](#connectivity-check)).
224
+
170
225
  ## Development
171
226
 
172
227
  ```
@@ -189,7 +244,7 @@ through k8s-rails are unaffected (it does not overwrite an already-set
189
244
  kruby's own configuration behavior (outside this gem), check this token-key
190
245
  issue first.
191
246
 
192
- ## Roadmap (v0.2+)
247
+ ## Roadmap (v0.3+)
193
248
 
194
249
  - Ruby 3.5 / 4.0 support (after verifying against the stable releases, then
195
250
  widening the declared range and the CI matrix)
data/docs/design.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # k8s-rails 設計書
2
2
 
3
3
  - 文書番号: KBR-DESIGN-001
4
- - 版: 0.1.10(案)
4
+ - 版: 0.1.12(案)
5
5
  - 日付: 2026-09-15
6
6
  - 対象リポジトリ: k8s-rails(本設計の実装先)
7
7
  - ライセンス: MIT(LICENSE は main に既存)
@@ -41,7 +41,7 @@ K8s CRD を扱えるようにし、(b) 実際の consumer アプリをこの gem
41
41
  ### 2.2 非スコープ(本設計の外)
42
42
 
43
43
  - core v1 リソース(Pod / Deployment / Service 等)のフルサポート — CRD 中心の gem。core v1 は built-in リソースであり CustomObjects API では扱えない(`group=""`/`version="v1"` の宣言は**無効**)ため、別途 core API 経路が必要。v0.1 では対象外(将来候補、§11)
44
- - **watch(ストリーム)** — v0.1 では非対応。kruby の watch は get/list より成熟度が低く、初版の API 保証範囲から外す(§11 で v0.2 候補)
44
+ - **watch(ストリーム)** — v0.1 では非対応。kruby の watch は get/list より成熟度が低く、初版の API 保証範囲から外す(§10 で v0.3 対象)
45
45
  - アプリの**デプロイ**(helm / kustomize 生成等) — `kuby-core` の領域
46
46
  - RBAC 権限の付与・管理 — 呼び出しアプリ側の ClusterRole/Role の責務
47
47
  - 複数クラスタ同時接続 — v0.1 は単一クラスタ前提(§11 展望)
@@ -80,7 +80,8 @@ K8s CRD を扱えるようにし、(b) 実際の consumer アプリをこの gem
80
80
  │ │
81
81
  ┌──────────────────────────┴────────────────┴────────┐
82
82
  │ kruby (~> 1.36.0) → Kubernetes API Server │
83
- │ (in-cluster SA token | KUBECONFIG)
83
+ │ (KUBECONFIG | ~/.kube/config | in-cluster SA token)
84
+ │ ※kruby 1.36.x の探索順序は左→右(in-cluster は最後) │
84
85
  └────────────────────────────────────────────────────┘
85
86
  ```
86
87
 
@@ -105,7 +106,7 @@ k8s-rails/
105
106
  ├── lib/
106
107
  │ ├── k8s-rails.rb # エントリ。require 集 + モジュール定義
107
108
  │ └── k8s_rails/
108
- │ ├── version.rb # VERSION = "0.1.0"
109
+ │ ├── version.rb # VERSION = "0.2.0"
109
110
  │ ├── configuration.rb # Config: namespace, connection, 計測 ON/OFF
110
111
  │ ├── client.rb # 接続解決・BearerToken 橋渡し・計測ラップ
111
112
  │ ├── crd.rb # K8sRails.crd 宣言 → Resource 生成
@@ -129,7 +130,8 @@ k8s-rails/
129
130
  # config/initializers/k8s-rails.rb
130
131
  K8sRails.configure do |config|
131
132
  config.namespace = ENV.fetch("K8S_NAMESPACE", "default")
132
- # 任意上書き(省略時は default_config の自動検出: in-cluster → KUBECONFIG)
133
+ # 任意上書き(省略時は default_config の自動検出。kruby 1.36.x の探索順序は
134
+ # KUBECONFIG → ~/.kube/config → in-cluster で、in-cluster が最後)
133
135
  # config.connection = Kubernetes::Configuration.default_config
134
136
  # config.instrumentation = true # 既定 true(ActiveSupport 存在時のみ有効)
135
137
  end
@@ -140,11 +142,18 @@ end
140
142
  | キー | 既定 | 説明 |
141
143
  |---|---|---|
142
144
  | `namespace` | `"default"` | CRD 宣言が namespace 未指定時のデフォルト |
143
- | `connection` | `nil`(自動検出) | `Kubernetes::Configuration` インスタンス。認証を上書きする場合に指定 |
144
- | `api_client` | `nil` | **テスト専用**: kruby の `CustomObjectsApi` と同型の 4 メソッド(`get_namespaced_custom_object` / `list_namespaced_custom_object` / `create_namespaced_custom_object` / `patch_namespaced_custom_object`)を実装する素のオブジェクト。指定時は `Client.build` が接続解決をスキープして `StringKeyedAdapter` で包んで使う(§5.2・§9) |
145
+ | `connection` | `nil`(自動検出) | `Kubernetes::Configuration` インスタンス。認証を上書きする場合に指定。省略時の探索順序(kruby 1.36.x の loader 実装順): **KUBECONFIG → `~/.kube/config` → in-cluster**(in-cluster はファイル系が両方無効な場合の**最後**。kruby 上げ替え時に loader を再確認すること — §7) |
146
+ | `api_client` | `nil` | **テスト専用**: kruby の `CustomObjectsApi` と同型のメソッド(namespaced 4 メソッド `get_namespaced_custom_object` / `list_namespaced_custom_object` / `create_namespaced_custom_object` / `patch_namespaced_custom_object` + cluster 4 メソッド `get_cluster_custom_object` / `list_cluster_custom_object` / `create_cluster_custom_object` / `patch_cluster_custom_object`)を実装する素のオブジェクト(namespaced 宣言のみを使う場合は namespaced 4 メソッドで足りる)。指定時は `Client.build` が接続解決をスキープして `StringKeyedAdapter` で包んで使う(§5.2・§9) |
145
147
  | `instrumentation` | `true` | `ActiveSupport::Notifications` で計測する(§8) |
146
148
 
147
149
  - 設定は `K8sRails.configure` で**一度だけ**。再実行は警告(`Warning`)+ 無視。
150
+ **「一度だけ」はブロックが正常終了した場合に限る**: ブロックが異常終了した
151
+ (任意の例外送出 — `LoadError` / `ScriptError` を含む — / `throw` /
152
+ non-local return 等)場合は設定済みフラグがリセットされ、後続の `configure`
153
+ は通常どおり実行される。ただし異常終了前に書き込まれた属性は共有 Configuration
154
+ に**残存する**(部分的な設定状態になり得るため、再実行ブロックは依存する属性を
155
+ 全て設定する責務を負う。アトミックなロールバックは行わない — Configuration
156
+ は純データで 4 属性のみのため、複写+スワップの複雑さに見合わない)。
148
157
  - `K8sRails.reset!`(テスト用)で接続キャッシュ・宣言済 CRD を破棄できる。
149
158
 
150
159
  ### 5.2 接続
@@ -154,6 +163,8 @@ K8sRails::Client.build # → Kubernetes::CustomObjectsApi(lazy。初回呼
154
163
  K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailable / ApiError を raise
155
164
  # (false を返す経路なし)。/version 相当の軽量確認
156
165
  # (kruby 1.36.x の VersionApi#get_code(GET /version/)1 回)
166
+ # `config.api_client` 注入時は I/O なしで true(§5.1 の
167
+ # 注入が接続面そのものだから。Resource 操作と整合させる)
157
168
  ```
158
169
 
159
170
  `Client.build` が内部で行うこと(consumer アプリの K8s サービスの custom objects 生成部を移設):
@@ -164,6 +175,11 @@ K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailab
164
175
  3. `Kubernetes::ApiClient` → `Kubernetes::CustomObjectsApi` を生成し、**文字列キー化**(K2)を API レスポンス後に行う。ActiveSupport 非依存の gem 内部の純 Ruby 再帰変換(`K8sRails::Normalizer`)を使う(v0.1.1 以降: 常に Normalizer。`deep_stringify_keys` 経路は廃止)
165
176
  4. **接続レベルの失敗**(DNS 失敗 / タイムアウト / 接続拒否等)は `K8sRails::Unavailable` に変換して `raise`(リトライはしない)。**kruby 1.36.x ではこれらの転送失敗は HTTP ステータスが無いため `Kubernetes::ApiError`(`code == 0`)として surfacing する**(§5.4 の変換表参照)。認可失敗(401/403)は §5.4 により `K8sRails::ApiError`
166
177
 
178
+ `connected?` の解決順序:
179
+
180
+ 0. `config.api_client` があれば(テスト注入、§5.1)**I/O なしで `true` を返す**。注入されたトランスポートが接続面そのもののため、実エンドポイントへのプローブは同一注入下の Resource 操作と矛盾する(#18 対応)。クラスタ到達性の真の確認が必要な場合は注入を解除した環境で行う
181
+ 1. 以下 `Client.build` と同一(`config.connection` → `default_config` → K1 橋渡し → VersionApi プローブ)
182
+
167
183
  ### 5.3 CRD 宣言
168
184
 
169
185
  ```ruby
@@ -176,9 +192,19 @@ Workflow = K8sRails.crd(
176
192
  namespace: K8sRails.config.namespace, # 省略可
177
193
  readonly: false, # 既定 true。false で create/patch 有効化(K4)
178
194
  )
195
+
196
+ # cluster-scoped CRD(ClusterIssuer / ClusterWorkflowTemplate 等)は
197
+ # scope: :cluster を付け、namespace: は省略する(併用は ArgumentError)
198
+ ClusterIssuer = K8sRails.crd(
199
+ group: "cert-manager.io",
200
+ version: "v1",
201
+ plural: "clusterissuers",
202
+ kind: "ClusterIssuer",
203
+ scope: :cluster,
204
+ )
179
205
  ```
180
206
 
181
- 宣言で生成されるメソッド(全て class メソッド。各メソッドは任意の `namespace:` 引数を受け取り、宣言時のデフォルト namespace を上書き可能):
207
+ 宣言で生成されるメソッド(全て class メソッド。namespaced 系は任意の `namespace:` 引数を受け取り、宣言時のデフォルト namespace を上書き可能):
182
208
 
183
209
  | メソッド | 引数 | 戻り値 | readonly 制限 |
184
210
  |---|---|---|---|
@@ -186,6 +212,17 @@ Workflow = K8sRails.crd(
186
212
  | `find(name)` | 必須 | 同型 or `K8sRails::NotFound`(raise) | 常に有効 |
187
213
  | `create(attributes)` | CRD body hash | 作成済みオブジェクト(文字列キー) | `readonly: false` のみ |
188
214
  | `patch(name, operations)` | JSON Patch 操作配列 | 更新済みオブジェクト | `readonly: false` のみ |
215
+ | `list_cluster` | `{}` | 同上(クラスタ横断。`namespace:` なし) | 常に有効 |
216
+ | `find_cluster(name)` | 必須 | 同上 or `K8sRails::NotFound` | 常に有効 |
217
+ | `create_cluster(attributes)` | CRD body hash | 作成済みオブジェクト | `readonly: false` のみ |
218
+ | `patch_cluster(name, operations)` | JSON Patch 操作配列 | 更新済みオブジェクト | `readonly: false` のみ |
219
+
220
+ スコープの契約(#17 対応):
221
+
222
+ - `scope: :namespaced`(既定)の宣言では `*_cluster` メソッドは `ArgumentError`(CRD が namespaced のためクラスタ endpoint を呼んでも 404 になるだけ)。
223
+ - `scope: :cluster` の宣言では素の `list` / `find` / `create` / `patch` は `ArgumentError`(namespaced endpoint を呼ぶと 404 になるだけ。`namespace:` 引数は意味を持たない)。
224
+ - 宣言時に `scope: :cluster` と `namespace:` を併用した場合は `ArgumentError`(設定ミスの fail fast)。
225
+ - transport は kruby の `*_cluster_custom_object` 4 メソッド(`namespace` 非持参の endpoint)を使う。テスト注入スタブは 2 セット 8 メソッドを実装する(§9)。
189
226
 
190
227
  - **戻り値は常に文字列キーの Hash**(K2 の規約を API 契約として固定)。
191
228
  `find` は存在しない場合 `K8sRails::NotFound` を raise(consumer アプリ側が `return nil` にしていたのは
@@ -232,8 +269,8 @@ end
232
269
 
233
270
  | 依存 | 制約 | 理由 |
234
271
  |---|---|---|
235
- | Ruby | `>= 3.3, < 4.0` | 下限: kruby 1.36.x が `required_ruby_version ">= 3.3"` を宣言(RubyGems API で実測 2026-09-21、1.36.0.1〜1.36.4.1 全バージョン)。上限: 「宣言した Ruby minor を必ず CI で検証する」方針(レビュー対応・2026-09-21)— 2026-09-21 時点で Ruby 4.0 は stable(v4.0.7)だが未検証、3.5 は preview(v3_5_0_preview1)のため、宣言範囲を 3.x に限定。4.0 / 3.5 対応は v0.2 以降で検証の上宣言に含める |
236
- | `kruby` | `~> 1.36.0` | consumer アプリと同一 pin。`~> 1.36.0` は 1.36.x のみ許可(`~> 1.36` 形式は 1.37 以降も許容してしまうため使用しない)。新しめの kruby に対応する場合は §7 の確認事項(client.rb 4 メソッド・K1 橋渡し)を済ませてから明示的に上げ替える |
272
+ | Ruby | `>= 3.3, < 4.0` | 下限: kruby 1.36.x が `required_ruby_version ">= 3.3"` を宣言(RubyGems API で実測 2026-09-21、1.36.0.1〜1.36.4.1 全バージョン)。上限: 「宣言した Ruby minor を必ず CI で検証する」方針(レビュー対応・2026-09-21)— 2026-09-21 時点で Ruby 4.0 は stable(v4.0.7)だが未検証、3.5 は preview(v3_5_0_preview1)のため、宣言範囲を 3.x に限定。4.0 / 3.5 対応は v0.3 で検証の上宣言に含める(§10) |
273
+ | `kruby` | `~> 1.36.0` | consumer アプリと同一 pin。`~> 1.36.0` は 1.36.x のみ許可(`~> 1.36` 形式は 1.37 以降も許容してしまうため使用しない)。新しめの kruby に対応する場合は §7 の確認事項(client.rb 8 メソッド(namespaced 4 + cluster 4)・K1 橋渡し・`default_config` 探索順序)を済ませてから明示的に上げ替える |
237
274
  | `activesupport` | **任意**(`>= 7.0`) | `defined?(ActiveSupport::Notifications)` でガード(計測のみ、§8)。Rails 無し環境(Cron スクリプト等)でも動作する必要がある — レスポンスの文字列キー化(K2)はこれに依存せず、gem 内部の純 Ruby 変換で担う(§5.2) |
238
275
  | `rspec` / `rubocop` | 開発依存 | spec / lint |
239
276
 
@@ -245,10 +282,15 @@ end
245
282
  - `lib/k8s_rails/client.rb` **のみ**が `require "kubernetes"` してよい。
246
283
  他のファイルは kruby 定数・クラスを参照しない。
247
284
  - kruby の `CustomObjectsApi` メソッド呼び出しは `client.rb` 内の
248
- `*_namespaced_custom_object` の 4 メソッド(`get_namespaced_custom_object` 等)に集約する。`resource.rb` は
285
+ `*_namespaced_custom_object` の 4 メソッド(`get_namespaced_custom_object` 等)と
286
+ `*_cluster_custom_object` の 4 メソッド(`get_cluster_custom_object` 等)に集約する。
287
+ `resource.rb` は
249
288
  `K8sRails.client.get(group, version, ns, plural, name)` のような **gem 内部 API** だけを使う。
250
- - kruby 上げ替え時の作業は (1) client.rb 4 メソッドのシグネチャ確認、
251
- (2) K1 橋渡しの要否確認、に収まることをテスト(§9)で担保する。
289
+ - kruby 上げ替え時の作業は (1) client.rb 8 メソッド(namespaced 4 + cluster 4)のシグネチャ確認、
290
+ (2) K1 橋渡しの要否確認、(3) **`Kubernetes::Configuration.default_config` の探索順序確認**
291
+ (kruby 1.36.x の loader 実装順は **KUBECONFIG → `~/.kube/config` → in-cluster** で in-cluster が
292
+ 最後。README / 設計書 / 設定コメントがこの順序を明記しているため、loader が変わった場合は
293
+ 全箇所を同期する — #19 対応)に収まることをテスト(§9)で担保する。
252
294
 
253
295
  ## 8. 計測(ActiveSupport 任意)
254
296
 
@@ -274,11 +316,11 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
274
316
 
275
317
  | レイヤー | 手法 | 対象 |
276
318
  |---|---|---|
277
- | ユニット | `K8sRails.config.api_client` に**スタブ**(kruby `CustomObjectsApi` と同型の 4 メソッド `get_namespaced_custom_object` / `list_namespaced_custom_object` / `create_namespaced_custom_object` / `patch_namespaced_custom_object` を実装する素のオブジェクト。`StringKeyedAdapter` がこの形式を呼ぶ)を注入 | client(橋渡し・例外変換)、resource(整形・readonly 制限)、crd(メソッド生成) |
319
+ | ユニット | `K8sRails.config.api_client` に**スタブ**(kruby `CustomObjectsApi` と同型のメソッド。namespaced 4 メソッド `get_namespaced_custom_object` / `list_namespaced_custom_object` / `create_namespaced_custom_object` / `patch_namespaced_custom_object` + cluster 4 メソッド `get_cluster_custom_object` / `list_cluster_custom_object` / `create_cluster_custom_object` / `patch_cluster_custom_object` を実装する素のオブジェクト。`StringKeyedAdapter` がこの形式を呼ぶ)を注入 | client(橋渡し・例外変換)、resource(整形・readonly 制限・スコープ制限)、crd(メソッド生成・scope 検証) |
278
320
  | 設定 | spec 間で `K8sRails.reset!` | 宣言の破棄・再接続 |
279
321
  | 集積(任意) | GitHub Actions で **kind**(または既存 microk8s に接続するジョブ)で実クラスタ E2E | v0.1 の必須ではない。**推奨**: consumer アプリ移行時の検証を兼ねる |
280
322
 
281
- - 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.2 以降で
323
+ - 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.3
282
324
  GitHub Actions の追加として扱う(実クラスタへの接続 CI はネットワーク依存のため採用しない)。
283
325
 
284
326
  ## 10. リリース計画
@@ -286,8 +328,9 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
286
328
  | バージョン | 内容 | 出口基準 |
287
329
  |---|---|---|
288
330
  | **v0.1** | §5 の公開 API(CRD 宣言 / list / find / create / patch / 例外 / 計測 / スタブテスト)+ README | rspec 全緑 + **consumer アプリの K8s サービスを `k8s-rails` に移行して動作確認**(§12) |
289
- | v0.2 | watch(`watch` メソッド、kruby watch サポート上)、core v1 built-in リソース対応(CustomObjects API では不可なため別途 core API 経路、§2.2)、kind E2E CI | v0.1 運用 1 ヶ月後のフィードバック |
290
- | v0.3 | (展望)複数クラスタ(ネームスペース化された client 集合)、リトライポリシー | |
331
+ | **v0.2** | #16–#19 の公開 API 拡充・修正(cluster-scoped CRD `scope:` + `*_cluster` メソッド、`configure` のアトミック契約、`connected?` の注入契約、kruby loader 探索順序の修正)。設計書 v0.1.12(案) | rspec 全緑(クラスタ不要)+ 公開 gem push(v0.1.0 と同導線) |
332
+ | v0.3 | watch(`watch` メソッド、kruby watch サポート上)、core v1 built-in リソース対応(CustomObjects API では不可なため別途 core API 経路、§2.2)、kind E2E の CI 化、Ruby 3.5 / 4.0 対応(検証の上宣言範囲・matrix を拡大) | v0.2 運用のフィードバック |
333
+ | v0.4 | (展望)複数クラスタ(ネームスペース化された client 集合)、リトライポリシー | — |
291
334
 
292
335
  v0.1 の milestone 分割(開発セッション向けのタスク単位目安):
293
336
 
@@ -357,12 +400,12 @@ PR の差分を最小化)。
357
400
  gemspec の宣言範囲(`>= 3.3, < 4.0`)を matrix で検証:
358
401
  3.3.0(下限・kruby 1.36.x の `>= 3.3`)・3.3.8(開発)・
359
402
  3.4.10(3.x 系の最新 stable・2026-09-21 時点。3.5 は preview、
360
- 4.0 は宣言範囲外のため未検証・v0.2 以降で検討)。
403
+ 4.0 は宣言範囲外のため未検証・v0.3 で検討)。
361
404
  **宣言範囲を常に matrix がカバーする**こと(`< 4.0` 上限により、
362
405
  4.x のリリースは宣言範囲外。stable 化された新 3.x minor が出たら
363
406
  matrix への追加を忘れないこと)。
364
407
  テストはクラスタ不要(§9・スタブ注入)のため v0.1 は runner 上のユニットのみ。
365
- kind / 実クラスタ E2E の CI 化は v0.2 対象(§9・§10)
408
+ kind / 実クラスタ E2E の CI 化は v0.3 対象(§9・§10)
366
409
  - `README` に「kruby pin」「対応 k8s バージョン(実測 v1.33.x で検証済み)」「K1 橋渡しの背景」
367
410
  を明記する(検索でヒットする重要な注意点のため)
368
411
 
@@ -381,3 +424,5 @@ PR の差分を最小化)。
381
424
  | 0.1.8 | 2026-09-21 | PR #11 レビュー第 4 波対応(Copilot ×3): 指摘(「Ruby 3.5 が stable 化したため matrix に追加せよ」)を検証した結果 **3.5 は preview であり claim は誤り**(ruby/ruby タグ `v3_5_0_preview1`・2026-09-21 実測)と判明。ただし指摘の根本(宣言と検証範囲のズレ)は**Ruby 4.0 が stable(v4.0.7)だったため**実際に存在した。対策として宣言範囲を **`>= 3.3, < 4.0` に改訂**(gemspec / §6 / README / CHANGELOG)し、宣言範囲 = matrix 検証範囲(3.3.0 / 3.3.8 / 3.4.10)を一致。4.0 / 3.5 対応は v0.2 以降で検証の上宣言に含める方針 | 実装反映済み |
382
425
  | 0.1.9 | 2026-09-21 | public リポジトリ化の準備: ①公開導線を**ローカル PC から手動 `gem push`** に変更(kruby と同様の運用方針・CI 自動公開は廃止、publish workflow を削除、test workflow の push/PR テストのみ残す)②§13 公開手順の手動化(tag は追溯性のため推奨)③内部 consumer アプリの名称・構造への言及を §1〜§14 全箇所から除去し「consumer アプリ」に一般化 | 実装反映済み |
383
426
  | 0.1.10 | 2026-09-21 | PR #12 レビュー対応(Copilot): §13 の公開手順で tag の **remote への push**(`git push origin v<VERSION>`)が欠落しており、GitHub 上のリリースコミットとの対応付け(追溯性)が確保できないとの指摘を反映 | 実装反映済み |
427
+ | 0.1.11 | 2026-09-22 | Issue #16–#19 対応: ①#16 `configure` の例外送出時は設定済みフラグをリセット(「一度だけ」はブロック正常終了時にのみ成立)。例外前に書かれた属性は残存することを契約として明文化(§5.1)②#17 `scope: :namespaced`(既定)/ `:cluster` を宣言 API に追加。cluster 系 4 メソッド(`list_cluster` / `find_cluster` / `find_or_nil_cluster` / `create_cluster` / `patch_cluster`)と双方向の ArgumentError 契約(§5.3)。transport は kruby の `*_cluster_custom_object` 4 メソッドを新たに使用③#18 `connected?` は `config.api_client` 注入時に I/O なしで `true`(§5.2)。注入下で Resource 操作と接続確認の挙動を一致させる④#19 kruby 1.36.x の loader 実装順(**KUBECONFIG → `~/.kube/config` → in-cluster**)を README / 設計書 / 設定コメントに明記し、§7 の上げ替え確認事項に探索順序の再確認を追加(in-cluster は最後。従来の「in-cluster → KUBECONFIG」記述は誤り) | 実装反映済み |
428
+ | 0.1.12 | 2026-09-22 | PR #20 レビュー対応(Codex P2 + Copilot M/L 4 系統): ①#16 のリセット範囲を `rescue StandardError` から**任意の異常終了**(`LoadError` / `ScriptError` / `throw` / non-local return 等)に拡大(成功マーカー + `ensure` で実装、spec 2 件追加)。§5.1 / README の契約文言も「任意の異常終了」に修正②README の「`namespace:` 引数を受け取る」記述を namespaced メソッドに限定(`*_cluster` は受け付けない)③§6 の kruby 上げ替え確認事項を 8 メソッド + 探索順序に同期④`api_client` 注入スタブの契約を namespaced 4 + cluster 4 の 8 メソッドに統一(§5.1 表 / configuration.rb コメント / §9) | 実装反映済み |
data/lib/k8s-rails.rb CHANGED
@@ -23,14 +23,22 @@ module K8sRails
23
23
 
24
24
  # Configure the gem. Runs once — a second call warns and is ignored
25
25
  # (design §5.1). Yields the Configuration object.
26
+ #
27
+ # Atomicity contract: "runs once" holds only when the block returns
28
+ # normally. If the block exits abnormally (any exception — including
29
+ # LoadError/ScriptError — throw, non-local return, ...), the flag is
30
+ # reset, so a later `configure` call re-runs normally. However,
31
+ # attribute writes made before the exit REMAIN on the shared
32
+ # Configuration — a failed block may leave a partially applied state;
33
+ # the re-run block is responsible for setting every attribute it
34
+ # depends on (documented in design §5.1).
26
35
  def configure
27
36
  if @configured
28
37
  warn "[K8sRails] K8sRails.configure called more than once; ignoring the second call."
29
38
  return config
30
39
  end
31
40
 
32
- @configured = true
33
- yield config
41
+ run_configure_block { yield config }
34
42
  config
35
43
  end
36
44
 
@@ -57,9 +65,10 @@ module K8sRails
57
65
  # Declare a CRD and return its Resource class (design §5.3, K5).
58
66
  # Workflow = K8sRails.crd(group: "argoproj.io", version: "v1alpha1",
59
67
  # plural: "workflows", kind: "Workflow")
68
+ # Cluster-scoped CRDs: pass scope: :cluster (and no namespace:).
60
69
  # Re-declaring the same kind raises K8sRails::RedeclarationError.
61
- def crd(group:, version:, plural:, kind:, namespace: nil, readonly: true)
62
- CRD.declare(group:, version:, plural:, kind:, namespace:, readonly:)
70
+ def crd(group:, version:, plural:, kind:, namespace: nil, readonly: true, scope: :namespaced)
71
+ CRD.declare(group:, version:, plural:, kind:, namespace:, readonly:, scope:)
63
72
  end
64
73
 
65
74
  # Design §8: run an API call inside a `k8s-rails.request` notification.
@@ -106,6 +115,25 @@ module K8sRails
106
115
  def instrumentation_enabled?
107
116
  config.instrumentation && defined?(ActiveSupport::Notifications)
108
117
  end
118
+
119
+ private
120
+
121
+ # Runs the configure block with the configured-once contract (§5.1):
122
+ # the flag is set only if the block returns normally. ANY abnormal exit
123
+ # (StandardError, LoadError/ScriptError, throw, non-local return, other
124
+ # Exception subclasses) resets it so a later `configure` re-runs — but
125
+ # attribute writes made before the exit remain on the shared
126
+ # Configuration.
127
+ def run_configure_block
128
+ @configured = true
129
+ ok = false
130
+ begin
131
+ yield
132
+ ok = true
133
+ ensure
134
+ @configured = false unless ok
135
+ end
136
+ end
109
137
  end
110
138
 
111
139
  # Whether the kruby-dependent client file has actually been required yet
@@ -14,6 +14,11 @@ module K8sRails
14
14
  # api.get(group, version, namespace, plural, name)
15
15
  # api.create(group, version, namespace, plural, body)
16
16
  # api.patch(group, version, namespace, plural, name, body)
17
+ # # cluster-scoped (namespace argument omitted, design §5.3)
18
+ # api.list_cluster(group, version, plural)
19
+ # api.get_cluster(group, version, plural, name)
20
+ # api.create_cluster(group, version, plural, body)
21
+ # api.patch_cluster(group, version, plural, name, body)
17
22
  #
18
23
  # Connection is LAZY: `build` does no network I/O — it only resolves a
19
24
  # `Kubernetes::Configuration` and builds an in-memory `CustomObjectsApi`.
@@ -44,7 +49,15 @@ module K8sRails
44
49
  # Lightweight connectivity probe (design §5.2). Performs one lightweight
45
50
  # `/version` call via VersionApi and returns true on success. Raises
46
51
  # K8sRails::Unavailable / ApiError on failure (the app may rescue).
52
+ #
53
+ # Injection contract: when `config.api_client` is set (test injection),
54
+ # the injected transport IS the connection surface — there is no real
55
+ # network to probe, so this returns true without I/O. This keeps
56
+ # `connected?` consistent with Resource operations under injection
57
+ # (design §5.2).
47
58
  def connected?
59
+ return true if K8sRails.config.api_client
60
+
48
61
  config = build_configuration
49
62
  # The probe also authenticates — apply the K1 bridge or the Authorization
50
63
  # header would be empty on clusters where /version requires auth.
@@ -150,6 +163,43 @@ module K8sRails
150
163
  end
151
164
  end
152
165
 
166
+ # Cluster-scoped variants (design §5.3): the same four operations on
167
+ # kruby's *_cluster_custom_object endpoints (no namespace in the path).
168
+ # An injected test double must implement both the *_namespaced_* and
169
+ # *_cluster_* quadruples.
170
+
171
+ def list_cluster(group, version, plural)
172
+ handle do
173
+ Normalizer.stringify(
174
+ @transport.list_cluster_custom_object(group, version, plural)
175
+ )
176
+ end
177
+ end
178
+
179
+ def get_cluster(group, version, plural, name)
180
+ handle do
181
+ Normalizer.stringify(
182
+ @transport.get_cluster_custom_object(group, version, plural, name)
183
+ )
184
+ end
185
+ end
186
+
187
+ def create_cluster(group, version, plural, body)
188
+ handle do
189
+ Normalizer.stringify(
190
+ @transport.create_cluster_custom_object(group, version, plural, body)
191
+ )
192
+ end
193
+ end
194
+
195
+ def patch_cluster(group, version, plural, name, body)
196
+ handle do
197
+ Normalizer.stringify(
198
+ @transport.patch_cluster_custom_object(group, version, plural, name, body)
199
+ )
200
+ end
201
+ end
202
+
153
203
  private
154
204
 
155
205
  def handle
@@ -13,13 +13,17 @@ module K8sRails
13
13
  attr_accessor :namespace
14
14
 
15
15
  # Kubernetes::Configuration インスタンス。省略時は default_config の自動
16
- # 検出(in-cluster → KUBECONFIG)。認証を上書きする場合に指定する。
16
+ # 検出(kruby 1.36.x の実装順: KUBECONFIG → ~/.kube/config → in-cluster
17
+ # kruby 更新時にこの順序が変更されていないか loader を再確認すること)。
18
+ # 認証を上書きする場合に指定する。
17
19
  attr_accessor :connection
18
20
 
19
- # テスト専用(§5.1)。CustomObjectsApi と同型の 4 メソッド
20
- # (`get_namespaced_custom_object` 等の *_namespaced_custom_object 4 メソッド)を実装した素の
21
+ # テスト専用(§5.1)。CustomObjectsApi と同型の 8 メソッド
22
+ # (namespaced 4 メソッド `get_namespaced_custom_object` cluster 4
23
+ # メソッド `get_cluster_custom_object` 等)を実装した素の
21
24
  # オブジェクトを指定すると、Client.build は接続解決をスキープしてこれを
22
- # 内部トランスポートとして使う。
25
+ # 内部トランスポートとして使う(namespaced 宣言だけを使う場合は
26
+ # namespaced 4 メソッドのみの実装で足りる)。
23
27
  attr_accessor :api_client
24
28
 
25
29
  # ActiveSupport::Notifications での計測の ON/OFF(§8。M3 で実装)。
data/lib/k8s_rails/crd.rb CHANGED
@@ -12,6 +12,9 @@ module K8sRails
12
12
  # readonly: false, # default true
13
13
  # )
14
14
  #
15
+ # Cluster-scoped CRDs (ClusterIssuer, ClusterWorkflowTemplate, ...) use
16
+ # `scope: :cluster` — `namespace` must be omitted (design §5.3).
17
+ #
15
18
  # `crd` returns a `Resource` subclass with the coordinates bound; the same
16
19
  # class is registered under its kind name so a second declaration of the
17
20
  # same kind raises K8sRails::RedeclarationError (config-mistake detection).
@@ -30,12 +33,24 @@ module K8sRails
30
33
  # kind are explicit — never guessed). `readonly` must be an explicit
31
34
  # boolean: mutations are enabled ONLY by `readonly: false` (design §5.3 /
32
35
  # K4), so nil/other values fail fast instead of silently allowing writes.
33
- def self.declare(group:, version:, plural:, kind:, namespace: nil, readonly: true)
36
+ # `scope` is :namespaced (default) or :cluster; `namespace` and
37
+ # `scope: :cluster` are mutually exclusive (a cluster CRD has no
38
+ # namespace to bind).
39
+ def self.declare(group:, version:, plural:, kind:, namespace: nil, readonly: true, scope: :namespaced)
34
40
  unless [true, false].include?(readonly)
35
41
  raise ArgumentError,
36
42
  "readonly must be true or false (got #{readonly.inspect}) — mutations require an explicit readonly: false"
37
43
  end
38
44
 
45
+ unless %i[namespaced cluster].include?(scope)
46
+ raise ArgumentError, "scope must be :namespaced or :cluster (got #{scope.inspect})"
47
+ end
48
+
49
+ if scope == :cluster && !namespace.nil?
50
+ raise ArgumentError,
51
+ "namespace cannot be combined with scope: :cluster — cluster-scoped CRDs have no namespace"
52
+ end
53
+
39
54
  if registered.key?(kind)
40
55
  raise RedeclarationError, "CRD kind #{kind} is already declared — re-declaration is a configuration mistake"
41
56
  end
@@ -46,7 +61,8 @@ module K8sRails
46
61
  plural: plural,
47
62
  kind: kind,
48
63
  namespace: namespace,
49
- readonly: readonly
64
+ readonly: readonly,
65
+ scope: scope
50
66
  )
51
67
  registered[kind] = resource
52
68
  resource
@@ -10,13 +10,13 @@ module K8sRails
10
10
  # touches kruby itself (§7).
11
11
  #
12
12
  # Workflow = K8sRails.crd(group: "argoproj.io", version: "v1alpha1",
13
- # plural: "workflows", kind: "Workflow")
14
- # Workflow.list
15
- # Workflow.find("wf-1")
13
+ # plural: "workflows", kind: "Workflow")
16
14
  class Resource
17
15
  # Bind declared coordinates to a fresh anonymous subclass. `namespace`
18
16
  # may be nil — resolved from `K8sRails.config.namespace` at call time.
19
- def self.declare(group:, version:, plural:, kind:, namespace: nil, readonly: true)
17
+ # `scope` is :namespaced (default) or :cluster (design §5.3); cluster
18
+ # declarations must not set `namespace`.
19
+ def self.declare(group:, version:, plural:, kind:, namespace: nil, readonly: true, scope: :namespaced)
20
20
  Class.new(self) do
21
21
  define_singleton_method(:group_name) { group }
22
22
  define_singleton_method(:version_name) { version }
@@ -24,13 +24,16 @@ module K8sRails
24
24
  define_singleton_method(:kind_name) { kind }
25
25
  define_singleton_method(:declared_namespace) { namespace }
26
26
  define_singleton_method(:readonly?) { readonly }
27
+ define_singleton_method(:cluster_scoped?) { scope == :cluster }
27
28
  end
28
29
  end
29
30
 
30
31
  class << self
31
- # All objects in the namespace. Returns an array of string-keyed Hashes
32
- # (design §5.3: `[{ "name" => "...", ... }]`).
32
+ # All objects in the namespace (namespaced declarations only
33
+ # cluster-scoped declarations raise ArgumentError). Returns an array
34
+ # of string-keyed Hashes (design §5.3: `[{"name" => "...", ...}]`).
33
35
  def list(namespace: resolved_namespace)
36
+ assert_namespaced
34
37
  K8sRails.instrument(:list, instrument_meta(namespace)) do
35
38
  transport.list(group_name, version_name, namespace, plural_name)["items"] || []
36
39
  end
@@ -38,6 +41,7 @@ module K8sRails
38
41
 
39
42
  # One object by name. Raises K8sRails::NotFound when absent.
40
43
  def find(name, namespace: resolved_namespace)
44
+ assert_namespaced
41
45
  K8sRails.instrument(:find, instrument_meta(namespace)) do
42
46
  transport.get(group_name, version_name, namespace, plural_name, name)
43
47
  end
@@ -53,6 +57,7 @@ module K8sRails
53
57
  # Create from a CRD body hash. `readonly: true` declarations raise
54
58
  # K8sRails::ReadOnlyError (K4).
55
59
  def create(attributes, namespace: resolved_namespace)
60
+ assert_namespaced
56
61
  assert_writable
57
62
  K8sRails.instrument(:create, instrument_meta(namespace)) do
58
63
  transport.create(group_name, version_name, namespace, plural_name, attributes)
@@ -61,12 +66,57 @@ module K8sRails
61
66
 
62
67
  # JSON Patch a named object. Same readonly restriction as `create`.
63
68
  def patch(name, operations, namespace: resolved_namespace)
69
+ assert_namespaced
64
70
  assert_writable
65
71
  K8sRails.instrument(:patch, instrument_meta(namespace)) do
66
72
  transport.patch(group_name, version_name, namespace, plural_name, name, operations)
67
73
  end
68
74
  end
69
75
 
76
+ # Cluster-scoped variants (design §5.3). Available on EVERY declared
77
+ # class; a `scope: :namespaced` declaration raises ArgumentError (its
78
+ # CRD is namespaced, so cluster endpoints 404 anyway).
79
+ # All objects cluster-wide. Returns an array of string-keyed Hashes.
80
+ def list_cluster
81
+ assert_cluster_scoped
82
+ K8sRails.instrument(:list, instrument_meta(nil)) do
83
+ transport.list_cluster(group_name, version_name, plural_name)["items"] || []
84
+ end
85
+ end
86
+
87
+ # One cluster-scoped object by name. Raises K8sRails::NotFound when absent.
88
+ def find_cluster(name)
89
+ assert_cluster_scoped
90
+ K8sRails.instrument(:find, instrument_meta(nil)) do
91
+ transport.get_cluster(group_name, version_name, plural_name, name)
92
+ end
93
+ end
94
+
95
+ # Like `find_cluster`, but returns nil instead of raising on NotFound.
96
+ def find_or_nil_cluster(name)
97
+ find_cluster(name)
98
+ rescue NotFound
99
+ nil
100
+ end
101
+
102
+ # Create a cluster-scoped object. Same readonly restriction as `create`.
103
+ def create_cluster(attributes)
104
+ assert_cluster_scoped
105
+ assert_writable
106
+ K8sRails.instrument(:create, instrument_meta(nil)) do
107
+ transport.create_cluster(group_name, version_name, plural_name, attributes)
108
+ end
109
+ end
110
+
111
+ # JSON Patch a cluster-scoped object. Same readonly restriction.
112
+ def patch_cluster(name, operations)
113
+ assert_cluster_scoped
114
+ assert_writable
115
+ K8sRails.instrument(:patch, instrument_meta(nil)) do
116
+ transport.patch_cluster(group_name, version_name, plural_name, name, operations)
117
+ end
118
+ end
119
+
70
120
  private
71
121
 
72
122
  # Notification metadata for design §8 (`k8s-rails.request`).
@@ -80,15 +130,30 @@ module K8sRails
80
130
  declared_namespace || K8sRails.config.namespace
81
131
  end
82
132
 
83
- def transport
84
- K8sRails.client
85
- end
133
+ # Endless method: keeps the class under the ClassLength budget while
134
+ # delegating to the shared transport (design §5.2).
135
+ def transport = K8sRails.client
86
136
 
87
137
  def assert_writable
88
138
  return unless readonly?
89
139
 
90
140
  raise ReadOnlyError, "#{kind_name} is declared readonly — create/patch are disabled (K4)"
91
141
  end
142
+
143
+ def assert_cluster_scoped
144
+ return if cluster_scoped?
145
+
146
+ raise ArgumentError,
147
+ "#{kind_name} is namespaced — *_cluster methods require a scope: :cluster declaration"
148
+ end
149
+
150
+ def assert_namespaced
151
+ return unless cluster_scoped?
152
+
153
+ raise ArgumentError,
154
+ "#{kind_name} is declared scope: :cluster — use the *_cluster methods " \
155
+ "(there is no namespace endpoint for a cluster-scoped CRD)"
156
+ end
92
157
  end
93
158
  end
94
159
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K8sRails
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: k8s-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian - Takahiro Ishida