kube_schema 1.3.4 → 1.3.6
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/Gemfile.lock +1 -1
- data/Rakefile +4 -4
- data/bin/increment-version +2 -0
- data/bin/test +4 -6
- data/lib/kube/errors.rb +450 -0
- data/lib/kube/monkey_patches.rb +13 -0
- data/lib/kube/schema/instance.rb +100 -0
- data/lib/kube/schema/manifest.rb +323 -0
- data/lib/kube/schema/resource.rb +406 -0
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +149 -0
- data/schemas/crd-definitions.json +220478 -595
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40d05fe2759f67a63efc0f207fe42c311893fc35c5f40d3a8aac02a8b86c1ec7
|
|
4
|
+
data.tar.gz: de1aef2abe561eed7956296c9b471545a808f037c0e38edf7b4c1d8f3238e7b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c44beed6e0457d616a2c553282a1004ce0b58fe33698b3608c881549b4bd05d7ef603bdb7d06271f74b87c8966d45ed3334792733fae0b3ef94bec2bfaf560d7
|
|
7
|
+
data.tar.gz: a521d2724f4c293465c2357d60f8043ac743f37b26fa690e3a6a31fd7e4a29ad26fcf40adcd4452f84245fc28507ea82122cc0ca5ac2daedeeee33079a7df635
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
data/bin/increment-version
CHANGED
data/bin/test
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "bundler/setup"
|
|
5
|
-
|
|
6
4
|
Dir.chdir(File.expand_path("..", __dir__))
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
files = ARGV.empty? ? Dir.glob("lib/**/*.rb").sort : ARGV
|
|
7
|
+
|
|
8
|
+
files.each do |f|
|
|
9
|
+
system("bundle", "exec", "ruby", f, exception: true)
|
|
12
10
|
end
|
data/lib/kube/errors.rb
CHANGED
|
@@ -309,3 +309,453 @@ module Kube
|
|
|
309
309
|
end
|
|
310
310
|
end
|
|
311
311
|
end
|
|
312
|
+
|
|
313
|
+
if __FILE__ == $0
|
|
314
|
+
require "bundler/setup"
|
|
315
|
+
require "rspec/autorun"
|
|
316
|
+
require "kube/schema"
|
|
317
|
+
|
|
318
|
+
RSpec.describe Kube::ValidationError do
|
|
319
|
+
# Helper to build a minimal JSONSchemer-style error hash.
|
|
320
|
+
def error_hash(overrides = {})
|
|
321
|
+
{
|
|
322
|
+
"data" => nil,
|
|
323
|
+
"data_pointer" => "",
|
|
324
|
+
"schema" => {},
|
|
325
|
+
"schema_pointer" => "",
|
|
326
|
+
"root_schema" => {},
|
|
327
|
+
"type" => "unknown",
|
|
328
|
+
"error" => "something went wrong"
|
|
329
|
+
}.merge(overrides)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
describe "#message" do
|
|
333
|
+
context "header" do
|
|
334
|
+
it "includes kind and name when both are provided" do
|
|
335
|
+
err = described_class.new(
|
|
336
|
+
[error_hash],
|
|
337
|
+
kind: "Deployment",
|
|
338
|
+
name: "web"
|
|
339
|
+
)
|
|
340
|
+
expect(err.message).to include('Schema validation failed for Deployment "web"')
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "includes kind without name" do
|
|
344
|
+
err = described_class.new(
|
|
345
|
+
[error_hash],
|
|
346
|
+
kind: "Service"
|
|
347
|
+
)
|
|
348
|
+
expect(err.message).to include("Schema validation failed for Service")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "shows a generic header when no kind or name" do
|
|
352
|
+
err = described_class.new([error_hash])
|
|
353
|
+
expect(err.message).to include("Schema validation failed")
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
context "type errors" do
|
|
358
|
+
%w[string integer number boolean array object null].each do |type|
|
|
359
|
+
it "formats #{type} type mismatch with the actual value" do
|
|
360
|
+
err = described_class.new([
|
|
361
|
+
error_hash(
|
|
362
|
+
"data_pointer" => "/spec/replicas",
|
|
363
|
+
"type" => type,
|
|
364
|
+
"data" => "bad_value"
|
|
365
|
+
)
|
|
366
|
+
])
|
|
367
|
+
expect(err.message).to include("spec.replicas = \"bad_value\" — expected #{type}, got String")
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it "shows integer value for string type error" do
|
|
372
|
+
err = described_class.new([
|
|
373
|
+
error_hash(
|
|
374
|
+
"data_pointer" => "/spec/template/image",
|
|
375
|
+
"type" => "string",
|
|
376
|
+
"data" => 2343
|
|
377
|
+
)
|
|
378
|
+
])
|
|
379
|
+
expect(err.message).to include("spec.template.image = 2343 — expected string, got Integer")
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
context "required errors" do
|
|
384
|
+
it "expands each missing key onto its own line" do
|
|
385
|
+
err = described_class.new([
|
|
386
|
+
error_hash(
|
|
387
|
+
"data_pointer" => "/spec",
|
|
388
|
+
"type" => "required",
|
|
389
|
+
"details" => { "missing_keys" => %w[selector template] }
|
|
390
|
+
)
|
|
391
|
+
])
|
|
392
|
+
expect(err.message).to include("spec.selector is required but missing")
|
|
393
|
+
expect(err.message).to include("spec.template is required but missing")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it "handles required at root level" do
|
|
397
|
+
err = described_class.new([
|
|
398
|
+
error_hash(
|
|
399
|
+
"data_pointer" => "",
|
|
400
|
+
"type" => "required",
|
|
401
|
+
"details" => { "missing_keys" => ["spec"] }
|
|
402
|
+
)
|
|
403
|
+
])
|
|
404
|
+
expect(err.message).to include("spec is required but missing")
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
it "falls back gracefully if missing_keys is absent" do
|
|
408
|
+
err = described_class.new([
|
|
409
|
+
error_hash(
|
|
410
|
+
"data_pointer" => "/spec",
|
|
411
|
+
"type" => "required",
|
|
412
|
+
"details" => {}
|
|
413
|
+
)
|
|
414
|
+
])
|
|
415
|
+
# Should not raise, should produce some message
|
|
416
|
+
expect(err.message).to include("spec")
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
context "numeric constraint errors" do
|
|
421
|
+
it "formats minimum violations" do
|
|
422
|
+
err = described_class.new([
|
|
423
|
+
error_hash(
|
|
424
|
+
"data_pointer" => "/spec/replicas",
|
|
425
|
+
"type" => "minimum",
|
|
426
|
+
"data" => -1,
|
|
427
|
+
"schema" => { "minimum" => 0 }
|
|
428
|
+
)
|
|
429
|
+
])
|
|
430
|
+
expect(err.message).to include("spec.replicas = -1 — must be >= 0")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
it "formats maximum violations" do
|
|
434
|
+
err = described_class.new([
|
|
435
|
+
error_hash(
|
|
436
|
+
"data_pointer" => "/spec/replicas",
|
|
437
|
+
"type" => "maximum",
|
|
438
|
+
"data" => 999,
|
|
439
|
+
"schema" => { "maximum" => 100 }
|
|
440
|
+
)
|
|
441
|
+
])
|
|
442
|
+
expect(err.message).to include("spec.replicas = 999 — must be <= 100")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
it "formats exclusiveMinimum violations" do
|
|
446
|
+
err = described_class.new([
|
|
447
|
+
error_hash(
|
|
448
|
+
"data_pointer" => "/spec/value",
|
|
449
|
+
"type" => "exclusiveMinimum",
|
|
450
|
+
"data" => 0,
|
|
451
|
+
"schema" => { "exclusiveMinimum" => 0 }
|
|
452
|
+
)
|
|
453
|
+
])
|
|
454
|
+
expect(err.message).to include("spec.value = 0 — must be > 0")
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "formats exclusiveMaximum violations" do
|
|
458
|
+
err = described_class.new([
|
|
459
|
+
error_hash(
|
|
460
|
+
"data_pointer" => "/spec/value",
|
|
461
|
+
"type" => "exclusiveMaximum",
|
|
462
|
+
"data" => 100,
|
|
463
|
+
"schema" => { "exclusiveMaximum" => 100 }
|
|
464
|
+
)
|
|
465
|
+
])
|
|
466
|
+
expect(err.message).to include("spec.value = 100 — must be < 100")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "formats multipleOf violations" do
|
|
470
|
+
err = described_class.new([
|
|
471
|
+
error_hash(
|
|
472
|
+
"data_pointer" => "/spec/count",
|
|
473
|
+
"type" => "multipleOf",
|
|
474
|
+
"data" => 7,
|
|
475
|
+
"schema" => { "multipleOf" => 3 }
|
|
476
|
+
)
|
|
477
|
+
])
|
|
478
|
+
expect(err.message).to include("spec.count = 7 — must be a multiple of 3")
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
context "string constraint errors" do
|
|
483
|
+
it "formats pattern violations" do
|
|
484
|
+
err = described_class.new([
|
|
485
|
+
error_hash(
|
|
486
|
+
"data_pointer" => "/metadata/name",
|
|
487
|
+
"type" => "pattern",
|
|
488
|
+
"data" => "INVALID",
|
|
489
|
+
"schema" => { "pattern" => "^[a-z0-9-]+$" }
|
|
490
|
+
)
|
|
491
|
+
])
|
|
492
|
+
expect(err.message).to include('metadata.name = "INVALID" — does not match pattern: ^[a-z0-9-]+$')
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it "formats minLength violations" do
|
|
496
|
+
err = described_class.new([
|
|
497
|
+
error_hash(
|
|
498
|
+
"data_pointer" => "/metadata/name",
|
|
499
|
+
"type" => "minLength",
|
|
500
|
+
"data" => "",
|
|
501
|
+
"schema" => { "minLength" => 1 }
|
|
502
|
+
)
|
|
503
|
+
])
|
|
504
|
+
expect(err.message).to include('metadata.name = "" — length must be >= 1')
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
it "formats maxLength violations" do
|
|
508
|
+
err = described_class.new([
|
|
509
|
+
error_hash(
|
|
510
|
+
"data_pointer" => "/metadata/name",
|
|
511
|
+
"type" => "maxLength",
|
|
512
|
+
"data" => "a" * 300,
|
|
513
|
+
"schema" => { "maxLength" => 253 }
|
|
514
|
+
)
|
|
515
|
+
])
|
|
516
|
+
expect(err.message).to include("metadata.name =")
|
|
517
|
+
expect(err.message).to include("— length must be <= 253")
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
context "enum errors" do
|
|
522
|
+
it "shows allowed values" do
|
|
523
|
+
err = described_class.new([
|
|
524
|
+
error_hash(
|
|
525
|
+
"data_pointer" => "/spec/type",
|
|
526
|
+
"type" => "enum",
|
|
527
|
+
"data" => "InvalidType",
|
|
528
|
+
"schema" => { "enum" => %w[ClusterIP NodePort LoadBalancer] }
|
|
529
|
+
)
|
|
530
|
+
])
|
|
531
|
+
expect(err.message).to include('"InvalidType" — must be one of:')
|
|
532
|
+
expect(err.message).to include("ClusterIP")
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
context "format errors" do
|
|
537
|
+
it "shows the expected format" do
|
|
538
|
+
err = described_class.new([
|
|
539
|
+
error_hash(
|
|
540
|
+
"data_pointer" => "/spec/startTime",
|
|
541
|
+
"type" => "format",
|
|
542
|
+
"data" => "not-a-date",
|
|
543
|
+
"schema" => { "format" => "date-time" }
|
|
544
|
+
)
|
|
545
|
+
])
|
|
546
|
+
expect(err.message).to include('spec.startTime = "not-a-date" — invalid date-time format')
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
context "array constraint errors" do
|
|
551
|
+
it "formats minItems violations" do
|
|
552
|
+
err = described_class.new([
|
|
553
|
+
error_hash(
|
|
554
|
+
"data_pointer" => "/spec/containers",
|
|
555
|
+
"type" => "minItems",
|
|
556
|
+
"data" => [],
|
|
557
|
+
"schema" => { "minItems" => 1 }
|
|
558
|
+
)
|
|
559
|
+
])
|
|
560
|
+
expect(err.message).to include("spec.containers — array must have >= 1 items, got 0")
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it "formats uniqueItems violations" do
|
|
564
|
+
err = described_class.new([
|
|
565
|
+
error_hash(
|
|
566
|
+
"data_pointer" => "/spec/ports",
|
|
567
|
+
"type" => "uniqueItems",
|
|
568
|
+
"data" => [80, 80]
|
|
569
|
+
)
|
|
570
|
+
])
|
|
571
|
+
expect(err.message).to include("spec.ports — array items must be unique")
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
context "unknown error types" do
|
|
576
|
+
it "falls back to JSONSchemer's error message" do
|
|
577
|
+
err = described_class.new([
|
|
578
|
+
error_hash(
|
|
579
|
+
"data_pointer" => "/spec/something",
|
|
580
|
+
"type" => "oneOf",
|
|
581
|
+
"error" => "value at `/spec/something` does not match exactly one schema"
|
|
582
|
+
)
|
|
583
|
+
])
|
|
584
|
+
expect(err.message).to include("spec.something: value at `/spec/something` does not match exactly one schema")
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
it "falls back to the type name when no error message exists" do
|
|
588
|
+
err = described_class.new([
|
|
589
|
+
error_hash(
|
|
590
|
+
"data_pointer" => "/spec/x",
|
|
591
|
+
"type" => "customKeyword",
|
|
592
|
+
"error" => nil
|
|
593
|
+
)
|
|
594
|
+
])
|
|
595
|
+
expect(err.message).to include("spec.x: customKeyword")
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
context "value truncation" do
|
|
600
|
+
it "truncates long values" do
|
|
601
|
+
long_value = "a" * 200
|
|
602
|
+
err = described_class.new([
|
|
603
|
+
error_hash(
|
|
604
|
+
"data_pointer" => "/spec/data",
|
|
605
|
+
"type" => "integer",
|
|
606
|
+
"data" => long_value
|
|
607
|
+
)
|
|
608
|
+
])
|
|
609
|
+
expect(err.message).to include("...")
|
|
610
|
+
expect(err.message.length).to be < 500
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
context "dot-notation path conversion" do
|
|
615
|
+
it "converts JSON pointers to dot notation" do
|
|
616
|
+
err = described_class.new([
|
|
617
|
+
error_hash(
|
|
618
|
+
"data_pointer" => "/spec/template/spec/containers/0/image",
|
|
619
|
+
"type" => "string",
|
|
620
|
+
"data" => 123
|
|
621
|
+
)
|
|
622
|
+
])
|
|
623
|
+
expect(err.message).to include("spec.template.spec.containers.0.image = 123")
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
it "uses 'root' for empty pointer" do
|
|
627
|
+
err = described_class.new([
|
|
628
|
+
error_hash(
|
|
629
|
+
"data_pointer" => "",
|
|
630
|
+
"type" => "object",
|
|
631
|
+
"data" => "not_an_object"
|
|
632
|
+
)
|
|
633
|
+
])
|
|
634
|
+
expect(err.message).to include("root = ")
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
describe "#errors" do
|
|
640
|
+
it "exposes the raw JSONSchemer error hashes" do
|
|
641
|
+
raw = [error_hash("type" => "string")]
|
|
642
|
+
err = described_class.new(raw)
|
|
643
|
+
expect(err.errors).to equal(raw)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
describe "annotated manifest output" do
|
|
648
|
+
it "includes the resource YAML when manifest is provided" do
|
|
649
|
+
manifest = {
|
|
650
|
+
"apiVersion" => "apps/v1",
|
|
651
|
+
"kind" => "Deployment",
|
|
652
|
+
"metadata" => { "name" => "web", "namespace" => "prod" },
|
|
653
|
+
"spec" => { "replicas" => "bad" }
|
|
654
|
+
}
|
|
655
|
+
err = described_class.new(
|
|
656
|
+
[error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
|
|
657
|
+
kind: "Deployment",
|
|
658
|
+
name: "web",
|
|
659
|
+
manifest: manifest
|
|
660
|
+
)
|
|
661
|
+
expect(err.message).to include("apiVersion: apps/v1")
|
|
662
|
+
expect(err.message).to include("kind: Deployment")
|
|
663
|
+
expect(err.message).to include("name: web")
|
|
664
|
+
expect(err.message).to include("namespace: prod")
|
|
665
|
+
expect(err.message).to include("replicas: bad")
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
it "highlights error lines in red (ANSI)" do
|
|
669
|
+
manifest = {
|
|
670
|
+
"apiVersion" => "apps/v1",
|
|
671
|
+
"kind" => "Deployment",
|
|
672
|
+
"spec" => { "replicas" => "bad" }
|
|
673
|
+
}
|
|
674
|
+
err = described_class.new(
|
|
675
|
+
[error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
|
|
676
|
+
manifest: manifest
|
|
677
|
+
)
|
|
678
|
+
# The replicas line should be wrapped in red ANSI
|
|
679
|
+
expect(err.message).to include("\033[31m")
|
|
680
|
+
expect(err.message).to match(/\033\[31m\s*replicas: bad\033\[0m/)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
it "appends a yellow inline comment on error lines" do
|
|
684
|
+
manifest = {
|
|
685
|
+
"apiVersion" => "v1",
|
|
686
|
+
"kind" => "Pod",
|
|
687
|
+
"spec" => { "replicas" => "bad" }
|
|
688
|
+
}
|
|
689
|
+
err = described_class.new(
|
|
690
|
+
[error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
|
|
691
|
+
manifest: manifest
|
|
692
|
+
)
|
|
693
|
+
expect(err.message).to match(/\033\[33m# expected integer, got String\033\[0m/)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it "injects missing required keys as red lines" do
|
|
697
|
+
manifest = {
|
|
698
|
+
"apiVersion" => "apps/v1",
|
|
699
|
+
"kind" => "Deployment",
|
|
700
|
+
"spec" => { "replicas" => 3 }
|
|
701
|
+
}
|
|
702
|
+
err = described_class.new(
|
|
703
|
+
[error_hash(
|
|
704
|
+
"data_pointer" => "/spec",
|
|
705
|
+
"type" => "required",
|
|
706
|
+
"details" => { "missing_keys" => ["selector"] }
|
|
707
|
+
)],
|
|
708
|
+
manifest: manifest
|
|
709
|
+
)
|
|
710
|
+
expect(err.message).to match(/\033\[31m\s*selector:\033\[0m/)
|
|
711
|
+
expect(err.message).to include("MISSING")
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
it "does not color lines that have no errors" do
|
|
715
|
+
manifest = {
|
|
716
|
+
"apiVersion" => "apps/v1",
|
|
717
|
+
"kind" => "Deployment",
|
|
718
|
+
"metadata" => { "name" => "web" },
|
|
719
|
+
"spec" => { "replicas" => "bad" }
|
|
720
|
+
}
|
|
721
|
+
err = described_class.new(
|
|
722
|
+
[error_hash("data_pointer" => "/spec/replicas", "type" => "integer", "data" => "bad")],
|
|
723
|
+
manifest: manifest
|
|
724
|
+
)
|
|
725
|
+
# The "name: web" line should NOT have red ANSI
|
|
726
|
+
err.message.each_line do |line|
|
|
727
|
+
next unless line.include?("name: web")
|
|
728
|
+
expect(line).not_to include("\033[31m")
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
it "handles array items with correct pointer mapping" do
|
|
733
|
+
manifest = {
|
|
734
|
+
"apiVersion" => "v1",
|
|
735
|
+
"kind" => "Pod",
|
|
736
|
+
"spec" => {
|
|
737
|
+
"containers" => [
|
|
738
|
+
{ "name" => "app", "image" => 2343 }
|
|
739
|
+
]
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
err = described_class.new(
|
|
743
|
+
[error_hash(
|
|
744
|
+
"data_pointer" => "/spec/containers/0/image",
|
|
745
|
+
"type" => "string",
|
|
746
|
+
"data" => 2343
|
|
747
|
+
)],
|
|
748
|
+
manifest: manifest
|
|
749
|
+
)
|
|
750
|
+
expect(err.message).to match(/\033\[31m\s*image: 2343\033\[0m/)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
it "omits the manifest section when manifest is nil" do
|
|
754
|
+
err = described_class.new(
|
|
755
|
+
[error_hash("data_pointer" => "/spec/x", "type" => "string", "data" => 1)]
|
|
756
|
+
)
|
|
757
|
+
expect(err.message).not_to include("---")
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
end
|
data/lib/kube/monkey_patches.rb
CHANGED
|
@@ -20,4 +20,17 @@ class Hash
|
|
|
20
20
|
#def respond_to_missing(name, _priv = false)
|
|
21
21
|
# name.to_s.end_with?("=") || key?(name.to_sym)
|
|
22
22
|
#end
|
|
23
|
+
|
|
24
|
+
# Build a Hash with autovivification via a block DSL.
|
|
25
|
+
#
|
|
26
|
+
# Hash.vivify {
|
|
27
|
+
# metadata.name = "web"
|
|
28
|
+
# spec.replicas = 3
|
|
29
|
+
# spec.selector.matchLabels.app = "web"
|
|
30
|
+
# }
|
|
31
|
+
# # => { metadata: { name: "web" }, spec: { replicas: 3, selector: { matchLabels: { app: "web" } } } }
|
|
32
|
+
#
|
|
33
|
+
def self.vivify(&block)
|
|
34
|
+
new.tap { |h| h.instance_exec(&block) }
|
|
35
|
+
end
|
|
23
36
|
end
|
data/lib/kube/schema/instance.rb
CHANGED
|
@@ -222,3 +222,103 @@ module Kube
|
|
|
222
222
|
end
|
|
223
223
|
end
|
|
224
224
|
end
|
|
225
|
+
|
|
226
|
+
if __FILE__ == $0
|
|
227
|
+
require "bundler/setup"
|
|
228
|
+
require "rspec/autorun"
|
|
229
|
+
require "kube/schema"
|
|
230
|
+
|
|
231
|
+
RSpec.describe Kube::Schema::Instance do
|
|
232
|
+
subject(:instance) { described_class.new("1.34") }
|
|
233
|
+
|
|
234
|
+
describe "#initialize" do
|
|
235
|
+
it "stores the version" do
|
|
236
|
+
expect(instance.version).to eq("1.34")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "raises UnknownVersionError for a non-version string" do
|
|
240
|
+
expect { described_class.new("not-a-version") }.to raise_error(Kube::UnknownVersionError)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe "#[]" do
|
|
245
|
+
it "returns a Class that subclasses Resource" do
|
|
246
|
+
klass = instance["Deployment"]
|
|
247
|
+
expect(klass).to be_a(Class)
|
|
248
|
+
expect(klass).to be < Kube::Schema::Resource
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "caches resource classes by key" do
|
|
252
|
+
a = instance["Deployment"]
|
|
253
|
+
b = instance["Deployment"]
|
|
254
|
+
expect(a).to be(b)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "raises for an unknown resource" do
|
|
258
|
+
expect { instance["ThisDoesNotExist999"] }.to raise_error(RuntimeError, /No resource schema found/)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "returns different classes for different resources" do
|
|
262
|
+
deployment = instance["Deployment"]
|
|
263
|
+
service = instance["Service"]
|
|
264
|
+
expect(deployment).not_to eq(service)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "attaches a schema to the resource class" do
|
|
268
|
+
klass = instance["Deployment"]
|
|
269
|
+
expect(klass.schema).not_to be_nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "is case-insensitive" do
|
|
273
|
+
a = instance["Deployment"]
|
|
274
|
+
b = instance["deployment"]
|
|
275
|
+
expect(a).to be < Kube::Schema::Resource
|
|
276
|
+
expect(b).to be < Kube::Schema::Resource
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "produces resources that can be instantiated with the block DSL" do
|
|
280
|
+
resource = instance["Deployment"].new {
|
|
281
|
+
metadata.name = "test"
|
|
282
|
+
}
|
|
283
|
+
expect(resource.to_h[:apiVersion]).to eq("apps/v1")
|
|
284
|
+
expect(resource.to_h[:kind]).to eq("Deployment")
|
|
285
|
+
expect(resource.to_h[:metadata][:name]).to eq("test")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "attaches defaults (apiVersion and kind) to the resource class" do
|
|
289
|
+
klass = instance["Deployment"]
|
|
290
|
+
expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it "derives correct apiVersion for core resources (empty group)" do
|
|
294
|
+
klass = instance["Pod"]
|
|
295
|
+
expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it "derives correct apiVersion for grouped resources" do
|
|
299
|
+
klass = instance["Ingress"]
|
|
300
|
+
expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" })
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
describe "#list_resources" do
|
|
305
|
+
it "returns a sorted array of kind strings" do
|
|
306
|
+
kinds = instance.list_resources
|
|
307
|
+
expect(kinds).to be_an(Array)
|
|
308
|
+
expect(kinds).not_to be_empty
|
|
309
|
+
expect(kinds).to include("Deployment", "Service", "Namespace", "Pod")
|
|
310
|
+
expect(kinds).to eq(kinds.sort)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
describe "class-level schemer cache" do
|
|
315
|
+
it "shares the schemer across instances of the same version" do
|
|
316
|
+
a = described_class.new("1.34")
|
|
317
|
+
b = described_class.new("1.34")
|
|
318
|
+
# Both should resolve without error and return equivalent classes
|
|
319
|
+
expect(a["Deployment"]).to be < Kube::Schema::Resource
|
|
320
|
+
expect(b["Deployment"]).to be < Kube::Schema::Resource
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|