kube_schema 1.12.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,24 +5,20 @@ require "json_schemer"
5
5
 
6
6
  module Kube
7
7
  module Schema
8
- # Represents a single Kubernetes version's OpenAPI schema.
9
- # Lazily loads the Swagger JSON once per version (shared across instances)
10
- # and builds resource classes by kind.
8
+ # One Kubernetes version's OpenAPI schema.
11
9
  #
10
+ # @example
12
11
  # instance = Kube::Schema::Instance.new("1.34")
13
12
  # instance["Deployment"] # => Resource subclass
14
13
  # instance["NetworkPolicy"] # => Resource subclass
15
14
  # instance.list_resources # => sorted array of kind strings
16
- #
17
15
  class Instance
18
16
  attr_reader :version
19
17
 
20
- # Class-level cache so multiple Instance objects for the same version
21
- # share the parsed Swagger schema.
22
- @schemers = {}
18
+ @catalogs = {}
23
19
 
24
20
  class << self
25
- attr_reader :schemers
21
+ attr_reader :catalogs
26
22
  end
27
23
 
28
24
  def initialize(version)
@@ -40,16 +36,14 @@ module Kube
40
36
 
41
37
  # Look up a resource by kind or full GVK string.
42
38
  #
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
- #
49
- # Returns a class that inherits from Kube::Schema::Resource.
39
+ # @example
40
+ # instance["Deployment"] # kind-only lookup
41
+ # instance["apps/v1/Deployment"] # group/version/kind
42
+ # instance["v1/Pod"] # version/kind, empty group
43
+ # instance["networking.k8s.io/v1/Ingress"] # fully qualified
50
44
  #
51
- # Custom schemas registered via Kube::Schema.register take precedence
52
- # over built-in definitions (kind-only lookups only).
45
+ # @parameter input [String] a kind, or a "group/version/kind" string.
46
+ # @returns [Class] a class that inherits from Kube::Schema::Resource.
53
47
  def [](input)
54
48
  @resource_classes[input] ||= begin
55
49
  if input.include?("/")
@@ -69,64 +63,66 @@ module Kube
69
63
  # Custom schemas take precedence on full-GVK lookups too.
70
64
  custom = find_custom_entry_by_gvk(group, version, kind)
71
65
  if custom
72
- return build_resource_class(custom[:schema], custom[:defaults])
66
+ entry = nil
67
+ else
68
+ entry = find_gvk_entry_by_full_gvk(group, version, kind)
73
69
  end
74
-
75
- entry = find_gvk_entry_by_full_gvk(group, version, kind)
76
70
  else
77
71
  # Kind-only lookup — custom schemas take precedence.
78
72
  custom = find_custom_entry(input)
79
73
  if custom
80
- return build_resource_class(custom[:schema], custom[:defaults])
74
+ entry = nil
75
+ else
76
+ entry = find_gvk_entry(input)
81
77
  end
82
-
83
- entry = find_gvk_entry(input)
84
78
  end
85
79
 
86
- if entry.nil?
87
- raise "No resource schema found for #{input.inspect}!" \
88
- "\nUse #list_resources to see available kinds for v#{@version}."
89
- end
80
+ if custom
81
+ build_resource_class(Lazy::Source.spilled(catalog, custom[:entry]), custom[:defaults])
82
+ else
83
+ if entry.nil?
84
+ raise "No resource schema found for #{input.inspect}!" \
85
+ "\nUse #list_resources to see available kinds for v#{@version}."
86
+ end
90
87
 
91
- ref_schema = schemer.ref("#/definitions/#{entry[:definition_key]}")
92
- build_resource_class(ref_schema, entry[:defaults].freeze)
88
+ ref_schema = Lazy::Source.indexed(catalog, entry[:definition_key])
89
+ build_resource_class(ref_schema, entry[:defaults].freeze)
90
+ end
93
91
  end
94
92
  end
95
93
 
96
- # All available resource kinds for this version, including any
97
- # custom schemas registered via Kube::Schema.register.
98
- #
99
- # @return [Array<String>] sorted kind names
94
+ # All resource kinds for this version, plus custom ones.
95
+ # @returns [Array(String)] sorted kind names.
100
96
  def list_resources
101
97
  custom_kinds = Schema.custom_schemas.values.map { |v| v[:defaults]["kind"] }
102
98
  (gvk_index.keys + custom_kinds).uniq.sort
103
99
  end
104
100
 
105
- # All API groups known to this version, including any custom schemas
106
- # registered via Kube::Schema.register. The core API group is the
107
- # empty string.
101
+ # All API groups for this version, plus custom ones.
108
102
  #
103
+ # @example
109
104
  # instance.api_groups # => ["", "apps", "argoproj.io", "batch", ...]
110
105
  #
111
- # @return [Array<String>] sorted, deduplicated group names
106
+ # @returns [Array(String)] sorted, deduplicated group names.
112
107
  def api_groups
