kube_cluster 0.3.8 → 0.3.9
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/Gemfile.lock +9 -5
- data/Rakefile +1 -1
- data/bin/test +2 -15
- data/kube_cluster.gemspec +3 -2
- data/lib/kube/cluster/manifest.rb +160 -193
- data/lib/kube/cluster/middleware/annotations.rb +43 -51
- data/lib/kube/cluster/middleware/hpa_for_deployment.rb +136 -158
- data/lib/kube/cluster/middleware/ingress_for_service.rb +84 -110
- data/lib/kube/cluster/middleware/labels.rb +60 -73
- data/lib/kube/cluster/middleware/namespace.rb +49 -55
- data/lib/kube/cluster/middleware/pod_anti_affinity.rb +102 -113
- data/lib/kube/cluster/middleware/resource_preset.rb +145 -169
- data/lib/kube/cluster/middleware/security_context.rb +127 -154
- data/lib/kube/cluster/middleware/service_for_deployment.rb +123 -146
- data/lib/kube/cluster/resource/dirty_tracking.rb +328 -418
- data/lib/kube/cluster/version.rb +1 -1
- data/lib/kube/cluster.rb +4 -9
- data/lib/kube/helm/chart.rb +326 -325
- data/lib/kube/helm/endpoint.rb +59 -65
- data/lib/kube/helm/repo.rb +159 -166
- metadata +7 -7
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "kube/cluster"
|
|
6
|
-
end
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "kube/cluster"
|
|
7
5
|
|
|
8
6
|
module Kube
|
|
9
7
|
module Cluster
|
|
@@ -117,8 +115,7 @@ module Kube
|
|
|
117
115
|
end
|
|
118
116
|
end
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
require "minitest/autorun"
|
|
118
|
+
test do
|
|
122
119
|
require "json"
|
|
123
120
|
|
|
124
121
|
# ---------------------------------------------------------------------------
|
|
@@ -199,540 +196,453 @@ if __FILE__ == $0
|
|
|
199
196
|
end
|
|
200
197
|
end
|
|
201
198
|
|
|
202
|
-
|
|
203
|
-
# Integration tests — exercises DirtyTracking through the Persistence layer,
|
|
204
|
-
# driving the full Resource → Persistence → kubectl → DirtyTracking cycle.
|
|
205
|
-
# ===========================================================================
|
|
206
|
-
class DirtyTrackingIntegrationTest < Minitest::Test
|
|
207
|
-
include ResourceHelper
|
|
208
|
-
|
|
209
|
-
# -------------------------------------------------------------------------
|
|
210
|
-
# Full lifecycle: apply → mutate → detect changes → patch → clean
|
|
211
|
-
# -------------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
def test_full_apply_mutate_patch_lifecycle
|
|
214
|
-
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
|
|
215
|
-
|
|
216
|
-
# Stub the reload after apply — server echoes back what we sent
|
|
217
|
-
ctl.stub_response("get", server_state(
|
|
218
|
-
metadata: { name: "app-config", namespace: "production", resourceVersion: "100" },
|
|
219
|
-
spec: { key: "original" }
|
|
220
|
-
))
|
|
221
|
-
|
|
222
|
-
resource.apply
|
|
223
|
-
|
|
224
|
-
# Post-apply the resource should be clean (reload calls snapshot!)
|
|
225
|
-
refute resource.changed?, "resource should be clean after apply + reload"
|
|
226
|
-
assert_equal({}, resource.changes)
|
|
227
|
-
assert_equal [], resource.changed
|
|
228
|
-
|
|
229
|
-
# Mutate
|
|
230
|
-
resource.instance_variable_get(:@data).spec.key = "updated"
|
|
231
|
-
|
|
232
|
-
# Now dirty
|
|
233
|
-
assert resource.changed?
|
|
234
|
-
|
|
235
|
-
# Stub reload after patch
|
|
236
|
-
ctl.stub_response("get", server_state(
|
|
237
|
-
metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
|
|
238
|
-
spec: { key: "updated" }
|
|
239
|
-
))
|
|
240
|
-
|
|
241
|
-
result = resource.patch
|
|
242
|
-
assert_equal true, result
|
|
243
|
-
|
|
244
|
-
# Post-patch the resource should be clean again
|
|
245
|
-
refute resource.changed?
|
|
246
|
-
assert_equal({}, resource.changes)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# -------------------------------------------------------------------------
|
|
250
|
-
# Patch returns false when nothing changed
|
|
251
|
-
# -------------------------------------------------------------------------
|
|
252
|
-
|
|
253
|
-
def test_patch_returns_false_when_clean
|
|
254
|
-
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" })
|
|
255
|
-
|
|
256
|
-
ctl.stub_response("get", server_state(
|
|
257
|
-
metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" }
|
|
258
|
-
))
|
|
259
|
-
|
|
260
|
-
result = resource.patch
|
|
261
|
-
assert_equal false, result, "patch should return false when nothing changed"
|
|
262
|
-
|
|
263
|
-
# No patch command should have been issued
|
|
264
|
-
patch_commands = ctl.commands.select { |c| c.include?("patch") }
|
|
265
|
-
assert_empty patch_commands, "no kubectl patch should be issued when resource is clean"
|
|
266
|
-
end
|
|
199
|
+
include ResourceHelper
|
|
267
200
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
201
|
+
# -------------------------------------------------------------------------
|
|
202
|
+
# Full lifecycle: apply → mutate → detect changes → patch → clean
|
|
203
|
+
# -------------------------------------------------------------------------
|
|
271
204
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
metadata: { name: "my-config", namespace: "staging" },
|
|
275
|
-
spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
276
|
-
)
|
|
205
|
+
it "full_apply_mutate_patch_lifecycle" do
|
|
206
|
+
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
|
|
277
207
|
|
|
278
|
-
|
|
279
|
-
|
|
208
|
+
# Stub the reload after apply — server echoes back what we sent
|
|
209
|
+
ctl.stub_response("get", server_state(
|
|
210
|
+
metadata: { name: "app-config", namespace: "production", resourceVersion: "100" },
|
|
211
|
+
spec: { key: "original" }
|
|
212
|
+
))
|
|
280
213
|
|
|
281
|
-
|
|
282
|
-
metadata: { name: "my-config", namespace: "staging" },
|
|
283
|
-
spec: { db_host: "new-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
284
|
-
))
|
|
214
|
+
resource.apply
|
|
285
215
|
|
|
286
|
-
|
|
216
|
+
# Mutate
|
|
217
|
+
resource.instance_variable_get(:@data).spec.key = "updated"
|
|
287
218
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
219
|
+
# Stub reload after patch
|
|
220
|
+
ctl.stub_response("get", server_state(
|
|
221
|
+
metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
|
|
222
|
+
spec: { key: "updated" }
|
|
223
|
+
))
|
|
291
224
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
225
|
+
result = resource.patch
|
|
226
|
+
result.should == true
|
|
227
|
+
end
|
|
295
228
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
end
|
|
229
|
+
# -------------------------------------------------------------------------
|
|
230
|
+
# Patch returns false when nothing changed
|
|
231
|
+
# -------------------------------------------------------------------------
|
|
300
232
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
# -------------------------------------------------------------------------
|
|
233
|
+
it "patch_returns_false_when_clean" do
|
|
234
|
+
resource, ctl = build_resource(metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" })
|
|
304
235
|
|
|
305
|
-
|
|
306
|
-
|
|
236
|
+
ctl.stub_response("get", server_state(
|
|
237
|
+
metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" }
|
|
238
|
+
))
|
|
307
239
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
240
|
+
result = resource.patch
|
|
241
|
+
result.should == false
|
|
242
|
+
end
|
|
311
243
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
))
|
|
244
|
+
# -------------------------------------------------------------------------
|
|
245
|
+
# Patch sends only the diff, not the full resource
|
|
246
|
+
# -------------------------------------------------------------------------
|
|
316
247
|
|
|
317
|
-
|
|
248
|
+
it "patch_sends_only_changed_fields" do
|
|
249
|
+
resource, ctl = build_resource(
|
|
250
|
+
metadata: { name: "my-config", namespace: "staging" },
|
|
251
|
+
spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
252
|
+
)
|
|
318
253
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
assert_equal "v1", resource.to_h[:spec][:key]
|
|
322
|
-
end
|
|
254
|
+
# Mutate one field
|
|
255
|
+
resource.instance_variable_get(:@data).spec.db_host = "new-db.internal"
|
|
323
256
|
|
|
324
|
-
|
|
325
|
-
|
|
257
|
+
ctl.stub_response("get", server_state(
|
|
258
|
+
metadata: { name: "my-config", namespace: "staging" },
|
|
259
|
+
spec: { db_host: "new-db.internal", db_port: "5432", cache_ttl: "300" }
|
|
260
|
+
))
|
|
326
261
|
|
|
327
|
-
|
|
328
|
-
ctl.stub_response("get", server_state(
|
|
329
|
-
metadata: { name: "my-config", namespace: "default", resourceVersion: "200" },
|
|
330
|
-
spec: { key: "server-updated" }
|
|
331
|
-
))
|
|
262
|
+
resource.patch
|
|
332
263
|
|
|
333
|
-
|
|
264
|
+
# Find the patch command
|
|
265
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
334
266
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
end
|
|
267
|
+
# Extract the JSON payload from the command (last arg after -p)
|
|
268
|
+
json_start = patch_cmd.index("-p ") + 3
|
|
269
|
+
payload = JSON.parse(patch_cmd[json_start..])
|
|
339
270
|
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
271
|
+
# The payload should contain the spec subtree but NOT metadata
|
|
272
|
+
payload.key?("spec").should.be.true
|
|
273
|
+
end
|
|
343
274
|
|
|
344
|
-
|
|
345
|
-
|
|
275
|
+
# -------------------------------------------------------------------------
|
|
276
|
+
# Reload resets dirty state from server response
|
|
277
|
+
# -------------------------------------------------------------------------
|
|
346
278
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
metadata: { name: "my-config", resourceVersion: "1", uid: "abc-123" },
|
|
350
|
-
spec: { key: "v1" }
|
|
351
|
-
))
|
|
279
|
+
it "reload_resets_dirty_state" do
|
|
280
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
352
281
|
|
|
353
|
-
|
|
282
|
+
# Local mutation
|
|
283
|
+
resource.instance_variable_get(:@data).spec.key = "local-change"
|
|
354
284
|
|
|
355
|
-
|
|
285
|
+
# Server still has original
|
|
286
|
+
ctl.stub_response("get", server_state(
|
|
287
|
+
metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" }
|
|
288
|
+
))
|
|
356
289
|
|
|
357
|
-
|
|
358
|
-
# the original field shows the correct old value
|
|
359
|
-
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
360
|
-
changes = resource.changes
|
|
290
|
+
resource.reload
|
|
361
291
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
assert_equal "v2", new_spec[:key]
|
|
292
|
+
# After reload, local changes are gone and resource is clean
|
|
293
|
+
resource.to_h[:spec][:key].should == "v1"
|
|
294
|
+
end
|
|
366
295
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
end
|
|
296
|
+
it "reload_picks_up_server_side_changes" do
|
|
297
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
370
298
|
|
|
371
|
-
#
|
|
372
|
-
|
|
373
|
-
|
|
299
|
+
# Server has been mutated externally
|
|
300
|
+
ctl.stub_response("get", server_state(
|
|
301
|
+
metadata: { name: "my-config", namespace: "default", resourceVersion: "200" },
|
|
302
|
+
spec: { key: "server-updated" }
|
|
303
|
+
))
|
|
374
304
|
|
|
375
|
-
|
|
376
|
-
resource, _ctl = build_resource(spec: { key: "value" })
|
|
377
|
-
# No name → not persisted
|
|
305
|
+
resource.reload
|
|
378
306
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
307
|
+
# Resource reflects server state and is clean
|
|
308
|
+
resource.to_h[:spec][:key].should == "server-updated"
|
|
309
|
+
end
|
|
382
310
|
|
|
383
|
-
|
|
384
|
-
|
|
311
|
+
# -------------------------------------------------------------------------
|
|
312
|
+
# Apply snapshots after the server round-trip
|
|
313
|
+
# -------------------------------------------------------------------------
|
|
385
314
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
end
|
|
315
|
+
it "apply_snapshots_server_response" do
|
|
316
|
+
resource, ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
389
317
|
|
|
390
|
-
|
|
391
|
-
|
|
318
|
+
# Server adds metadata on apply
|
|
319
|
+
ctl.stub_response("get", server_state(
|
|
320
|
+
metadata: { name: "my-config", resourceVersion: "1", uid: "abc-123" },
|
|
321
|
+
spec: { key: "v1" }
|
|
322
|
+
))
|
|
392
323
|
|
|
393
|
-
|
|
394
|
-
assert_match(/cannot reload/, error.message)
|
|
395
|
-
end
|
|
324
|
+
resource.apply
|
|
396
325
|
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
|
|
326
|
+
# The snapshot should include server-added fields, so mutating
|
|
327
|
+
# the original field shows the correct old value
|
|
328
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
329
|
+
changes = resource.changes
|
|
400
330
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
331
|
+
# changes[:spec] is [old_hash, new_hash]
|
|
332
|
+
old_spec, new_spec = changes[:spec]
|
|
333
|
+
old_spec[:key].should == "v1"
|
|
334
|
+
end
|
|
405
335
|
|
|
406
|
-
|
|
407
|
-
|
|
336
|
+
# -------------------------------------------------------------------------
|
|
337
|
+
# Error cases: unpersisted resources
|
|
338
|
+
# -------------------------------------------------------------------------
|
|
408
339
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
assert_kind_of Hash, patch[:metadata][:labels], "patch_data should nest into labels"
|
|
412
|
-
assert_equal ["frontend", "backend"], patch[:metadata][:labels][:tier]
|
|
340
|
+
it "patch_raises_on_unpersisted_resource" do
|
|
341
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
413
342
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
refute patch.key?(:spec), "unchanged top-level key should not appear in patch"
|
|
417
|
-
end
|
|
343
|
+
lambda { resource.patch }.should.raise Kube::CommandError
|
|
344
|
+
end
|
|
418
345
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
metadata: { name: "my-config", labels: { app: "web" } }
|
|
422
|
-
)
|
|
346
|
+
it "delete_raises_on_unpersisted_resource" do
|
|
347
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
423
348
|
|
|
424
|
-
|
|
425
|
-
|
|
349
|
+
lambda { resource.delete }.should.raise Kube::CommandError
|
|
350
|
+
end
|
|
426
351
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
# -------------------------------------------------------------------------
|
|
352
|
+
it "reload_raises_on_unpersisted_resource" do
|
|
353
|
+
resource, _ctl = build_resource(spec: { key: "value" })
|
|
430
354
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
434
|
-
data: { host: "db-1", port: "5432", pool: "5" }
|
|
435
|
-
)
|
|
355
|
+
lambda { resource.reload }.should.raise Kube::CommandError
|
|
356
|
+
end
|
|
436
357
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
d.pool = "10"
|
|
358
|
+
# -------------------------------------------------------------------------
|
|
359
|
+
# Nested mutation flows through patch_data correctly
|
|
360
|
+
# -------------------------------------------------------------------------
|
|
441
361
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
362
|
+
it "nested_mutation_produces_nested_patch" do
|
|
363
|
+
resource, ctl = build_resource(
|
|
364
|
+
metadata: { name: "my-config", namespace: "default", labels: { app: "web", tier: "frontend" } }
|
|
365
|
+
)
|
|
446
366
|
|
|
447
|
-
|
|
367
|
+
# Mutate only a nested field
|
|
368
|
+
resource.instance_variable_get(:@data).metadata.labels.tier = "backend"
|
|
448
369
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
370
|
+
patch = resource.patch_data
|
|
371
|
+
patch[:metadata][:labels][:tier].should == ["frontend", "backend"]
|
|
372
|
+
end
|
|
452
373
|
|
|
453
|
-
|
|
374
|
+
it "deeply_nested_no_change_produces_empty_patch" do
|
|
375
|
+
resource, _ctl = build_resource(
|
|
376
|
+
metadata: { name: "my-config", labels: { app: "web" } }
|
|
377
|
+
)
|
|
454
378
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
assert_equal ["5432", "5433"], payload["data"]["port"]
|
|
458
|
-
assert_equal ["5", "10"], payload["data"]["pool"]
|
|
459
|
-
end
|
|
379
|
+
resource.patch_data.should == {}
|
|
380
|
+
end
|
|
460
381
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
382
|
+
# -------------------------------------------------------------------------
|
|
383
|
+
# Multiple mutations before patch coalesce into a single diff
|
|
384
|
+
# -------------------------------------------------------------------------
|
|
464
385
|
|
|
465
|
-
|
|
466
|
-
|
|
386
|
+
it "multiple_mutations_coalesce_in_single_patch" do
|
|
387
|
+
resource, ctl = build_resource(
|
|
388
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
389
|
+
data: { host: "db-1", port: "5432", pool: "5" }
|
|
390
|
+
)
|
|
467
391
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
392
|
+
d = resource.instance_variable_get(:@data).data
|
|
393
|
+
d.host = "db-2"
|
|
394
|
+
d.port = "5433"
|
|
395
|
+
d.pool = "10"
|
|
471
396
|
|
|
472
|
-
|
|
473
|
-
|
|
397
|
+
ctl.stub_response("get", server_state(
|
|
398
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
399
|
+
data: { host: "db-2", port: "5433", pool: "10" }
|
|
400
|
+
))
|
|
474
401
|
|
|
475
|
-
|
|
476
|
-
assert_equal({}, resource.changes)
|
|
402
|
+
resource.patch
|
|
477
403
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
404
|
+
# Exactly one patch command
|
|
405
|
+
patch_commands = ctl.commands.select { |c| c.include?("patch") }
|
|
406
|
+
patch_commands.size.should == 1
|
|
407
|
+
end
|
|
481
408
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
"baseline should be v2 after changes_applied" if changes[:spec].is_a?(Hash)
|
|
486
|
-
assert_equal({ spec: [{ key: "v2" }, { key: "v3" }] }, changes) if changes[:spec].is_a?(Array)
|
|
487
|
-
end
|
|
409
|
+
# -------------------------------------------------------------------------
|
|
410
|
+
# changes_applied mid-workflow resets the baseline
|
|
411
|
+
# -------------------------------------------------------------------------
|
|
488
412
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
492
|
-
data: { a: "1", b: "2", c: "3" }
|
|
493
|
-
)
|
|
413
|
+
it "changes_applied_resets_baseline_without_server_roundtrip" do
|
|
414
|
+
resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
494
415
|
|
|
495
|
-
|
|
496
|
-
resource.instance_variable_get(:@data).data.a = "changed-a"
|
|
497
|
-
resource.changes_applied
|
|
416
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
498
417
|
|
|
499
|
-
|
|
500
|
-
|
|
418
|
+
# Accept changes locally (no kubectl call)
|
|
419
|
+
resource.changes_applied
|
|
501
420
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
data: { a: "changed-a", b: "changed-b", c: "3" }
|
|
505
|
-
))
|
|
421
|
+
resource.changes.should == {}
|
|
422
|
+
end
|
|
506
423
|
|
|
507
|
-
|
|
424
|
+
it "changes_applied_then_patch_sends_only_subsequent_changes" do
|
|
425
|
+
resource, ctl = build_resource(
|
|
426
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
427
|
+
data: { a: "1", b: "2", c: "3" }
|
|
428
|
+
)
|
|
508
429
|
|
|
509
|
-
|
|
510
|
-
|
|
430
|
+
# First wave of changes
|
|
431
|
+
resource.instance_variable_get(:@data).data.a = "changed-a"
|
|
432
|
+
resource.changes_applied
|
|
511
433
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
assert_equal ["2", "changed-b"], payload["data"]["b"]
|
|
515
|
-
refute payload["data"].key?("a"), "already-accepted change 'a' should not be in patch"
|
|
516
|
-
end
|
|
434
|
+
# Second wave — only b changes from the new baseline
|
|
435
|
+
resource.instance_variable_get(:@data).data.b = "changed-b"
|
|
517
436
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
437
|
+
ctl.stub_response("get", server_state(
|
|
438
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
439
|
+
data: { a: "changed-a", b: "changed-b", c: "3" }
|
|
440
|
+
))
|
|
521
441
|
|
|
522
|
-
|
|
523
|
-
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
442
|
+
resource.patch
|
|
524
443
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
spec: { key: "v1" }
|
|
528
|
-
))
|
|
444
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
445
|
+
payload = JSON.parse(patch_cmd.split("-p ").last)
|
|
529
446
|
|
|
530
|
-
|
|
447
|
+
# Only b should be in the patch, not a (already accepted via changes_applied)
|
|
448
|
+
payload["data"]["b"].should == ["2", "changed-b"]
|
|
449
|
+
end
|
|
531
450
|
|
|
532
|
-
|
|
533
|
-
|
|
451
|
+
# -------------------------------------------------------------------------
|
|
452
|
+
# Dynamic attr_changed? tracks through full lifecycle
|
|
453
|
+
# -------------------------------------------------------------------------
|
|
534
454
|
|
|
535
|
-
|
|
455
|
+
it "attr_changed_through_apply_mutate_patch_cycle" do
|
|
456
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
536
457
|
|
|
537
|
-
|
|
538
|
-
|
|
458
|
+
ctl.stub_response("get", server_state(
|
|
459
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
460
|
+
spec: { key: "v1" }
|
|
461
|
+
))
|
|
539
462
|
|
|
540
|
-
|
|
541
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
542
|
-
spec: { key: "v2" }
|
|
543
|
-
))
|
|
463
|
+
resource.apply
|
|
544
464
|
|
|
545
|
-
|
|
465
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
546
466
|
|
|
547
|
-
|
|
548
|
-
|
|
467
|
+
ctl.stub_response("get", server_state(
|
|
468
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
469
|
+
spec: { key: "v2" }
|
|
470
|
+
))
|
|
549
471
|
|
|
550
|
-
|
|
551
|
-
resource, _ctl = build_resource(metadata: { name: "test" })
|
|
472
|
+
resource.patch
|
|
552
473
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
assert resource.respond_to?(:anything_at_all_changed?)
|
|
556
|
-
refute resource.respond_to?(:some_random_method)
|
|
557
|
-
end
|
|
474
|
+
resource.spec_changed?.should.be.false
|
|
475
|
+
end
|
|
558
476
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
# -------------------------------------------------------------------------
|
|
477
|
+
it "respond_to_for_dynamic_changed_predicates" do
|
|
478
|
+
resource, _ctl = build_resource(metadata: { name: "test" })
|
|
562
479
|
|
|
563
|
-
|
|
564
|
-
|
|
480
|
+
resource.should.respond_to :metadata_changed?
|
|
481
|
+
end
|
|
565
482
|
|
|
566
|
-
|
|
483
|
+
# -------------------------------------------------------------------------
|
|
484
|
+
# Snapshot isolation: reload doesn't leak into captured references
|
|
485
|
+
# -------------------------------------------------------------------------
|
|
567
486
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
patch_before = resource.patch_data
|
|
487
|
+
it "reload_does_not_corrupt_previously_captured_changes" do
|
|
488
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
571
489
|
|
|
572
|
-
|
|
573
|
-
ctl.stub_response("get", server_state(
|
|
574
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
575
|
-
spec: { key: "v3-from-server" }
|
|
576
|
-
))
|
|
490
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
577
491
|
|
|
578
|
-
|
|
492
|
+
# Capture changes before reload
|
|
493
|
+
changes_before = resource.changes
|
|
494
|
+
patch_before = resource.patch_data
|
|
579
495
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
end
|
|
496
|
+
# Reload with different server state
|
|
497
|
+
ctl.stub_response("get", server_state(
|
|
498
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
499
|
+
spec: { key: "v3-from-server" }
|
|
500
|
+
))
|
|
586
501
|
|
|
587
|
-
|
|
588
|
-
resource, _ctl = build_resource(metadata: { name: "test" }, data: { counter: "1" })
|
|
502
|
+
resource.reload
|
|
589
503
|
|
|
590
|
-
|
|
591
|
-
|
|
504
|
+
# Previously captured hashes should be unaffected
|
|
505
|
+
extract_nested_value(changes_before, :spec, :key, 1).should == "v2"
|
|
506
|
+
end
|
|
592
507
|
|
|
593
|
-
|
|
508
|
+
it "snapshot_isolation_across_multiple_changes_applied" do
|
|
509
|
+
resource, _ctl = build_resource(metadata: { name: "test" }, data: { counter: "1" })
|
|
594
510
|
|
|
595
|
-
|
|
596
|
-
|
|
511
|
+
resource.instance_variable_get(:@data).data.counter = "2"
|
|
512
|
+
snapshot_1_changes = resource.changes
|
|
597
513
|
|
|
598
|
-
|
|
599
|
-
assert_equal "1", extract_nested_value(snapshot_1_changes, :data, :counter, 0)
|
|
600
|
-
assert_equal "2", extract_nested_value(snapshot_1_changes, :data, :counter, 1)
|
|
514
|
+
resource.changes_applied
|
|
601
515
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
end
|
|
516
|
+
resource.instance_variable_get(:@data).data.counter = "3"
|
|
517
|
+
snapshot_2_changes = resource.changes
|
|
605
518
|
|
|
606
|
-
#
|
|
607
|
-
|
|
608
|
-
|
|
519
|
+
# Each snapshot's changes should be independent
|
|
520
|
+
extract_nested_value(snapshot_1_changes, :data, :counter, 0).should == "1"
|
|
521
|
+
end
|
|
609
522
|
|
|
610
|
-
|
|
611
|
-
|
|
523
|
+
# -------------------------------------------------------------------------
|
|
524
|
+
# Edge case: resource with no initial spec data
|
|
525
|
+
# -------------------------------------------------------------------------
|
|
612
526
|
|
|
613
|
-
|
|
527
|
+
it "empty_resource_tracks_all_additions" do
|
|
528
|
+
resource, _ctl = build_resource(metadata: { name: "empty-config" })
|
|
614
529
|
|
|
615
|
-
|
|
616
|
-
assert_includes resource.changed, :spec
|
|
617
|
-
end
|
|
530
|
+
resource.instance_variable_get(:@data).spec.key = "added"
|
|
618
531
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
# -------------------------------------------------------------------------
|
|
532
|
+
resource.changed.should.include :spec
|
|
533
|
+
end
|
|
622
534
|
|
|
623
|
-
|
|
624
|
-
|
|
535
|
+
# -------------------------------------------------------------------------
|
|
536
|
+
# Edge case: patch type parameter is forwarded
|
|
537
|
+
# -------------------------------------------------------------------------
|
|
625
538
|
|
|
626
|
-
|
|
539
|
+
it "patch_forwards_type_parameter" do
|
|
540
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
627
541
|
|
|
628
|
-
|
|
629
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
630
|
-
spec: { key: "v2" }
|
|
631
|
-
))
|
|
542
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
632
543
|
|
|
633
|
-
|
|
544
|
+
ctl.stub_response("get", server_state(
|
|
545
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
546
|
+
spec: { key: "v2" }
|
|
547
|
+
))
|
|
634
548
|
|
|
635
|
-
|
|
636
|
-
assert_includes patch_cmd, "--type merge", "patch type should be forwarded to kubectl"
|
|
637
|
-
end
|
|
549
|
+
resource.patch(type: "merge")
|
|
638
550
|
|
|
639
|
-
|
|
640
|
-
|
|
551
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
552
|
+
patch_cmd.should.include "--type merge"
|
|
553
|
+
end
|
|
641
554
|
|
|
642
|
-
|
|
555
|
+
it "patch_defaults_to_strategic_type" do
|
|
556
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
|
|
643
557
|
|
|
644
|
-
|
|
645
|
-
metadata: { name: "my-config", namespace: "default" },
|
|
646
|
-
spec: { key: "v2" }
|
|
647
|
-
))
|
|
558
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
648
559
|
|
|
649
|
-
|
|
560
|
+
ctl.stub_response("get", server_state(
|
|
561
|
+
metadata: { name: "my-config", namespace: "default" },
|
|
562
|
+
spec: { key: "v2" }
|
|
563
|
+
))
|
|
650
564
|
|
|
651
|
-
|
|
652
|
-
assert_includes patch_cmd, "--type strategic"
|
|
653
|
-
end
|
|
565
|
+
resource.patch
|
|
654
566
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
567
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
568
|
+
patch_cmd.should.include "--type strategic"
|
|
569
|
+
end
|
|
658
570
|
|
|
659
|
-
|
|
660
|
-
|
|
571
|
+
# -------------------------------------------------------------------------
|
|
572
|
+
# Edge case: namespace flags are included correctly
|
|
573
|
+
# -------------------------------------------------------------------------
|
|
661
574
|
|
|
662
|
-
|
|
575
|
+
it "patch_includes_namespace_flags" do
|
|
576
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "kube-system" }, spec: { key: "v1" })
|
|
663
577
|
|
|
664
|
-
|
|
665
|
-
metadata: { name: "my-config", namespace: "kube-system" },
|
|
666
|
-
spec: { key: "v2" }
|
|
667
|
-
))
|
|
578
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
668
579
|
|
|
669
|
-
|
|
580
|
+
ctl.stub_response("get", server_state(
|
|
581
|
+
metadata: { name: "my-config", namespace: "kube-system" },
|
|
582
|
+
spec: { key: "v2" }
|
|
583
|
+
))
|
|
670
584
|
|
|
671
|
-
|
|
672
|
-
assert_includes patch_cmd, "--namespace kube-system"
|
|
673
|
-
end
|
|
585
|
+
resource.patch
|
|
674
586
|
|
|
675
|
-
|
|
676
|
-
|
|
587
|
+
patch_cmd = ctl.commands.find { |c| c.include?("patch") }
|
|
588
|
+
patch_cmd.should.include "--namespace kube-system"
|
|
589
|
+
end
|
|
677
590
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
spec: { key: "v1" }
|
|
681
|
-
))
|
|
591
|
+
it "reload_includes_namespace_flags" do
|
|
592
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "monitoring" }, spec: { key: "v1" })
|
|
682
593
|
|
|
683
|
-
|
|
594
|
+
ctl.stub_response("get", server_state(
|
|
595
|
+
metadata: { name: "my-config", namespace: "monitoring" },
|
|
596
|
+
spec: { key: "v1" }
|
|
597
|
+
))
|
|
684
598
|
|
|
685
|
-
|
|
686
|
-
assert_includes get_cmd, "--namespace monitoring"
|
|
687
|
-
end
|
|
599
|
+
resource.reload
|
|
688
600
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
601
|
+
get_cmd = ctl.commands.find { |c| c.include?("get") }
|
|
602
|
+
get_cmd.should.include "--namespace monitoring"
|
|
603
|
+
end
|
|
692
604
|
|
|
693
|
-
|
|
694
|
-
|
|
605
|
+
# -------------------------------------------------------------------------
|
|
606
|
+
# Edge case: delete on persisted resource issues command
|
|
607
|
+
# -------------------------------------------------------------------------
|
|
695
608
|
|
|
696
|
-
|
|
697
|
-
|
|
609
|
+
it "delete_issues_kubectl_delete" do
|
|
610
|
+
resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" })
|
|
698
611
|
|
|
699
|
-
|
|
700
|
-
refute_nil delete_cmd
|
|
701
|
-
assert_includes delete_cmd, "configmap"
|
|
702
|
-
assert_includes delete_cmd, "my-config"
|
|
703
|
-
assert_includes delete_cmd, "--namespace default"
|
|
704
|
-
end
|
|
612
|
+
result = resource.delete
|
|
705
613
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
614
|
+
delete_cmd = ctl.commands.find { |c| c.include?("delete") }
|
|
615
|
+
delete_cmd.should.include "my-config"
|
|
616
|
+
end
|
|
709
617
|
|
|
710
|
-
|
|
711
|
-
|
|
618
|
+
# -------------------------------------------------------------------------
|
|
619
|
+
# Regression: the original bug — build_changes used `result` instead of `hash`
|
|
620
|
+
# -------------------------------------------------------------------------
|
|
712
621
|
|
|
713
|
-
|
|
622
|
+
it "changes_does_not_raise_name_error" do
|
|
623
|
+
resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
|
|
714
624
|
|
|
715
|
-
|
|
716
|
-
changes = resource.changes
|
|
625
|
+
resource.instance_variable_get(:@data).spec.key = "v2"
|
|
717
626
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
end
|
|
627
|
+
# This would raise NameError with the original bug
|
|
628
|
+
changes = resource.changes
|
|
721
629
|
|
|
722
|
-
|
|
630
|
+
changes.should.be.kind_of Hash
|
|
631
|
+
end
|
|
723
632
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
633
|
+
private
|
|
634
|
+
|
|
635
|
+
# Navigate into nested change structures.
|
|
636
|
+
# changes[:spec] could be [old_hash, new_hash] or a nested diff hash.
|
|
637
|
+
def extract_nested_value(hash, top_key, nested_key, index)
|
|
638
|
+
val = hash[top_key]
|
|
639
|
+
case val
|
|
640
|
+
when Array
|
|
641
|
+
# [old_hash, new_hash]
|
|
642
|
+
val[index].is_a?(Hash) ? val[index][nested_key] : val[index]
|
|
643
|
+
when Hash
|
|
644
|
+
# nested diff: { key: [old, new] }
|
|
645
|
+
val[nested_key].is_a?(Array) ? val[nested_key][index] : val[nested_key]
|
|
736
646
|
end
|
|
737
|
-
|
|
647
|
+
end
|
|
738
648
|
end
|