kube_schema 1.4.8 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23eda6bd933e2383e7aebc9b536d0d23ed1f55dff09d6c84968a1471de17a0b4
4
- data.tar.gz: 257506c09fbdd153f1eaba48bd38b7012a584a5294086cdbbe517dfd84778e74
3
+ metadata.gz: 3fe7b4efafcce28b1cead68ac163369fa93a88bf8fb20c1b4c93f184df24383c
4
+ data.tar.gz: c98ac341e688a2d93a58e470ebc307542628a2682ddc6821756ca13c75221aeb
5
5
  SHA512:
6
- metadata.gz: 8584a14a894bdfc04a6c26a4ebc5d10a0a0923872b6c747b7fc3d3b0cd1f412a274aeadedf9aff04750dada41780260e6cd882f1aa83ca0a26606078227bbf7b
7
- data.tar.gz: ddc8542cdb6a43ca16774fc1abf26990e36c9acb3feeced54d77064010438895027c9f5d22c4ee07b36f129f86f7c65d8fed43701648d25d91fd1e9bb5da6cf1
6
+ metadata.gz: 8123faed36719b2de640ba907fa86a229eb20fa3f97b38bf72dd030dfce82c2e83673d3f61c7c1c61f51589b418baabbc3619fb61b37c8938ce4fb67d7317ac6
7
+ data.tar.gz: 07ea4b9cb8cfc4f9fd2b5b8721b884df1ca23cd2a586e7765e4e8a7fef0373ad9074f2f53d5987cf6deb2d5e144a0f5e35bb1f5e66078a90d4a4d738c79251b4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_schema (1.4.8)
4
+ kube_schema (1.5.0)
5
5
  json_schemer (~> 2.5.0)
6
6
  rubyshell (~> 1.5.0)
7
7
 
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Rewrites schemas/cdi-definitions.json in-place to:
4
+ # 1. Add x-kubernetes-group-version-kind annotations
5
+ # 2. Fix $ref pointers from bare v1.* to io.k8s.* prefix
6
+ #
7
+ set -euo pipefail
8
+
9
+ DEFS_FILE="schemas/cdi-definitions.json"
10
+
11
+ echo "Rewriting $DEFS_FILE..."
12
+
13
+ jq '
14
+ # GVK map: definition key -> [{ group, version, kind }]
15
+ {
16
+ "v1beta1.CDI": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "CDI" }],
17
+ "v1beta1.CDIConfig": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "CDIConfig" }],
18
+ "v1beta1.DataImportCron": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "DataImportCron" }],
19
+ "v1beta1.DataSource": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "DataSource" }],
20
+ "v1beta1.DataVolume": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "DataVolume" }],
21
+ "v1beta1.ObjectTransfer": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "ObjectTransfer" }],
22
+ "v1beta1.StorageProfile": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "StorageProfile" }],
23
+ "v1beta1.VolumeCloneSource": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "VolumeCloneSource" }],
24
+ "v1beta1.VolumeImportSource": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "VolumeImportSource" }],
25
+ "v1beta1.VolumeUploadSource": [{ group: "cdi.kubevirt.io", version: "v1beta1", kind: "VolumeUploadSource" }],
26
+ "v1beta1.UploadTokenRequest": [{ group: "upload.cdi.kubevirt.io", version: "v1beta1", kind: "UploadTokenRequest" }]
27
+ } as $gvk_map |
28
+
29
+ # Explicit $ref rewrite map: bare v1.* -> io.k8s.* full paths
30
+ {
31
+ "#/definitions/v1.Affinity": "#/definitions/io.k8s.api.core.v1.Affinity",
32
+ "#/definitions/v1.Condition": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Condition",
33
+ "#/definitions/v1.Duration": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Duration",
34
+ "#/definitions/v1.LabelSelector": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector",
35
+ "#/definitions/v1.ListMeta": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta",
36
+ "#/definitions/v1.LocalObjectReference": "#/definitions/io.k8s.api.core.v1.LocalObjectReference",
37
+ "#/definitions/v1.ObjectMeta": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
38
+ "#/definitions/v1.PersistentVolumeClaimSpec": "#/definitions/io.k8s.api.core.v1.PersistentVolumeClaimSpec",
39
+ "#/definitions/v1.ResourceRequirements": "#/definitions/io.k8s.api.core.v1.ResourceRequirements",
40
+ "#/definitions/v1.Time": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
41
+ "#/definitions/v1.Toleration": "#/definitions/io.k8s.api.core.v1.Toleration",
42
+ "#/definitions/v1.TypedLocalObjectReference": "#/definitions/io.k8s.api.core.v1.TypedLocalObjectReference",
43
+ "#/definitions/v1.TypedObjectReference": "#/definitions/io.k8s.api.core.v1.TypedObjectReference",
44
+ "#/definitions/v1.VolumeResourceRequirements": "#/definitions/io.k8s.api.core.v1.VolumeResourceRequirements"
45
+ } as $ref_map |
46
+
47
+ # Inject x-kubernetes-group-version-kind annotations
48
+ with_entries(
49
+ if $gvk_map[.key] then
50
+ .value["x-kubernetes-group-version-kind"] = $gvk_map[.key]
51
+ else . end
52
+ ) |
53
+
54
+ # Fix $ref pointers using explicit mapping
55
+ walk(
56
+ if type == "string" and $ref_map[.] then $ref_map[.]
57
+ else . end
58
+ )
59
+ ' "$DEFS_FILE" > "${DEFS_FILE}.tmp" && mv "${DEFS_FILE}.tmp" "$DEFS_FILE"
60
+
61
+ ANNOTATED=$(jq '[.[] | select(has("x-kubernetes-group-version-kind"))] | length' "$DEFS_FILE")
62
+ TOTAL=$(jq 'length' "$DEFS_FILE")
63
+ STALE_REFS=$(jq -r '[.. | strings | select(startswith("#/definitions/v1."))] | length' "$DEFS_FILE")
64
+ echo "Done. $ANNOTATED/$TOTAL definitions annotated. $STALE_REFS stale v1.* refs remaining."
@@ -1,4 +1,3 @@
1
- # https://github.com/mickey/black-hole-struct/blob/master/lib/black_hole_struct.rb
2
1
  class Hash