113
108
  custom_groups = Schema.custom_schemas.keys.filter_map do |key|
114
109
  parts = key.split("/")
115
- parts.first if parts.size == 3
110
+ if parts.size == 3
111
+ parts.first
112
+ end
116
113
  end
117
114
  (gvk_index.values.flatten.map { |entry| entry[:group] } + custom_groups).uniq.sort
118
115
  end
119
116
 
120
- # Look up a sub-spec definition by short name (e.g. "Container",
121
- # "ContainerPort", "Volume", "Probe"). Returns a class that
122
- # inherits from Kube::Schema::SubSpec.
123
- #
124
- # Also accepts a full definition key like
125
- # "io.k8s.api.core.v1.Container" for disambiguation.
117
+ # Look up a sub-spec definition by short or full name.
126
118
  #
119
+ # @example
127
120
  # instance.sub_spec("Container") # => SubSpec subclass
128
121
  # instance.sub_spec("ContainerPort") # => SubSpec subclass
122
+ # instance.sub_spec("io.k8s.api.core.v1.Container") # disambiguated
129
123
  #
124
+ # @parameter name [String] a short name or a full definition key.
125
+ # @returns [Class] a class that inherits from Kube::Schema::SubSpec.
130
126
  def sub_spec(name)
131
127
  @sub_spec_classes[name] ||= begin
132
128
  definition_key = find_definition_key(name)
@@ -136,31 +132,23 @@ module Kube
136
132
  "\nUse #list_definitions to see available definitions for v#{version}."
137
133
  end
138
134
 
139
- ref_schema = schemer.ref("#/definitions/#{definition_key}")
135
+ ref_schema = Lazy::Source.indexed(catalog, definition_key)
140
136
  build_sub_spec_class(ref_schema, name)
141
137
  end
142
138
  end
143
139
 
144
- # All available definition short names for this version.
145
- #
146
- # @return [Array<String>] sorted, deduplicated short names
140
+ # All definition short names for this version.
141
+ # @returns [Array(String)] sorted, deduplicated short names.
147
142
  def list_definitions
148
- schemer.value.fetch("definitions", {}).keys
143
+ catalog.keys
149
144
  .map { |k| k.split(".").last }
150
145
  .uniq.sort
151
146
  end
152
147
 
153
148
  private
154
149
 
155
- # The JSONSchemer instance for this version's Swagger document.
156
- # Cached at the class level so it's built once per version.
157
- #
158
- # After loading the base Swagger JSON, merges in any extra definition
159
- # files found in the schemas directory (e.g. crd-definitions.json,
160
- # loft-definitions.json). These files are flat JSON objects where keys
161
- # are definition names and values are OpenAPI v2 schema objects.
162
- def schemer
163
- self.class.schemers[@version] ||= begin
150
+ def catalog
151
+ self.class.catalogs[@version] ||= begin
164
152
  path = File.join(SCHEMAS_DIR, "v#{version}.json")
165
153
 
166
154
  unless File.exist?(path)
@@ -169,70 +157,54 @@ module Kube
169
157
  "\nUse `Kube::Schema.schema_versions` to get a list."
170
158
  end
171
159
 
172
- schema = JSON.parse(File.read(path))
160
+ extra = Dir.glob(File.join(SCHEMAS_DIR, "*-definitions.json")).sort
173
161
 
174
- # Merge extra definition files (*-definitions.json) into the
175
- # base schema so CRD and aggregated-API types (e.g. loft,
176
- # gateway-api) are available alongside built-in k8s types.
177
- Dir.glob(File.join(SCHEMAS_DIR, "*-definitions.json")).each do |defs_path|
178
- extra = JSON.parse(File.read(defs_path))
179
- schema["definitions"] ||= {}
180
- schema["definitions"].merge!(extra)
181
- end
182
-
183
- # Kubernetes OpenAPI v2 defines IntOrString as "type": "string"
184
- # with "format": "int-or-string". This is a limitation of
185
- # OpenAPI v2 which cannot express union types. Patch the
186
- # definition so JSONSchemer accepts both integers and strings.
187
- if (int_or_str = schema.dig("definitions", "io.k8s.apimachinery.pkg.util.intstr.IntOrString"))
188
- int_or_str["type"] = ["string", "integer"]
189
- end
190
-
191
- JSONSchemer.schema(schema)
162
+ Lazy::Index.build([path] + extra)
192
163
  end
193
164
  end
194
165
 
195
- # Builds a map from kind to full GVK entry:
166
+ # Builds a map from kind to full GVK entry.
167
+ # @returns [Hash] kind => Array of entry hashes, keyed by GVK.
168
+ #
169
+ # @example
196
170
  # {
197
- # "Deployment" => {
171
+ # "Deployment" => [{
198
172
  # definition_key: "io.k8s.api.apps.v1.Deployment",
199
173
  # group: "apps",
200
174
  # version: "v1",
201
175
  # kind: "Deployment",
