k8s-rails 0.3.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +189 -75
- data/README.md +106 -11
- data/docs/design.md +79 -24
- data/lib/k8s-rails.rb +12 -6
- data/lib/k8s_rails/client.rb +184 -18
- data/lib/k8s_rails/configuration.rb +13 -0
- data/lib/k8s_rails/resource.rb +24 -12
- data/lib/k8s_rails/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8241efc182ed9a328aa6a4b2ac652588f08282100e47f0255b1f3dfd76020dbb
|
|
4
|
+
data.tar.gz: fa4fb9ae8b57a2a4aac1b60a7868e58a840d6ad8d3a85a6e22ec26b34e08b045
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99dbf84cca496a64160719e1e3e7da245de3192eee25c5978b38bf5831794ca258a5c28f4f6aff301da86c9d33da61c4b5a301a539de81d4c20f4be96aa5de32
|
|
7
|
+
data.tar.gz: 40f07ca07971b618592938444ce7071b1e312ec5dbe9146754523ed017263f8337bc081f203b1ecd24ef21d52b049e7ddaa6b4cba32bbc38fa57ae3c96d1b6db
|
data/CHANGELOG.md
CHANGED
|
@@ -1,92 +1,206 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
`k8s-rails`
|
|
3
|
+
All notable changes to `k8s-rails` are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
- **kubeconfig load failures now surface as `K8sRails::Unavailable`** (#30):
|
|
8
|
+
`Kubernetes::Configuration.default_config` runs before any request, so a
|
|
9
|
+
broken kubeconfig (e.g. a `current-context` that does not exist) leaked the
|
|
10
|
+
raw `Kubernetes::ConfigError` out of `K8sRails.client` / `K8sRails.connected?`.
|
|
11
|
+
The config-resolution stage (`Client.build_configuration`) now applies the
|
|
12
|
+
same conversion as the transport-time path — `Unavailable` with the original
|
|
13
|
+
`Kubernetes::ConfigError` preserved as `#cause` — for both `Resource.list`
|
|
14
|
+
and `connected?`. The rescue is narrow (`ConfigError` only), so programming
|
|
15
|
+
errors still propagate unconverted.
|
|
16
|
+
- **`connected?` now probes the same cached connection as Resource
|
|
17
|
+
operations** (#31): the resolved configuration is memoized alongside the
|
|
18
|
+
cached transport (`@resolved_connection_config`), so a config resolution that re-resolves
|
|
19
|
+
to a different connection after the first call no longer lets the probe
|
|
20
|
+
validate connection B while operations still send to connection A. There is
|
|
21
|
+
no implicit re-resolution; `K8sRails.reset!` remains the explicit reload
|
|
22
|
+
path (after it, both the probe and the next API call resolve the
|
|
23
|
+
configuration anew). A failed resolution (e.g. a broken kubeconfig) is not
|
|
24
|
+
memoized and is re-attempted on the next call. The first resolution, the
|
|
25
|
+
K1 bridge, and the transport build run inside one internal lock
|
|
26
|
+
(`Client::BUILD_LOCK`), so a concurrent first Resource operation and first
|
|
27
|
+
`connected?` call resolve the configuration exactly once in a threaded
|
|
28
|
+
Rails process (the probe's HTTP I/O stays outside the lock) — a
|
|
29
|
+
deterministic regression test asserts a single resolution under a racing
|
|
30
|
+
build/probe and fails on the pre-lock code.
|
|
31
|
+
|
|
32
|
+
- **`find` / `patch` / `delete` (and `*_cluster`) reject invalid target names
|
|
33
|
+
before any request is issued** (#28): `nil`, an empty string, `"."` or
|
|
34
|
+
`".."` raise `ArgumentError`. Previously `delete(nil)` built the
|
|
35
|
+
collection URL, which is a Delete Collection call — an unintended bulk
|
|
36
|
+
delete when the caller has the `deletecollection` permission; `"."` /
|
|
37
|
+
`".."` were normalized away by the HTTP client into a different resource
|
|
38
|
+
path. Gate order is unchanged: scope gate, then readonly gate, then name
|
|
39
|
+
validation (rejected calls are not instrumented).
|
|
40
|
+
- **The K1 bearer-token bridge keeps a rotated token fresh** (#29): the
|
|
41
|
+
bridge no longer freezes the first copy of
|
|
42
|
+
`api_key['authorization']` → `api_key['BearerToken']`; it re-syncs on
|
|
43
|
+
every request (kruby calls `auth_settings` per request, which is exactly
|
|
44
|
+
where kruby's in-cluster token-refresh hook runs). Long-running processes
|
|
45
|
+
using an in-cluster ServiceAccount token now pick up rotated tokens
|
|
46
|
+
without rebuilding the client. The token sent by a request is taken from
|
|
47
|
+
the value refreshed on that same call and is never re-read from the
|
|
48
|
+
shared configuration afterwards, so concurrent requests in a threaded
|
|
49
|
+
process cannot send a stale copy. A `BearerToken` set explicitly by the
|
|
50
|
+
application is never bridged or overwritten: before client creation the
|
|
51
|
+
bridge is not installed, and if the app replaces the value after the
|
|
52
|
+
client exists (explicit credential rotation), the bridge stops syncing
|
|
53
|
+
and the app's value stays authoritative.
|
|
54
|
+
- **`k8s-rails.request` notifications now use the standard ActiveSupport block
|
|
55
|
+
form** (#32): the API call runs inside
|
|
56
|
+
`ActiveSupport::Notifications.instrument`, so the standard start/finish
|
|
57
|
+
times bracket the real processing
|
|
58
|
+
(`ActiveSupport::Notifications::Event#duration` now reports the actual call
|
|
59
|
+
duration — previously the event was emitted after the call, so the standard
|
|
60
|
+
duration was ~0) and failures carry the standard
|
|
61
|
+
`exception` / `exception_object` metadata. The custom §8 payload
|
|
62
|
+
(`operation` / resource coordinates / `duration_ms` / closed `status` enum)
|
|
63
|
+
and the no-op / return-value contracts are unchanged; `status` and
|
|
64
|
+
`duration_ms` are set in the block's `ensure`, so the finish event payload
|
|
65
|
+
always carries them and the original exception is still re-raised.
|
|
66
|
+
|
|
67
|
+
- **A finite HTTP timeout (30s) is now applied by default** (#33,
|
|
68
|
+
behavior change): kruby's `Kubernetes::Configuration` defaults `timeout`
|
|
69
|
+
to `0`, which in libcurl means NO timeout — an unresponsive API server
|
|
70
|
+
could hold a Rails request or background job indefinitely. The gem now
|
|
71
|
+
applies a new `config.http_timeout` (default 30 seconds) at connection
|
|
72
|
+
resolution, ONLY when the resolved connection's `timeout` still carries
|
|
73
|
+
kruby's unset value (`0`): an explicitly set `config.connection.timeout`
|
|
74
|
+
is never overridden. Set `config.http_timeout = 0` to opt out and restore
|
|
75
|
+
the previous unlimited wait. The same policy covers Resource operations
|
|
76
|
+
and `K8sRails.connected?`. `http_timeout` accepts a non-negative Integer
|
|
77
|
+
or nil; other values raise `ArgumentError` (a `Float` like `0.5` would be
|
|
78
|
+
truncated by libcurl to `0` = unlimited). A finite timeout surfaces a hung
|
|
79
|
+
connection as `K8sRails::Unavailable` (kruby reports it as `ApiError`
|
|
80
|
+
code 0, "Connection timed out"). The gem still adds no retry layer of its
|
|
81
|
+
own; kruby's optional `retry_configuration` re-runs the full request per
|
|
82
|
+
retryable status, so the total wait scales with `max_retries` (timed-out
|
|
83
|
+
requests are never retried) — documented in the README.
|
|
84
|
+
- **kruby 1.36.5.2 compatibility verified** (#41): the dependency pin
|
|
85
|
+
(`~> 1.36.0`) already admits the new release, so no pin change and no
|
|
86
|
+
consumer change is needed. The client library source is byte-identical to
|
|
87
|
+
1.36.2.1 except the version string (verified by a gem-dir diff), so the
|
|
88
|
+
§7 upgrade facts (call_api signature, K1 bridge, config discovery order)
|
|
89
|
+
hold unchanged. The full suite (132 examples / 0 failures, 22 files no
|
|
90
|
+
offenses) was re-run on Ruby 3.3.8, 3.5.0-preview1, and 4.0.7 with kruby
|
|
91
|
+
1.36.5.2 installed. kruby 1.36.5.2 declares `logger` as a runtime
|
|
92
|
+
dependency (Ruby 4.0 promoted it to a bundled gem); it resolves
|
|
93
|
+
transitively for this gem.
|
|
94
|
+
- **Ruby 4.1.0-preview1 is excluded from the declared range** (#41, PR #42
|
|
95
|
+
review): it was published on 2026-10-17 (ruby/ruby tags) and, because
|
|
96
|
+
`Gem.ruby_version` reports the prerelease form `4.1.0.preview1`, the
|
|
97
|
+
previous `>= 3.3, < 4.1` interval admitted it at runtime. The gemspec now
|
|
98
|
+
adds `!= 4.1.0.preview1` — RubyGems requirements are conjunctive, so a
|
|
99
|
+
single-version exclusion is expressible without a union (verified:
|
|
100
|
+
`4.1.0.preview1` rejected; `4.0.7` and `3.5.0.preview1` admitted; `4.1`
|
|
101
|
+
rejected). The preview stays out of the CI matrix because `bundle install`
|
|
102
|
+
fails under it with the upstream RubyGems/Bundler bug
|
|
103
|
+
`uninitialized constant Pathname::SEPARATOR_PAT` (Bundler 4.0.23,
|
|
104
|
+
verified locally 2026-10-17); drop the exclusion and add the shard once
|
|
105
|
+
rubygems fixes it or 4.1 goes stable. `!=` excludes exactly one version,
|
|
106
|
+
so future previews (4.1.0.preview2+) need their own entries until then.
|
|
107
|
+
Ruby 4.0 support itself is unaffected and re-verified on 4.0.7 (see above).
|
|
4
108
|
|
|
5
109
|
## 0.3.1
|
|
6
110
|
|
|
7
|
-
- **
|
|
8
|
-
v0.3.0
|
|
9
|
-
0.3.1
|
|
10
|
-
|
|
11
|
-
|
|
111
|
+
- **Gemspec wording sync (RubyGems page text update)**: `summary` / `description`
|
|
112
|
+
updated to match the v0.3.0 scope (PR #25). Published gem text cannot be
|
|
113
|
+
changed after push, so publishing 0.3.1 switches the RubyGems page to the new
|
|
114
|
+
wording ("Kubernetes API access convention layer for Rails applications").
|
|
115
|
+
**No code or behavior change.**
|
|
12
116
|
|
|
13
117
|
## 0.3.0
|
|
14
118
|
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
core v1 / named
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
119
|
+
- **Expanded resource support: core v1 built-ins (Pod / Service / ConfigMap /
|
|
120
|
+
Node, etc.) are now supported.** Previously the transport relied on kruby's
|
|
121
|
+
`CustomObjectsApi` (fixed `/apis/{group}/...` paths), so core v1 resources
|
|
122
|
+
with `group: ""` (`/api/v1/...`) could not be reached (404), while named
|
|
123
|
+
built-ins worked all along. The transport was rewritten as a **unified REST
|
|
124
|
+
layer** on `Kubernetes::ApiClient#call_api`, reaching core v1 / named
|
|
125
|
+
built-in / CRD alike by building the path from the declaration coordinates.
|
|
126
|
+
Existing named built-in and CRD declarations behave exactly as before.
|
|
127
|
+
- **Added `delete` / `delete_cluster`** (completing CRUD). The readonly gate is
|
|
128
|
+
the same shape as create / patch (calling with a `readonly: true` declaration
|
|
129
|
+
raises `ReadOnlyError`). K8s returns a Status object
|
|
130
|
+
(`{"kind":"Status","status":"Success"}`) on successful delete.
|
|
131
|
+
- **Test injection stub contract change (affects test code)**: the method
|
|
132
|
+
`config.api_client` must implement is now the single
|
|
133
|
+
`call_api(http_method, path, opts)` (returning `[data, status_code, headers]`,
|
|
134
|
+
http_method being `:GET` / `:POST` / `:PATCH` / `:DELETE`), unified from the
|
|
135
|
+
8 CustomObjectsApi methods. Existing stubs need to be replaced with
|
|
136
|
+
`call_api` (production code talking to a real cluster is unaffected).
|
|
137
|
+
- Added `:delete` to the `operation` of the `k8s-rails.request` notification.
|
|
138
|
+
- E2E-verified on a real cluster (microk8s v1.33.13): Pod list / Deployment
|
|
139
|
+
find / ConfigMap create/patch/delete / Node list_cluster.
|
|
34
140
|
|
|
35
141
|
## 0.2.1
|
|
36
142
|
|
|
37
|
-
- **Ruby 3.5 / 4.0
|
|
38
|
-
2026-09-22
|
|
39
|
-
Ruby
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
CI matrix
|
|
44
|
-
|
|
45
|
-
|
|
143
|
+
- **Declared support for Ruby 3.5 / 4.0** (declaration range widened only — no
|
|
144
|
+
code or behavior change): on 2026-09-22, the full rspec / rubocop suite ran
|
|
145
|
+
green on Ruby 4.0.7 (stable, v4.0.7, released 2026-09-15) and
|
|
146
|
+
Ruby 3.5.0-preview1 (the only released 3.5 build) (kruby 1.36.4.1
|
|
147
|
+
installed and verified on both), so `required_ruby_version` was widened from
|
|
148
|
+
`>= 3.3, < 4.0` to **`>= 3.3, < 4.1`**. The upper bound is set to the
|
|
149
|
+
verified 4.0 series, and the CI matrix covers 3.3.0 / 3.3.8 / 3.4.10 /
|
|
150
|
+
3.5.0-preview1 / 4.0.7 — every released Ruby in the range (declared range =
|
|
151
|
+
tested range). Design doc KBR-DESIGN-001 updated to v0.1.13 (draft)
|
|
152
|
+
(§6 / §10 / §13 / §14).
|
|
46
153
|
|
|
47
154
|
## 0.2.0
|
|
48
155
|
|
|
49
|
-
- **
|
|
50
|
-
|
|
51
|
-
`scope: :cluster
|
|
52
|
-
`
|
|
53
|
-
`
|
|
54
|
-
`*_cluster` /
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
156
|
+
- **Cluster-scoped CRD support** (#17): `K8sRails.crd` gains
|
|
157
|
+
`scope: :namespaced` (default) / `:cluster`. Cluster-scoped CRDs
|
|
158
|
+
(ClusterIssuer, etc.) are declared with `scope: :cluster` (must not be
|
|
159
|
+
combined with `namespace:`) and used via `list_cluster` / `find_cluster` /
|
|
160
|
+
`find_or_nil_cluster` / `create_cluster` / `patch_cluster`. Scope-asymmetric
|
|
161
|
+
calls (`*_cluster` on a namespaced declaration / plain methods on a cluster
|
|
162
|
+
declaration) raise `ArgumentError`. The transport now also uses kruby's
|
|
163
|
+
4 `*_cluster_custom_object` methods.
|
|
164
|
+
- **Atomic contract for `configure`** (#16): "only once" holds only when the
|
|
165
|
+
block finishes normally. If the block terminates abnormally (any exception —
|
|
166
|
+
including `LoadError` / `ScriptError` — / `throw` / non-local return, etc.),
|
|
167
|
+
the configured flag is reset and a subsequent `configure` runs as usual.
|
|
168
|
+
Attributes written before the abnormal exit remain (the re-run block is
|
|
169
|
+
responsible for setting all attributes it depends on; no rollback is
|
|
170
|
+
performed).
|
|
171
|
+
- **Injection contract for `connected?`** (#18): when `config.api_client` is
|
|
172
|
+
injected, it returns `true` without I/O (the injected transport IS the
|
|
173
|
+
connection surface). Without injection, the VersionApi probe runs as before.
|
|
174
|
+
- **Connection lookup order fix** (#19): the documented auto-detection order in
|
|
175
|
+
README / design doc / config comments was corrected to kruby 1.36.x's loader
|
|
176
|
+
implementation order (**KUBECONFIG → `~/.kube/config` → in-cluster**,
|
|
177
|
+
in-cluster LAST; the previous "in-cluster → KUBECONFIG" wording was wrong).
|
|
178
|
+
A re-verification procedure for kruby upgrades was added to design doc §7.
|
|
179
|
+
- Test injection stub (`config.api_client`) contract: 4 namespaced methods +
|
|
180
|
+
4 cluster methods (the 4 namespaced methods suffice if only namespaced
|
|
181
|
+
declarations are used).
|
|
182
|
+
- Design doc KBR-DESIGN-001 updated to v0.1.12 (draft) (§5.1 / §5.2 / §5.3 /
|
|
183
|
+
§6 / §7 / §9 / §14).
|
|
74
184
|
|
|
75
185
|
## 0.1.0
|
|
76
186
|
|
|
77
|
-
- **M0**: gem
|
|
78
|
-
- **M1**: Configuration + Client
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
187
|
+
- **M0**: gem skeleton (gemspec / Gemfile / Rakefile / version / require layout)
|
|
188
|
+
- **M1**: Configuration + Client (lazy connect, K1 BearerToken bridging),
|
|
189
|
+
exception hierarchy (`Unavailable` / `NotFound` / `ApiError` +
|
|
190
|
+
`ReadOnlyError` / `RedeclarationError`)
|
|
191
|
+
- **M2**: CRD declaration DSL + Resource (`list` / `find` / `find_or_nil` /
|
|
192
|
+
`create` / `patch`; `readonly` is an explicit boolean; re-declaration raises
|
|
193
|
+
`RedeclarationError`)
|
|
194
|
+
- **M3**: instrumentation (via ActiveSupport::Notifications, no-op when
|
|
195
|
+
disabled) + README + rubocop
|
|
196
|
+
- **M4**: verified in a consumer Rails app's K8s service migration (real
|
|
197
|
+
cluster, microk8s v1.33.13)
|
|
198
|
+
- Responses are always string-keyed (K2); pure-Ruby `Normalizer` (no
|
|
199
|
+
ActiveSupport dependency)
|
|
200
|
+
- Supported Ruby: `>= 3.3, < 4.0` (floor from kruby 1.36.x's `>= 3.3`; the
|
|
201
|
+
upper bound excludes unverified Ruby 4.x. 4.0 / 3.5 support to be verified
|
|
202
|
+
and declared in v0.2+). CI (GitHub Actions) matrix over 3.3.0 / 3.3.8 / 3.4.10
|
|
203
|
+
- Supported Kubernetes server: verified on v1.33.x against a real cluster (see
|
|
204
|
+
README)
|
|
205
|
+
- Publishing path: manual `gem push` from the owner's local PC (after rake
|
|
206
|
+
fully green + CHANGELOG finalized; no CI auto-publish)
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ For the design rationale, see the [design document (docs/design.md)](docs/design
|
|
|
16
16
|
|
|
17
17
|
| Item | Supported range | Notes |
|
|
18
18
|
|------|-----------------|-------|
|
|
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-
|
|
19
|
+
| Ruby | `>= 3.3, < 4.1` (excluding `4.1.0.preview1`) | Floor: kruby 1.36.x requires Ruby 3.3. Ruby 4.0 support verified on Ruby 4.0.7 (2026-09-22, re-verified with kruby 1.36.5.2 on 2026-10-18); Ruby 3.5 verified on 3.5.0-preview1 (the only released 3.5, re-verified 2026-10-18). CI verifies the declared range with a 3.3.0 / 3.3.8 / 3.4.10 / 3.5.0-preview1 / 4.0.7 matrix. Ruby 4.1.0-preview1 (published 2026-10-17) is excluded from the range with `!= 4.1.0.preview1`: `Gem.ruby_version` reports the prerelease form `4.1.0.preview1`, which `< 4.1` alone admits. The preview has no runnable CI shard yet (`bundle install` fails under it with the upstream RubyGems/Bundler bug `uninitialized constant Pathname::SEPARATOR_PAT`, Bundler 4.0.23; verified 2026-10-17) — the exclusion lifts and the shard lands once rubygems fixes it or 4.1 goes stable. `!=` excludes exactly one version, so later previews (4.1.0.preview2+) need their own entries until then |
|
|
20
20
|
| kruby | `~> 1.36.0` | The official Kubernetes OpenAPI client |
|
|
21
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 |
|
|
22
22
|
| Dependencies | **kruby only** at runtime | ActiveSupport is used only for optional [instrumentation](#instrumentation-optional-activesupport) (no-op when absent) |
|
|
@@ -25,12 +25,12 @@ For the design rationale, see the [design document (docs/design.md)](docs/design
|
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
27
|
# Gemfile
|
|
28
|
-
gem "k8s-rails", "~> 0.
|
|
28
|
+
gem "k8s-rails", "~> 0.4"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
require "k8s-rails"
|
|
33
|
-
K8sRails::VERSION # => "0.
|
|
33
|
+
K8sRails::VERSION # => "0.4.0"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`require` is side-effect-free and needs no cluster. kruby itself is loaded
|
|
@@ -137,6 +137,7 @@ Workflow.delete("wf-1") # readonly: f
|
|
|
137
137
|
```ruby
|
|
138
138
|
K8sRails.configure do |config|
|
|
139
139
|
config.namespace = "team-a" # default namespace (default: "default")
|
|
140
|
+
# config.http_timeout = 30 # HTTP timeout in seconds (default: 30; 0 = unlimited)
|
|
140
141
|
# config.connection = my_config # pass a Kubernetes::Configuration directly (optional)
|
|
141
142
|
# config.instrumentation = false # disable instrumentation (default true; only effective when ActiveSupport is present)
|
|
142
143
|
# config.api_client = stub # test-only: inject a transport (see [Testing](#testing))
|
|
@@ -159,6 +160,63 @@ end
|
|
|
159
160
|
in-cluster. Note in-cluster is tried **last**, after the file-based
|
|
160
161
|
sources — re-verify `kruby`'s loader when upgrading kruby).
|
|
161
162
|
|
|
163
|
+
### HTTP timeouts (default 30s)
|
|
164
|
+
|
|
165
|
+
kruby's `Kubernetes::Configuration` defaults `timeout` to `0`, which in
|
|
166
|
+
libcurl means **no timeout** — an unresponsive API server can hold a Rails
|
|
167
|
+
request or background job indefinitely. To make that safe by default, the gem
|
|
168
|
+
applies a finite timeout at connection resolution:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
K8sRails.configure do |config|
|
|
172
|
+
config.http_timeout = 30 # seconds (default: 30)
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- The value applies **only** when the resolved connection's `timeout` still
|
|
177
|
+
carries kruby's unset value (`0`). If you set one explicitly, the gem never
|
|
178
|
+
overrides it:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
require "kubernetes"
|
|
182
|
+
|
|
183
|
+
my_config = Kubernetes::Configuration.default_config
|
|
184
|
+
my_config.timeout = 60 # explicit value — the gem never overrides it
|
|
185
|
+
K8sRails.configure do |config|
|
|
186
|
+
config.connection = my_config
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
- `config.http_timeout = 0` opts out entirely and restores kruby's unlimited
|
|
190
|
+
wait. (kruby's own `timeout = 0` is indistinguishable from "never set", so
|
|
191
|
+
the opt-out lives on this knob rather than on the kruby configuration.)
|
|
192
|
+
- The same policy covers Resource operations **and** `K8sRails.connected?` —
|
|
193
|
+
both resolve the connection through the same path.
|
|
194
|
+
- A finite timeout turns a hung connection into `K8sRails::Unavailable`
|
|
195
|
+
(kruby surfaces it as `ApiError` code 0 with a "Connection timed out"
|
|
196
|
+
message; the gem converts it — see [Exception hierarchy](#exception-hierarchy)).
|
|
197
|
+
- Set `http_timeout` before the first `K8sRails.client` / API call: it is
|
|
198
|
+
applied once, when the connection is resolved. Changing
|
|
199
|
+
`config.connection.timeout` directly works at any time (kruby re-reads it
|
|
200
|
+
for every request).
|
|
201
|
+
- `http_timeout` accepts a non-negative **Integer** or `nil` (built-in
|
|
202
|
+
default). Other values raise `ArgumentError` — a `Float` like `0.5` would
|
|
203
|
+
be truncated by libcurl to `0` (= unlimited), silently recreating the
|
|
204
|
+
unbounded wait this default exists to prevent.
|
|
205
|
+
|
|
206
|
+
### Retries
|
|
207
|
+
|
|
208
|
+
The gem does not add its own retry/backoff layer. kruby 1.36.x offers retries
|
|
209
|
+
on the resolved connection's `retry_configuration` hash
|
|
210
|
+
(`{ max_retries: N, base_interval_seconds: F, retry_statuses: [...] }`;
|
|
211
|
+
disabled by default — `max_retries` 0, and even when enabled only the status
|
|
212
|
+
codes 429/500/501/502/503 are retried, with the server's `Retry-After` taken
|
|
213
|
+
into account when present). When enabled, **the total wait scales with
|
|
214
|
+
`max_retries`**: every retryable response re-runs the full request plus the
|
|
215
|
+
exponential backoff between attempts, so one call can take roughly
|
|
216
|
+
`(1 + max_retries) × timeout` plus the intervals. Timed-out requests are
|
|
217
|
+
**not** retried. For broader failure handling, rely on Rails middleware /
|
|
218
|
+
background-job retries.
|
|
219
|
+
|
|
162
220
|
## Resource declaration and access
|
|
163
221
|
|
|
164
222
|
The same `K8sRails.crd` API covers core v1 built-ins, named-group built-ins,
|
|
@@ -182,6 +240,13 @@ declaration (the `*_cluster` methods take no `namespace:`).
|
|
|
182
240
|
`ArgumentError`). Writes (`create` / `patch` / `delete`) are enabled **only**
|
|
183
241
|
by `readonly: false`, so a missing flag can never fail open.
|
|
184
242
|
|
|
243
|
+
Object-name arguments of `find` / `find_or_nil` / `patch` / `delete` (and
|
|
244
|
+
their `*_cluster` counterparts) are validated **before any request is
|
|
245
|
+
issued**: `nil`, an empty string, `"."` or `".."` raise `ArgumentError`.
|
|
246
|
+
This keeps `delete(nil)` from degrading into a Delete Collection call
|
|
247
|
+
against the whole collection, and keeps `"."` / `".."` from being normalized
|
|
248
|
+
away by the HTTP client into a different resource path.
|
|
249
|
+
|
|
185
250
|
### Cluster-scoped resources
|
|
186
251
|
|
|
187
252
|
Both namespaced and cluster-scoped resources are supported. Declare a
|
|
@@ -231,6 +296,17 @@ It never returns `false` — a connection failure surfaces as an exception
|
|
|
231
296
|
(handle it with `rescue`). The actual API connection is established lazily on
|
|
232
297
|
the first API call.
|
|
233
298
|
|
|
299
|
+
The probe inspects the **same cached connection** the Resource operations
|
|
300
|
+
use — the resolved configuration is memoized for the lifetime of the cached
|
|
301
|
+
transport, so a config resolution that changes after the first call is not
|
|
302
|
+
re-resolved for the probe (it would otherwise validate connection B while
|
|
303
|
+
operations still use connection A). The first resolution, the K1 bridge, and
|
|
304
|
+
the transport build run inside one internal lock, so a concurrent Resource
|
|
305
|
+
operation and `connected?` call resolve the configuration exactly once (the
|
|
306
|
+
probe's HTTP I/O stays outside the lock). `K8sRails.reset!` is the explicit reload
|
|
307
|
+
path: after it, both the probe and the next API call resolve the
|
|
308
|
+
configuration anew.
|
|
309
|
+
|
|
234
310
|
When a test transport is injected via `config.api_client`, `connected?`
|
|
235
311
|
returns `true` without any network I/O — the injected transport **is** the
|
|
236
312
|
connection surface, so probing a real endpoint would contradict the Resource
|
|
@@ -240,7 +316,7 @@ operations that the same injection serves.
|
|
|
240
316
|
|
|
241
317
|
```
|
|
242
318
|
K8sRails::Error < StandardError
|
|
243
|
-
├── K8sRails::Unavailable #
|
|
319
|
+
├── K8sRails::Unavailable # connection-level failure: transport (DNS/timeout/connection refused — kruby 1.36.x reports these as ApiError code 0) and kubeconfig load failures (original Kubernetes::ConfigError kept as #cause)
|
|
244
320
|
├── K8sRails::NotFound # HTTP 404
|
|
245
321
|
├── K8sRails::ApiError # other API errors (401/403/409/422/5xx); holds #code and #response
|
|
246
322
|
├── K8sRails::ReadOnlyError # create/patch/delete called on a readonly: true declaration
|
|
@@ -271,6 +347,12 @@ k8s-rails.request
|
|
|
271
347
|
status: "ok" | "unavailable" | "api_error" }
|
|
272
348
|
```
|
|
273
349
|
|
|
350
|
+
The API call runs inside the standard notification block, so the event's
|
|
351
|
+
start/finish times bracket the real processing (`Event#duration` reports the
|
|
352
|
+
actual call duration) and a failure attaches the standard
|
|
353
|
+
`exception` / `exception_object` metadata next to the custom `status`. The
|
|
354
|
+
finish event payload always carries `status` and `duration_ms`.
|
|
355
|
+
|
|
274
356
|
In a Rails app you can subscribe with `ActiveSupport::Notifications`:
|
|
275
357
|
|
|
276
358
|
```ruby
|
|
@@ -326,16 +408,29 @@ not match, **the Authorization header ends up empty and requests fail with
|
|
|
326
408
|
401** when using kruby's configuration directly.
|
|
327
409
|
|
|
328
410
|
k8s-rails automatically copies `authorization` to `BearerToken` when building
|
|
329
|
-
its client (the design document calls this the "K1 bridge")
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
kruby's
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
411
|
+
its client (the design document calls this the "K1 bridge"). The copy is not
|
|
412
|
+
frozen: the bridge re-syncs `BearerToken` from `authorization` on every
|
|
413
|
+
request (kruby calls `auth_settings` per request, which is exactly where
|
|
414
|
+
kruby's in-cluster token-refresh hook runs), so a rotated ServiceAccount
|
|
415
|
+
token reaches long-running processes without rebuilding the client. The
|
|
416
|
+
token sent by a request is taken from the value refreshed on that same call
|
|
417
|
+
and is never re-read from the shared configuration afterwards, so concurrent
|
|
418
|
+
requests in a threaded process cannot send a stale copy. A `BearerToken` set
|
|
419
|
+
by your application is never bridged or overwritten — before client creation
|
|
420
|
+
the bridge is simply not installed, and if your app replaces the value after
|
|
421
|
+
the client exists (explicit credential rotation), the bridge detects the
|
|
422
|
+
override, stops syncing, and your value stays authoritative. If you see 401s
|
|
423
|
+
from a cluster connection that relies on kruby's own configuration behavior
|
|
424
|
+
(outside this gem), check this token-key issue first.
|
|
425
|
+
|
|
426
|
+
## Roadmap (v0.5+)
|
|
336
427
|
|
|
337
428
|
- Update the CI matrix when Ruby 3.5 reaches a stable release (3.5 is
|
|
338
429
|
verified today on 3.5.0-preview1, the only released 3.5)
|
|
430
|
+
- When Ruby 4.1 goes stable, drop the `!= 4.1.0.preview1` gemspec
|
|
431
|
+
exclusion, extend the declared range to `< 4.2`, and add 4.1 to the
|
|
432
|
+
matrix after verification (the preview is excluded until the upstream
|
|
433
|
+
Bundler `Pathname::SEPARATOR_PAT` bug is fixed)
|
|
339
434
|
- CI-based E2E tests using kind
|
|
340
435
|
- watch (streaming) support, under consideration
|
|
341
436
|
|
data/docs/design.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# k8s-rails 設計書
|
|
2
2
|
|
|
3
3
|
- 文書番号: KBR-DESIGN-001
|
|
4
|
-
- 版: 0.1.
|
|
5
|
-
- 日付: 2026-
|
|
4
|
+
- 版: 0.1.23(案)
|
|
5
|
+
- 日付: 2026-10-19
|
|
6
6
|
- 対象リポジトリ: k8s-rails(本設計の実装先)
|
|
7
7
|
- ライセンス: MIT(LICENSE は main に既存)
|
|
8
8
|
|
|
@@ -41,7 +41,7 @@ K8s CRD を扱えるようにし、(b) 実際の consumer アプリをこの gem
|
|
|
41
41
|
### 2.2 非スコープ(本設計の外)
|
|
42
42
|
|
|
43
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.
|
|
44
|
+
- **watch(ストリーム)** — v0.1 では非対応。kruby の watch は get/list より成熟度が低く、初版の API 保証範囲から外す(§10 で v0.5 対象)
|
|
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.
|
|
109
|
+
│ ├── version.rb # VERSION = "0.4.0"
|
|
110
110
|
│ ├── configuration.rb # Config: namespace, connection, 計測 ON/OFF
|
|
111
111
|
│ ├── client.rb # 接続解決・BearerToken 橋渡し・計測ラップ
|
|
112
112
|
│ ├── crd.rb # K8sRails.crd 宣言 → Resource 生成
|
|
@@ -130,6 +130,9 @@ k8s-rails/
|
|
|
130
130
|
# config/initializers/k8s-rails.rb
|
|
131
131
|
K8sRails.configure do |config|
|
|
132
132
|
config.namespace = ENV.fetch("K8S_NAMESPACE", "default")
|
|
133
|
+
# HTTP タイムアウト(秒・#33)。既定 30。解決済み接続の `timeout` が kruby の
|
|
134
|
+
# 未設定値 `0` のときのみ適用。`0` で無制限にオプトアウト
|
|
135
|
+
# config.http_timeout = 30
|
|
133
136
|
# 任意上書き(省略時は default_config の自動検出。kruby 1.36.x の探索順序は
|
|
134
137
|
# KUBECONFIG → ~/.kube/config → in-cluster で、in-cluster が最後)
|
|
135
138
|
# config.connection = Kubernetes::Configuration.default_config
|
|
@@ -142,6 +145,7 @@ end
|
|
|
142
145
|
| キー | 既定 | 説明 |
|
|
143
146
|
|---|---|---|
|
|
144
147
|
| `namespace` | `"default"` | CRD 宣言が namespace 未指定時のデフォルト |
|
|
148
|
+
| `http_timeout` | `30` | HTTP タイムアウト(秒・#33)。接続解決時に、解決済み `Kubernetes::Configuration` の `timeout` が kruby の未設定値 `0` のときのみ適用(kruby 既定の `0` は libcurl 上**無制限待ち**で、Rails のリクエスト / ジョブを占有し得る)。`config.connection.timeout` に非 0 を明示した場合は**上書きしない**。`0` で kruby 既定の無制限にオプトアウト(kruby 側の `0` は「未設定」と区別できないため、オプトアウトは本キーで行う)。`nil` で組み込み既定値に戻す。非負 Integer / `nil` 以外は `ArgumentError`(`0.5` 等の Float は libcurl で `0`=無制限に丸められるため fail-fast)。`Resource` 操作と `connected?` の**双方**に同一適用(§5.2 の共通経路) |
|
|
145
149
|
| `connection` | `nil`(自動検出) | `Kubernetes::Configuration` インスタンス。認証を上書きする場合に指定。省略時の探索順序(kruby 1.36.x の loader 実装順): **KUBECONFIG → `~/.kube/config` → in-cluster**(in-cluster はファイル系が両方無効な場合の**最後**。kruby 上げ替え時に loader を再確認すること — §7) |
|
|
146
150
|
| `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
151
|
| `instrumentation` | `true` | `ActiveSupport::Notifications` で計測する(§8) |
|
|
@@ -153,7 +157,7 @@ end
|
|
|
153
157
|
は通常どおり実行される。ただし異常終了前に書き込まれた属性は共有 Configuration
|
|
154
158
|
に**残存する**(部分的な設定状態になり得るため、再実行ブロックは依存する属性を
|
|
155
159
|
全て設定する責務を負う。アトミックなロールバックは行わない — Configuration
|
|
156
|
-
は純データで
|
|
160
|
+
は純データで 5 属性のみのため、複写+スワップの複雑さに見合わない)。
|
|
157
161
|
- `K8sRails.reset!`(テスト用)で接続キャッシュ・宣言済 CRD を破棄できる。
|
|
158
162
|
|
|
159
163
|
### 5.2 接続
|
|
@@ -171,8 +175,8 @@ K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailab
|
|
|
171
175
|
旧実装は `CustomObjectsApi` 経由で CRD / named-group のみ、core v1 は 404):
|
|
172
176
|
|
|
173
177
|
0. `config.api_client` があれば(テスト注入、§5.1)それを直接返し、以降の接続解決をスキップ
|
|
174
|
-
1. `config.connection` があればそれ、なければ `Kubernetes::Configuration.default_config`
|
|
175
|
-
2. **K1 橋渡し**: `api_key['authorization']` が `api_key['BearerToken']`
|
|
178
|
+
1. `config.connection` があればそれ、なければ `Kubernetes::Configuration.default_config`。**解決済み config は cached transport の寿命とともに memoize**(#31)— `connected?` はこの memoized config をプローブし、Resource 操作と同じ接続を検査する。暗黙の再解決はない。明示的な再読み込みは `reset!`(§5.1)のみ。解決に失敗した場合(kubeconfig 不正等)は memoize せず次回呼び出しで再試行。**解決と transport 生成は同一クリティカルセクション(`Client::BUILD_LOCK`)で行う**(#31 レビュー指摘: `||=` は非アトミック — スレッド環境で並行する初回操作と初回プローブが双方 `build_configuration` を実行し、後書き勝ちで再び接続不一致を起こし得る。ロック内で config を 1 回だけ解決し、transport 生成・K1 ブリッジ・probe 用 config 取得のすべてが同じ解決結果を共有する。`reset!` も同一ロックを取り reset と build の交錯を防ぐ。プローブの HTTP I/O はロック外)。**HTTP タイムアウト適用(#33)**: 解決済み接続の `timeout` が kruby の未設定値 `0` のとき、`config.http_timeout`(既定 30 秒)を適用する(明示設定は上書きしない。`http_timeout = 0` で無制限にオプトアウト — kruby 側の `0` は「未設定」と区別できないためオプトアウトは gem 側キーで行う。適用は冪等)。kruby は `config.timeout` を**リクエストごとに**読むため、解決後に利用者が直接 `config.connection.timeout` を変更しても以降のリクエストに反映される
|
|
179
|
+
2. **K1 橋渡し**: `api_key['authorization']` が `api_key['BearerToken']` に書かれていなければ複製し、その後の**全 `auth_settings` 呼び出し(= 全リクエスト)で再同期する**(kruby はリクエストごとに `auth_settings` を呼ぶ。in-cluster のトークン更新フックは `api_key_with_prefix("authorization")` 呼び出し時のみ発火するため、初回コピーに固定するとローテーション後のトークンがヘッダに到達せず長時間稼働プロセスで 401 が続く — #29 対応)。**スレッドセーフ**: リクエストが送出するトークンは当該呼び出しで新規取得した `authorization` 値を、その呼び出しの `auth_settings` 戻り値ハッシュに直接書き込むもので、共有 `api_key` への書き戻し後に再読みなし(並行リクエストの書き込みが挟んでも stale トークンを送出しない。共有キーは直接リーダ用の backing copy として更新を継続)。**app 側上書き**: ブリッジが最後に書いた値(`@k8s_rails_bridged_bearer_value` を追跡)と本次書き込み予定値のどちらも現 `BearerToken` と一致しない場合は、アプリが明示的に置き換えた(credential rotation)と判定し**同期を恒久的に停止** — app 側の値が権威となる(client 生成前の明示設定はブリッジ非設置、生成後の明示設定は同期停止で、いずれも上書きしない)
|
|
176
180
|
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 非依存)
|
|
177
181
|
4. **接続レベルの失敗**(DNS 失敗 / タイムアウト / 接続拒否等)は `K8sRails::Unavailable` に変換して `raise`(リトライはしない)。**kruby 1.36.x ではこれらの転送失敗は HTTP ステータスが無いため `Kubernetes::ApiError`(`code == 0`)として surfacing する**(§5.4 の変換表参照)。認可失敗(401/403)は §5.4 により `K8sRails::ApiError`
|
|
178
182
|
|
|
@@ -184,7 +188,7 @@ K8sRails.connected? # → 成功時は true。失敗は K8sRails::Unavailab
|
|
|
184
188
|
`connected?` の解決順序:
|
|
185
189
|
|
|
186
190
|
0. `config.api_client` があれば(テスト注入、§5.1)**I/O なしで `true` を返す**。注入されたトランスポートが接続面そのもののため、実エンドポイントへのプローブは同一注入下の Resource 操作と矛盾する(#18 対応)。クラスタ到達性の真の確認が必要な場合は注入を解除した環境で行う
|
|
187
|
-
1. 以下 `Client.build`
|
|
191
|
+
1. 以下 `Client.build` と同一だが、**解決済み config は memoize されたものを使用**(#31)— 既に client が構築済みなら再解決せず、Resource 操作と同一の接続・認証設定を `/version` プローブで検査する。未構築なら 1 回解決して memoize(以後の操作と共有)。`reset!` が明示的な再読み込み経路(#31)。解決失敗時(kubeconfig 不正等)は memoize せず次回再試行(§5.4 の ConfigError 変換は維持)。**解決と K1 橋渡しは `BUILD_LOCK` 内、プローブの HTTP I/O はロック外**で実行(#31 レビュー指摘: `||=` 非アトミック対策 — 並行する初回操作と初回プローブで解決が厳密に 1 回しか走らないことを決定論的回帰テストで担保)
|
|
188
192
|
|
|
189
193
|
### 5.3 リソース宣言(CRD / built-in 共通)
|
|
190
194
|
|
|
@@ -248,6 +252,11 @@ ClusterIssuer = K8sRails.crd(
|
|
|
248
252
|
- 宣言時に `scope: :cluster` と `namespace:` を併用した場合は `ArgumentError`(設定ミスの fail fast)。
|
|
249
253
|
- transport は kruby の `ApiClient#call_api` 上の統一 REST 層で、namespaced / cluster の区別はパス構築(`/namespaces/{ns}` の有無)のみ。テスト注入スタブは `call_api` の 1 メソッドを実装する(§9)。
|
|
250
254
|
|
|
255
|
+
対象名検証(#28 対応):
|
|
256
|
+
|
|
257
|
+
- `find` / `patch` / `delete`(cluster 版を含む)は**通信前に**無効な対象名を拒否する: `nil` / 空文字列 / `.` / `..` → `ArgumentError`。`nil` 名はパス構築上コレクション URL になり、その DELETE は Delete Collection 呼び出し(一括削除リスク)になるため、fail fast する。`.` / `..` は HTTP クライアントのパス正規化で送信先が変わる(実測で確認)。
|
|
258
|
+
- ゲート順: スコープゲート → readonly ゲート → 対象名検証。全ゲートは計測(§8)より前に発火するため、拒否された操作は計測対象にならない。
|
|
259
|
+
|
|
251
260
|
- **戻り値は常に文字列キーの Hash**(K2 の規約を API 契約として固定)。
|
|
252
261
|
`find` は存在しない場合 `K8sRails::NotFound` を raise(consumer アプリ側が `return nil` にしていたのは
|
|
253
262
|
呼び出し側の都合。gem としては例外が明示的)。「存在しない場合は nil」が欲しい場合は
|
|
@@ -260,7 +269,7 @@ ClusterIssuer = K8sRails.crd(
|
|
|
260
269
|
|
|
261
270
|
```ruby
|
|
262
271
|
K8sRails::Error < StandardError
|
|
263
|
-
├── K8sRails::Unavailable # 接続不能・タイムアウト・DNS
|
|
272
|
+
├── K8sRails::Unavailable # 接続不能・タイムアウト・DNS 失敗・kubeconfig 読み込み失敗等(クラスタ / 接続設定起因)
|
|
264
273
|
├── K8sRails::NotFound # リソース不在(HTTP 404)
|
|
265
274
|
├── K8sRails::ApiError # その他の API エラー(401/403/409/422 等)。#code と #response を保持
|
|
266
275
|
├── K8sRails::ReadOnlyError # readonly: true 宣言で create/patch/delete が呼ばれた(設定ミス)
|
|
@@ -272,6 +281,7 @@ K8sRails::Error < StandardError
|
|
|
272
281
|
| kruby 側 | → gem 側 |
|
|
273
282
|
|---|---|
|
|
274
283
|
| kruby 転送層例外(DNS 失敗 / タイムアウト / 接続拒否等。kruby 1.36.x では **`ApiError`(`code == 0`)** として surfacing) | `Unavailable` |
|
|
284
|
+
| `Kubernetes::ConfigError`(kubeconfig 読み込み / context 解決の失敗。**設定読み込み時** = `Client.build` / `connected?` の設定解決段階、**通信時** = adapter / probe の双方で発生し得る) | `Unavailable`(元例外を `cause` に保持 — 設定解決段階では raw `ConfigError` を公開しない) |
|
|
275
285
|
| その他の `StandardError`(プログラミング/設定ミス、例: `NoMethodError`) | そのまま伝播(変換せず隠さない) |
|
|
276
286
|
| `Kubernetes::ApiError` code 404 | `NotFound` |
|
|
277
287
|
| `Kubernetes::ApiError` その他 | `ApiError`(code / response body を保持) |
|
|
@@ -293,7 +303,7 @@ end
|
|
|
293
303
|
|
|
294
304
|
| 依存 | 制約 | 理由 |
|
|
295
305
|
|---|---|---|
|
|
296
|
-
| Ruby | `>= 3.3, < 4.1` | 下限: kruby 1.36.x が `required_ruby_version ">= 3.3"` を宣言(RubyGems API で実測 2026-
|
|
306
|
+
| Ruby | `>= 3.3, < 4.1`(`!= 4.1.0.preview1` で preview を除外) | 下限: kruby 1.36.x が `required_ruby_version ">= 3.3"` を宣言(RubyGems API で実測 2026-10-18、1.36.0.1〜1.36.5.2 全バージョン)。上限: 「宣言した minor を必ず CI で検証する」方針 — 2026-09-22 に Ruby 4.0.7(stable)と Ruby 3.5.0-preview1(3.5 系で公開済みの唯一のリリース)で全 suite / rubocop を実行し緑を確認(matrix に 4.0.7 / 3.5.0-preview1 追加・v0.2.1)。kruby 1.36.5.2(2026-10-17 公開)でも 3.3.8 / 3.5.0-preview1 / 4.0.7 を再実測(#41)。**Ruby 4.1.0-preview1(2026-10-17 公開)は `!= 4.1.0.preview1` で区間から除外**(PR #42 レビュー指摘・Gem::Requirement の conjunctive `!=` で単一バージョン除外が可能 — 集合和は不要。実測: 4.1.0.preview1 = 不許可 / 4.0.7・3.5.0.preview1 = 許可 / 4.1 = 不許可。Gem.ruby_version は preview でも prerelease 形式 `4.1.0.preview1` を報告するため `< 4.1` 単独では除外されない実測)。matrix に追加できない理由: preview1 環境で `bundle install` が RubyGems/Bundler 既知の `uninitialized constant Pathname::SEPARATOR_PAT`(Bundler 4.0.23 でも再現)で失敗・ローカル実測 2026-10-17。rubygems の修正後または 4.1 stable リリース時に除外を外して matrix 追加。**注意: `!=` は 1 バージョンのみ除外** — 将来の preview(4.1.0.preview2 等)は再び区間内に現れるためその都度 `!=` を追加するか 4.1 stable 時に上限を `< 4.2` に上げる。3.5 stable / 4.1 stable がリリースされたら matrix を追加して検証(§10 / §13) |
|
|
297
307
|
| `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` 探索順序)を済ませてから明示的に上げ替える |
|
|
298
308
|
| `activesupport` | **任意**(`>= 7.0`) | `defined?(ActiveSupport::Notifications)` でガード(計測のみ、§8)。Rails 無し環境(Cron スクリプト等)でも動作する必要がある — レスポンスの文字列キー化(K2)はこれに依存せず、gem 内部の純 Ruby 変換で担う(§5.2) |
|
|
299
309
|
| `rspec` / `rubocop` | 開発依存 | spec / lint |
|
|
@@ -311,7 +321,10 @@ end
|
|
|
311
321
|
`resource.rb` は
|
|
312
322
|
`K8sRails.client.get(group, version, ns, plural, name)` のような **gem 内部 API** だけを使う。
|
|
313
323
|
- kruby 上げ替え時の作業は (1) `ApiClient#call_api` のシグネチャ・`opts` 形式(header_params /
|
|
314
|
-
query_params / body / auth_names / return_type)の確認、(2) K1
|
|
324
|
+
query_params / body / auth_names / return_type)の確認、(2) K1 橋渡しの要否と機構の確認
|
|
325
|
+
(kruby 1.36.x 依存の事実: `auth_settings` が `BearerToken` を読む・in-cluster の更新フックが
|
|
326
|
+
`api_key_with_prefix("authorization")` 呼び出し時のみ発火する — #29 のブリッジは `auth_settings`
|
|
327
|
+
呼び出しごとに同期する設計のため、両事実が崩れた場合は再設計)、
|
|
315
328
|
(3) **`Kubernetes::Configuration.default_config` の探索順序確認**
|
|
316
329
|
(kruby 1.36.x の loader 実装順は **KUBECONFIG → `~/.kube/config` → in-cluster** で in-cluster が
|
|
317
330
|
最後。README / 設計書 / 設定コメントがこの順序を明記しているため、loader が変わった場合は
|
|
@@ -332,6 +345,19 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
|
|
|
332
345
|
`operation` は **symbol**。例外は notification を発した上で **そのまま raise**
|
|
333
346
|
される(計測は swallow しない)。
|
|
334
347
|
|
|
348
|
+
- **API 処理は `ActiveSupport::Notifications.instrument` のブロック内で実行される**(#32)。
|
|
349
|
+
標準の開始・終了時刻が実処理を挟むため、標準の
|
|
350
|
+
`ActiveSupport::Notifications::Event#duration` が実処理時間を反映する(旧実装は
|
|
351
|
+
実処理の後にブロックなしで発行していたため標準 duration はほぼ 0 だった)。購読者が
|
|
352
|
+
受信するのは finish イベント(裸名 `k8s-rails.request` を購読した場合、1 呼び出しに
|
|
353
|
+
つき 1 イベント — AS 8.1.3.1 実測)で、`status` / `duration_ms` はブロックの
|
|
354
|
+
`ensure` 内で設定されるため finish イベントの payload に常に含まれる。
|
|
355
|
+
失敗時は `status` に加えて ActiveSupport 標準の `exception`
|
|
356
|
+
(`[クラス名, メッセージ]`)と `exception_object`(例外オブジェクト・再送出される
|
|
357
|
+
元例外と同一)が payload に付与される。K8sRails 例外階層外の例外
|
|
358
|
+
(プログラミングエラー等)も `status` フォールバック `"api_error"` と標準メタデータの
|
|
359
|
+
両方が付く。
|
|
360
|
+
|
|
335
361
|
- Rails アプリではこの notification を `ActiveSupport::Notifications` /
|
|
336
362
|
`log_subscription` で拾える(ログ・ダッシュボード表示)。
|
|
337
363
|
- ActiveSupport 無い環境(または `instrumentation: false`)では no-op。
|
|
@@ -345,7 +371,7 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
|
|
|
345
371
|
| 設定 | spec 間で `K8sRails.reset!` | 宣言の破棄・再接続 |
|
|
346
372
|
| 集積(任意) | GitHub Actions で **kind**(または既存 microk8s に接続するジョブ)で実クラスタ E2E | v0.1 の必須ではない。**推奨**: consumer アプリ移行時の検証を兼ねる |
|
|
347
373
|
|
|
348
|
-
- 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.
|
|
374
|
+
- 本設計では CI は `rspec` + `rubocop` のみを必須とし、kind E2E は v0.5 で
|
|
349
375
|
GitHub Actions の追加として扱う(実クラスタへの接続 CI はネットワーク依存のため採用しない)。
|
|
350
376
|
|
|
351
377
|
## 10. リリース計画
|
|
@@ -355,8 +381,9 @@ k8s-rails.request payload: { operation: :list, group:, version:, plural:, names
|
|
|
355
381
|
| **v0.1** | §5 の公開 API(CRD 宣言 / list / find / create / patch / 例外 / 計測 / スタブテスト)+ README | rspec 全緑 + **consumer アプリの K8s サービスを `k8s-rails` に移行して動作確認**(§12) |
|
|
356
382
|
| **v0.2** | #16–#19 の公開 API 拡充・修正(cluster-scoped CRD `scope:` + `*_cluster` メソッド、`configure` のアトミック契約、`connected?` の注入契約、kruby loader 探索順序の修正)。設計書 v0.1.12(案) | rspec 全緑(クラスタ不要)+ 公開 gem push(v0.1.0 と同導線) |
|
|
357
383
|
| **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 |
|
|
359
|
-
| v0.5 |
|
|
384
|
+
| **v0.4** | post-v0.3 バッチ: #28(対象名検証による一括削除防止 — `find` / `patch` / `delete`(cluster 版を含む)が `nil` / 空文字列 / `.` / `..` を通信前に `ArgumentError` で拒否)、#29(K1 ブリッジを全リクエストで再同期 — in-cluster ServiceAccount トークンローテーション反映・app の明示設定は上書きしない)、#30(kubeconfig 読み込み失敗を `Unavailable` に統一 — 元例外を `cause` 保持)、#31(`connected?` が Resource 操作と同一キャッシュ接続を検査・初回解決 + K1 ブリッジ + transport 生成を直列化)、#32(通知を ActiveSupport ブロック形式に移行 — 標準 `exception` / 実処理時間)、#33(**既定 HTTP タイムアウト 30 秒 — 挙動変更**。明示値は保持・`http_timeout = 0` でオプトアウト)、#41(kruby 1.36.5.2 実測検証・Ruby 4.1.0-preview1 を宣言範囲から `!=` で除外)。設計書 v0.1.23(案) | rspec 全緑(クラスタ不要)+README 実行例の実クラスタ検証(#33・#41)+公開 gem push(v0.1.0 と同導線) |
|
|
385
|
+
| v0.5 | watch(`watch` メソッド、kruby の watch サポート上)、kind E2E の CI 化、Ruby 3.5 stable / 4.1 stable の matrix 追加(stable リリースされたら検証の上追加。Ruby 3.5 preview / 4.0 は v0.2.1 で対応済み。4.1.0-preview1 はツールチェーン不具合(Bundler `Pathname::SEPARATOR_PAT`)で保留・宣言範囲から `!=` 除外済み — #41)、リトライポリシー | v0.4 運用のフィードバック |
|
|
386
|
+
| v0.6 | (展望)複数クラスタ(ネームスペース化された client 集合) | — |
|
|
360
387
|
|
|
361
388
|
v0.1 の milestone 分割(開発セッション向けのタスク単位目安):
|
|
362
389
|
|
|
@@ -373,9 +400,16 @@ v0.1 の milestone 分割(開発セッション向けのタスク単位目安
|
|
|
373
400
|
形で、ポーリング置き換えのユースケース(consumer アプリの 30 秒ポーリング等)から設計する。
|
|
374
401
|
- **複数クラスタ**: `K8sRails.cluster("prod") { ... }` のような名前付き client 集合。
|
|
375
402
|
現時点で需要がないため v0.1 では単一クラスタ。
|
|
376
|
-
- **retries / timeout**: kruby
|
|
377
|
-
|
|
378
|
-
|
|
403
|
+
- **retries / timeout**: **timeout は実装済み(#33)** — kruby 1.36.x 既定の
|
|
404
|
+
`timeout = 0`(libcurl 上**無制限待ち**)に対し、gem は接続解決時に
|
|
405
|
+
`config.http_timeout`(既定 30 秒)を未設定のときのみ適用し、明示設定は
|
|
406
|
+
上書きしない(§5.1 / §5.2 / README)。
|
|
407
|
+
**リトライ**: gem 独自のバックオフは持たない(Rails 側の middleware /
|
|
408
|
+
sidekiq retry で吸収するのが慣習)。kruby の `retry_configuration`
|
|
409
|
+
(`max_retries > 0` で 429 / 5xx を指数バックオフで再試行)は利用者側の
|
|
410
|
+
表面で、README に**総待ち時間が `max_retries` に依存する**点を含めて説明
|
|
411
|
+
済み(タイムアウト自体は kruby 実装上リトライ対象外 — `retry_request?` は
|
|
412
|
+
`timed_out?` で即 false)。
|
|
379
413
|
- **OpenTelemetry**: `instrumentation` を notification 経由にしているため、
|
|
380
414
|
OTel instrumentation を別途足せる状態に留める(v0.1 で実装しない)。
|
|
381
415
|
|
|
@@ -426,17 +460,31 @@ PR の差分を最小化)。
|
|
|
426
460
|
gemspec の宣言範囲(`>= 3.3, < 4.1`)で公開済みの Ruby を matrix で検証:
|
|
427
461
|
3.3.0(下限・kruby 1.36.x の `>= 3.3`)・3.3.8(開発)・
|
|
428
462
|
3.4.10(3.4 系の最新 stable)・3.5.0-preview1(3.5 系の唯一の公開
|
|
429
|
-
リリース・2026-09-22 にローカルで全 suite / rubocop
|
|
463
|
+
リリース・2026-09-22 にローカルで全 suite / rubocop 検証済み、
|
|
464
|
+
kruby 1.36.5.2 で 2026-10-18 再検証)・
|
|
430
465
|
4.0.7(4.0 系の最新 stable・2026-09-22 にローカルで全 suite /
|
|
431
|
-
rubocop
|
|
432
|
-
|
|
433
|
-
4.1
|
|
434
|
-
|
|
435
|
-
|
|
466
|
+
rubocop 検証済み、kruby 1.36.5.2 で 2026-10-18 再検証)。
|
|
467
|
+
**Ruby 4.1.0-preview1**(2026-10-17 公開・ruby/ruby タグ実測)は
|
|
468
|
+
`!= 4.1.0.preview1` で**宣言区間から除外**(#41・PR #42 レビュー指摘):
|
|
469
|
+
Gem.ruby_version は preview でも prerelease 形式(実測:
|
|
470
|
+
4.1.0-preview1 → `4.1.0.preview1`)を報告するため `< 4.1` 単独では
|
|
471
|
+
除外されない。conjunctive `!=` で名指し除外する(実測:
|
|
472
|
+
4.1.0.preview1 = 不許可 / 4.0.7・3.5.0.preview1 = 許可 / 4.1 = 不許可)。
|
|
473
|
+
matrix に追加できない理由: preview1 環境では `bundle install` が
|
|
474
|
+
RubyGems/Bundler 既知の
|
|
475
|
+
`uninitialized constant Pathname::SEPARATOR_PAT`(Bundler 4.0.23 でも
|
|
476
|
+
再現・ローカル実測 2026-10-17)で失敗し、検証基盤が動かない。
|
|
477
|
+
rubygems の修正後または 4.1 stable リリース時に除外を外して
|
|
478
|
+
matrix へ追加する。`!=` は名指した 1 バージョンのみ除外するため、
|
|
479
|
+
将来の preview(4.1.0.preview2 等)はその都度 `!=` を追加する。
|
|
480
|
+
**宣言範囲を常に matrix がカバーする**こと(RubyGems の version
|
|
481
|
+
requirement 配列は conjunctive(AND)で `!=` による単一バージョン除外が
|
|
482
|
+
可能。集合和は不要。未検証 minor を区間で挟み込む形(`< 5.0` 等)は
|
|
483
|
+
取らない — matrix 追加と範囲拡大をセットで行う。
|
|
436
484
|
3.5 stable / 4.1 系等 stable 化された新リリースが出たら
|
|
437
485
|
matrix への追加を忘れないこと)。
|
|
438
486
|
テストはクラスタ不要(§9・スタブ注入)のため v0.1 は runner 上のユニットのみ。
|
|
439
|
-
kind / 実クラスタ E2E の CI 化は v0.
|
|
487
|
+
kind / 実クラスタ E2E の CI 化は v0.5 対象(§9・§10)
|
|
440
488
|
- `README` に「kruby pin」「対応 k8s バージョン(実測 v1.33.x で検証済み)」「K1 橋渡しの背景」
|
|
441
489
|
を明記する(検索でヒットする重要な注意点のため)
|
|
442
490
|
|
|
@@ -461,3 +509,10 @@ PR の差分を最小化)。
|
|
|
461
509
|
| 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
510
|
| 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 に修正 | 実装反映済み |
|
|
463
511
|
| 0.1.16 | 2026-09-22 | v0.3.1 リリース準備: gemspec の `summary` / `description` 文言更新(PR #25)を**公開 gem として反映するための docs みのパッチリリース**。公開済み gem の文言は公開後に変更できない(RubyGems に gem メタの編集 API / UI なし)ため、0.3.1 の公開で RubyGems ページ表示が新文言に切り替わる。①`VERSION` を 0.3.0 → **0.3.1**(version.rb + version spec)②CHANGELOG に 0.3.1 セクション追加③README の `VERSION` 例・§3 ツリーコメントを同期。**コード・挙動の変更なし** | 実装反映済み |
|
|
512
|
+
| 0.1.17 | 2026-10-13 | Issue #28–#29 対応(両方 priority:high・v0.4 予定): ①#28 `find` / `patch` / `delete`(cluster 版を含む)が無効な対象名(`nil` / 空文字列 / `.` / `..`)を**通信前に** `ArgumentError` で拒否(従来 `delete(nil)` は Delete Collection URL を生成し一括削除リスク、`.` / `..` は HTTP クライアントのパス正規化で送信先が変化。ゲート順は scope → readonly → 名検証・計測より前)。§5.3 / README に明記②#29 K1 ブリッジを「初回コピー」から「全 `auth_settings` 呼び出し(全リクエスト)で再同期」に改訂: kruby 1.36.x の in-cluster 更新フックは `api_key_with_prefix("authorization")` 呼び出し時のみ発火するため、初回コピー固定では ServiceAccount トークンローテーションが反映されず長時間稼働プロセスで 401 が続く問題を実修正。解決済み config の singleton class に sync モジュールを prepend。ユーザー明示設定の `BearerToken` はブリッジ・上書きしない(区別マーカー付き)。§5.2 / §7(上げ替えチェックリスト)/ README を同期③PR #36 レビュー対応: (a) 送出トークンを「共有 `api_key` 書き戻し後 `super` が再読みなし」に変更 — `super` が先に per-call settings を構築し、当該呼び出しで新規取得した `authorization` 値をその戻り値ハッシュに直接書き込む(スレッド環境で並行リクエストの stale 書き込みが挟んでも stale トークンを送出しない)(b) ブリッジが最後に書いた値を追跡(`@k8s_rails_bridged_bearer_value`)し、app が client 生成後に `BearerToken` を置き換えた場合は同期を恒久的に停止して app 側の値を権威とする(「上書きしない」保証を生成後の上書きまで拡張)| 実装反映済み |
|
|
513
|
+
| 0.1.18 | 2026-10-14 | Issue #32 対応: `k8s-rails.request` 通知を **ActiveSupport ブロック形式に移行**(API 処理を `ActiveSupport::Notifications.instrument` のブロック内で実行)。①標準 start/finish が実処理を挟み `Event#duration` が実処理時間を反映(旧実装は実処理後に発行し標準 duration はほぼ 0 だった)②失敗時に標準 `exception` / `exception_object` メタデータが付与(`status` / `duration_ms` はブロック `ensure` で設定され finish イベントに常に含まれる。元例外はそのまま再送出)③既存 payload 契約(operation / 座標 / duration_ms / status 列挙)・no-op・戻り値契約は不変。AS 8.1.3.1 実測: 裸名購読者は finish イベント 1 件のみ受信(§8)。※版数 0.1.17 は PR #36(#28/#29)が使用 | 実装反映済み |
|
|
514
|
+
| 0.1.19 | 2026-10-14 | Issue #30 対応: 接続設定の解決・クライアント生成段階(`Client.build` / `connected?` → `build_configuration`)での `Kubernetes::ConfigError` を `K8sRails::Unavailable` に**統一**(元例外を `cause` に保持 — rescue 内 raise の自動 cause。通信時の adapter / probe 変換と同一契約で、raw `ConfigError` は公開されない)。§5.4 変換規則表に ConfigError 行を追加・§5.4 例外ツリーの説明を同期・README 例外ツリーも同期。rescue は `ConfigError` のみの狭い形で、`NoMethodError` 等のプログラミングエラーは変換せず伝播。回帰テスト: 不正 context(kubeconfig の `current-context: missing`)で `Resource.list` / `connected?` の双方が `Unavailable` + `cause` = ConfigError | 実装反映済み |
|
|
515
|
+
| 0.1.20 | 2026-10-14 | Issue #31 対応: `connected?` が **Resource 操作と同じキャッシュ済み接続を検査**するように改訂 — 解決済み config を cached transport の寿命とともに memoize(`@resolved_connection_config`)し、`Client.build` と `connected?` の双方が同一解決結果を使用。旧実装は `connected?` が毎回 `build_configuration` を再解決し、設定解決結果が変わった場合にプローブは新しい接続 (B) を検査する一方 Resource 操作は旧接続 (A) を使い続ける不整合があった。暗黙の再解決はせず、明示的な再読み込み経路は `reset!`(§5.1)。解決失敗時は memoize せず次回再試行。§5.2(build 手順・connected? 解決順序)と README Connectivity check を同期。回帰テスト: `default_config` が接続 A → B の順で返す環境で (1) probe が A に対して発行されること(B 実測 0 リクエスト・2 つのローカル HTTP サーバで実測)、(2) `reset!` 後に B へ切り替わること。④PR #39 レビュー対応(Codex P2 + Copilot 同一指摘): memoize(`||=`)は非アトミックで、スレッド環境で初回 Resource 操作と初回 `connected?` が並行すると双方が `build_configuration` を実行し後書き勝ちで再び接続不一致を起こし得る → `Client::BUILD_LOCK`(Mutex)を導入し、初回の config 解決・K1 橋渡し・transport 生成を同一クリティカルセクションに閉じた(プローブの HTTP I/O はロック外)。`reset!` も同一ロックで build との交錯を防ぐ。決定論的並行回帰テスト: 解決スタブが入口を観測できるよう待ち合わせし、(a) 解決呼び出しが厳密に 1 回(ロック待ちの 2 スレッド目は入口に入れない)(b) op/probe がともに接続 A へ発行(B 実測 0 リクエスト)を検証 — 修正前 lib でこの spec は失敗(revert-proof) | 実装反映済み |
|
|
516
|
+
| 0.1.21 | 2026-10-15 | Issue #33 対応: **既定の HTTP タイムアウト導入(挙動変更・#33)**。kruby 1.36.x の既定 `timeout = 0` は libcurl 上**無制限待ち**で、応答しない API が Rails のリクエスト / ジョブを占有し得る。`config.http_timeout`(既定 30 秒)を新設し、接続解決時に解決済み接続の `timeout` が未設定値 `0` のときのみ適用(明示設定は上書きしない。`http_timeout = 0` で無制限にオプトアウト — kruby 側 `0` は未設定と区別できないためオプトアウトは gem 側キーで行う。適用は冪等)。`Resource` 操作と `connected?` の双方に同一適用(§5.2 共通経路)。非負 Integer / `nil` 以外は `ArgumentError`(Float は libcurl で `0`=無制限に丸められるため fail-fast)。README に実行可能な設定例(kruby 読み込み含む)・kruby 既定 `0` = 無制限であること・kruby `retry_configuration` 使用時の総待ち時間が `max_retries` に依存する点(タイムアウト自体は kruby 実装上リトライ対象外)を追記。§11 の「retries / timeout」検討事項を実装済み方針に更新。実測: 3 秒遅延ループバックに対し `timeout = 1` で Resource 操作と `connected?` が双方 ~1.01 秒で `Unavailable`。※版数 0.1.20 はオープン PR #39(#31)が使用(並行 PR の事前割当規約) | 実装反映済み |
|
|
517
|
+
| 0.1.22 | 2026-10-18 | Issue #41 対応: kruby 1.36.5.2(2026-10-17 公開)への追従検証 — §6 pin(`~> 1.36.0`)は 1.36.5.2 を既に許容するため **pin 変更なし**。1.36.2.1 と 1.36.5.2 の gem lib 差分を実測で確認(version.rb の VERSION 文字列のみ・kruby CHANGELOG の記載と一致し OpenAPI・挙動契約は不変)→ §7 の 3 事実(call_api シグネチャ / K1 橋渡し事実 / loader 探索順序)は lib 同一のため機械的に成立。全 suite(132 examples / 0 failures・22 files no offenses)を Ruby 3.3.8 / 3.5.0-preview1 / 4.0.7 で再実測。kruby 1.36.5.2 の新ランタイム依存 `logger`(Ruby 4.0 での bundled gem 昇格対応)は transitive で解決・consumer 変更不要。matrix 未追加の理由(preview1 環境で `bundle install` が RubyGems/Bundler 既知の `uninitialized constant Pathname::SEPARATOR_PAT`(Bundler 4.0.23 でも再現)で失敗・ローカル rbenv 4.1.0-preview1 実測)は gemspec / README / §6・§13 / CI コメントに記録。**PR #42 レビュー指摘で修正**: 初版の「RubyGems は集合和を表現できないため除外不能」は誤り — `Gem::Requirement` の conjunctive `!=` で単一バージョン除外は可能(実測: 4.1.0.preview1 不許可 / 4.0.7・3.5.0.preview1 許可 / 4.1 不許可)で、gemspec を `">= 3.3"` / `"< 4.1"` / `"!= 4.1.0.preview1"` の 3 件の conjunctive requirement に改訂。Gem.ruby_version は preview でも prerelease 形式(`4.1.0.preview1`)を報告し `< 4.1` 単独では除外されない実測も追記。`!=` は 1 バージョンのみ除外のため将来の preview はその都度追加または 4.1 stable 時に上限 `< 4.2` へ。§6 / §10 / §13 / gemspec / README / CI コメント / CHANGELOG を同期 | 実装反映済み |
|
|
518
|
+
| 0.1.23 | 2026-10-19 | v0.4.0 リリース準備: ①`VERSION` を 0.3.1 → **0.4.0**(version.rb + version spec。#33 が既定タイムアウト導入という挙動変更を含むため minor バンプ — patch ではない)②CHANGELOG の `Unreleased`(#28–#33・#41 分)を **0.4.0** に確定(タグ時点で Unreleased バケットを残さない)③README の install pin(`~> 0.3` → `~> 0.4`)・`VERSION` 例・Roadmap 見出し(v0.4+ → v0.5+)同期と Ruby 4.1 stable 時の解除手順(`!=` 除外の削除・`< 4.2` 拡張・matrix 追加)追記④§10 リリース計画表で v0.4 行を「実際にリリースした内容」(#28–#33・#41)に書き換え、旧 v0.4 予定内容(watch / kind E2E CI 化 / matrix 追加 / リトライポリシー)を v0.5 へ、複数クラスタ展望を v0.6 へ移動⑤§2.2 / §9 / §13 / test.yml コメントの旧版参照を v0.5 に修正(test.yml コメントは v0.3 表記のまま残存していたため併せて修正) | 実装反映済み |
|
data/lib/k8s-rails.rb
CHANGED
|
@@ -80,17 +80,24 @@ module K8sRails
|
|
|
80
80
|
# The notification payload is
|
|
81
81
|
# { operation:, group:, version:, plural:, namespace:, duration_ms:,
|
|
82
82
|
# status: "ok" | "unavailable" | "api_error" }.
|
|
83
|
-
# The
|
|
84
|
-
#
|
|
83
|
+
# The API call runs INSIDE the ActiveSupport notification block (#32), so
|
|
84
|
+
# the standard start/finish times bracket the real processing
|
|
85
|
+
# (`Event#duration` measures it) and failures carry the standard
|
|
86
|
+
# `exception` / `exception_object` metadata alongside the custom `status`.
|
|
87
|
+
# `status` / `duration_ms` are set in the block's `ensure` so the finish
|
|
88
|
+
# event already carries them. The block's return value always flows
|
|
89
|
+
# through unchanged. No-op when `config.instrumentation` is false or
|
|
90
|
+
# ActiveSupport is not loaded.
|
|
85
91
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- fixed §8 wrapper
|
|
86
92
|
def instrument(operation, metadata)
|
|
87
93
|
return yield unless instrumentation_enabled?
|
|
88
94
|
|
|
89
95
|
payload = { operation: operation }.merge(metadata)
|
|
90
96
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
91
|
-
|
|
97
|
+
ActiveSupport::Notifications.instrument("k8s-rails.request", payload) do
|
|
92
98
|
result = yield
|
|
93
99
|
payload[:status] = "ok"
|
|
100
|
+
result
|
|
94
101
|
rescue K8sRails::Unavailable
|
|
95
102
|
payload[:status] = "unavailable"
|
|
96
103
|
raise
|
|
@@ -101,12 +108,11 @@ module K8sRails
|
|
|
101
108
|
# Fallback for exceptions outside the K8sRails hierarchy (e.g. a
|
|
102
109
|
# programming error like NoMethodError from a malformed stub): keep
|
|
103
110
|
# the documented status enum (ok/unavailable/api_error) intact while
|
|
104
|
-
# re-raising the original exception
|
|
111
|
+
# re-raising the original exception (ActiveSupport attaches its
|
|
112
|
+
# standard `exception` / `exception_object` metadata on this path).
|
|
105
113
|
payload[:status] ||= "api_error"
|
|
106
114
|
payload[:duration_ms] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1_000).round(2)
|
|
107
|
-
ActiveSupport::Notifications.instrument("k8s-rails.request", payload)
|
|
108
115
|
end
|
|
109
|
-
result
|
|
110
116
|
end
|
|
111
117
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
112
118
|
|
data/lib/k8s_rails/client.rb
CHANGED
|
@@ -34,6 +34,14 @@ module K8sRails
|
|
|
34
34
|
# `Kubernetes::Configuration` and builds an in-memory `ApiClient`. The first
|
|
35
35
|
# real call is where DNS/timeout/TLS can fail.
|
|
36
36
|
class Client
|
|
37
|
+
# Serializes the first connection resolution + transport build (and
|
|
38
|
+
# `reset!`) so a concurrent Resource operation and `connected?` probe
|
|
39
|
+
# cannot resolve DIFFERENT configs (kubeconfig changing between the two
|
|
40
|
+
# resolutions would recreate the #31 probe/operation mismatch in a
|
|
41
|
+
# threaded process — `||=` alone is not atomic). The probe's HTTP I/O
|
|
42
|
+
# runs OUTSIDE the lock: only first-time resolution pays for it.
|
|
43
|
+
BUILD_LOCK = Mutex.new
|
|
44
|
+
|
|
37
45
|
class << self
|
|
38
46
|
# Return the shared transport, building it on first call (lazy connect).
|
|
39
47
|
#
|
|
@@ -43,16 +51,20 @@ module K8sRails
|
|
|
43
51
|
# 1. `config.connection` if set, else `Kubernetes::Configuration.default_config`
|
|
44
52
|
# 2. K1 bridge: duplicate `api_key['authorization']` into
|
|
45
53
|
# `api_key['BearerToken']` (kruby 1.36 in-cluster/KUBECONFIG write the
|
|
46
|
-
# token under 'authorization' but auth_settings reads 'BearerToken')
|
|
54
|
+
# token under 'authorization' but auth_settings reads 'BearerToken'),
|
|
55
|
+
# and keep the bridge in sync on every `auth_settings` call so a
|
|
56
|
+
# rotated ServiceAccount token is picked up (#29).
|
|
47
57
|
# 3. Build `ApiClient` (in-memory, no I/O).
|
|
48
58
|
# 4. Wrap in a StringKeyedAdapter that normalizes responses (K2) and
|
|
49
59
|
# converts kruby errors to K8sRails exceptions.
|
|
50
60
|
def build
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
BUILD_LOCK.synchronize do
|
|
62
|
+
@build ||= if (injected = K8sRails.config.api_client)
|
|
63
|
+
StringKeyedAdapter.new(injected)
|
|
64
|
+
else
|
|
65
|
+
StringKeyedAdapter.new(build_api_client)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# Lightweight connectivity probe (design §5.2). Performs one lightweight
|
|
@@ -67,16 +79,24 @@ module K8sRails
|
|
|
67
79
|
def connected?
|
|
68
80
|
return true if K8sRails.config.api_client
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
# Resolution + K1 bridge happen under BUILD_LOCK (one critical section
|
|
83
|
+
# with the transport build); only the probe's HTTP I/O stays outside.
|
|
84
|
+
config = BUILD_LOCK.synchronize do
|
|
85
|
+
resolved = resolved_connection_config
|
|
86
|
+
bridge_bearer_token(resolved)
|
|
87
|
+
resolved
|
|
88
|
+
end
|
|
74
89
|
VersionApiProbe.new(config).probe
|
|
75
90
|
end
|
|
76
91
|
|
|
77
|
-
# Reset cached connection + transport (test support, §5.1).
|
|
92
|
+
# Reset cached connection + transport (test support, §5.1). Also drops
|
|
93
|
+
# the cached resolved config so the next build/probe re-resolves (#31).
|
|
94
|
+
# Takes BUILD_LOCK so a reset cannot interleave with a build.
|
|
78
95
|
def reset!
|
|
79
|
-
|
|
96
|
+
BUILD_LOCK.synchronize do
|
|
97
|
+
@build = nil
|
|
98
|
+
@resolved_connection_config = nil
|
|
99
|
+
end
|
|
80
100
|
end
|
|
81
101
|
|
|
82
102
|
# Convert a kruby ApiError to the K8sRails exception tree (K3, §5.4).
|
|
@@ -105,26 +125,172 @@ module K8sRails
|
|
|
105
125
|
end
|
|
106
126
|
|
|
107
127
|
# Build a lazy in-memory ApiClient from the resolved configuration.
|
|
128
|
+
# Runs while BUILD_LOCK is held (from `build`) so the transport and the
|
|
129
|
+
# cached config are selected in one critical section.
|
|
108
130
|
def build_api_client
|
|
109
|
-
config =
|
|
131
|
+
config = resolved_connection_config
|
|
110
132
|
bridge_bearer_token(config)
|
|
111
133
|
Kubernetes::ApiClient.new(config)
|
|
112
134
|
end
|
|
113
135
|
|
|
136
|
+
# The resolved connection config, cached for the lifetime of the cached
|
|
137
|
+
# transport (#31): `connected?` and Resource operations MUST inspect /
|
|
138
|
+
# use the SAME connection — re-resolving the config would let the probe
|
|
139
|
+
# validate connection B while ops still send to connection A. MUST be
|
|
140
|
+
# read/written while holding BUILD_LOCK so a concurrent first build and
|
|
141
|
+
# first probe resolve the config EXACTLY ONCE (review: `||=` alone is
|
|
142
|
+
# not atomic). There is no implicit reload; `reset!` (test support,
|
|
143
|
+
# §5.1) is the explicit reload path. Cached only on success — a failed
|
|
144
|
+
# resolution (e.g. a broken kubeconfig) is re-attempted on the next call.
|
|
145
|
+
def resolved_connection_config
|
|
146
|
+
@resolved_connection_config ||= build_configuration
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Resolve the connection configuration. The kubeconfig load happens HERE
|
|
150
|
+
# (kruby's default_config reads KUBECONFIG -> ~/.kube/config ->
|
|
151
|
+
# in-cluster, design §5.2), so a broken kubeconfig / missing context
|
|
152
|
+
# surfaces at build time — BEFORE any request. Convert it to the public
|
|
153
|
+
# exception tree with the original error as `cause` (#30), matching the
|
|
154
|
+
# transport-time conversion in StringKeyedAdapter#request /
|
|
155
|
+
# VersionApiProbe#probe. The rescue is deliberately narrow: programming
|
|
156
|
+
# errors must not be swallowed (design §5.4).
|
|
157
|
+
#
|
|
158
|
+
# The resolved configuration is where the HTTP timeout policy is
|
|
159
|
+
# applied (apply_http_timeout), so Resource operations (build_api_client)
|
|
160
|
+
# and the `connected?` probe carry the SAME value (#33).
|
|
114
161
|
def build_configuration
|
|
115
|
-
K8sRails.config.connection || Kubernetes::Configuration.default_config
|
|
162
|
+
config = K8sRails.config.connection || Kubernetes::Configuration.default_config
|
|
163
|
+
apply_http_timeout(config)
|
|
164
|
+
config
|
|
165
|
+
rescue Kubernetes::ConfigError => e
|
|
166
|
+
raise Unavailable, "K8s に接続できません: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# HTTP timeout policy (design §5.1 / #33). kruby 1.36.x defaults
|
|
170
|
+
# `Configuration#timeout` to 0, which means NO timeout — an
|
|
171
|
+
# unresponsive API would hold a Rails request / job forever. Apply the
|
|
172
|
+
# configured `http_timeout` (K8sRails::Configuration, default 30s) ONLY
|
|
173
|
+
# when the resolved configuration still carries kruby's unset value
|
|
174
|
+
# (0): an explicitly set `config.connection.timeout` (any non-zero
|
|
175
|
+
# value) is never overridden. `http_timeout = 0` opts out entirely and
|
|
176
|
+
# restores kruby's unlimited default — kruby's own `timeout = 0` is
|
|
177
|
+
# indistinguishable from "never set", so it is treated as unset and the
|
|
178
|
+
# opt-out lives on the K8sRails knob instead.
|
|
179
|
+
#
|
|
180
|
+
# Idempotent: after the first application the resolved timeout is
|
|
181
|
+
# non-zero, so later resolutions skip (the value reads as explicit).
|
|
182
|
+
#
|
|
183
|
+
# The "unset" test is kruby's own 0, compared in its ORIGINAL type —
|
|
184
|
+
# never via to_i coercion, which would read an explicit fractional
|
|
185
|
+
# value (0.5) as unset and overwrite it. A non-Numeric (or nil)
|
|
186
|
+
# connection timeout is likewise treated as explicitly set and
|
|
187
|
+
# preserved; the Integer restriction is the http_timeout knob's own
|
|
188
|
+
# contract (validate_http_timeout!), not the connection's.
|
|
189
|
+
def apply_http_timeout(config)
|
|
190
|
+
requested = K8sRails.config.http_timeout
|
|
191
|
+
validate_http_timeout!(requested)
|
|
192
|
+
return if requested&.zero? || !connection_timeout_unset?(config)
|
|
193
|
+
|
|
194
|
+
config.timeout = requested || Configuration::DEFAULT_HTTP_TIMEOUT
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# True only when the resolved connection's timeout still carries
|
|
198
|
+
# kruby's unset value — numeric 0, its initialize default. A nil or
|
|
199
|
+
# non-Numeric timeout is never kruby's produced value, so it reads as
|
|
200
|
+
# explicitly set by the app and is preserved untouched.
|
|
201
|
+
def connection_timeout_unset?(config)
|
|
202
|
+
value = config.timeout
|
|
203
|
+
value.is_a?(Numeric) && value.zero?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# `http_timeout` accepts nil (built-in default) or a non-negative
|
|
207
|
+
# Integer (0 = opt out, N >= 1 = seconds). Rejecting non-Integers is
|
|
208
|
+
# fail-fast on purpose: a Float like 0.5 would be truncated by
|
|
209
|
+
# typhoeus/libcurl to CURLOPT_TIMEOUT 0 = unlimited — silently
|
|
210
|
+
# recreating the unbounded wait this policy exists to prevent.
|
|
211
|
+
def validate_http_timeout!(value)
|
|
212
|
+
return if value.nil? || (value.is_a?(Integer) && value >= 0)
|
|
213
|
+
|
|
214
|
+
raise ArgumentError,
|
|
215
|
+
"http_timeout must be a non-negative Integer or nil (got #{value.inspect})"
|
|
116
216
|
end
|
|
117
217
|
|
|
118
218
|
# K1 bridge (kruby 1.36.x quirk): InClusterConfig writes the Bearer token
|
|
119
219
|
# to `api_key['authorization']` (and the KUBECONFIG path can too), but
|
|
120
220
|
# `Configuration#auth_settings` reads `api_key['BearerToken']` for the
|
|
121
221
|
# `Authorization` header. Without the bridge the header is empty → 401.
|
|
122
|
-
#
|
|
222
|
+
#
|
|
223
|
+
# The bridge must NOT freeze the first copy (#29): kruby's in-cluster
|
|
224
|
+
# refresh hook only runs inside `api_key_with_prefix("authorization")`
|
|
225
|
+
# (it re-reads the token file once the refresh deadline passes), and
|
|
226
|
+
# normal header generation never queries that key. So after the initial
|
|
227
|
+
# copy, a module is prepended to the resolved config's singleton class
|
|
228
|
+
# that re-syncs the bridged `BearerToken` from the upstream key on every
|
|
229
|
+
# `auth_settings` call — kruby calls it for every request, so a rotated
|
|
230
|
+
# token reaches the header without rebuilding the client.
|
|
231
|
+
#
|
|
232
|
+
# The bridge tracks the value it last wrote
|
|
233
|
+
# (`@k8s_rails_bridged_bearer_value`). If the app replaces
|
|
234
|
+
# `api_key['BearerToken']` after client creation (explicit credential
|
|
235
|
+
# rotation), the current value matches neither the bridge's last write
|
|
236
|
+
# nor the value the bridge is about to write, so the bridge stops
|
|
237
|
+
# syncing and the app's value stays authoritative — a user-set
|
|
238
|
+
# `BearerToken` is never bridged or overwritten, before or after client
|
|
239
|
+
# creation.
|
|
123
240
|
def bridge_bearer_token(config)
|
|
124
|
-
|
|
125
|
-
return if
|
|
241
|
+
return if config.api_key["authorization"].nil?
|
|
242
|
+
return if config.api_key.key?("BearerToken")
|
|
243
|
+
return if config.instance_variable_get(:@k8s_rails_bridged_bearer)
|
|
244
|
+
|
|
245
|
+
initial = config.api_key_with_prefix("authorization")
|
|
246
|
+
config.api_key["BearerToken"] = initial
|
|
247
|
+
config.instance_variable_set(:@k8s_rails_bridged_bearer, true)
|
|
248
|
+
config.instance_variable_set(:@k8s_rails_bridged_bearer_value, initial)
|
|
249
|
+
config.singleton_class.prepend(BridgedBearerSync)
|
|
250
|
+
end
|
|
126
251
|
|
|
127
|
-
|
|
252
|
+
# Prepend target of the K1 bridge (see bridge_bearer_token). kruby calls
|
|
253
|
+
# `auth_settings` for every request; syncing here (not once at build
|
|
254
|
+
# time) is what keeps a rotated in-cluster token fresh.
|
|
255
|
+
#
|
|
256
|
+
# Thread-safety: the token for THIS request is taken from the freshly
|
|
257
|
+
# computed `authorization` value and written into THIS call's returned
|
|
258
|
+
# settings hash directly — it is never read back from the shared
|
|
259
|
+
# `api_key` (where a concurrent request's write could land in between),
|
|
260
|
+
# so concurrent requests in a threaded Rails process cannot send a
|
|
261
|
+
# stale copy. The shared key is still updated as the backing copy for
|
|
262
|
+
# direct readers.
|
|
263
|
+
module BridgedBearerSync
|
|
264
|
+
def auth_settings
|
|
265
|
+
settings = super
|
|
266
|
+
sync_bridged_bearer(settings)
|
|
267
|
+
settings
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def sync_bridged_bearer(settings)
|
|
273
|
+
return unless instance_variable_get(:@k8s_rails_bridged_bearer)
|
|
274
|
+
|
|
275
|
+
fresh = api_key_with_prefix("authorization")
|
|
276
|
+
return if fresh.nil?
|
|
277
|
+
return if app_overrode_bridged_token?(fresh)
|
|
278
|
+
|
|
279
|
+
settings["BearerToken"][:value] = fresh
|
|
280
|
+
api_key["BearerToken"] = fresh
|
|
281
|
+
instance_variable_set(:@k8s_rails_bridged_bearer_value, fresh)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The app replaced the bridged value (explicit credential rotation):
|
|
285
|
+
# it matches neither the bridge's last write nor the value written
|
|
286
|
+
# now. Stops the sync so the app's credential stays authoritative.
|
|
287
|
+
def app_overrode_bridged_token?(fresh)
|
|
288
|
+
current = api_key["BearerToken"]
|
|
289
|
+
return false if current == instance_variable_get(:@k8s_rails_bridged_bearer_value) || current == fresh
|
|
290
|
+
|
|
291
|
+
instance_variable_set(:@k8s_rails_bridged_bearer, false)
|
|
292
|
+
true
|
|
293
|
+
end
|
|
128
294
|
end
|
|
129
295
|
end
|
|
130
296
|
|
|
@@ -9,6 +9,12 @@ module K8sRails
|
|
|
9
9
|
#
|
|
10
10
|
# This class is pure data: it never touches the network or kruby.
|
|
11
11
|
class Configuration
|
|
12
|
+
# 既定の HTTP タイムアウト(秒)。kruby 1.36.x の既定 `timeout = 0` は
|
|
13
|
+
# **無制限待ち**を意味し、応答しない接続が Rails のリクエストやジョブを
|
|
14
|
+
# 占有し得るため(#33)、接続解決時に未設定ならこの値を適用する。
|
|
15
|
+
# `http_timeout = 0` で kruby 既定の無制限に戻せる(オプトアウト)。
|
|
16
|
+
DEFAULT_HTTP_TIMEOUT = 30
|
|
17
|
+
|
|
12
18
|
# CRD 宣言が namespace 未指定時のデフォルト。
|
|
13
19
|
attr_accessor :namespace
|
|
14
20
|
|
|
@@ -18,6 +24,12 @@ module K8sRails
|
|
|
18
24
|
# 認証を上書きする場合に指定する。
|
|
19
25
|
attr_accessor :connection
|
|
20
26
|
|
|
27
|
+
# HTTP タイムアウト(秒、§5.1・#33)。接続解決時に、解決済み
|
|
28
|
+
# Kubernetes::Configuration の `timeout` が kruby の未設定値 `0` で
|
|
29
|
+
# あるときのみ適用する。利用者が `config.connection.timeout` に非 0 を
|
|
30
|
+
# 明示した場合は勝手に上書きしない。`0` で無制限にオプトアウト。
|
|
31
|
+
attr_accessor :http_timeout
|
|
32
|
+
|
|
21
33
|
# テスト専用(§5.1)。kruby の `Kubernetes::ApiClient#call_api` と同型の
|
|
22
34
|
# `call_api(http_method, path, opts)` メソッドを実装した素のオブジェクトを
|
|
23
35
|
# 指定すると、Client.build は接続解決をスキープしてこれを内部トランスポート
|
|
@@ -32,6 +44,7 @@ module K8sRails
|
|
|
32
44
|
def initialize
|
|
33
45
|
@namespace = "default"
|
|
34
46
|
@connection = nil
|
|
47
|
+
@http_timeout = DEFAULT_HTTP_TIMEOUT
|
|
35
48
|
@api_client = nil
|
|
36
49
|
@instrumentation = true
|
|
37
50
|
end
|
data/lib/k8s_rails/resource.rb
CHANGED
|
@@ -33,7 +33,7 @@ module K8sRails
|
|
|
33
33
|
|
|
34
34
|
# One object by name. Raises K8sRails::NotFound when absent.
|
|
35
35
|
def find(name, namespace: resolved_namespace)
|
|
36
|
-
run_operation(:find, :namespaced, namespace: namespace) do
|
|
36
|
+
run_operation(:find, :namespaced, namespace: namespace, object_name: name) do
|
|
37
37
|
transport.get(group_name, version_name, namespace, plural_name, name)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -54,7 +54,7 @@ module K8sRails
|
|
|
54
54
|
|
|
55
55
|
# JSON Patch a named object. Same readonly restriction as `create`.
|
|
56
56
|
def patch(name, operations, namespace: resolved_namespace)
|
|
57
|
-
run_operation(:patch, :namespaced, namespace: namespace, writable: true) do
|
|
57
|
+
run_operation(:patch, :namespaced, namespace: namespace, writable: true, object_name: name) do
|
|
58
58
|
transport.patch(group_name, version_name, namespace, plural_name, name, operations)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -62,7 +62,7 @@ module K8sRails
|
|
|
62
62
|
# Delete a named object; returns the API status object. Same readonly
|
|
63
63
|
# restriction; raises NotFound when already gone.
|
|
64
64
|
def delete(name, namespace: resolved_namespace)
|
|
65
|
-
run_operation(:delete, :namespaced, namespace: namespace, writable: true) do
|
|
65
|
+
run_operation(:delete, :namespaced, namespace: namespace, writable: true, object_name: name) do
|
|
66
66
|
transport.delete(group_name, version_name, namespace, plural_name, name)
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -77,7 +77,7 @@ module K8sRails
|
|
|
77
77
|
|
|
78
78
|
# One cluster-scoped object by name. Raises NotFound when absent.
|
|
79
79
|
def find_cluster(name)
|
|
80
|
-
run_operation(:find, :cluster) do
|
|
80
|
+
run_operation(:find, :cluster, object_name: name) do
|
|
81
81
|
transport.get_cluster(group_name, version_name, plural_name, name)
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -98,26 +98,28 @@ module K8sRails
|
|
|
98
98
|
|
|
99
99
|
# JSON Patch a cluster-scoped object. Same readonly restriction.
|
|
100
100
|
def patch_cluster(name, operations)
|
|
101
|
-
run_operation(:patch, :cluster, writable: true) do
|
|
101
|
+
run_operation(:patch, :cluster, writable: true, object_name: name) do
|
|
102
102
|
transport.patch_cluster(group_name, version_name, plural_name, name, operations)
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Delete a cluster-scoped object. Same readonly restriction as `delete`.
|
|
107
107
|
def delete_cluster(name)
|
|
108
|
-
run_operation(:delete, :cluster, writable: true) do
|
|
108
|
+
run_operation(:delete, :cluster, writable: true, object_name: name) do
|
|
109
109
|
transport.delete_cluster(group_name, version_name, plural_name, name)
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
private
|
|
114
114
|
|
|
115
|
-
# Scope gate + readonly gate (§5.3) +
|
|
116
|
-
# operation. The block value (and any
|
|
117
|
-
# unchanged. Cluster-scoped
|
|
118
|
-
|
|
115
|
+
# Scope gate + readonly gate (§5.3) + object-name validation (#28) +
|
|
116
|
+
# §8 instrumentation for every operation. The block value (and any
|
|
117
|
+
# K8sRails exception) flows through unchanged. Cluster-scoped
|
|
118
|
+
# operations carry no namespace. Gate order: scope → readonly → name.
|
|
119
|
+
def run_operation(operation, scope, namespace: nil, writable: false, object_name: nil, &block)
|
|
119
120
|
scope == :cluster ? assert_cluster_scoped : assert_namespaced
|
|
120
121
|
assert_writable if writable
|
|
122
|
+
assert_object_name(object_name) if %i[find patch delete].include?(operation)
|
|
121
123
|
ns = scope == :cluster ? nil : namespace
|
|
122
124
|
K8sRails.instrument(operation, instrument_meta(ns)) { block.call }
|
|
123
125
|
end
|
|
@@ -138,11 +140,21 @@ module K8sRails
|
|
|
138
140
|
raise ReadOnlyError, "#{kind_name} is declared readonly — create/patch/delete are disabled (K4)"
|
|
139
141
|
end
|
|
140
142
|
|
|
143
|
+
# Object-scoped operations (find/patch/delete) must reject an invalid
|
|
144
|
+
# target name BEFORE any request is built (#28): a nil name resolves to
|
|
145
|
+
# the collection URL, where a DELETE is a Delete Collection call, and
|
|
146
|
+
# "." / ".." are normalized away by the HTTP client into a different
|
|
147
|
+
# resource path.
|
|
148
|
+
def assert_object_name(name)
|
|
149
|
+
return unless name.nil? || name.to_s.empty? || %w[. ..].include?(name.to_s)
|
|
150
|
+
|
|
151
|
+
raise ArgumentError, "#{kind_name} object name must be a non-empty string (got #{name.inspect})"
|
|
152
|
+
end
|
|
153
|
+
|
|
141
154
|
def assert_cluster_scoped
|
|
142
155
|
return if cluster_scoped?
|
|
143
156
|
|
|
144
|
-
raise ArgumentError,
|
|
145
|
-
"#{kind_name} is namespaced — *_cluster methods require a scope: :cluster declaration"
|
|
157
|
+
raise ArgumentError, "#{kind_name} is namespaced — *_cluster methods require a scope: :cluster declaration"
|
|
146
158
|
end
|
|
147
159
|
|
|
148
160
|
def assert_namespaced
|
data/lib/k8s_rails/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dorian - Takahiro Ishida
|
|
@@ -117,6 +117,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
117
117
|
- - "<"
|
|
118
118
|
- !ruby/object:Gem::Version
|
|
119
119
|
version: '4.1'
|
|
120
|
+
- - "!="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: 4.1.0.preview1
|
|
120
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
124
|
requirements:
|
|
122
125
|
- - ">="
|