k8s-rails 0.2.0 → 0.3.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: 708bfa5afaf656a7fb64a0ab781ef429eaa3f643d049d209941260178d6859d8
4
- data.tar.gz: e9a0184c46ad0e5afd2f66bfdb1857d7f170c9a573825363a6910087ff4b8995
3
+ metadata.gz: 8e99e17a8bbceb09aac19b96621477b798646b95104223a5603dd5a4785866f3
4
+ data.tar.gz: a1ae1cb4c96639738a320d533b71ffbdbfda6a07d67f87065c2ac395bbee691a
5
5
  SHA512:
6
- metadata.gz: e54a31eee779538219001aec14d679ca11ff2ec2334f7f97a4f6bcb77d274b4b477a032e092daa85333f318656dc2f00feb45d1f163aeb76e36196dfd985ba20
7
- data.tar.gz: 77ccd48603ee30f6145855fb6808001042bc5dd0cd74f6172a247fa6e2acef6bcde07a1ce21f497b9d823bf9b50f036ceb83aaa36daaabe5c85c07175e6b70ef
6
+ metadata.gz: ae8df68ba2ed241704cf052df3a9b4a0e74ad407136f7d79d1a0573e985b2ff884df1dfb1dd5d808b7488304a6424080b224ad1b72f504112afe718c6be57360
7
+ data.tar.gz: fae4996c36d5f3e24546750f132b0efb6018826c4c56eea13a1465423d96f2717015be3d701546a83427bdb2fc379a4739a16983aefffdf69159067e64e21464
data/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  `k8s-rails` の全 notable な変更はこのファイルに記録する。
4
4
 
5
+ ## 0.3.0
6
+
7
+ - **対応リソースの拡大: core v1 built-in(Pod / Service / ConfigMap / Node 等)に対応**。
8
+ 従来、transport は kruby の `CustomObjectsApi`(`/apis/{group}/...` 固定パス)
9
+ に依存しており、`group: ""` の core v1 リソース(`/api/v1/...`)だけは 404
10
+ で到達できなかった(named built-in は従来から動作)。transport を
11
+ `Kubernetes::ApiClient#call_api` 上の**統一 REST 層**に改訂し、
12
+ core v1 / named built-in / CRD の全てを宣言座標からのパス構築で到達させる。
13
+ named 系・CRD の既存宣言の挙動は不変。
14
+ - **`delete` / `delete_cluster` を追加**(CRUD 完成)。readonly ゲートは
15
+ create / patch と同型(`readonly: true` 宣言で呼ぶと `ReadOnlyError`)。
16
+ K8s は削除成功時に Status オブジェクト(`{"kind":"Status","status":"Success"}`)を返す。
17
+ - **テスト注入スタブの契約変更(テストコードへの影響)**: `config.api_client`
18
+ が実装すべきメソッドは CustomObjectsApi の 8 メソッドから
19
+ `call_api(http_method, path, opts)` の 1 メソッドに統一
20
+ (戻り値 `[data, status_code, headers]`、http_method は
21
+ `:GET` / `:POST` / `:PATCH` / `:DELETE`)。既存スタブは `call_api` に
22
+ 置き換える必要がある(実クラスタ接続の production コードには影響なし)。
23
+ - `k8s-rails.request` notification の `operation` に `:delete` を追加。
24
+ - 実クラスタ(microk8s v1.33.13)で Pod list / Deployment find /
25
+ ConfigMap create・patch・delete / Node list_cluster を E2E 検証済み。
26
+
27
+ ## 0.2.1
28
+
29
+ - **Ruby 3.5 / 4.0 対応の宣言**(宣言範囲拡大のみ・コード・挙動変更なし):
30
+ 2026-09-22 に Ruby 4.0.7(stable・v4.0.7、2026-09-15 リリース)と
31
+ Ruby 3.5.0-preview1(3.5 系で公開済みの唯一のリリース)で全
32
+ rspec / rubocop を実行し緑を確認したため(kruby 1.36.4.1 は両方で
33
+ install / 動作確認)、`required_ruby_version` を `>= 3.3, < 4.0` から
34
+ **`>= 3.3, < 4.1`** に拡大。上限は検証済みの 4.0 系に設定し、
35
+ CI matrix は 3.3.0 / 3.3.8 / 3.4.10 / 3.5.0-preview1 / 4.0.7 の 5 系統で
36
+ 範囲内の公開済み Ruby を全カバー(宣言範囲 = 検証範囲)。
37
+ 設計書 KBR-DESIGN-001 v0.1.13(案)へ更新(§6 / §10 / §13 / §14)。
38
+
5
39
  ## 0.2.0
6
40
 