202
176
  # defaults: { "apiVersion" => "apps/v1", "kind" => "Deployment" }
203
- # },
204
- # "Pod" => {
205
- # definition_key: "io.k8s.api.core.v1.Pod",
206
- # group: "",
207
- # version: "v1",
208
- # kind: "Pod",
209
- # defaults: { "apiVersion" => "v1", "kind" => "Pod" }
210
- # },
211
- # ...
177
+ # }]
212
178
  # }
213
179
  def gvk_index
214
180
  @gvk_index ||= begin
215
181
  index = Hash.new { |h, k| h[k] = [] }
216
182
 
217
- schemer.value.fetch("definitions", {}).each do |key, definition|
218
- gvks = definition["x-kubernetes-group-version-kind"]
219
- next unless gvks
183
+ catalog.entries.each do |key, entry|
184
+ gvks = entry.gvks
185
+ unless gvks
186
+ next
187
+ end
220
188
 
221
189
  gvks.each do |gvk|
222
190
  group = gvk["group"].to_s
223
191
  version = gvk["version"]
224
192
  kind = gvk["kind"]
225
- api_version = group.empty? ? version : "#{group}/#{version}"
193
+ if group.empty?
194
+ api_version = version
195
+ else
196
+ api_version = "#{group}/#{version}"
197
+ end
226
198
 
227
199
  index[kind] << {
228
200
  definition_key: key,
229
- group: group,
230
- version: version,
231
- kind: kind,
232
- defaults: {
201
+ group: group,
202
+ version: version,
203
+ kind: kind,
204
+ defaults: {
233
205
  "apiVersion" => api_version,
234
- "kind" => kind
235
- }
206
+ "kind" => kind,
207
+ },
236
208
  }
237
209
  end
238
210
  end
@@ -241,50 +213,66 @@ module Kube
241
213
  end
242
214
  end
243
215
 
244
- # Find a GVK entry by kind name (case-insensitive).
245
- # Returns the first matching entry hash or nil.
216
+ # Find a GVK entry by kind name, case-insensitively.
217
+ # @parameter kind [String] the kind name to look up.
218
+ # @returns [Hash, nil] the first matching entry, if any.
246
219
  def find_gvk_entry(kind)
247
220
  entries = gvk_index[kind]
248
- return entries.first if entries && !entries.empty?
249
-
250
- gvk_index.each do |k, v|
251
- return v.first if k.downcase == kind.downcase
221
+ if entries && !entries.empty?
222
+ entries.first
223
+ else
224
+ match = gvk_index.find { |k, _v| k.downcase == kind.downcase }
225
+ match && match[1].first
252
226
  end
253
-
254
- nil
255
227
  end
256
228
 
257
229
  # Find a GVK entry by exact group, version, and kind.
258
- # Returns the matching entry hash or nil.
230
+ # @parameter group [String] the API group, empty for core.
231
+ # @parameter version [String] the API version, e.g. "v1".
232
+ # @parameter kind [String] the kind name.
233
+ # @returns [Hash, nil] the matching entry, if any.
259
234
  def find_gvk_entry_by_full_gvk(group, version, kind)
260
235
  entries = gvk_index[kind]
261
- return nil if entries.nil? || entries.empty?
262
-
263
- entries.find do |entry|
264
- entry[:group] == group && entry[:version] == version
236
+ if entries.nil? || entries.empty?
237
+ nil
238
+ else
239
+ entries.find do |entry|
240
+ entry[:group] == group && entry[:version] == version
241
+ end
265
242
  end
266
243
  end
267
244
 
268
- # Find a custom schema entry by kind (case-insensitive). The registry
269
- # is keyed by full GVK, so match on each entry's recorded kind. When
270
- # several groups register the same kind, the first registered wins.
271
- # Returns the { schema:, defaults: } hash or nil.
245
+ # Find a custom schema entry by kind, case-insensitively.
246
+ # @parameter kind [String] the kind name to look up.
247
+ # @returns [Hash, nil] the first-registered { entry:, defaults: } hash.
272
248
  def find_custom_entry(kind)
273
249
  registry = Schema.custom_schemas
274
- registry.each_value { |v| return v if v[:defaults]["kind"] == kind }
275
- registry.each_value { |v| return v if v[:defaults]["kind"].casecmp?(kind) }
276
-
277
- nil
250
+ exact = registry.values.find { |v| v[:defaults]["kind"] == kind }
251
+ if exact
252
+ exact
253
+ else
254
+ registry.values.find { |v| v[:defaults]["kind"].casecmp?(kind) }
255
+ end
278
256
  end
279
257
 
280
- # Find a custom schema entry by exact group, version, and kind.
281
- # Returns the { schema:, defaults: } hash or nil.
258
+ # Find a custom schema entry by group, version, and kind.
259
+ # @parameter group [String] the API group, empty for core.
260
+ # @parameter version [String] the API version, e.g. "v1".
261
+ # @parameter kind [String] the kind name.
262
+ # @returns [Hash, nil] the { entry:, defaults: } hash, if any.
282
263
  def find_custom_entry_by_gvk(group, version, kind)