3
2
  def method_missing(name, *args)
4
3
  key = name.to_s
@@ -38,29 +38,52 @@ module Kube
38
38
  @version = version
39
39
  end
40
40
 
41
- # Look up a resource by kind (e.g. "Deployment", "NetworkPolicy").
41
+ # Look up a resource by kind or full GVK string.
42
+ #
43
+ # Accepts:
44
+ # instance["Deployment"] — kind-only lookup
45
+ # instance["apps/v1/Deployment"] — group/version/kind
46
+ # instance["v1/Pod"] — version/kind (core, empty group)
47
+ # instance["networking.k8s.io/v1/Ingress"] — fully qualified
48
+ #
42
49
  # Returns a class that inherits from Kube::Schema::Resource.
43
50
  #
44
51
  # Custom schemas registered via Kube::Schema.register take precedence
45
- # over built-in definitions, allowing users to override or extend the
46
- # schema for any kind.
47
- def [](kind)
48
- @resource_classes[kind] ||= begin
49
- # Custom schemas win over built-in definitions.
50
- custom = find_custom_entry(kind)
51
- if custom
52
- build_resource_class(custom[:schema], custom[:defaults])
53
- else
54
- entry = find_gvk_entry(kind)
52
+ # over built-in definitions (kind-only lookups only).
53
+ def [](input)
54
+ @resource_classes[input] ||= begin
55
+ if input.include?("/")
56
+ parts = input.split("/")
57
+
58
+ case parts.length
59
+ when 3
60
+ group, version, kind = parts
61
+ when 2
62
+ group = ""
63
+ version, kind = parts
64
+ else
65
+ raise "Invalid GVK format: #{input.inspect}." \
66
+ "\nExpected \"group/version/kind\" or \"version/kind\"."
67
+ end
55
68
 
56
- if entry.nil?
57
- raise "No resource schema found for #{kind}!" \
58
- "\nUse #list_resources to see available kinds for v#{version}."
69
+ entry = find_gvk_entry_by_full_gvk(group, version, kind)
70
+ else
71
+ # Kind-only lookup custom schemas take precedence.
72
+ custom = find_custom_entry(input)
73
+ if custom
74
+ return build_resource_class(custom[:schema], custom[:defaults])
59
75
  end
60
76
 
61
- ref_schema = schemer.ref("#/definitions/#{entry[:definition_key]}")
62
- build_resource_class(ref_schema, entry[:defaults].freeze)
77
+ entry = find_gvk_entry(input)
78
+ end
79
+
80
+ if entry.nil?
81
+ raise "No resource schema found for #{input.inspect}!" \
82
+ "\nUse #list_resources to see available kinds for v#{@version}."
63
83
  end
84
+
85
+ ref_schema = schemer.ref("#/definitions/#{entry[:definition_key]}")
86
+ build_resource_class(ref_schema, entry[:defaults].freeze)
64
87
  end
