kube_cluster 0.20.1 → 1.0.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/.gitignore +1 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +5 -37
- data/flake.nix +2 -1
- data/kube_cluster.gemspec +2 -4
- data/lib/kube/cluster/container.rb +17 -17
- data/lib/kube/cluster/manifest.rb +158 -158
- data/lib/kube/cluster/middleware/annotations.rb +42 -42
- data/lib/kube/cluster/middleware/hpa_for_deployment.rb +134 -134
- data/lib/kube/cluster/middleware/ingress_for_service.rb +75 -75
- data/lib/kube/cluster/middleware/labels.rb +58 -58
- data/lib/kube/cluster/middleware/pod_anti_affinity.rb +104 -104
- data/lib/kube/cluster/middleware/resource_preset.rb +145 -145
- data/lib/kube/cluster/middleware/security_context.rb +127 -127
- data/lib/kube/cluster/middleware/service_for_deployment.rb +120 -120
- data/lib/kube/cluster/middleware.rb +219 -221
- data/lib/kube/cluster/resource/dirty_tracking.rb +388 -388
- data/lib/kube/cluster/resource.rb +16 -16
- data/lib/kube/cluster/standard/cdi/data_volume.rb +13 -13
- data/lib/kube/cluster/standard/cloud_native_pg/cluster.rb +11 -11
- data/lib/kube/cluster/standard/cloud_native_pg/database_with_external_secret.rb +11 -11
- data/lib/kube/cluster/standard/cloud_native_pg/external_secret.rb +11 -11
- data/lib/kube/cluster/standard/cloud_native_pg/helm.rb +26 -26
- data/lib/kube/cluster/standard/config_map.rb +11 -11
- data/lib/kube/cluster/standard/cron_job.rb +13 -13
- data/lib/kube/cluster/standard/cron_job_with_service_account.rb +12 -12
- data/lib/kube/cluster/standard/custom_resource_definition.rb +12 -12
- data/lib/kube/cluster/standard/daemon_set.rb +12 -12
- data/lib/kube/cluster/standard/deployment_with_service.rb +82 -82
- data/lib/kube/cluster/standard/env_processing.rb +15 -15
- data/lib/kube/cluster/standard/eso/external_secret.rb +13 -13
- data/lib/kube/cluster/standard/forgejo/helm.rb +11 -11
- data/lib/kube/cluster/standard/gateway_api/http_route.rb +322 -19
- data/lib/kube/cluster/standard/job.rb +12 -12
- data/lib/kube/cluster/standard/job_with_service_account.rb +11 -11
- data/lib/kube/cluster/standard/kube_virt/virtual_machine.rb +11 -11
- data/lib/kube/cluster/standard/meta_controller/composite_controller.rb +13 -13
- data/lib/kube/cluster/standard/meta_controller/decorator_controller.rb +13 -13
- data/lib/kube/cluster/standard/perses/perses.rb +9 -9
- data/lib/kube/cluster/standard/perses/perses_datasource.rb +13 -13
- data/lib/kube/cluster/standard/persistent_volume_claim.rb +12 -12
- data/lib/kube/cluster/standard/role.rb +12 -12
- data/lib/kube/cluster/standard/role_binding.rb +10 -10
- data/lib/kube/cluster/standard/secret.rb +11 -11
- data/lib/kube/cluster/standard/service.rb +12 -12
- data/lib/kube/cluster/standard/service_account.rb +9 -9
- data/lib/kube/cluster/standard/service_account_with_role.rb +10 -10
- data/lib/kube/cluster/standard/victoria_metrics/vl_agent.rb +9 -9
- data/lib/kube/cluster/standard/victoria_metrics/vl_single.rb +9 -9
- data/lib/kube/cluster/standard/victoria_metrics/vm_agent.rb +9 -9
- data/lib/kube/cluster/standard/victoria_metrics/vm_node_scrape.rb +9 -9
- data/lib/kube/cluster/standard/victoria_metrics/vm_service_scrape.rb +9 -9
- data/lib/kube/cluster/standard/victoria_metrics/vm_single.rb +9 -9
- data/lib/kube/cluster/standard/volume_processing.rb +3 -3
- data/lib/kube/cluster/version.rb +1 -1
- data/lib/kube/cluster.rb +137 -1
- data/lib/kube/helm/chart.rb +323 -323
- data/lib/kube/helm/endpoint.rb +58 -58
- data/lib/kube/helm/repo.rb +160 -160
- metadata +7 -20
|
@@ -115,534 +115,534 @@ module Kube
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
require "json"
|
|
120
|
-
|
|
121
|
-
# ---------------------------------------------------------------------------
|
|
122
|
-
# Fake ctl that records every command and returns canned responses.
|
|
123
|
-
# The test wires this into the cluster → connection → ctl chain so that
|
|
124
|
-
# Persistence#kubectl goes through it without touching a real cluster.
|
|
125
|
-
# ---------------------------------------------------------------------------
|
|
126
|
-
class FakeCtl
|
|
127
|
-
attr_reader :commands
|
|
128
|
-
|
|
129
|
-
def initialize
|
|
130
|
-
@commands = []
|
|
131
|
-
@responses = {}
|
|
132
|
-
end
|
|
118
|
+
__END__
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
def stub_response(substring, response)
|
|
136
|
-
@responses[substring] = response
|
|
137
|
-
end
|
|
120
|
+
require "json"
|
|
138
121
|
|
|
139
|
-
|
|
140
|
-
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Fake ctl that records every command and returns canned responses.
|
|
124
|
+
# The test wires this into the cluster → connection → ctl chain so that
|
|
125
|
+
# Persistence#kubectl goes through it without touching a real cluster.
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
class FakeCtl
|
|
128
|
+
attr_reader :commands
|
|
141
129
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
130
|
+
def initialize
|
|
131
|
+
@commands = []
|
|
132
|
+
@responses = {}
|
|
133
|
+
end
|
|
147
134
|
|
|
148
|
-
|
|
149
|
-
|
|
135
|
+
# Queue a response for the next command that includes +substring+.
|
|
136
|
+
def stub_response(substring, response)
|
|
137
|
+
@responses[substring] = response
|
|
150
138
|
end
|
|
151
139
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# ---------------------------------------------------------------------------
|
|
155
|
-
class FakeConnection
|
|
156
|
-
attr_reader :ctl
|
|
140
|
+
def run(string)
|
|
141
|
+
@commands << string
|
|
157
142
|
|
|
158
|
-
|
|
159
|
-
|
|
143
|
+
@responses.each do |substring, response|
|
|
144
|
+
if string.include?(substring)
|
|
145
|
+
return response
|
|
146
|
+
end
|
|
160
147
|
end
|
|
148
|
+
|
|
149
|
+
"" # default: empty response
|
|
161
150
|
end
|
|
151
|
+
end
|
|
162
152
|
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Minimal cluster double that provides .connection.ctl
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
class FakeConnection
|
|
157
|
+
attr_reader :ctl
|
|
165
158
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
159
|
+
def initialize(ctl)
|
|
160
|
+
@ctl = ctl
|
|
169
161
|
end
|
|
162
|
+
end
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# ---------------------------------------------------------------------------
|
|
174
|
-
module ResourceHelper
|
|
175
|
-
def build_resource(hash = {})
|
|
176
|
-
ctl = FakeCtl.new
|
|
177
|
-
cluster = FakeCluster.new(ctl)
|
|
178
|
-
resource = Kube::Cluster["ConfigMap"].new(hash.merge(kind: "ConfigMap", cluster: cluster))
|
|
179
|
-
[resource, ctl]
|
|
180
|
-
end
|
|
164
|
+
class FakeCluster
|
|
165
|
+
attr_reader :connection
|
|
181
166
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
end
|
|
167
|
+
def initialize(ctl)
|
|
168
|
+
@connection = FakeConnection.new(ctl)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
187
171
|
|
|
188
|
-
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Helper to build a resource wired to a fake cluster.
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
module ResourceHelper
|
|
176
|
+
def build_resource(hash = {})
|
|
177
|
+
ctl = FakeCtl.new
|
|
178
|
+
cluster = FakeCluster.new(ctl)
|
|
179
|
+
resource = Kube::Cluster["ConfigMap"].new(hash.merge(kind: "ConfigMap", cluster: cluster))
|
|
180
|
+
[resource, ctl]
|
|
181
|
+
end
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
else obj
|
|
195
|
-
end
|
|
196
|
-
end
|
|
183
|
+
# Simulate what kubectl returns: the server adds extra fields.
|
|
184
|
+
def server_state(resource_hash, extra = {})
|
|
185
|
+
merged = resource_hash.merge(extra)
|
|
186
|
+
JSON.generate(stringify_keys(merged))
|
|
197
187
|
end
|
|
198
188
|
|
|
199
|
-
|
|
189
|
+
private
|
|
200
190
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
def stringify_keys(obj)
|
|
192
|
+
case obj
|
|
193
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = stringify_keys(v) }
|
|
194
|
+
when Array then obj.map { |v| stringify_keys(v) }
|
|
195
|
+
else obj
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
204
199
|
|
|
205
|
-
|
|
206
|
-
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
|
|
200
|
+
include ResourceHelper
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
spec: { key: "original" }
|
|
212
|
-
))
|
|
202
|
+
# -------------------------------------------------------------------------
|
|
203
|
+
# Full lifecycle: apply → mutate → detect changes → patch → clean
|
|
204
|
+
# -------------------------------------------------------------------------
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
it "full_apply_mutate_patch_lifecycle" do
|
|
207
|
+
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
|
|
215
208
|
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
# Stub the reload after apply — server echoes back what we sent
|
|
210
|
+
ctl.stub_response("get", server_state(
|
|
211
|
+
metadata: { name: "app-config", namespace: "production", resourceVersion: "100" },
|
|
212
|
+
spec: { key: "original" }
|
|
213
|
+
))
|
|
218
214
|
|
|
219
|
-
|
|
220
|
-
ctl.stub_response("get", server_state(
|
|
221
|
-
metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
|
|
222
|
-
spec: { key: "updated" }
|
|
223
|
-
))
|
|
215
|
+
resource.apply
|
|
224
216
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
end
|
|
217
|
+
# Mutate
|
|
218
|
+
resource.instance_variable_get(:@data).spec.key = "updated"
|
|
228
219
|
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
220
|
+
# Stub reload after patch
|
|
221
|
+
ctl.stub_response("get", server_state(
|
|
222
|
+
metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
|
|
223
|
+
spec: { key: "updated" }
|
|
224
|
+
))
|
|
232
225
|
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
result = resource.patch
|
|
227
|
+
result.should == true
|
|
228
|
+
end
|
|
235
229
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
230
|
+
# -------------------------------------------------------------------------
|
|
231
|
+
# Patch returns false when nothing changed
|
|
232
|
+
# -------------------------------------------------------------------------
|
|
239
233
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
end
|
|
234
|
+
it "patch_returns_false_when_clean" do
|
|
235
|
+
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" })
|
|
243
236
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
237
|
+
ctl.stub_response("get", server_state(
|
|
238
|
+
metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" }
|
|
239
|
+
))
|
|
247
240
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
252
|
-
)
|
|
241
|
+
result = resource.patch
|
|
242
|
+
result.should == false
|
|
243
|
+
end
|
|
253
244
|
|
|
254
|
-
|
|
255
|
-
|
|
245
|
+
# -------------------------------------------------------------------------
|
|
246
|
+
# Patch sends only the diff, not the full resource
|
|
247
|
+
# -------------------------------------------------------------------------
|
|
256
248
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
249
|
+
it "patch_sends_only_changed_fields" do
|
|
250
|
+
resource, ctl = build_resource(
|
|
251
|
+
metadata: { name: "my-config", namespace: "staging" },
|
|
252
|
+
spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
253
|
+
)
|
|
261
254
|
|
|
262
|
-
|
|
255
|
+
# Mutate one field
|
|
256
|
+
resource.instance_variable_get(:@data).spec.db_host = "new-db.internal"
|
|
263
257
|
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
ctl.stub_response("get", server_state(
|
|
259
|
+
metadata: { name: "my-config", namespace: "staging" },
|
|
260
|
+
spec: { db_host: "new-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
261
|
+
))
|
|
266
262
|
|
|
267
|
-
|
|
268
|
-
json_start = patch_cmd.index("-p ") + 3
|
|
269
|
-
payload = JSON.parse(patch_cmd[json_start..])
|
|
263
|
+
resource.patch
|
|
270
264
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
end
|
|
265
|
+
# Find the patch command
|
|
266
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
274
267
|
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
|
|
268
|
+
# Extract the JSON payload from the command (last arg after -p)
|
|
269
|
+
json_start = patch_cmd.index("-p ") + 3
|
|
270
|
+
payload = JSON.parse(patch_cmd[json_start..])
|
|
278
271
|
|
|
279
|
-
|
|
280
|
-
|
|
272
|
+
# The payload should contain the spec subtree but NOT metadata
|
|
273
|
+
payload.key?("spec").should.be.true
|
|
274
|
+
end
|
|
281
275
|
|
|
282
|
-
|
|
283
|
-
|
|
276
|
+
# -------------------------------------------------------------------------
|
|
277
|
+
# Reload resets dirty state from server response
|
|
278
|
+
# -------------------------------------------------------------------------
|
|
284
279
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" }
|
|
288
|
-
))
|
|
280
|
+
it "reload_resets_dirty_state" do
|
|
281
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
289
282
|
|
|
290
|
-
|
|
283
|
+
# Local mutation
|
|
284
|
+
resource.instance_variable_get(:@data).spec.key = "local-change"
|
|
291
285
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
286
|
+
# Server still has original
|
|
287
|
+
ctl.stub_response("get", server_state(
|
|
288
|
+
metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" }
|
|
289
|
+
))
|
|
295
290
|
|
|
296
|
-
|
|
297
|
-
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
291
|
+
resource.reload
|
|
298
292
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
spec: { key: "server-updated" }
|
|
303
|
-
))
|
|
293
|
+
# After reload, local changes are gone and resource is clean
|
|
294
|
+
resource.to_h[:spec][:key].should == "v1"
|
|
295
|
+
end
|
|
304
296
|
|
|
305
|
-
|
|
297
|
+
it "reload_picks_up_server_side_changes" do
|
|
298
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
306
299
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
300
|
+
# Server has been mutated externally
|
|
301
|
+
ctl.stub_response("get", server_state(
|
|
302
|
+
metadata: { name: "my-config", namespace: "default", resourceVersion: "200" },
|
|
303
|
+
spec: { key: "server-updated" }
|
|
304
|
+
))
|
|
310
305
|
|
|
311
|
-
|
|
312
|
-
# Apply snapshots after the server round-trip
|
|
313
|
-
# -------------------------------------------------------------------------
|
|
306
|
+
resource.reload
|
|
314
307
|
|
|
315
|
-
|
|
316
|
-
|
|
308
|
+
# Resource reflects server state and is clean
|
|
309
|
+
resource.to_h[:spec][:key].should == "server-updated"
|
|
310
|
+
end
|
|
317
311
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
spec: { key: "v1" }
|
|
322
|
-
))
|
|
312
|
+
# -------------------------------------------------------------------------
|
|
313
|
+
# Apply snapshots after the server round-trip
|
|
314
|
+
# -------------------------------------------------------------------------
|
|
323
315
|
|
|
324
|
-
|
|
316
|
+
it "apply_snapshots_server_response" do
|
|
317
|
+
resource, ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
325
318
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
319
|
+
# Server adds metadata on apply
|
|
320
|
+
ctl.stub_response("get", server_state(
|
|
321
|
+
metadata: { name: "my-config", resourceVersion: "1", uid: "abc-123" },
|
|
322
|
+
spec: { key: "v1" }
|
|
323
|
+
))
|
|
330
324
|
|
|
331
|
-
|
|
332
|
-
old_spec, new_spec = changes[:spec]
|
|
333
|
-
old_spec[:key].should == "v1"
|
|
334
|
-
end
|
|
325
|
+
resource.apply
|
|
335
326
|
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
|
|
327
|
+
# The snapshot should include server-added fields, so mutating
|
|
328
|
+
# the original field shows the correct old value
|
|
329
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
330
|
+
changes = resource.changes
|
|
339
331
|
|
|
340
|
-
|
|
341
|
-
|
|
332
|
+
# changes[:spec] is [old_hash, new_hash]
|
|
333
|
+
old_spec, new_spec = changes[:spec]
|
|
334
|
+
old_spec[:key].should == "v1"
|
|
335
|
+
end
|
|
342
336
|
|
|
343
|
-
|
|
344
|
-
|
|
337
|
+
# -------------------------------------------------------------------------
|
|
338
|
+
# Error cases: unpersisted resources
|
|
339
|
+
# -------------------------------------------------------------------------
|
|
345
340
|
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
it "patch_raises_on_unpersisted_resource" do
|
|
342
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
348
343
|
|
|
349
|
-
|
|
350
|
-
|
|
344
|
+
lambda { resource.patch }.should.raise Kube::CommandError
|
|
345
|
+
end
|
|
351
346
|
|
|
352
|
-
|
|
353
|
-
|
|
347
|
+
it "delete_raises_on_unpersisted_resource" do
|
|
348
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
354
349
|
|
|
355
|
-
|
|
356
|
-
|
|
350
|
+
lambda { resource.delete }.should.raise Kube::CommandError
|
|
351
|
+
end
|
|
357
352
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
# -------------------------------------------------------------------------
|
|
353
|
+
it "reload_raises_on_unpersisted_resource" do
|
|
354
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
361
355
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
metadata: { name: "my-config", namespace: "default", labels: { app: "web", tier: "frontend" } }
|
|
365
|
-
)
|
|
356
|
+
lambda { resource.reload }.should.raise Kube::CommandError
|
|
357
|
+
end
|
|
366
358
|
|
|
367
|
-
|
|
368
|
-
|
|
359
|
+
# -------------------------------------------------------------------------
|
|
360
|
+
# Nested mutation flows through patch_data correctly
|
|
361
|
+
# -------------------------------------------------------------------------
|
|
369
362
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
363
|
+
it "nested_mutation_produces_nested_patch" do
|
|
364
|
+
resource, ctl = build_resource(
|
|
365
|
+
metadata: { name: "my-config", namespace: "default", labels: { app: "web", tier: "frontend" } }
|
|
366
|
+
)
|
|
373
367
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
metadata: { name: "my-config", labels: { app: "web" } }
|
|
377
|
-
)
|
|
368
|
+
# Mutate only a nested field
|
|
369
|
+
resource.instance_variable_get(:@data).metadata.labels.tier = "backend"
|
|
378
370
|
|
|
379
|
-
|
|
380
|
-
|
|
371
|
+
patch = resource.patch_data
|
|
372
|
+
patch[:metadata][:labels][:tier].should == ["frontend", "backend"]
|
|
373
|
+
end
|
|
381
374
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
375
|
+
it "deeply_nested_no_change_produces_empty_patch" do
|
|
376
|
+
resource, _ctl = build_resource(
|
|
377
|
+
metadata: { name: "my-config", labels: { app: "web" } }
|
|
378
|
+
)
|
|
385
379
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
389
|
-
data: { host: "db-1", port: "5432", pool: "5" }
|
|
390
|
-
)
|
|
380
|
+
resource.patch_data.should == {}
|
|
381
|
+
end
|
|
391
382
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
d.pool = "10"
|
|
383
|
+
# -------------------------------------------------------------------------
|
|
384
|
+
# Multiple mutations before patch coalesce into a single diff
|
|
385
|
+
# -------------------------------------------------------------------------
|
|
396
386
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
387
|
+
it "multiple_mutations_coalesce_in_single_patch" do
|
|
388
|
+
resource, ctl = build_resource(
|
|
389
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
390
|
+
data: { host: "db-1", port: "5432", pool: "5" }
|
|
391
|
+
)
|
|
401
392
|
|
|
402
|
-
|
|
393
|
+
d = resource.instance_variable_get(:@data).data
|
|
394
|
+
d.host = "db-2"
|
|
395
|
+
d.port = "5433"
|
|
396
|
+
d.pool = "10"
|
|
403
397
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
398
|
+
ctl.stub_response("get", server_state(
|
|
399
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
400
|
+
data: { host: "db-2", port: "5433", pool: "10" }
|
|
401
|
+
))
|
|
408
402
|
|
|
409
|
-
|
|
410
|
-
# changes_applied mid-workflow resets the baseline
|
|
411
|
-
# -------------------------------------------------------------------------
|
|
403
|
+
resource.patch
|
|
412
404
|
|
|
413
|
-
|
|
414
|
-
|
|
405
|
+
# Exactly one patch command
|
|
406
|
+
patch_commands = ctl.commands.select { |c| c.include?("patch") }
|
|
407
|
+
patch_commands.size.should == 1
|
|
408
|
+
end
|
|
415
409
|
|
|
416
|
-
|
|
410
|
+
# -------------------------------------------------------------------------
|
|
411
|
+
# changes_applied mid-workflow resets the baseline
|
|
412
|
+
# -------------------------------------------------------------------------
|
|
417
413
|
|
|
418
|
-
|
|
419
|
-
|
|
414
|
+
it "changes_applied_resets_baseline_without_server_roundtrip" do
|
|
415
|
+
resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
420
416
|
|
|
421
|
-
|
|
422
|
-
end
|
|
417
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
423
418
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
427
|
-
data: { a: "1", b: "2", c: "3" }
|
|
428
|
-
)
|
|
419
|
+
# Accept changes locally (no kubectl call)
|
|
420
|
+
resource.changes_applied
|
|
429
421
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
resource.changes_applied
|
|
422
|
+
resource.changes.should == {}
|
|
423
|
+
end
|
|
433
424
|
|
|
434
|
-
|
|
435
|
-
|
|
425
|
+
it "changes_applied_then_patch_sends_only_subsequent_changes" do
|
|
426
|
+
resource, ctl = build_resource(
|
|
427
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
428
|
+
data: { a: "1", b: "2", c: "3" }
|
|
429
|
+
)
|
|
436
430
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
))
|
|
431
|
+
# First wave of changes
|
|
432
|
+
resource.instance_variable_get(:@data).data.a = "changed-a"
|
|
433
|
+
resource.changes_applied
|
|
441
434
|
|
|
442
|
-
|
|
435
|
+
# Second wave — only b changes from the new baseline
|
|
436
|
+
resource.instance_variable_get(:@data).data.b = "changed-b"
|
|
443
437
|
|
|
444
|
-
|
|
445
|
-
|
|
438
|
+
ctl.stub_response("get", server_state(
|
|
439
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
440
|
+
data: { a: "changed-a", b: "changed-b", c: "3" }
|
|
441
|
+
))
|
|
446
442
|
|
|
447
|
-
|
|
448
|
-
payload["data"]["b"].should == ["2", "changed-b"]
|
|
449
|
-
end
|
|
443
|
+
resource.patch
|
|
450
444
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
# -------------------------------------------------------------------------
|
|
445
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
446
|
+
payload = JSON.parse(patch_cmd.split("-p ").last)
|
|
454
447
|
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
# Only b should be in the patch, not a (already accepted via changes_applied)
|
|
449
|
+
payload["data"]["b"].should == ["2", "changed-b"]
|
|
450
|
+
end
|
|
457
451
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
))
|
|
452
|
+
# -------------------------------------------------------------------------
|
|
453
|
+
# Dynamic attr_changed? tracks through full lifecycle
|
|
454
|
+
# -------------------------------------------------------------------------
|
|
462
455
|
|
|
463
|
-
|
|
456
|
+
it "attr_changed_through_apply_mutate_patch_cycle" do
|
|
457
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
464
458
|
|
|
465
|
-
|
|
459
|
+
ctl.stub_response("get", server_state(
|
|
460
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
461
|
+
spec: { key: "v1" }
|
|
462
|
+
))
|
|
466
463
|
|
|
467
|
-
|
|
468
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
469
|
-
spec: { key: "v2" }
|
|
470
|
-
))
|
|
464
|
+
resource.apply
|
|
471
465
|
|
|
472
|
-
|
|
466
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
473
467
|
|
|
474
|
-
|
|
475
|
-
|
|
468
|
+
ctl.stub_response("get", server_state(
|
|
469
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
470
|
+
spec: { key: "v2" }
|
|
471
|
+
))
|
|
476
472
|
|
|
477
|
-
|
|
478
|
-
resource, _ctl = build_resource(metadata: { name: "test" })
|
|
473
|
+
resource.patch
|
|
479
474
|
|
|
480
|
-
|
|
481
|
-
|
|
475
|
+
resource.spec_changed?.should.be.false
|
|
476
|
+
end
|
|
482
477
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
# -------------------------------------------------------------------------
|
|
478
|
+
it "respond_to_for_dynamic_changed_predicates" do
|
|
479
|
+
resource, _ctl = build_resource(metadata: { name: "test" })
|
|
486
480
|
|
|
487
|
-
|
|
488
|
-
|
|
481
|
+
resource.should.respond_to :metadata_changed?
|
|
482
|
+
end
|
|
489
483
|
|
|
490
|
-
|
|
484
|
+
# -------------------------------------------------------------------------
|
|
485
|
+
# Snapshot isolation: reload doesn't leak into captured references
|
|
486
|
+
# -------------------------------------------------------------------------
|
|
491
487
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
patch_before = resource.patch_data
|
|
488
|
+
it "reload_does_not_corrupt_previously_captured_changes" do
|
|
489
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
495
490
|
|
|
496
|
-
|
|
497
|
-
ctl.stub_response("get", server_state(
|
|
498
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
499
|
-
spec: { key: "v3-from-server" }
|
|
500
|
-
))
|
|
491
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
501
492
|
|
|
502
|
-
|
|
493
|
+
# Capture changes before reload
|
|
494
|
+
changes_before = resource.changes
|
|
495
|
+
patch_before = resource.patch_data
|
|
503
496
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
497
|
+
# Reload with different server state
|
|
498
|
+
ctl.stub_response("get", server_state(
|
|
499
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
500
|
+
spec: { key: "v3-from-server" }
|
|
501
|
+
))
|
|
502
|
+
|
|
503
|
+
resource.reload
|
|
507
504
|
|
|
508
|
-
|
|
509
|
-
|
|
505
|
+
# Previously captured hashes should be unaffected
|
|
506
|
+
extract_nested_value(changes_before, :spec, :key, 1).should == "v2"
|
|
507
|
+
end
|
|
510
508
|
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
it "snapshot_isolation_across_multiple_changes_applied" do
|
|
510
|
+
resource, _ctl = build_resource(metadata: { name: "test" }, data: { counter: "1" })
|
|
513
511
|
|
|
514
|
-
|
|
512
|
+
resource.instance_variable_get(:@data).data.counter = "2"
|
|
513
|
+
snapshot_1_changes = resource.changes
|
|
515
514
|
|
|
516
|
-
|
|
517
|
-
snapshot_2_changes = resource.changes
|
|
515
|
+
resource.changes_applied
|
|
518
516
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
end
|
|
517
|
+
resource.instance_variable_get(:@data).data.counter = "3"
|
|
518
|
+
snapshot_2_changes = resource.changes
|
|
522
519
|
|
|
523
|
-
#
|
|
524
|
-
|
|
525
|
-
|
|
520
|
+
# Each snapshot's changes should be independent
|
|
521
|
+
extract_nested_value(snapshot_1_changes, :data, :counter, 0).should == "1"
|
|
522
|
+
end
|
|
526
523
|
|
|
527
|
-
|
|
528
|
-
|
|
524
|
+
# -------------------------------------------------------------------------
|
|
525
|
+
# Edge case: resource with no initial spec data
|
|
526
|
+
# -------------------------------------------------------------------------
|
|
529
527
|
|
|
530
|
-
|
|
528
|
+
it "empty_resource_tracks_all_additions" do
|
|
529
|
+
resource, _ctl = build_resource(metadata: { name: "empty-config" })
|
|
531
530
|
|
|
532
|
-
|
|
533
|
-
end
|
|
531
|
+
resource.instance_variable_get(:@data).spec.key = "added"
|
|
534
532
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
# -------------------------------------------------------------------------
|
|
533
|
+
resource.changed.should.include :spec
|
|
534
|
+
end
|
|
538
535
|
|
|
539
|
-
|
|
540
|
-
|
|
536
|
+
# -------------------------------------------------------------------------
|
|
537
|
+
# Edge case: patch type parameter is forwarded
|
|
538
|
+
# -------------------------------------------------------------------------
|
|
541
539
|
|
|
542
|
-
|
|
540
|
+
it "patch_forwards_type_parameter" do
|
|
541
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
543
542
|
|
|
544
|
-
|
|
545
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
546
|
-
spec: { key: "v2" }
|
|
547
|
-
))
|
|
543
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
548
544
|
|
|
549
|
-
|
|
545
|
+
ctl.stub_response("get", server_state(
|
|
546
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
547
|
+
spec: { key: "v2" }
|
|
548
|
+
))
|
|
550
549
|
|
|
551
|
-
|
|
552
|
-
patch_cmd.should.include "--type merge"
|
|
553
|
-
end
|
|
550
|
+
resource.patch(type: "merge")
|
|
554
551
|
|
|
555
|
-
|
|
556
|
-
|
|
552
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
553
|
+
patch_cmd.should.include "--type merge"
|
|
554
|
+
end
|
|
557
555
|
|
|
558
|
-
|
|
556
|
+
it "patch_defaults_to_strategic_type" do
|
|
557
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
559
558
|
|
|
560
|
-
|
|
561
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
562
|
-
spec: { key: "v2" }
|
|
563
|
-
))
|
|
559
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
564
560
|
|
|
565
|
-
|
|
561
|
+
ctl.stub_response("get", server_state(
|
|
562
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
563
|
+
spec: { key: "v2" }
|
|
564
|
+
))
|
|
566
565
|
|
|
567
|
-
|
|
568
|
-
patch_cmd.should.include "--type strategic"
|
|
569
|
-
end
|
|
566
|
+
resource.patch
|
|
570
567
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
568
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
569
|
+
patch_cmd.should.include "--type strategic"
|
|
570
|
+
end
|
|
574
571
|
|
|
575
|
-
|
|
576
|
-
|
|
572
|
+
# -------------------------------------------------------------------------
|
|
573
|
+
# Edge case: namespace flags are included correctly
|
|
574
|
+
# -------------------------------------------------------------------------
|
|
577
575
|
|
|
578
|
-
|
|
576
|
+
it "patch_includes_namespace_flags" do
|
|
577
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "kube-system" }, spec: { key: "v1" })
|
|
579
578
|
|
|
580
|
-
|
|
581
|
-
metadata: { name: "my-config", namespace: "kube-system" },
|
|
582
|
-
spec: { key: "v2" }
|
|
583
|
-
))
|
|
579
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
584
580
|
|
|
585
|
-
|
|
581
|
+
ctl.stub_response("get", server_state(
|
|
582
|
+
metadata: { name: "my-config", namespace: "kube-system" },
|
|
583
|
+
spec: { key: "v2" }
|
|
584
|
+
))
|
|
586
585
|
|
|
587
|
-
|
|
588
|
-
patch_cmd.should.include "--namespace kube-system"
|
|
589
|
-
end
|
|
586
|
+
resource.patch
|
|
590
587
|
|
|
591
|
-
|
|
592
|
-
|
|
588
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
589
|
+
patch_cmd.should.include "--namespace kube-system"
|
|
590
|
+
end
|
|
593
591
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
spec: { key: "v1" }
|
|
597
|
-
))
|
|
592
|
+
it "reload_includes_namespace_flags" do
|
|
593
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "monitoring" }, spec: { key: "v1" })
|
|
598
594
|
|
|
599
|
-
|
|
595
|
+
ctl.stub_response("get", server_state(
|
|
596
|
+
metadata: { name: "my-config", namespace: "monitoring" },
|
|
597
|
+
spec: { key: "v1" }
|
|
598
|
+
))
|
|
600
599
|
|
|
601
|
-
|
|
602
|
-
get_cmd.should.include "--namespace monitoring"
|
|
603
|
-
end
|
|
600
|
+
resource.reload
|
|
604
601
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
602
|
+
get_cmd = ctl.commands.find { |c| c.include?("get") }
|
|
603
|
+
get_cmd.should.include "--namespace monitoring"
|
|
604
|
+
end
|
|
608
605
|
|
|
609
|
-
|
|
610
|
-
|
|
606
|
+
# -------------------------------------------------------------------------
|
|
607
|
+
# Edge case: delete on persisted resource issues command
|
|
608
|
+
# -------------------------------------------------------------------------
|
|
611
609
|
|
|
612
|
-
|
|
610
|
+
it "delete_issues_kubectl_delete" do
|
|
611
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" })
|
|
613
612
|
|
|
614
|
-
|
|
615
|
-
delete_cmd.should.include "my-config"
|
|
616
|
-
end
|
|
613
|
+
result = resource.delete
|
|
617
614
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
615
|
+
delete_cmd = ctl.commands.find { |c| c.include?("delete") }
|
|
616
|
+
delete_cmd.should.include "my-config"
|
|
617
|
+
end
|
|
621
618
|
|
|
622
|
-
|
|
623
|
-
|
|
619
|
+
# -------------------------------------------------------------------------
|
|
620
|
+
# Regression: the original bug — build_changes used `result` instead of `hash`
|
|
621
|
+
# -------------------------------------------------------------------------
|
|
624
622
|
|
|
625
|
-
|
|
623
|
+
it "changes_does_not_raise_name_error" do
|
|
624
|
+
resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
626
625
|
|
|
627
|
-
|
|
628
|
-
changes = resource.changes
|
|
626
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
629
627
|
|
|
630
|
-
|
|
631
|
-
|
|
628
|
+
# This would raise NameError with the original bug
|
|
629
|
+
changes = resource.changes
|
|
632
630
|
|
|
633
|
-
|
|
631
|
+
changes.should.be.kind_of Hash
|
|
632
|
+
end
|
|
634
633
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
634
|
+
private
|
|
635
|
+
|
|
636
|
+
# Navigate into nested change structures.
|
|
637
|
+
# changes[:spec] could be [old_hash, new_hash] or a nested diff hash.
|
|
638
|
+
def extract_nested_value(hash, top_key, nested_key, index)
|
|
639
|
+
val = hash[top_key]
|
|
640
|
+
case val
|
|
641
|
+
when Array
|
|
642
|
+
# [old_hash, new_hash]
|
|
643
|
+
val[index].is_a?(Hash) ? val[index][nested_key] : val[index]
|
|
644
|
+
when Hash
|
|
645
|
+
# nested diff: { key: [old, new] }
|
|
646
|
+
val[nested_key].is_a?(Array) ? val[nested_key][index] : val[nested_key]
|
|
647
647
|
end
|
|
648
|
-
end
|
|
648
|
+
end
|