283
- api_version = group.empty? ? version : "#{group}/#{version}"
264
+ if group.empty?
265
+ api_version = version
266
+ else
267
+ api_version = "#{group}/#{version}"
268
+ end
284
269
  Schema.custom_schemas["#{api_version}/#{kind}"]
285
270
  end
286
271
 
287
- # Build a Resource subclass from a JSONSchemer instance and defaults hash.
272
+ # Build a Resource subclass from a schema and defaults.
273
+ # @parameter schema_instance [JSONSchemer::Schema] the resource's schema.
274
+ # @parameter defaults [Hash] the frozen apiVersion/kind defaults.
275
+ # @returns [Class] a Resource subclass bound to the schema.
288
276
  def build_resource_class(schema_instance, defaults)
289
277
  Class.new(::Kube::Schema::Resource) do
290
278
  @schema = schema_instance
@@ -311,38 +299,45 @@ module Kube
311
299
  end
312
300
  end
313
301
 
314
- # Resolve a short name like "Container" to its full definition key
315
- # "io.k8s.api.core.v1.Container".
302
+ # Resolve a short name to its full definition key.
303
+ #
304
+ # @example
305
+ # find_definition_key("Container") # => "io.k8s.api.core.v1.Container"
316
306
  #
317
- # Strategy:
318
- # 1. Exact match on full key (for power users)
319
- # 2. Short-name match on last segment
320
- # 3. Prefer stable API versions (v1 > v1beta1 > v1alpha1)
307
+ # @parameter name [String] a short name or a full definition key.
308
+ # @returns [String, nil] a full key, preferring stable API versions.
321
309
  def find_definition_key(name)
322
- definitions = schemer.value.fetch("definitions", {})
310
+ definitions = catalog.entries
323
311
 
324
312
  # Exact full-key match
325
- return name if definitions.key?(name)
326
-
327
- # Short-name matches (last segment after final ".")
328
- candidates = definitions.keys.select { |k| k.split(".").last == name }
329
- return nil if candidates.empty?
330
- return candidates.first if candidates.size == 1
331
-
332
- # Disambiguation: prefer stable versions, then beta, then alpha
333
- candidates.min_by { |k|
334
- version_segment = k.split(".")[-2].to_s
335
- case version_segment
336
- when /\Av\d+\z/ then [0, version_segment]
337
- when /beta/ then [1, version_segment]
338
- when /alpha/ then [2, version_segment]
339
- else [3, version_segment]
313
+ if definitions.key?(name)
314
+ name
315
+ else
316
+ candidates = definitions.keys.select { |k| k.split(".").last == name }
317
+ if candidates.empty?
318
+ nil
319
+ else
320
+ if candidates.size == 1
321
+ candidates.first
322
+ else
323
+ candidates.min_by { |k|
324
+ version_segment = k.split(".")[-2].to_s
325
+ case version_segment
326
+ when /\Av\d+\z/ then [0, version_segment]
327
+ when /beta/ then [1, version_segment]
328
+ when /alpha/ then [2, version_segment]
329
+ else [3, version_segment]
330
+ end
331
+ }
332
+ end
340
333
  end
341
- }
334
+ end
342
335
  end
343
336
 
344
- # Build a SubSpec subclass from a JSONSchemer instance and a
345
- # human-readable definition name (for error messages).
337
+ # Build a SubSpec subclass from a schema and definition name.
338
+ # @parameter schema_instance [JSONSchemer::Schema] the definition's schema.
339
+ # @parameter definition_name [String] the name shown in error messages.
340
+ # @returns [Class] a SubSpec subclass bound to the schema.
346
341
  def build_sub_spec_class(schema_instance, definition_name)
347
342
  Class.new(::Kube::Schema::SubSpec) do
348
343
  @schema = schema_instance
@@ -367,8 +362,8 @@ module Kube
367
362
  end
368
363
  end
369
364
 
370
- # Called by Kube::Schema.register and reset_custom_schemas! to
371
- # invalidate cached resource classes so new registrations take effect.
365
+ # Invalidates cached resource classes after a schema registration.
366
+ # @returns [void] clears the resource class cache.
372
367
  def clear_resource_cache!
373
368
  @resource_classes.clear
374
369
  end
@@ -376,266 +371,252 @@ module Kube
376
371
  end
377
372
  end
378
373
 
379
- if __FILE__ == $0
380
- require "bundler/setup"
381
- require "rspec/autorun"
382
- require "kube/schema"
383
374
 
384
- RSpec.describe Kube::Schema::Instance do
385
- subject(:instance) { described_class.new("1.34") }
375
+ __END__
386
376
 
387
- describe "#initialize" do
388
- it "stores the version" do
389
- expect(instance.version).to eq("1.34")
390
- end
377
+ require "kube/schema"
391
378
 