7
41
  - **cluster-scoped CRD サポート**(#17): `K8sRails.crd` に `scope: :namespaced`
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # k8s-rails
2
- A Kubernetes API / CRD convention layer for Rails applications.
2
+ A Kubernetes API access convention layer for Rails applications.
3
3
 
4
- `k8s-rails` provides the **connection, CRD access, and error-handling convention
5
- layer** for Rails applications that talk to the Kubernetes API.
4
+ `k8s-rails` provides the **connection, resource access, and error-handling
5
+ convention layer** for Rails applications that talk to the Kubernetes API. It
6
+ covers every resource kruby can address: **core v1** built-ins (Pod, Service,
7
+ ConfigMap, ...), **named-group** built-ins (Deployment, Ingress, ...), and
8
+ **CRDs**.
6
9
 
7
10
  [![Test](https://github.com/doridoridoriand/k8s-rails/actions/workflows/test.yml/badge.svg)](https://github.com/doridoridoriand/k8s-rails/actions/workflows/test.yml)
8
11
  [![Gem Version](https://badge.fury.io/rb/k8s-rails.svg)](https://rubygems.org/gems/k8s-rails)
@@ -13,21 +16,21 @@ For the design rationale, see the [design document (docs/design.md)](docs/design
13
16
 
14
17
  | Item | Supported range | Notes |
15
18
  |------|-----------------|-------|
16
- | Ruby | `>= 3.3, < 4.0` | Floor: kruby 1.36.x requires Ruby 3.3. Upper bound: unverified Ruby 4.x is excluded from the declared range. CI verifies the declared range with a 3.3.0 / 3.3.8 / 3.4.10 matrix |
19
+ | Ruby | `>= 3.3, < 4.1` | Floor: kruby 1.36.x requires Ruby 3.3. Ruby 4.0 support verified on Ruby 4.0.7 (2026-09-22); Ruby 3.5 verified on 3.5.0-preview1 (the only released 3.5, 2026-09-22). CI verifies the declared range with a 3.3.0 / 3.3.8 / 3.4.10 / 3.5.0-preview1 / 4.0.7 matrix — every released Ruby in the range |
17
20
  | kruby | `~> 1.36.0` | The official Kubernetes OpenAPI client |
18
- | Kubernetes server | **Verified on v1.33.x** (a real cluster, microk8s v1.33.13, 2026-09-21) | kruby 1.36.x is a 1.36-series client. Newer servers (1.36, etc.) use the same API (CustomObjects API v1), so compatibility is expected, but has not yet been verified against a real cluster |
21
+ | Kubernetes server | **Verified on v1.33.x** (real cluster, microk8s v1.33.13, 2026-09-21) | The transport speaks the generic Kubernetes REST API, so newer servers (1.36, etc.) are expected to work but are not yet verified on a real cluster |
19
22
  | Dependencies | **kruby only** at runtime | ActiveSupport is used only for optional [instrumentation](#instrumentation-optional-activesupport) (no-op when absent) |
20
23
 
21
24
  ## Installation
22
25
 
23
26
  ```ruby
24
27
  # Gemfile
25
- gem "k8s-rails", "~> 0.2"
28
+ gem "k8s-rails", "~> 0.3"
26
29
  ```
27
30
 
28
31
  ```ruby
29
32
  require "k8s-rails"
30
- K8sRails::VERSION # => "0.2.0"
33
+ K8sRails::VERSION # => "0.3.0"
31
34
  ```
32
35
 
33
36
  `require` is side-effect-free and needs no cluster. kruby itself is loaded
@@ -37,30 +40,96 @@ reachable.
37
40
 
38
41
  ## Quick start
39
42
 
43
+ `k8s-rails` reaches **every resource the Kubernetes API exposes** — core v1
44
+ built-ins (Pod, Service, ConfigMap, ...), named-group built-ins (Deployment,
45
+ Ingress, ...), and CRDs — through one declaration API. The transport speaks
46
+ the generic REST API: a core v1 resource is declared with an **empty group**
47
+ (`group: ""` → `/api/v1/...`), any other resource with its API group
48
+ (`/apis/{group}/{version}/...`).
49
+
50
+ ### 1. Configure (once, in an initializer)
51
+
40
52
  ```ruby
41
53
  # config/initializers/k8s-rails.rb
42
54
  K8sRails.configure do |config|
43
55
  config.namespace = "team-a" # default namespace (can be overridden per declaration or per call)
44
56
  end
57
+ ```
58
+
59
+ ### 2. Declare the resource you work with
60
+
61
+ Coordinates are declared explicitly — group / version / plural / kind are
62
+ **never guessed**. A core v1 resource (Pods) uses an empty group:
63
+
64
+ ```ruby
65
+ Pod = K8sRails.crd(
66
+ group: "", # core v1 (the "/api/v1" API group)
67
+ version: "v1",
68
+ plural: "pods",
69
+ kind: "Pod",
70
+ readonly: false, # default true; only an explicit false enables create/patch/delete
71
+ )
72
+ ```
73
+
74
+ A named-group built-in (Deployments) is identical except for the group:
75
+
76
+ ```ruby
77
+ Deployment = K8sRails.crd(
78
+ group: "apps", # "/apis/apps/v1"
79
+ version: "v1",
80
+ plural: "deployments",
81
+ kind: "Deployment",
82
+ readonly: false, # enables create/patch/delete in the example below
83
+ )
84
+ ```
85
+
86
+ ### 3. Read and write (CRUD)
45
87
 
46
- # Declare the CRD you work with (group / version / plural / kind are never guessed)
88
+ All return values are string-keyed hashes:
89
+
90
+ ```ruby
91
+ Pod.list # => [{"metadata" => {"name" => "..."}, "spec" => {...}}, ...]
92
+ Pod.find("api-7d9f8b6c5-xk2lt") # the full object; raises K8sRails::NotFound when absent
93
+ Pod.find_or_nil("missing") # same, but returns nil instead of raising
94
+
95
+ # readonly: false only
96
+ Pod.create({
97
+ apiVersion: "v1",
98
+ kind: "Pod",
99
+ metadata: { name: "busybox" },
100
+ spec: { containers: [{ name: "busybox", image: "busybox" }] }
101
+ })
102
+ Pod.patch("busybox", [{ op: "replace", path: "/spec/containers/0/image", value: "busybox:1.36" }])
103
+ Pod.delete("busybox") # => {"kind" => "Status", "status" => "Success", ...}
104
+
105
+ # Deployment (named group) is used exactly the same way
106
+ Deployment.list # all Deployments in the default namespace
107
+ Deployment.find("web")
108
+ Deployment.delete("web") # readonly: false only
109
+
110
+ K8sRails.connected? # true on success; raises K8sRails::Unavailable / ApiError on failure
111
+ ```
112
+
113
+ ### Example: Argo Workflows (CRD)
114
+
115
+ The same declaration and operations work for any CRD — here, the Argo
116
+ Workflows `Workflow` CRD:
117
+
118
+ ```ruby
47
119
  Workflow = K8sRails.crd(
48
120
  group: "argoproj.io",
49
121
  version: "v1alpha1",
50
122
  plural: "workflows",
51
123
  kind: "Workflow",
52
- readonly: false, # default true; false enables create/patch
124
+ readonly: false, # default true; false enables create/patch/delete
53
125
  )
54
- ```
55
126
 
56
- ```ruby
57
- Workflow.list # => [{"name" => "...", "labels" => {...}}, ...]
127
+ Workflow.list # => [{"metadata" => {"name" => "...", ...}}, ...]
58
128
  Workflow.find("wf-1") # same shape / raises K8sRails::NotFound when absent
59
129
  Workflow.find_or_nil("wf-1") # same, but returns nil instead of raising
60
130
  Workflow.create({ metadata: { name: "wf-1" } }) # readonly: false only
61
131
  Workflow.patch("wf-1", [{ op: "replace", path: "/spec/a", value: 2 }]) # readonly: false only
62
-
63
- K8sRails.connected? # true on success; raises on failure
132
+ Workflow.delete("wf-1") # readonly: false only
64
133
  ```
65
134
 
66
135
  ## Configuration
@@ -90,28 +159,34 @@ end
90
159
  in-cluster. Note in-cluster is tried **last**, after the file-based
91
160
  sources — re-verify `kruby`'s loader when upgrading kruby).
92
161
 
93
- ## CRD declaration and access
162
+ ## Resource declaration and access
94
163
 
95
- `plural` / `kind` are **never guessed** (many CRDs do not follow the obvious
96
- naming convention). Re-declaring the same kind raises
164
+ The same `K8sRails.crd` API covers core v1 built-ins, named-group built-ins,
165
+ and CRDs. `plural` / `kind` are **never guessed** (many resources do not
166
+ follow the obvious naming convention). Re-declaring the same kind raises
97
167
  `K8sRails::RedeclarationError` (configuration-mistake detection).
98
168
 
169
+ - **core v1** resources (Pod, Service, ConfigMap, ...): `group: ""`
170
+ - **named-group** resources (Deployment = `apps`, Ingress = `networking.k8s.io`,
171
+ any CRD group): the resource's API group
172
+ - **cluster-scoped** resources (Node, ClusterIssuer, ...): `scope: :cluster`
173
+ and no `namespace:` (see [Cluster-scoped resources](#cluster-scoped-resources))
174
+
99
175
  Return values are **always string-keyed hashes**. kruby returns symbol keys,
100
176
  but Rails-side JSON/views work with string keys, so the gem normalizes
101
177
  internally in pure Ruby (no ActiveSupport dependency). Every namespaced
102
178
  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)).
179
+ declaration (the `*_cluster` methods take no `namespace:`).
105
180
 
106
181
  `readonly` must be an **explicit boolean** (`nil` or other values raise
107
- `ArgumentError`). Writes are enabled **only** by `readonly: false`, so a
108
- missing flag can never fail open.
182
+ `ArgumentError`). Writes (`create` / `patch` / `delete`) are enabled **only**
183
+ by `readonly: false`, so a missing flag can never fail open.
109
184
 
110
- ### Cluster-scoped CRDs
185
+ ### Cluster-scoped resources
111
186
 
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
187
+ Both namespaced and cluster-scoped resources are supported. Declare a
188
+ cluster-scoped resource (Node, ClusterIssuer, ClusterWorkflowTemplate, ...)
189
+ with `scope: :cluster` and **without** `namespace:` (combining the two raises
115
190
  `ArgumentError`):
116
191
 
117
192
  ```ruby
@@ -128,13 +203,22 @@ ClusterIssuer.find_cluster("letsencrypt")
128
203
  ClusterIssuer.find_or_nil_cluster("x")
129
204
  ClusterIssuer.create_cluster({ ... }) # readonly: false only
130
205
  ClusterIssuer.patch_cluster("letsencrypt", [{ ... }]) # readonly: false only
206
+ ClusterIssuer.delete_cluster("letsencrypt") # readonly: false only
207
+ ```
208
+
209
+ Core v1 has cluster-scoped resources too (Node, Namespace):
210
+
211
+ ```ruby
212
+ Node = K8sRails.crd(group: "", version: "v1", plural: "nodes", kind: "Node", scope: :cluster)
213
+ Node.list_cluster # all nodes
214
+ Node.find_cluster("n1")
131
215
  ```
132
216
 
133
217
  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).
218
+ raise `ArgumentError` — the resource is namespaced, so the cluster endpoints
219
+ would 404 anyway. For a cluster-scoped declaration, use the `*_cluster`
220
+ methods (the plain `list`/`find`/... would call the namespaced endpoints and
221
+ 404, so they raise `ArgumentError` there as well).
138
222
 
139
223
  ## Connectivity check
140
224
 
@@ -159,8 +243,8 @@ K8sRails::Error < StandardError
159
243
  ├── K8sRails::Unavailable # transport-layer failure (DNS/timeout/connection refused; kruby 1.36.x reports it as ApiError code 0)
160
244
  ├── K8sRails::NotFound # HTTP 404
161
245
  ├── K8sRails::ApiError # other API errors (401/403/409/422/5xx); holds #code and #response
162
- ├── K8sRails::ReadOnlyError # create/patch called on a readonly: true declaration
163
- └── K8sRails::RedeclarationError # re-declaration of an already-declared CRD kind
246
+ ├── K8sRails::ReadOnlyError # create/patch/delete called on a readonly: true declaration
247
+ └── K8sRails::RedeclarationError # re-declaration of an already-declared kind
164
248
  ```
165
249
 
166
250
  Recommended caller pattern:
@@ -198,27 +282,31 @@ end
198
282
  ## Testing
199
283
 
200
284
  The test suite needs **no cluster**. Tests inject a transport stub via
201
- `config.api_client`. The stub is wrapped internally by an adapter, so it just
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:
285
+ `config.api_client`. The transport speaks kruby's `Kubernetes::ApiClient#call_api`
286
+ protocol, so a stub just implements that one method `call_api(http_method,
287
+ path, opts)` returning `[data, status_code, headers]`. `http_method` is one of
288
+ `:GET` / `:POST` / `:PATCH` / `:DELETE`, and `path` is the fully-built API
289
+ path (the gem builds it from the declaration — core v1 as `/api/v1/...`,
290
+ everything else as `/apis/{group}/{version}/...`):
205
291
 
206
292
  ```ruby
207
293
  class StubTransport
208
- def list_namespaced_custom_object(group, version, namespace, plural) = { items: [] }
209
- def get_namespaced_custom_object(group, version, namespace, plural, name) = {}
210
- def create_namespaced_custom_object(group, version, namespace, plural, body) = {}
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) = {}
294
+ def call_api(method, path, opts = {})
295
+ case method
296
+ when :GET then { items: [] }, 200, {} # ... or a single object
297
+ when :POST then { metadata: {} }, 200, {}
298
+ when :PATCH then { metadata: {} }, 200, {}
299
+ when :DELETE then { kind: "Status", status: "Success" }, 200, {}
300
+ end
301
+ end
217
302
  end
218
303
 
219
304
  K8sRails.configure { |c| c.api_client = StubTransport.new }
220
305
  ```
221
306
 
307
+ A realistic stub routes on `path` (and `opts[:body]` for writes). Responses
308
+ may use symbol keys — the gem stringifies them.
309
+
222
310
  With a transport injected, `K8sRails.connected?` returns `true` without
223
311
  network I/O (see [Connectivity check](#connectivity-check)).
224
312
 
@@ -244,10 +332,10 @@ through k8s-rails are unaffected (it does not overwrite an already-set
244
332
  kruby's own configuration behavior (outside this gem), check this token-key
245
333
  issue first.
246
334
 
247
- ## Roadmap (v0.3+)
335
+ ## Roadmap (v0.4+)
248
336
 
249
- - Ruby 3.5 / 4.0 support (after verifying against the stable releases, then
250
- widening the declared range and the CI matrix)
337
+ - Update the CI matrix when Ruby 3.5 reaches a stable release (3.5 is
338
+ verified today on 3.5.0-preview1, the only released 3.5)
251
339
  - CI-based E2E tests using kind
252
340
  - watch (streaming) support, under consideration
253
341
 
data/docs/design.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # k8s-rails 設計書
2
2
 
3
3
  - 文書番号: KBR-DESIGN-001
4
- - 版: 0.1.12(案)
4
+ - 版: 0.1.15(案)
5
5
  - 日付: 2026-09-15
6
6
  - 対象リポジトリ: k8s-rails(本設計の実装先)
7
7
  - ライセンス: MIT(LICENSE は main に既存)
@@ -40,8 +40,8 @@ K8s CRD を扱えるようにし、(b) 実際の consumer アプリをこの gem
40
40
 
41
41
  ### 2.2 非スコープ(本設計の外)
42
42
 
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 保証範囲から外す(§10 で v0.3 対象)
43
+ - **core v1 リソースの CustomObjects 経路でのアクセス** — 2026-09-22 に**解消済み**(unified REST transport 導入、§5.2 改訂)。以前は core v1(Pod / Service / ConfigMap / Node 等)が `/api/v1/...`(group 区間なし)のため `CustomObjectsApi` では扱えず、`group: ""` 宣言は 404 になった。**named-group の built-in(apps/v1 Deployment 等)は CustomObjects 経路でも動作していた**(generic な `/apis/{group}/...` クライアントのため)。現在の transport は `Kubernetes::ApiClient#call_api` 上の統一 REST 層で、core v1(`group: ""` `/api/v1/...`)・named built-in・CRD 1 つの経路で扱う
44
+ - **watch(ストリーム)** — v0.1 では非対応。kruby の watch は get/list より成熟度が低く、初版の API 保証範囲から外す(§10 で v0.4 対象)
45
45
  - アプリの**デプロイ**(helm / kustomize 生成等) — `kuby-core` の領域
46
46
  - RBAC 権限の付与・管理 — 呼び出しアプリ側の ClusterRole/Role の責務
47
47
  - 複数クラスタ同時接続 — v0.1 は単一クラスタ前提(§11 展望)
@@ -106,7 +106,7 @@ k8s-rails/
106
106
  ├── lib/
107
107
  │ ├── k8s-rails.rb # エントリ。require 集 + モジュール定義
108
108
  │ └── k8s_rails/
109
- │ ├── version.rb # VERSION = "0.2.0"
109
+ │ ├── version.rb # VERSION = "0.3.0"
110
110
  │ ├── configuration.rb # Config: namespace, connection, 計測 ON/OFF
111
111
  │ ├── client.rb # 接続解決・BearerToken 橋渡し・計測ラップ
112
112
  │ ├── crd.rb # K8sRails.crd 宣言 → Resource 生成
@@ -143,7 +143,7 @@ end
143
143
  |---|---|---|
144
144
  | `namespace` | `"default"` | CRD 宣言が namespace 未指定時のデフォルト |
145
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) |
146
+ | `api_client` | `nil` | **テスト専用**: kruby の `Kubernetes::ApiClient#call_api` と同型の 1 メソッド `call_api(http_method, path, opts)`(戻り値 `[data, status_code, headers]`。`http_method` `:GET` / `:POST` / `:PATCH` / `:DELETE`、`path` は宣言の座標から構築された REST パス)を実装する素のオブジェクト。指定時は `Client.build` が接続解決をスキープして `StringKeyedAdapter` で包んで使う(§5.2・§9) |
147
147
  | `instrumentation` | `true` | `ActiveSupport::Notifications` で計測する(§8) |
148
148
 
149
149
  - 設定は `K8sRails.configure` で**一度だけ**。再実行は警告(`Warning`)+ 無視。
@@ -159,7 +159,7 @@ end
159
159
  ### 5.2 接続
160
160
 
161
161
  ```ruby
162
- K8sRails::Client.build # → Kubernetes::CustomObjectsApi(lazy。初回呼び出し時に接続)
162
+ K8sRails::Client.build # → 統一 REST transportkruby ApiClient を包む。lazy。初回呼び出し時に接続)
163
163
  K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailable / ApiError を raise
164
164
  # (false を返す経路なし)。/version 相当の軽量確認
165
165
  # (kruby 1.36.x の VersionApi#get_code(GET /version/)1 回)
@@ -167,33 +167,55 @@ K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailab
167
167
  # 注入が接続面そのものだから。Resource 操作と整合させる)
168
168
  ```
169
169
 
170
- `Client.build` が内部で行うこと(consumer アプリの K8s サービスの custom objects 生成部を移設):
170
+ `Client.build` が内部で行うこと(2026-09-22 unified REST transport に改訂
171
+ 旧実装は `CustomObjectsApi` 経由で CRD / named-group のみ、core v1 は 404):
171
172
 
172
173
  0. `config.api_client` があれば(テスト注入、§5.1)それを直接返し、以降の接続解決をスキップ
173
174
  1. `config.connection` があればそれ、なければ `Kubernetes::Configuration.default_config`
174
175
  2. **K1 橋渡し**: `api_key['authorization']` が `api_key['BearerToken']` に書かれていなければ複製
175
- 3. `Kubernetes::ApiClient` `Kubernetes::CustomObjectsApi` を生成し、**文字列キー化**(K2)を API レスポンス後に行う。ActiveSupport 非依存の gem 内部の純 Ruby 再帰変換(`K8sRails::Normalizer`)を使う(v0.1.1 以降: 常に Normalizer。`deep_stringify_keys` 経路は廃止)
176
+ 3. `Kubernetes::ApiClient` を生成し、`StringKeyedAdapter` で包む。**パス構築**: 宣言の座標から REST パスを構築して `call_api` で実行 core v1(`group == ""`)は `/api/{version}[/namespaces/{ns}]/{plural}[/{name}]`、named(CRD・named built-in)は `/apis/{group}/{version}[/namespaces/{ns}]/{plural}[/{name}]`。`return_type: "Object"` で kruby が JSON をパース(symbol キー)したオブジェクトを返し、**文字列キー化**(K2)を API レスポンス後に行う(`K8sRails::Normalizer`、ActiveSupport 非依存)
176
177
  4. **接続レベルの失敗**(DNS 失敗 / タイムアウト / 接続拒否等)は `K8sRails::Unavailable` に変換して `raise`(リトライはしない)。**kruby 1.36.x ではこれらの転送失敗は HTTP ステータスが無いため `Kubernetes::ApiError`(`code == 0`)として surfacing する**(§5.4 の変換表参照)。認可失敗(401/403)は §5.4 により `K8sRails::ApiError`
177
178
 
179
+ **対応リソース**: kruby が到達できる全リソースを 1 つの経路で扱う —
180
+ - core v1 built-in(Pod / Service / ConfigMap / Node 等): `group: ""`
181
+ - named-group built-in(Deployment = `apps` / Ingress = `networking.k8s.io` 等): 各 API group(**旧実装の `CustomObjectsApi` 経路でもこれらは `/apis/{group}/...` generic クライアントとして動作していた**)
182
+ - CRD: 各 CRD の group
183
+
178
184
  `connected?` の解決順序:
179
185
 
180
186
  0. `config.api_client` があれば(テスト注入、§5.1)**I/O なしで `true` を返す**。注入されたトランスポートが接続面そのもののため、実エンドポイントへのプローブは同一注入下の Resource 操作と矛盾する(#18 対応)。クラスタ到達性の真の確認が必要な場合は注入を解除した環境で行う
181
187
  1. 以下 `Client.build` と同一(`config.connection` → `default_config` → K1 橋渡し → VersionApi プローブ)
182
188
 
183
- ### 5.3 CRD 宣言
189
+ ### 5.3 リソース宣言(CRD / built-in 共通)
190
+
191
+ `K8sRails.crd` は core v1 built-in・named built-in・CRD の全てに同一の API(2026-09-22 の unified REST transport 導入で core v1 が有効化):
184
192
 
185
193
  ```ruby
186
194
  # config/initializers/k8s-rails_crd.rb(またはアプリケーションクラス内)
195
+ # core v1 built-in(group が空文字)
196
+ Pod = K8sRails.crd(
197
+ group: "", # core v1(/api/v1 経路)
198
+ version: "v1",
199
+ plural: "pods",
200
+ kind: "Pod",
201
+ namespace: K8sRails.config.namespace, # 省略可
202
+ readonly: false, # 既定 true。false で create/patch/delete 有効化(K4)
203
+ )
204
+
205
+ # named-group built-in(apps/v1 の Deployment 等)も同形
206
+ Deployment = K8sRails.crd(group: "apps", version: "v1", plural: "deployments", kind: "Deployment")
207
+
208
+ # CRD(従来の形・不変)
187
209
  Workflow = K8sRails.crd(
188
210
  group: "argoproj.io",
189
211
  version: "v1alpha1",
190
212
  plural: "workflows",
191
213
  kind: "Workflow",
192
214
  namespace: K8sRails.config.namespace, # 省略可
193
- readonly: false, # 既定 true。false で create/patch 有効化(K4)
215
+ readonly: false, # 既定 true。false で create/patch/delete 有効化(K4)
194
216
  )
195
217
 
196
- # cluster-scoped CRD(ClusterIssuer / ClusterWorkflowTemplate 等)は
218
+ # cluster-scoped(ClusterIssuer / Node / ClusterWorkflowTemplate 等)は
197
219
  # scope: :cluster を付け、namespace: は省略する(併用は ArgumentError)
198
220
  ClusterIssuer = K8sRails.crd(
199
221
  group: "cert-manager.io",
@@ -210,19 +232,21 @@ ClusterIssuer = K8sRails.crd(
210
232
  |---|---|---|---|
211
233
  | `list` | `{}` | `[{"name" => "...", "labels" => {}, "spec" => {}, "status" => {}}, ...]`(**文字列キー**) | 常に有効 |
212
234
  | `find(name)` | 必須 | 同型 or `K8sRails::NotFound`(raise) | 常に有効 |
213
- | `create(attributes)` | CRD body hash | 作成済みオブジェクト(文字列キー) | `readonly: false` のみ |
235
+ | `create(attributes)` | body hash | 作成済みオブジェクト(文字列キー) | `readonly: false` のみ |
214
236
  | `patch(name, operations)` | JSON Patch 操作配列 | 更新済みオブジェクト | `readonly: false` のみ |
237
+ | `delete(name)` | 必須 | API の Status オブジェクト(文字列キー。`{"kind" => "Status", "status" => "Success", ...}`) | `readonly: false` のみ |
215
238
  | `list_cluster` | `{}` | 同上(クラスタ横断。`namespace:` なし) | 常に有効 |
216
239
  | `find_cluster(name)` | 必須 | 同上 or `K8sRails::NotFound` | 常に有効 |
217
- | `create_cluster(attributes)` | CRD body hash | 作成済みオブジェクト | `readonly: false` のみ |
240
+ | `create_cluster(attributes)` | body hash | 作成済みオブジェクト | `readonly: false` のみ |
218
241
  | `patch_cluster(name, operations)` | JSON Patch 操作配列 | 更新済みオブジェクト | `readonly: false` のみ |
242
+ | `delete_cluster(name)` | 必須 | API の Status オブジェクト(文字列キー) | `readonly: false` のみ |
219
243
 
220
244
  スコープの契約(#17 対応):
221
245
 
222
246
  - `scope: :namespaced`(既定)の宣言では `*_cluster` メソッドは `ArgumentError`(CRD が namespaced のためクラスタ endpoint を呼んでも 404 になるだけ)。
223
247
  - `scope: :cluster` の宣言では素の `list` / `find` / `create` / `patch` は `ArgumentError`(namespaced endpoint を呼ぶと 404 になるだけ。`namespace:` 引数は意味を持たない)。
224
248
  - 宣言時に `scope: :cluster` と `namespace:` を併用した場合は `ArgumentError`(設定ミスの fail fast)。
225
- - transport は kruby の `*_cluster_custom_object` 4 メソッド(`namespace` 非持参の endpoint)を使う。テスト注入スタブは 2 セット 8 メソッドを実装する(§9)。
249
+ - transport は kruby の `ApiClient#call_api` 上の統一 REST 層で、namespaced / cluster の区別はパス構築(`/namespaces/{ns}` の有無)のみ。テスト注入スタブは `call_api` 1 メソッドを実装する(§9)。
226
250
 
227
251
  - **戻り値は常に文字列キーの Hash**(K2 の規約を API 契約として固定)。
228
252
  `find` は存在しない場合 `K8sRails::NotFound` を raise(consumer アプリ側が `return nil` にしていたのは
@@ -239,7 +263,7 @@ K8sRails::Error < StandardError
239
263
  ├── K8sRails::Unavailable # 接続不能・タイムアウト・DNS 失敗等(クラスタ起因)
240
264
  ├── K8sRails::NotFound # リソース不在(HTTP 404)
241
265
  ├── K8sRails::ApiError # その他の API エラー(401/403/409/422 等)。#code と #response を保持
242
- ├── K8sRails::ReadOnlyError # readonly: true 宣言で create/patch が呼ばれた(設定ミス)
266
+ ├── K8sRails::ReadOnlyError # readonly: true 宣言で create/patch/delete が呼ばれた(設定ミス)
243
267
  └── K8sRails::RedeclarationError # 同名 CRD の再宣言(設定ミス)
244
268
  ```
245
269
 
@@ -251,7 +275,7 @@ K8sRails::Error < StandardError
251
275
  | その他の `StandardError`(プログラミング/設定ミス、例: `NoMethodError`) | そのまま伝播(変換せず隠さない) |
252
276
  | `Kubernetes::ApiError` code 404 | `NotFound` |
253
277
  | `Kubernetes::ApiError` その他 | `ApiError`(code / response body を保持) |
254
- | 宣言時に `readonly: true` で create/patch を呼ばれた | `K8sRails::ReadOnlyError`(**設定ミス**なので raise せずには済ませない) |
278
+ | 宣言時に `readonly: true` で create/patch/delete を呼ばれた | `K8sRails::ReadOnlyError`(**設定ミス**なので raise せずには済ませない) |
255
279
 
256
280
  呼び出しアプリ(Rails)側の推奨パターン:
257
281
 
@@ -269,8 +293,8 @@ end
269
293
 
270
294
  | 依存 | 制約 | 理由 |
271
295
  |---|---|---|
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 stablev4.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` 探索順序)を済ませてから明示的に上げ替える |
296
+ | Ruby | `>= 3.3, < 4.1` | 下限: kruby 1.36.x が `required_ruby_version ">= 3.3"` を宣言(RubyGems API で実測 2026-09-21、1.36.0.1〜1.36.4.1 全バージョン)。上限: 「宣言した minor を必ず CI で検証する」方針 2026-09-22 Ruby 4.0.7(stable)と Ruby 3.5.0-preview13.5 系で公開済みの唯一のリリース)で全 suite / rubocop を実行し緑を確認(matrix に 4.0.7 / 3.5.0-preview1 追加・v0.2.1)。この範囲で公開済みの Ruby 3.3.x / 3.4.x / 3.5.0-preview1 / 4.0.x のみ(ruby/ruby タグ実測・4.1 系リリースなし)のため宣言範囲 = 検証範囲。RubyGems は version requirement で集合和を表現できないため未検証 minor を区間で挟み込む形(`< 5.0` 等)は取らない。3.5 stable / 4.1 系がリリースされたら matrix を追加して検証(§10 / §13) |
297
+ | `kruby` | `~> 1.36.0` | consumer アプリと同一 pin。`~> 1.36.0` は 1.36.x のみ許可(`~> 1.36` 形式は 1.37 以降も許容してしまうため使用しない)。新しめの kruby に対応する場合は §7 の確認事項(client.rb `call_api` 依存箇所・K1 橋渡し・`default_config` 探索順序)を済ませてから明示的に上げ替える |
274
298
  | `activesupport` | **任意**(`>= 7.0`) | `defined?(ActiveSupport::Notifications)` でガード(計測のみ、§8)。Rails 無し環境(Cron スクリプト等)でも動作する必要がある — レスポンスの文字列キー化(K2)はこれに依存せず、gem 内部の純 Ruby 変換で担う(§5.2) |
275
299
  | `rspec` / `rubocop` | 開発依存 | spec / lint |
276
300
 
@@ -281,13 +305,14 @@ end
281
305
 
282
306
  - `lib/k8s_rails/client.rb` **のみ**が `require "kubernetes"` してよい。
283
307
  他のファイルは kruby 定数・クラスを参照しない。
284
- - kruby の `CustomObjectsApi` メソッド呼び出しは `client.rb` 内の
285
- `*_namespaced_custom_object` 4 メソッド(`get_namespaced_custom_object` 等)と
286
- `*_cluster_custom_object` 4 メソッド(`get_cluster_custom_object` 等)に集約する。
308
+ - kruby の API 呼び出しは `client.rb` 内の `Kubernetes::ApiClient#call_api`
309
+ 1 メソッドに集約する(unified REST transport。パスは `build_path` が宣言の
310
+ 座標から構築 core v1 は `/api/{version}...`、named は `/apis/{group}/{version}...`)。
287
311
  `resource.rb` は
288
312
  `K8sRails.client.get(group, version, ns, plural, name)` のような **gem 内部 API** だけを使う。
289
- - kruby 上げ替え時の作業は (1) client.rb 8 メソッド(namespaced 4 + cluster 4)のシグネチャ確認、
290
- (2) K1 橋渡しの要否確認、(3) **`Kubernetes::Configuration.default_config` の探索順序確認**
313
+ - kruby 上げ替え時の作業は (1) `ApiClient#call_api` のシグネチャ・`opts` 形式(header_params /
314
+ query_params / body / auth_names / return_type)の確認、(2) K1 橋渡しの要否確認、
315
+ (3) **`Kubernetes::Configuration.default_config` の探索順序確認**
291
316
  (kruby 1.36.x の loader 実装順は **KUBECONFIG → `~/.kube/config` → in-cluster** で in-cluster が
292
317
  最後。README / 設計書 / 設定コメントがこの順序を明記しているため、loader が変わった場合は
293
318
  全箇所を同期する — #19 対応)に収まることをテスト(§9)で担保する。
@@ -298,7 +323,7 @@ end
298
323
 
299
324
  ```
300
325
  k8s-rails.request payload: { operation: :list, group:, version:, plural:, namespace:
301
- # operation は symbol(:list / :find / :create / :patch)
326
+ # operation は symbol(:list / :find / :create / :patch / :delete
302
327
  duration_ms: # float(ミリ秒・小数点 2 桁)
303
328
  status: "ok" | "unavailable" | "api_error" }
304
329
  ```
@@ -316,11 +341,11 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
316
341
 
317
342
  | レイヤー | 手法 | 対象 |
318
343
  |---|---|---|
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 検証) |
344
+ | ユニット | `K8sRails.config.api_client` に**スタブ**(kruby `ApiClient#call_api` と同型の 1 メソッド `call_api(http_method, path, opts)` を実装する素のオブジェクト。`http_method` は `:GET` / `:POST` / `:PATCH` / `:DELETE`、`path` は宣言座標から構築された REST パス(core v1 `/api/v1/...` / named `/apis/{group}/{version}/...`)、戻り値 `[data, status_code, headers]`。`StringKeyedAdapter` がこの形式を呼ぶ)を注入 | client(橋渡し・例外変換・パス構築)、resource(整形・readonly 制限・スコープ制限)、crd(メソッド生成・scope 検証) |
320
345
  | 設定 | spec 間で `K8sRails.reset!` | 宣言の破棄・再接続 |
321
346
  | 集積(任意) | GitHub Actions で **kind**(または既存 microk8s に接続するジョブ)で実クラスタ E2E | v0.1 の必須ではない。**推奨**: consumer アプリ移行時の検証を兼ねる |
322
347
 
323
- - 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.3
348
+ - 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.4
324
349
  GitHub Actions の追加として扱う(実クラスタへの接続 CI はネットワーク依存のため採用しない)。
325
350
 
326
351
  ## 10. リリース計画
@@ -329,8 +354,9 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
329
354
  |---|---|---|
330
355
  | **v0.1** | §5 の公開 API(CRD 宣言 / list / find / create / patch / 例外 / 計測 / スタブテスト)+ README | rspec 全緑 + **consumer アプリの K8s サービスを `k8s-rails` に移行して動作確認**(§12) |
331
356
  | **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 集合)、リトライポリシー | |
357
+ | **v0.3** | unified REST transport(`ApiClient#call_api` 上の統一 REST 層)で**対応リソースを全リソース(core v1 built-in 含)に拡大**+`delete` / `delete_cluster` 追加(CRUD 完成)+README QuickStart 改稿(Pod / Deployment CRUD を主例)。設計書 v0.1.15(案) | rspec 全緑(クラスタ不要)+実クラスタ E2E(Pod / Deployment / Node 読み取り・ConfigMap create/patch/delete・readonly ゲート)+公開 gem push(v0.1.0 と同導線) |
358
+ | v0.4 | watch(`watch` メソッド、kruby watch サポート上)、kind E2E の CI 化、Ruby 3.5 stable / 4.1 系の matrix 追加(リリースされたら検証の上追加。Ruby 3.5 preview / 4.0 は v0.2.1 で対応済み)、リトライポリシー | v0.3 運用のフィードバック |
359
+ | v0.5 | (展望)複数クラスタ(ネームスペース化された client 集合) | — |
334
360
 
335
361
  v0.1 の milestone 分割(開発セッション向けのタスク単位目安):
336
362
 
@@ -397,15 +423,20 @@ PR の差分を最小化)。
397
423
  公開した gem のバージョンがどのコミットに基づくかを追跡できる。
398
424
  tag 名と gemspec の `VERSION` は一致させる
399
425
  - テスト CI(`.github/workflows/test.yml`)は push / PR 時に rspec + rubocop を実行。
400
- gemspec の宣言範囲(`>= 3.3, < 4.0`)を matrix で検証:
426
+ gemspec の宣言範囲(`>= 3.3, < 4.1`)で公開済みの Ruby を matrix で検証:
401
427
  3.3.0(下限・kruby 1.36.x の `>= 3.3`)・3.3.8(開発)・
402
- 3.4.10(3.x 系の最新 stable・2026-09-21 時点。3.5 は preview、
403
- 4.0 は宣言範囲外のため未検証・v0.3 で検討)。
404
- **宣言範囲を常に matrix がカバーする**こと(`< 4.0` 上限により、
405
- 4.x のリリースは宣言範囲外。stable 化された新 3.x minor が出たら
428
+ 3.4.10(3.4 系の最新 stable)・3.5.0-preview1(3.5 系の唯一の公開
429
+ リリース・2026-09-22 にローカルで全 suite / rubocop 検証済み)・
430
+ 4.0.7(4.0 系の最新 stable・2026-09-22 にローカルで全 suite /
431
+ rubocop 検証済み)。
432
+ **宣言範囲を常に matrix がカバーする**こと(`< 4.1` 上限により
433
+ 4.1 系以降のリリースは宣言範囲外。RubyGems は version requirement で
434
+ 集合和を表現できないため、未検証 minor を区間で挟み込む形
435
+ (`< 5.0` 等)は取らない — matrix 追加と範囲拡大をセットで行う。
436
+ 3.5 stable / 4.1 系等 stable 化された新リリースが出たら
406
437
  matrix への追加を忘れないこと)。
407
438
  テストはクラスタ不要(§9・スタブ注入)のため v0.1 は runner 上のユニットのみ。
408
- kind / 実クラスタ E2E の CI 化は v0.3 対象(§9・§10)
439
+ kind / 実クラスタ E2E の CI 化は v0.4 対象(§9・§10)
409
440
  - `README` に「kruby pin」「対応 k8s バージョン(実測 v1.33.x で検証済み)」「K1 橋渡しの背景」
410
441
  を明記する(検索でヒットする重要な注意点のため)
411
442
 
@@ -426,3 +457,6 @@ PR の差分を最小化)。
426
457
  | 0.1.10 | 2026-09-21 | PR #12 レビュー対応(Copilot): §13 の公開手順で tag の **remote への push**(`git push origin v<VERSION>`)が欠落しており、GitHub 上のリリースコミットとの対応付け(追溯性)が確保できないとの指摘を反映 | 実装反映済み |
427
458
  | 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
459
  | 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) | 実装反映済み |
460
+ | 0.1.13 | 2026-09-22 | v0.2.1 リリース準備 — Ruby 対応範囲の拡大・検証: ①Ruby 4.0.7(stable・v4.0.7)上で全 rspec / rubocop を実行し動作確認(kruby 1.36.4.1 の install / 動作を含む)②宣言範囲を **`>= 3.3, < 4.1`** に拡大(gemspec / §6 / README / test.yml)。上限は「宣言した minor を必ず CI で検証する」方針で検証済みの 4.0 系に設定(`< 5.0` 等未検証 minor を区間で挟み込む形は RubyGems が集合和を表現できないため不採用・§13 に明記)③RubyGems は `3.5.0-preview1` を範囲内に満たすため 3.5 も実際に対応・検証(3.5.0-preview1 で全 suite / rubocop 緑確認)。matrix は 3.3.0 / 3.3.8 / 3.4.10 / 3.5.0-preview1 / 4.0.7 の 5 系統で範囲内公開済み Ruby を全カバー(§13) | 実装反映済み |
461
+ | 0.1.14 | 2026-09-22 | **対応リソースの拡大: unified REST transport 導入で core v1 built-in(Pod / Service / ConfigMap / Node 等)に対応**(旧 `CustomObjectsApi` 経路では `group: ""` が 404 だった唯一のギャップ。named built-in は従来から動作)。①transport を `Kubernetes::ApiClient#call_api` 上の統一 REST 層に改訂(`client.rb`。パス構築: core `/api/v1/...` / named `/apis/{group}/{version}/...`・CGI escape・json-patch Content-Type 維持)②`delete` / `delete_cluster` を追加(CRUD 完成。readonly ゲート同型。Status オブジェクトを文字列キーで返す)③テスト注入スタブの契約を `call_api` の 1 メソッドに統一(§5.1 表 / configuration.rb コメント / §9)④実クラスタ(v1.33.13)で Pod list / Deployment find / ConfigMap create・patch・delete / Node list_cluster を E2E 検証 | 実装反映済み |
462
+ | 0.1.15 | 2026-09-22 | v0.3.0 リリース準備: ①`VERSION` を 0.2.1 → **0.3.0**(version.rb + version spec)②CHANGELOG の `Unreleased` を **0.3.0** に確定③README の install pin(`~> 0.2` → `~> 0.3`)・`VERSION` 例・Roadmap 見出し(v0.3+ → v0.4+)を同期④§10 リリース計画表で v0.3 行を「実際にリリースした内容」に書き換え(unified REST transport / delete / README 改稿)、旧 v0.3 予定内容(watch / kind E2E / matrix 追加)を v0.4 へ、リトライポリシーを v0.4 へ、複数クラスタ展望を v0.5 へ移動⑤§2.2 / §9 / §13 の旧「v0.3 対象」参照を v0.4 に修正 | 実装反映済み |
@@ -4,25 +4,35 @@
4
4
  # 他のファイルは kruby の定数・クラスを参照しない。
5
5
  # Typhoeus は kruby が require するため転送層例外もここに閉じ込める。
6
6
  require "kubernetes"
7
+ require "cgi"
7
8
 
8
9
  module K8sRails
9
- # Resolves a connection to the Kubernetes API and exposes the four
10
- # CustomObjects operations as a small internal transport (design §5.2 / §7).
10
+ # Resolves a connection to the Kubernetes API and exposes the read/write
11
+ # operations as a small internal REST transport (design §5.2 / §7).
11
12
  #
12
13
  # api = K8sRails::Client.build
13
14
  # api.list(group, version, namespace, plural)
14
15
  # api.get(group, version, namespace, plural, name)
15
16
  # api.create(group, version, namespace, plural, body)
16
17
  # api.patch(group, version, namespace, plural, name, body)
18
+ # api.delete(group, version, namespace, plural, name)
17
19
  # # cluster-scoped (namespace argument omitted, design §5.3)
18
20
  # api.list_cluster(group, version, plural)
19
21
  # api.get_cluster(group, version, plural, name)
20
22
  # api.create_cluster(group, version, plural, body)
21
23
  # api.patch_cluster(group, version, plural, name, body)
24
+ # api.delete_cluster(group, version, plural, name)
25
+ #
26
+ # The transport is a uniform REST layer over kruby's `Kubernetes::ApiClient`
27
+ # (`call_api`), NOT the `CustomObjectsApi`. A single path builder maps the
28
+ # declaration coordinates (group / version / scope / namespace / plural /
29
+ # name) onto the API path, so it reaches EVERY resource kruby can address:
30
+ # core v1 (`group: ""` → `/api/v1/...`), named built-in groups
31
+ # (`apps/v1` → `/apis/apps/v1/...`), and CRDs (`/apis/{group}/{version}/...`).
22
32
  #
23
33
  # Connection is LAZY: `build` does no network I/O — it only resolves a
24
- # `Kubernetes::Configuration` and builds an in-memory `CustomObjectsApi`.
25
- # The first real call is where DNS/timeout/TLS can fail.
34
+ # `Kubernetes::Configuration` and builds an in-memory `ApiClient`. The first
35
+ # real call is where DNS/timeout/TLS can fail.
26
36
  class Client
27
37
  class << self
28
38
  # Return the shared transport, building it on first call (lazy connect).
@@ -34,16 +44,15 @@ module K8sRails
34
44
  # 2. K1 bridge: duplicate `api_key['authorization']` into
35
45
  # `api_key['BearerToken']` (kruby 1.36 in-cluster/KUBECONFIG write the
36
46
  # token under 'authorization' but auth_settings reads 'BearerToken').
37
- # 3. Build `ApiClient` → `CustomObjectsApi` (in-memory, no I/O).
47
+ # 3. Build `ApiClient` (in-memory, no I/O).
38
48
  # 4. Wrap in a StringKeyedAdapter that normalizes responses (K2) and
39
49
  # converts kruby errors to K8sRails exceptions.
40
50
  def build
41
- @build ||=
42
- if (injected = K8sRails.config.api_client)
43
- StringKeyedAdapter.new(injected)
44
- else
45
- StringKeyedAdapter.new(build_custom_objects_api)
46
- end
51
+ @build ||= if (injected = K8sRails.config.api_client)
52
+ StringKeyedAdapter.new(injected)
53
+ else
54
+ StringKeyedAdapter.new(build_api_client)
55
+ end
47
56
  end
48
57
 
49
58
  # Lightweight connectivity probe (design §5.2). Performs one lightweight
@@ -95,12 +104,11 @@ module K8sRails
95
104
  kruby_error.message.to_s.sub(/\nHTTP status code:.*\z/, "").strip
96
105
  end
97
106
 
98
- # Build a lazy in-memory CustomObjectsApi from the resolved configuration.
99
- def build_custom_objects_api
107
+ # Build a lazy in-memory ApiClient from the resolved configuration.
108
+ def build_api_client
100
109
  config = build_configuration
101
110
  bridge_bearer_token(config)
102
- api_client = Kubernetes::ApiClient.new(config)
103
- Kubernetes::CustomObjectsApi.new(api_client)
111
+ Kubernetes::ApiClient.new(config)
104
112
  end
105
113
 
106
114
  def build_configuration
@@ -120,95 +128,113 @@ module K8sRails
120
128
  end
121
129
  end
122
130
 
123
- # Wraps a CustomObjectsApi (or an injected test double) so that
131
+ # Wraps an ApiClient (or an injected test double) so that
124
132
  # - every response is deep-stringified (K2), and
125
133
  # - kruby errors are converted to K8sRails exceptions (K3).
126
134
  #
127
- # The adapter is the ONLY place kruby response shapes / errors are touched,
128
- # so a kruby upgrade is a one-file change (§7).
135
+ # This is the ONLY place kruby response shapes / errors are touched, so a
136
+ # kruby upgrade is a one-file change (§7). It speaks kruby's `call_api`
137
+ # protocol directly, so it reaches core v1, named built-in groups, and
138
+ # CRDs uniformly.
129
139
  class StringKeyedAdapter
130
140
  def initialize(transport)
131
141
  @transport = transport
132
142
  end
133
143
 
144
+ # --- namespaced ------------------------------------------------------
145
+
134
146
  def list(group, version, namespace, plural)
135
- handle do
136
- Normalizer.stringify(
137
- @transport.list_namespaced_custom_object(group, version, namespace, plural)
138
- )
139
- end
147
+ request(:GET, build_path(:namespaced, group, version, namespace, plural))
140
148
  end
141
149
 
142
150
  def get(group, version, namespace, plural, name)
143
- handle do
144
- Normalizer.stringify(
145
- @transport.get_namespaced_custom_object(group, version, namespace, plural, name)
146
- )
147
- end
151
+ request(:GET, build_path(:namespaced, group, version, namespace, plural, name))
148
152
  end
149
153
 
150
154
  def create(group, version, namespace, plural, body)
151
- handle do
152
- Normalizer.stringify(
153
- @transport.create_namespaced_custom_object(group, version, namespace, plural, body)
154
- )
155
- end
155
+ request(:POST, build_path(:namespaced, group, version, namespace, plural), body: body)
156
156
  end
157
157
 
158
158
  def patch(group, version, namespace, plural, name, body)
159
- handle do
160
- Normalizer.stringify(
161
- @transport.patch_namespaced_custom_object(group, version, namespace, plural, name, body)
162
- )
163
- end
159
+ request(
160
+ :PATCH,
161
+ build_path(:namespaced, group, version, namespace, plural, name),
162
+ body: body,
163
+ content_type: "application/json-patch+json"
164
+ )
164
165
  end
165
166
 
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.
167
+ def delete(group, version, namespace, plural, name)
168
+ request(:DELETE, build_path(:namespaced, group, version, namespace, plural, name))
169
+ end
170
+
171
+ # --- cluster-scoped --------------------------------------------------
170
172
 
171
173
  def list_cluster(group, version, plural)
172
- handle do
173
- Normalizer.stringify(
174
- @transport.list_cluster_custom_object(group, version, plural)
175
- )
176
- end
174
+ request(:GET, build_path(:cluster, group, version, nil, plural))
177
175
  end
178
176
 
179
177
  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
178
+ request(:GET, build_path(:cluster, group, version, nil, plural, name))
185
179
  end
186
180
 
187
181
  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
182
+ request(:POST, build_path(:cluster, group, version, nil, plural), body: body)
193
183
  end
194
184
 
195
185
  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
186
+ request(
187
+ :PATCH,
188
+ build_path(:cluster, group, version, nil, plural, name),
189
+ body: body,
190
+ content_type: "application/json-patch+json"
191
+ )
192
+ end
193
+
194
+ def delete_cluster(group, version, plural, name)
195
+ request(:DELETE, build_path(:cluster, group, version, nil, plural, name))
201
196
  end
202
197
 
203
198
  private
204
199
 
205
- def handle
206
- yield
200
+ # Map the declaration coordinates to the API path (mirrors kubectl's
201
+ # discovery-based routing):
202
+ # - core (group == ""): /api/v1[/namespaces/{ns}]/{plural}[/{name}]
203
+ # - named (group != ""): /apis/{group}/{version}[/namespaces/{ns}]/{plural}[/{name}]
204
+ # `scope` :namespaced takes a namespace; :cluster passes nil (no
205
+ # namespace in the path). `name` (when present) is CGI-escaped.
206
+ def build_path(scope, group, version, namespace, plural, name = nil)
207
+ prefix = group.to_s.empty? ? "/api/#{version}" : "/apis/#{group}/#{version}"
208
+ middle = scope == :cluster ? "" : "/namespaces/#{CGI.escape(namespace.to_s)}"
209
+ path = "#{prefix}#{middle}/#{CGI.escape(plural.to_s)}"
210
+ name.nil? ? path : "#{path}/#{CGI.escape(name.to_s)}"
211
+ end
212
+
213
+ # Issue one request through kruby's call_api and normalize the result.
214
+ # `return_type "Object"` makes kruby hand back the parsed JSON (symbol
215
+ # keys); the Normalizer deep-stringifies it (K2). kruby errors are
216
+ # converted to the K8sRails hierarchy (K3).
217
+ def request(method, path, body: nil, content_type: "application/json")
218
+ data, _status_code, _headers = @transport.call_api(method, path, call_opts(body, content_type))
219
+ Normalizer.stringify(data)
207
220
  rescue Kubernetes::ApiError => e
208
221
  raise Client.convert_api_error(e)
209
222
  rescue Kubernetes::ConfigError, Typhoeus::Errors::TyphoeusError => e
210
223
  raise Unavailable, "K8s に接続できません: #{e.message}"
211
224
  end
225
+
226
+ # The kruby call_api options hash (mirrors how kruby's generated API
227
+ # methods assemble their request opts).
228
+ def call_opts(body, content_type)
229
+ {
230
+ operation: :k8s_rails_request,
231
+ header_params: { "Content-Type" => content_type },
232
+ query_params: {},
233
+ body: body,
234
+ auth_names: ["BearerToken"],
235
+ return_type: "Object"
236
+ }
237
+ end
212
238
  end
213
239
 
214
240
  # One-shot /version probe backed by VersionApi#get_code (the kruby 1.36.x
@@ -18,12 +18,12 @@ module K8sRails
18
18
  # 認証を上書きする場合に指定する。
19
19
  attr_accessor :connection
20
20
 
21
- # テスト専用(§5.1)。CustomObjectsApi と同型の 8 メソッド
22
- # (namespaced 4 メソッド `get_namespaced_custom_object` 等 + cluster 4
23
- # メソッド `get_cluster_custom_object` 等)を実装した素の
24
- # オブジェクトを指定すると、Client.build は接続解決をスキープしてこれを
25
- # 内部トランスポートとして使う(namespaced 宣言だけを使う場合は
26
- # namespaced 4 メソッドのみの実装で足りる)。
21
+ # テスト専用(§5.1)。kruby `Kubernetes::ApiClient#call_api` と同型の
22
+ # `call_api(http_method, path, opts)` メソッドを実装した素のオブジェクトを
23
+ # 指定すると、Client.build は接続解決をスキープしてこれを内部トランスポート
24
+ # として使う(`StringKeyedAdapter` が `call_api` を呼ぶ)。path は宣言の
25
+ # 座標から構築された API パス(core v1 は /api/v1/...、named グループは
26
+ # /apis/{group}/{version}/...)。
27
27
  attr_accessor :api_client
28
28
 
29
29
  # ActiveSupport::Notifications での計測の ON/OFF(§8。M3 で実装)。
@@ -1,21 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K8sRails
4
- # Base for generated CRD access classes (design §5.3).
4
+ # Base for generated CRD / built-in access classes (design §5.3).
5
5
  #
6
6
  # A `K8sRails.crd` declaration returns a `Class.new(Resource)` with the
7
- # declared coordinates bound as class methods. All operations go through
8
- # the shared transport (`K8sRails.client`), which handles kruby error
9
- # conversion (K3) and response stringification (K2) — this class never
10
- # touches kruby itself (§7).
11
- #
12
- # Workflow = K8sRails.crd(group: "argoproj.io", version: "v1alpha1",
13
- # plural: "workflows", kind: "Workflow")
7
+ # declared coordinates bound as class methods. All operations go through the
8
+ # shared transport (`K8sRails.client`), which handles kruby error conversion
9
+ # (K3) and response stringification (K2) — this class never touches kruby (§7).
14
10
  class Resource
15
- # Bind declared coordinates to a fresh anonymous subclass. `namespace`
16
- # may be nil resolved from `K8sRails.config.namespace` at call time.
17
- # `scope` is :namespaced (default) or :cluster (design §5.3); cluster
18
- # declarations must not set `namespace`.
11
+ # Bind declared coordinates to a fresh anonymous subclass. `namespace` may
12
+ # be nil (resolved from `K8sRails.config.namespace` at call time); `scope`
13
+ # is :namespaced (default) or :cluster (cluster declarations: no namespace).
19
14
  def self.declare(group:, version:, plural:, kind:, namespace: nil, readonly: true, scope: :namespaced)
20
15
  Class.new(self) do
21
16
  define_singleton_method(:group_name) { group }
@@ -29,20 +24,16 @@ module K8sRails
29
24
  end
30
25
 
31
26
  class << self
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" => "...", ...}]`).
27
+ # All objects in the namespace; returns an array of string-keyed Hashes.
35
28
  def list(namespace: resolved_namespace)
36
- assert_namespaced
37
- K8sRails.instrument(:list, instrument_meta(namespace)) do
29
+ run_operation(:list, :namespaced, namespace: namespace) do
38
30
  transport.list(group_name, version_name, namespace, plural_name)["items"] || []
39
31
  end
40
32
  end
41
33
 
42
34
  # One object by name. Raises K8sRails::NotFound when absent.
43
35
  def find(name, namespace: resolved_namespace)
44
- assert_namespaced
45
- K8sRails.instrument(:find, instrument_meta(namespace)) do
36
+ run_operation(:find, :namespaced, namespace: namespace) do
46
37
  transport.get(group_name, version_name, namespace, plural_name, name)
47
38
  end
48
39
  end
@@ -54,40 +45,39 @@ module K8sRails
54
45
  nil
55
46
  end
56
47
 
57
- # Create from a CRD body hash. `readonly: true` declarations raise
58
- # K8sRails::ReadOnlyError (K4).
48
+ # Create from a body hash. `readonly: true` declarations raise ReadOnlyError.
59
49
  def create(attributes, namespace: resolved_namespace)
60
- assert_namespaced
61
- assert_writable
62
- K8sRails.instrument(:create, instrument_meta(namespace)) do
50
+ run_operation(:create, :namespaced, namespace: namespace, writable: true) do
63
51
  transport.create(group_name, version_name, namespace, plural_name, attributes)
64
52
  end
65
53
  end
66
54
 
67
55
  # JSON Patch a named object. Same readonly restriction as `create`.
68
56
  def patch(name, operations, namespace: resolved_namespace)
69
- assert_namespaced
70
- assert_writable
71
- K8sRails.instrument(:patch, instrument_meta(namespace)) do
57
+ run_operation(:patch, :namespaced, namespace: namespace, writable: true) do
72
58
  transport.patch(group_name, version_name, namespace, plural_name, name, operations)
73
59
  end
74
60
  end
75
61
 
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.
62
+ # Delete a named object; returns the API status object. Same readonly
63
+ # restriction; raises NotFound when already gone.
64
+ def delete(name, namespace: resolved_namespace)
65
+ run_operation(:delete, :namespaced, namespace: namespace, writable: true) do
66
+ transport.delete(group_name, version_name, namespace, plural_name, name)
67
+ end
68
+ end
69
+
70
+ # Cluster-scoped variants (design §5.3); a namespaced declaration raises
71
+ # ArgumentError (its endpoints 404 on the cluster path anyway).
80
72
  def list_cluster
81
- assert_cluster_scoped
82
- K8sRails.instrument(:list, instrument_meta(nil)) do
73
+ run_operation(:list, :cluster) do
83
74
  transport.list_cluster(group_name, version_name, plural_name)["items"] || []
84
75
  end
85
76
  end
86
77
 
87
- # One cluster-scoped object by name. Raises K8sRails::NotFound when absent.
78
+ # One cluster-scoped object by name. Raises NotFound when absent.
88
79
  def find_cluster(name)
89
- assert_cluster_scoped
90
- K8sRails.instrument(:find, instrument_meta(nil)) do
80
+ run_operation(:find, :cluster) do
91
81
  transport.get_cluster(group_name, version_name, plural_name, name)
92
82
  end
93
83
  end
@@ -101,43 +91,51 @@ module K8sRails
101
91
 
102
92
  # Create a cluster-scoped object. Same readonly restriction as `create`.
103
93
  def create_cluster(attributes)
104
- assert_cluster_scoped
105
- assert_writable
106
- K8sRails.instrument(:create, instrument_meta(nil)) do
94
+ run_operation(:create, :cluster, writable: true) do
107
95
  transport.create_cluster(group_name, version_name, plural_name, attributes)
108
96
  end
109
97
  end
110
98
 
111
99
  # JSON Patch a cluster-scoped object. Same readonly restriction.
112
100
  def patch_cluster(name, operations)
113
- assert_cluster_scoped
114
- assert_writable
115
- K8sRails.instrument(:patch, instrument_meta(nil)) do
101
+ run_operation(:patch, :cluster, writable: true) do
116
102
  transport.patch_cluster(group_name, version_name, plural_name, name, operations)
117
103
  end
118
104
  end
119
105
 
106
+ # Delete a cluster-scoped object. Same readonly restriction as `delete`.
107
+ def delete_cluster(name)
108
+ run_operation(:delete, :cluster, writable: true) do
109
+ transport.delete_cluster(group_name, version_name, plural_name, name)
110
+ end
111
+ end
112
+
120
113
  private
121
114
 
122
- # Notification metadata for design §8 (`k8s-rails.request`).
123
- def instrument_meta(namespace)
124
- { group: group_name, version: version_name, plural: plural_name, namespace: }
115
+ # Scope gate + readonly gate (§5.3) + §8 instrumentation for every
116
+ # operation. The block value (and any K8sRails exception) flows through
117
+ # unchanged. Cluster-scoped operations carry no namespace.
118
+ def run_operation(operation, scope, namespace: nil, writable: false, &block)
119
+ scope == :cluster ? assert_cluster_scoped : assert_namespaced
120
+ assert_writable if writable
121
+ ns = scope == :cluster ? nil : namespace
122
+ K8sRails.instrument(operation, instrument_meta(ns)) { block.call }
125
123
  end
126
124
 
125
+ # Notification metadata for design §8 (`k8s-rails.request`).
126
+ def instrument_meta(namespace) = { group: group_name, version: version_name, plural: plural_name, namespace: }
127
+
127
128
  # Declared namespace wins; otherwise the gem default (resolved at call
128
129
  # time so `K8sRails.reset!` + reconfigure works in tests).
129
- def resolved_namespace
130
- declared_namespace || K8sRails.config.namespace
131
- end
130
+ def resolved_namespace = declared_namespace || K8sRails.config.namespace
132
131
 
133
- # Endless method: keeps the class under the ClassLength budget while
134
- # delegating to the shared transport (design §5.2).
132
+ # Endless method: delegates to the shared transport (design §5.2).
135
133
  def transport = K8sRails.client
136
134
 
137
135
  def assert_writable
138
136
  return unless readonly?
139
137
 
140
- raise ReadOnlyError, "#{kind_name} is declared readonly — create/patch are disabled (K4)"
138
+ raise ReadOnlyError, "#{kind_name} is declared readonly — create/patch/delete are disabled (K4)"
141
139
  end
142
140
 
143
141
  def assert_cluster_scoped
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K8sRails
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian - Takahiro Ishida
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
116
  version: '3.3'
117
117
  - - "<"
118
118
  - !ruby/object:Gem::Version
119
- version: '4.0'
119
+ version: '4.1'
120
120
  required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="