kube_schema 1.3.2 → 1.3.5
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/schema/instance.rb +119 -0
- data/lib/kube/schema/manifest.rb +359 -2
- data/lib/kube/schema/resource.rb +441 -13
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +173 -8
- data/schemas/crd-definitions.json +220478 -595
- metadata +1 -2
- data/AGENTS.md +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a57b14228a491e69ae291d85208d7a4d9ab0ff4dfe0d1c8fe516dee313da896d
|
|
4
|
+
data.tar.gz: f7ca2465bc0a0867db00d3f8dd6e28235f86a626746fa6c0fad35bedc153d377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05decddd173ef72baa520b33d00749bafe275c20411dda61984855b0a000446bc29731d861246b529b5d69148419631741af7e3485a1fcd2f019aa69d30840ec
|
|
7
|
+
data.tar.gz: 53aad4ff4cbc381fde9b8f41c6e2b0744c3e7427bb9e908e33d741450b15c751a8f827727ce229425bedaecf764e0572ef82915a8d9b885c46572ef6b7f57e27
|
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/schema/instance.rb
CHANGED
|
@@ -101,6 +101,14 @@ module Kube
|
|
|
101
101
|
schema["definitions"].merge!(extra)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Kubernetes OpenAPI v2 defines IntOrString as "type": "string"
|
|
105
|
+
# with "format": "int-or-string". This is a limitation of
|
|
106
|
+
# OpenAPI v2 which cannot express union types. Patch the
|
|
107
|
+
# definition so JSONSchemer accepts both integers and strings.
|
|
108
|
+
if (int_or_str = schema.dig("definitions", "io.k8s.apimachinery.pkg.util.intstr.IntOrString"))
|
|
109
|
+
int_or_str["type"] = ["string", "integer"]
|
|
110
|
+
end
|
|
111
|
+
|
|
104
112
|
JSONSchemer.schema(schema)
|
|
105
113
|
end
|
|
106
114
|
end
|
|
@@ -184,6 +192,7 @@ module Kube
|
|
|
184
192
|
Class.new(::Kube::Schema::Resource) do
|
|
185
193
|
@schema = schema_instance
|
|
186
194
|
@defaults = defaults
|
|
195
|
+
@schema_properties = @schema.value["properties"].keys.map(&:to_sym)
|
|
187
196
|
|
|
188
197
|
def self.schema
|
|
189
198
|
@schema || superclass.schema
|
|
@@ -192,6 +201,16 @@ module Kube
|
|
|
192
201
|
def self.defaults
|
|
193
202
|
@defaults || superclass.defaults
|
|
194
203
|
end
|
|
204
|
+
|
|
205
|
+
def self.schema_properties
|
|
206
|
+
@schema_properties
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
schema_instance.value["properties"].keys.then do |properties|
|
|
210
|
+
properties.each do |prop|
|
|
211
|
+
define_method(prop.to_sym) { @data[prop.to_sym] }
|
|
212
|
+
end
|
|
213
|
+
end
|
|
195
214
|
end
|
|
196
215
|
end
|
|
197
216
|
|
|
@@ -203,3 +222,103 @@ module Kube
|
|
|
203
222
|
end
|
|
204
223
|
end
|
|
205
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
|