392
- it "raises UnknownVersionError for a non-version string" do
393
- expect { described_class.new("not-a-version") }.to raise_error(Kube::UnknownVersionError)
394
- end
379
+ instance_class = Kube::Schema::Instance
380
+ instance = instance_class.new("1.34")
381
+
382
+ describe "Kube::Schema::Instance" do
383
+ describe "#initialize" do
384
+ it "stores the version" do
385
+ instance.version.should == "1.34"
395
386
  end
396
387
 
397
- describe "#[]" do
398
- it "returns a Class that subclasses Resource" do
399
- klass = instance["Deployment"]
400
- expect(klass).to be_a(Class)
401
- expect(klass).to be < Kube::Schema::Resource
402
- end
388
+ it "raises UnknownVersionError for a non-version string" do
389
+ lambda { instance_class.new("not-a-version") }.should.raise(Kube::UnknownVersionError)
390
+ end
391
+ end
403
392
 
404
- it "caches resource classes by key" do
405
- a = instance["Deployment"]
406
- b = instance["Deployment"]
407
- expect(a).to be(b)
408
- end
393
+ describe "#[]" do
394
+ it "returns a Class that subclasses Resource" do
395
+ klass = instance["Deployment"]
396
+ klass.should.be.kind_of Class
397
+ klass.should.be < Kube::Schema::Resource
398
+ end
409
399
 
410
- it "raises for an unknown resource" do
411
- expect { instance["ThisDoesNotExist999"] }.to raise_error(RuntimeError, /No resource schema found/)
412
- end
400
+ it "caches resource classes by key" do
401
+ instance["Deployment"].should.be.identical_to instance["Deployment"]
402
+ end
413
403
 
414
- it "returns different classes for different resources" do
415
- deployment = instance["Deployment"]
416
- service = instance["Service"]
417
- expect(deployment).not_to eq(service)
418
- end
404
+ it "raises for an unknown resource" do
405
+ error = lambda { instance["ThisDoesNotExist999"] }.should.raise(RuntimeError)
406
+ error.message.should.match(/No resource schema found/)
407
+ end
419
408
 
420
- it "attaches a schema to the resource class" do
421
- klass = instance["Deployment"]
422
- expect(klass.schema).not_to be_nil
423
- end
409
+ it "returns different classes for different resources" do
410
+ instance["Deployment"].should.not == instance["Service"]
411
+ end
424
412
 
425
- it "is case-insensitive" do
426
- a = instance["Deployment"]
427
- b = instance["deployment"]
428
- expect(a).to be < Kube::Schema::Resource
429
- expect(b).to be < Kube::Schema::Resource
430
- end
413
+ it "attaches a schema to the resource class" do
414
+ instance["Deployment"].schema.should.not.be.nil
415
+ end
431
416
 
432
- it "produces resources that can be instantiated with the block DSL" do
433
- resource = instance["Deployment"].new {
434
- metadata.name = "test"
435
- }
436
- expect(resource.to_h[:apiVersion]).to eq("apps/v1")
437
- expect(resource.to_h[:kind]).to eq("Deployment")
438
- expect(resource.to_h[:metadata][:name]).to eq("test")
439
- end
417
+ it "is case-insensitive" do
418
+ instance["Deployment"].should.be < Kube::Schema::Resource
419
+ instance["deployment"].should.be < Kube::Schema::Resource
420
+ end
440
421
 
441
- it "attaches defaults (apiVersion and kind) to the resource class" do
442
- klass = instance["Deployment"]
443
- expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
444
- end
422
+ it "produces resources that can be instantiated with the block DSL" do
423
+ resource = instance["Deployment"].new {
424
+ metadata.name = "test"
425
+ }
426
+ resource.to_h[:apiVersion].should == "apps/v1"
427
+ resource.to_h[:kind].should == "Deployment"
428
+ resource.to_h[:metadata][:name].should == "test"
429
+ end
445
430
 
446
- it "derives correct apiVersion for core resources (empty group)" do
447
- klass = instance["Pod"]
448
- expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
449
- end
431
+ it "attaches defaults (apiVersion and kind) to the resource class" do
432
+ instance["Deployment"].defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
433
+ end
450
434
 
451
- it "derives correct apiVersion for grouped resources" do
452
- klass = instance["Ingress"]
453
- expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" })
454
- end
435
+ it "derives correct apiVersion for core resources (empty group)" do
436
+ instance["Pod"].defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
455
437
  end
456
438
 
457
- describe "#[] with full GVK string" do
458
- it "resolves apps/v1/Deployment" do
459
- klass = instance["apps/v1/Deployment"]
460
- expect(klass).to be < Kube::Schema::Resource
461
- expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
462
- end
439
+ it "derives correct apiVersion for grouped resources" do
440
+ instance["Ingress"].defaults.should == { "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" }
441
+ end
442
+ end
463
443
 