65
88
  end
66
89
 
@@ -167,7 +190,7 @@ module Kube
167
190
  # }
168
191
  def gvk_index
169
192
  @gvk_index ||= begin
170
- index = {}
193
+ index = Hash.new { |h, k| h[k] = [] }
171
194
 
172
195
  schemer.value.fetch("definitions", {}).each do |key, definition|
173
196
  gvks = definition["x-kubernetes-group-version-kind"]
@@ -179,7 +202,7 @@ module Kube
179
202
  kind = gvk["kind"]
180
203
  api_version = group.empty? ? version : "#{group}/#{version}"
181
204
 
182
- index[kind] = {
205
+ index[kind] << {
183
206
  definition_key: key,
184
207
  group: group,
185
208
  version: version,
@@ -197,17 +220,29 @@ module Kube
197
220
  end
198
221
 
199
222
  # Find a GVK entry by kind name (case-insensitive).
200
- # Returns the full entry hash or nil.
223
+ # Returns the first matching entry hash or nil.
201
224
  def find_gvk_entry(kind)
202
- return gvk_index[kind] if gvk_index.key?(kind)
225
+ entries = gvk_index[kind]
226
+ return entries.first if entries && !entries.empty?
203
227
 
204
228
  gvk_index.each do |k, v|
205
- return v if k.downcase == kind.downcase
229
+ return v.first if k.downcase == kind.downcase
206
230
  end
207
231
 
208
232
  nil
209
233
  end
210
234
 
235
+ # Find a GVK entry by exact group, version, and kind.
236
+ # Returns the matching entry hash or nil.
237
+ def find_gvk_entry_by_full_gvk(group, version, kind)
238
+ entries = gvk_index[kind]
239
+ return nil if entries.nil? || entries.empty?
240
+
241
+ entries.find do |entry|
242
+ entry[:group] == group && entry[:version] == version
243
+ end
244
+ end
245
+
211
246
  # Find a custom schema entry by kind (case-insensitive).
212
247
  # Returns the { schema:, defaults: } hash or nil.
213
248
  def find_custom_entry(kind)
@@ -391,6 +426,46 @@ if __FILE__ == $0
391
426
  end
392
427
  end
393
428
 
429
+ describe "#[] with full GVK string" do
430
+ it "resolves apps/v1/Deployment" do
431
+ klass = instance["apps/v1/Deployment"]
432
+ expect(klass).to be < Kube::Schema::Resource
433
+ expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
434
+ end
435
+
436
+ it "resolves v1/Pod (core resource, empty group)" do
437
+ klass = instance["v1/Pod"]
438
+ expect(klass).to be < Kube::Schema::Resource
439
+ expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
440
+ end
441
+
442
+ it "resolves networking.k8s.io/v1/Ingress" do
443
+ klass = instance["networking.k8s.io/v1/Ingress"]
444
+ expect(klass).to be < Kube::Schema::Resource
445
+ expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" })
446
+ end
447
+
448
+ it "resolves kubevirt.io/v1/VirtualMachine" do
449
+ klass = instance["kubevirt.io/v1/VirtualMachine"]
450
+ expect(klass).to be < Kube::Schema::Resource
451
+ expect(klass.defaults).to eq({ "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" })
452
+ end
453
+
454
+ it "raises for invalid GVK format (too many slashes)" do
455
+ expect { instance["a/b/c/d"] }.to raise_error(RuntimeError, /Invalid GVK format/)
456
+ end
457
+
458
+ it "raises for non-existent GVK" do
459
+ expect { instance["fake.io/v99/Blah"] }.to raise_error(RuntimeError, /No resource schema found/)
460
+ end
461
+
462
+ it "caches GVK lookups" do
463
+ a = instance["apps/v1/Deployment"]
464
+ b = instance["apps/v1/Deployment"]
465
+ expect(a).to be(b)
466
+ end
467
+ end
468
+
394
469
  describe "#list_resources" do
395
470
  it "returns a sorted array of kind strings" do
396
471
  kinds = instance.list_resources
@@ -453,6 +528,52 @@ if __FILE__ == $0
453
528
  end
454
529
  end
455
530
 
531
+ describe "CloudnativePG schemas" do
532
+ it "resolves Cluster by kind" do
533
+ klass = instance["Cluster"]
534
+ expect(klass).to be < Kube::Schema::Resource
535
+ expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" })
536
+ end
537
+
538
+ it "resolves Cluster by full GVK string" do
539
+ klass = instance["postgresql.cnpg.io/v1/Cluster"]
540
+ expect(klass).to be < Kube::Schema::Resource
541
+ expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" })
542
+ end
543
+
544
+ it "includes Cluster in list_resources" do
545
+ expect(instance.list_resources).to include("Cluster")
546
+ end
547
+
548
+ it "resolves Backup" do
549
+ klass = instance["postgresql.cnpg.io/v1/Backup"]
550
+ expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Backup" })
551
+ end
552
+
553
+ it "resolves ScheduledBackup" do
554
+ klass = instance["postgresql.cnpg.io/v1/ScheduledBackup"]
555
+ expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "ScheduledBackup" })
556
+ end
557
+
558
+ it "resolves Pooler" do
559
+ klass = instance["postgresql.cnpg.io/v1/Pooler"]
560
+ expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Pooler" })
561
+ end
562
+
563
+ it "can instantiate a Cluster with the block DSL" do
564
+ resource = instance["postgresql.cnpg.io/v1/Cluster"].new {
565
+ metadata.name = "pg-cluster"
566
+ metadata.namespace = "databases"
567
+ spec.instances = 3
568
+ spec.storage.size = "10Gi"
569
+ }
570
+ expect(resource.to_h[:apiVersion]).to eq("postgresql.cnpg.io/v1")
571
+ expect(resource.to_h[:kind]).to eq("Cluster")
572
+ expect(resource.to_h[:metadata][:name]).to eq("pg-cluster")
573
+ expect(resource.to_h[:spec][:instances]).to eq(3)
574
+ end
575
+ end
576
+
456
577
  describe "class-level schemer cache" do