464
- it "resolves v1/Pod (core resource, empty group)" do
465
- klass = instance["v1/Pod"]
466
- expect(klass).to be < Kube::Schema::Resource
467
- expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
468
- end
444
+ describe "#[] with full GVK string" do
445
+ it "resolves apps/v1/Deployment" do
446
+ klass = instance["apps/v1/Deployment"]
447
+ klass.should.be < Kube::Schema::Resource
448
+ klass.defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
449
+ end
469
450
 
470
- it "resolves networking.k8s.io/v1/Ingress" do
471
- klass = instance["networking.k8s.io/v1/Ingress"]
472
- expect(klass).to be < Kube::Schema::Resource
473
- expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" })
474
- end
451
+ it "resolves v1/Pod (core resource, empty group)" do
452
+ klass = instance["v1/Pod"]
453
+ klass.should.be < Kube::Schema::Resource
454
+ klass.defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
455
+ end
475
456
 
476
- it "resolves kubevirt.io/v1/VirtualMachine" do
477
- klass = instance["kubevirt.io/v1/VirtualMachine"]
478
- expect(klass).to be < Kube::Schema::Resource
479
- expect(klass.defaults).to eq({ "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" })
480
- end
457
+ it "resolves networking.k8s.io/v1/Ingress" do
458
+ klass = instance["networking.k8s.io/v1/Ingress"]
459
+ klass.should.be < Kube::Schema::Resource
460
+ klass.defaults.should == { "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" }
461
+ end
481
462
 
482
- it "raises for invalid GVK format (too many slashes)" do
483
- expect { instance["a/b/c/d"] }.to raise_error(RuntimeError, /Invalid GVK format/)
484
- end
463
+ it "resolves kubevirt.io/v1/VirtualMachine" do
464
+ klass = instance["kubevirt.io/v1/VirtualMachine"]
465
+ klass.should.be < Kube::Schema::Resource
466
+ klass.defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" }
467
+ end
485
468
 
486
- it "raises for non-existent GVK" do
487
- expect { instance["fake.io/v99/Blah"] }.to raise_error(RuntimeError, /No resource schema found/)
488
- end
469
+ it "raises for invalid GVK format (too many slashes)" do
470
+ error = lambda { instance["a/b/c/d"] }.should.raise(RuntimeError)
471
+ error.message.should.match(/Invalid GVK format/)
472
+ end
489
473
 
490
- it "caches GVK lookups" do
491
- a = instance["apps/v1/Deployment"]
492
- b = instance["apps/v1/Deployment"]
493
- expect(a).to be(b)
494
- end
474
+ it "raises for non-existent GVK" do
475
+ error = lambda { instance["fake.io/v99/Blah"] }.should.raise(RuntimeError)
476
+ error.message.should.match(/No resource schema found/)
495
477
  end
496
478
 
497
- describe "#list_resources" do
498
- it "returns a sorted array of kind strings" do
499
- kinds = instance.list_resources
500
- expect(kinds).to be_an(Array)
501
- expect(kinds).not_to be_empty
502
- expect(kinds).to include("Deployment", "Service", "Namespace", "Pod")
503
- expect(kinds).to eq(kinds.sort)
504
- end
479
+ it "caches GVK lookups" do
480
+ instance["apps/v1/Deployment"].should.be.identical_to instance["apps/v1/Deployment"]
505
481
  end
482
+ end
506
483
 
507
- describe "#api_groups" do
508
- it "returns a sorted array of group names including the core group" do
509
- groups = instance.api_groups
510
- expect(groups).to be_an(Array)
511
- expect(groups).to eq(groups.sort)
512
- expect(groups).to include("")
513
- end
484
+ describe "#list_resources" do
485
+ it "returns a sorted array of kind strings" do
486
+ kinds = instance.list_resources
487
+ kinds.should.be.kind_of Array
488
+ kinds.should.not.be.empty
489
+ ["Deployment", "Service", "Namespace", "Pod"].each { |kind| kinds.should.include kind }
490
+ kinds.should == kinds.sort
491
+ end
492
+ end
514
493
 
515
- it "includes built-in groups, including ones without dots" do
516
- expect(instance.api_groups).to include("apps", "batch", "rbac.authorization.k8s.io")
517
- end
494
+ describe "#api_groups" do
495
+ it "returns a sorted array of group names including the core group" do
496
+ groups = instance.api_groups
497
+ groups.should.be.kind_of Array
498
+ groups.should == groups.sort
499
+ groups.should.include ""
500
+ end
518
501
 
519
- it "includes groups from the merged CRD definition bundles" do
520
- expect(instance.api_groups).to include("kubevirt.io", "argoproj.io")
521
- end
502
+ it "includes built-in groups, including ones without dots" do
503
+ ["apps", "batch", "rbac.authorization.k8s.io"].each { |group| instance.api_groups.should.include group }
504
+ end
522
505
 
523
- it "includes groups from registered custom schemas" do
524
- Kube::Schema.register("TenantVmClaim",
525
- schema: { "type" => "object" },
526
- api_version: "tradeportal.ai/v1")
527
- expect(instance.api_groups).to include("tradeportal.ai")
528
- ensure
529
- Kube::Schema.reset_custom_schemas!
530
- end
506
+ it "includes groups from the merged CRD definition bundles" do
507
+ ["kubevirt.io", "argoproj.io"].each { |group| instance.api_groups.should.include group }
531
508
  end
532
509
 
533
- describe "KubeVirt schemas" do
534
- it "resolves VirtualMachine" do
535
- klass = instance["VirtualMachine"]
536
- expect(klass).to be_a(Class)
537
- expect(klass).to be < Kube::Schema::Resource
538
- end
510
+ it "includes groups from registered custom schemas" do
511
+ Kube::Schema.register("TenantVmClaim",
512
+ schema: { "type" => "object" },
513
+ api_version: "tradeportal.ai/v1")
514
+ instance.api_groups.should.include "tradeportal.ai"
515
+ ensure
516
+ Kube::Schema.reset_custom_schemas!
517
+ end
518
+ end
539
519
 
540
- it "has correct defaults for VirtualMachine" do
541
- klass = instance["VirtualMachine"]
542
- expect(klass.defaults).to eq({ "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" })
543
- end
520
+ describe "KubeVirt schemas" do
521
+ it "resolves VirtualMachine" do
522
+ klass = instance["VirtualMachine"]
523
+ klass.should.be.kind_of Class
524
+ klass.should.be < Kube::Schema::Resource
525
+ end
544
526
 