457
578
  it "shares the schemer across instances of the same version" do
458
579
  a = described_class.new("1.34")
@@ -352,7 +352,6 @@ if __FILE__ == $0
352
352
 
353
353
  expect(yaml).to include("kind: Pod")
354
354
  expect(yaml).to include("apiVersion: v1")
355
- expect(yaml).not_to include("BlackHoleStruct")
356
355
  expect(yaml).not_to include("!ruby/object")
357
356
  end
358
357
 
@@ -435,7 +434,6 @@ if __FILE__ == $0
435
434
  yaml = deployment.to_yaml
436
435
 
437
436
  expect(yaml).not_to include("!ruby/object")
438
- expect(yaml).not_to include("BlackHoleStruct")
439
437
  expect(yaml).not_to include("table:")
440
438
  end
441
439
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Schema
5
- VERSION = "1.4.8"
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
data/lib/kube/schema.rb CHANGED
@@ -162,53 +162,6 @@ module Kube
162
162
  end
163
163
  end
164
164
 
165
- # Patch BlackHoleStruct to handle arrays consistently.
166
- #
167
- # The upstream gem does not recurse into arrays — hashes inside arrays
168
- # are not converted to BlackHoleStruct on construction, and are not
169
- # converted back to plain Hash on #to_h. This causes key-type
170
- # inconsistencies after a Resource round-trip (symbol keys become
171
- # string keys inside arrays).
172
- #
173
- # These two patches fix both directions:
174
- # initialize — converts hashes inside arrays to BlackHoleStruct
175
- # to_h — converts BlackHoleStruct/arrays back to plain objects
176
- class BlackHoleStruct
177
- def initialize(hash = {})
178
- raise ArgumentError, "Argument should be a Hash" unless hash.is_a?(Hash)
179
-
180
- @table = {}
181
- hash.each do |key, value|
182
- @table[key.to_sym] = deep_wrap(value)
183
- end
184
- end
185
-
186
- def to_h
187
- hash = {}
188
- @table.each do |key, value|
189
- hash[key] = deep_unwrap(value)
190
- end
191
- hash
192
- end
193
-
194
- private
195
-
196
- def deep_wrap(value)
197
- case value
198
- when Hash then self.class.new(value)
199
- when Array then value.map { |v| deep_wrap(v) }
200
- else value
201
- end
202
- end
203
-
204
- def deep_unwrap(value)
205
- case value
206
- when self.class then value.to_h
207
- when Array then value.map { |v| deep_unwrap(v) }
208
- else value
209
- end
210
- end
211
- end
212
165
 
213
166
  if __FILE__ == $0
214
167
  require "bundler/setup"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kube_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.8
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -104,6 +104,7 @@ files:
104
104
  - assets/validation-error.png
105
105
  - bin/console
106
106
  - bin/copy-schemas-over
107
+ - bin/create-cdi-x-values
107
108
  - bin/create-kubevirt-x-values
108
109
  - bin/increment-version
109
110
  - bin/release-gem