545
- it "includes VirtualMachine in list_resources" do
546
- expect(instance.list_resources).to include("VirtualMachine")
547
- end
527
+ it "has correct defaults for VirtualMachine" do
528
+ instance["VirtualMachine"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" }
529
+ end
548
530
 
549
- it "resolves VirtualMachineInstance" do
550
- klass = instance["VirtualMachineInstance"]
551
- expect(klass.defaults).to eq({ "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachineInstance" })
552
- end
531
+ it "includes VirtualMachine in list_resources" do
532
+ instance.list_resources.should.include "VirtualMachine"
533
+ end
553
534
 
554
- it "resolves KubeVirt" do
555
- klass = instance["KubeVirt"]
556
- expect(klass.defaults).to eq({ "apiVersion" => "kubevirt.io/v1", "kind" => "KubeVirt" })
557
- end
535
+ it "resolves VirtualMachineInstance" do
536
+ instance["VirtualMachineInstance"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachineInstance" }
537
+ end
558
538
 
559
- it "can instantiate a VirtualMachine with the block DSL" do
560
- resource = instance["VirtualMachine"].new {
561
- metadata.name = "test-vm"
562
- }
563
- expect(resource.to_h[:apiVersion]).to eq("kubevirt.io/v1")
564
- expect(resource.to_h[:kind]).to eq("VirtualMachine")
565
- expect(resource.to_h[:metadata][:name]).to eq("test-vm")
566
- end
539
+ it "resolves KubeVirt" do
540
+ instance["KubeVirt"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "KubeVirt" }
541
+ end
567
542
 
568
- it "validates a VirtualMachine without $ref resolution errors" do
569
- resource = instance["VirtualMachine"].new {
570
- metadata.name = "test-vm"
571
- spec.runStrategy = "Always"
572
- spec.template.metadata.labels = { "kubevirt.io/vm": "test-vm" }
573
- spec.template.spec.domain.cpu = { cores: 1 }
574
- spec.template.spec.domain.devices = {}
575
- spec.template.spec.domain.memory = { guest: "1Gi" }
576
- spec.template.spec.domain.resources = { requests: { memory: "1Gi" } }
577
- spec.template.spec.volumes = [
578
- { name: "rootdisk", containerDisk: { image: "registry.example.com/vm:latest" } }
579
- ]
580
- }
581
- expect { resource.to_yaml }.not_to raise_error
582
- end
543
+ it "can instantiate a VirtualMachine with the block DSL" do
544
+ resource = instance["VirtualMachine"].new {
545
+ metadata.name = "test-vm"
546
+ }
547
+ resource.to_h[:apiVersion].should == "kubevirt.io/v1"
548
+ resource.to_h[:kind].should == "VirtualMachine"
549
+ resource.to_h[:metadata][:name].should == "test-vm"
583
550
  end
584
551
 
585
- describe "CloudnativePG schemas" do
586
- it "resolves Cluster by kind" do
587
- klass = instance["Cluster"]
588
- expect(klass).to be < Kube::Schema::Resource
589
- expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" })
590
- end
552
+ it "validates a VirtualMachine without $ref resolution errors" do
553
+ resource = instance["VirtualMachine"].new {
554
+ metadata.name = "test-vm"
555
+ spec.runStrategy = "Always"
556
+ spec.template.metadata.labels = { "kubevirt.io/vm": "test-vm" }
557
+ spec.template.spec.domain.cpu = { cores: 1 }
558
+ spec.template.spec.domain.devices = {}
559
+ spec.template.spec.domain.memory = { guest: "1Gi" }
560
+ spec.template.spec.domain.resources = { requests: { memory: "1Gi" } }
561
+ spec.template.spec.volumes = [
562
+ { name: "rootdisk", containerDisk: { image: "registry.example.com/vm:latest" } }
563
+ ]
564
+ }
565
+ lambda { resource.to_yaml }.should.not.raise(StandardError)
566
+ end
567
+ end
591
568
 
592
- it "resolves Cluster by full GVK string" do
593
- klass = instance["postgresql.cnpg.io/v1/Cluster"]
594
- expect(klass).to be < Kube::Schema::Resource
595
- expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" })
596
- end
569
+ describe "CloudnativePG schemas" do
570
+ it "resolves Cluster by kind" do
571
+ klass = instance["Cluster"]
572
+ klass.should.be < Kube::Schema::Resource
573
+ klass.defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" }
574
+ end
597
575
 
598
- it "includes Cluster in list_resources" do
599
- expect(instance.list_resources).to include("Cluster")
600
- end
576
+ it "resolves Cluster by full GVK string" do
577
+ klass = instance["postgresql.cnpg.io/v1/Cluster"]
578
+ klass.should.be < Kube::Schema::Resource
579
+ klass.defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" }
580
+ end
601
581
 
602
- it "resolves Backup" do
603
- klass = instance["postgresql.cnpg.io/v1/Backup"]
604
- expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Backup" })
605
- end
582
+ it "includes Cluster in list_resources" do
583
+ instance.list_resources.should.include "Cluster"
584
+ end
606
585
 
607
- it "resolves ScheduledBackup" do
608
- klass = instance["postgresql.cnpg.io/v1/ScheduledBackup"]
609
- expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "ScheduledBackup" })
610
- end
586
+ it "resolves Backup" do
587
+ instance["postgresql.cnpg.io/v1/Backup"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Backup" }
588
+ end
611
589
 
612
- it "resolves Pooler" do
613
- klass = instance["postgresql.cnpg.io/v1/Pooler"]
614
- expect(klass.defaults).to eq({ "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Pooler" })
615
- end
590
+ it "resolves ScheduledBackup" do
591
+ instance["postgresql.cnpg.io/v1/ScheduledBackup"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "ScheduledBackup" }
592
+ end
616
593
 
617
- it "can instantiate a Cluster with the block DSL" do
618
- resource = instance["postgresql.cnpg.io/v1/Cluster"].new {
619
- metadata.name = "pg-cluster"
620
- metadata.namespace = "databases"
621
- spec.instances = 3
622
- spec.storage.size = "10Gi"
623
- }
624
- expect(resource.to_h[:apiVersion]).to eq("postgresql.cnpg.io/v1")
625
- expect(resource.to_h[:kind]).to eq("Cluster")
626
- expect(resource.to_h[:metadata][:name]).to eq("pg-cluster")
627
- expect(resource.to_h[:spec][:instances]).to eq(3)
628
- end
594
+ it "resolves Pooler" do
595
+ instance["postgresql.cnpg.io/v1/Pooler"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Pooler" }
629
596
  end
630
597
 
631
- describe "class-level schemer cache" do
632
- it "shares the schemer across instances of the same version" do
633
- a = described_class.new("1.34")
634
- b = described_class.new("1.34")
635
- # Both should resolve without error and return equivalent classes
636
- expect(a["Deployment"]).to be < Kube::Schema::Resource
637
- expect(b["Deployment"]).to be < Kube::Schema::Resource
638
- end
598
+ it "can instantiate a Cluster with the block DSL" do
599
+ resource = instance["postgresql.cnpg.io/v1/Cluster"].new {
600
+ metadata.name = "pg-cluster"
601
+ metadata.namespace = "databases"
602
+ spec.instances = 3
603
+ spec.storage.size = "10Gi"
604
+ }
605
+ resource.to_h[:apiVersion].should == "postgresql.cnpg.io/v1"
606
+ resource.to_h[:kind].should == "Cluster"
607
+ resource.to_h[:metadata][:name].should == "pg-cluster"
608
+ resource.to_h[:spec][:instances].should == 3
609
+ end
610
+ end
611
+
612
+ describe "class-level catalog cache" do
613
+ it "shares the catalog across instances of the same version" do
614
+ a = instance_class.new("1.34")
615
+ b = instance_class.new("1.34")
616
+
617
+ a["Deployment"].should.be < Kube::Schema::Resource
618
+ b["Deployment"].should.be < Kube::Schema::Resource
619
+ instance_class.catalogs["1.34"].should.be.kind_of Kube::Schema::Lazy::Index
639
620
  end
640
621
  end
641
622
  end