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.
@@ -11,12 +11,16 @@ module Kube
11
11
  # Therefore, they are ONLY ever set from the self.defaults
12
12
  # property.
13
13
  deep_symbolize_keys(hash).then do |symbolized|
14
- config = defaults.merge({
15
- metadata: symbolized.delete(:metadata) || {},
16
- spec: symbolized.delete(:spec) || {},
17
- })
14
+ @data = defaults
18
15
 
19
- @data = config
16
+ # This is extracting "top-level" properties from the input hash
17
+ # such as [apiVersion, spec, metadata, roleRef, ...]
18
+ # We then ignore the rest of the attributes by design.
19
+ self.class.schema_properties.each do |property|
20
+ if symbolized.key?(property)
21
+ @data[property] = symbolized.delete(property)
22
+ end
23
+ end
20
24
  end
21
25
  end
22
26
 
@@ -25,17 +29,20 @@ module Kube
25
29
  end
26
30
  end
27
31
 
28
- def apiVersion = @data.apiVersion
29
- def kind = @data.kind
30
- def spec = @data.spec
31
- def metadata = @data.metadata
32
-
33
32
  # Gets overridden by the factory in Kube::Schema::Instance
34
- def self.schema = nil
33
+ def self.schema
34
+ raise "Kube::Schema::Resource should NOT be instanciated directly"
35
+ end
36
+
37
+ def self.schema_properties
38
+ raise "Kube::Schema::Resource should NOT be instanciated directly"
39
+ end
35
40
 
36
41
  # Gets overridden by the factory in Kube::Schema::Instance.
37
42
  # Returns a frozen Hash like { "apiVersion" => "apps/v1", "kind" => "Deployment" }
38
- def self.defaults = nil
43
+ def self.defaults
44
+ raise "Kube::Schema::Resource should NOT be instanciated directly"
45
+ end
39
46
 
40
47
  def valid?
41
48
  if self.class.schema.nil?
@@ -69,7 +76,8 @@ module Kube
69
76
  # they are facts derived from the GVK metadata.
70
77
  def to_h
71
78
  defaults = self.class.defaults
72
- data = @data.reject { |_, v| v.is_a?(Hash) && v.empty? }
79
+ data = deep_compact(@data)
80
+ data = data.reject { |_, v| v.is_a?(Hash) && v.empty? }
73
81
 
74
82
  if defaults
75
83
  symbolized = deep_symbolize_keys(defaults)
@@ -95,6 +103,20 @@ module Kube
95
103
 
96
104
  private
97
105
 
106
+ def deep_compact(obj)
107
+ case obj
108
+ when Hash
109
+ obj.each_with_object({}) do |(k, v), result|
110
+ compacted = deep_compact(v)
111
+ result[k] = compacted unless compacted.nil?
112
+ end
113
+ when Array
114
+ obj.map { |v| deep_compact(v) }
115
+ else
116
+ obj
117
+ end
118
+ end
119
+
98
120
  def deep_stringify_keys(obj)
99
121
  case obj
100
122
  when Hash
@@ -123,3 +145,409 @@ module Kube
123
145
  end
124
146
  end
125
147
  end
148
+
149
+ if __FILE__ == $0
150
+ require "bundler/setup"
151
+ require "rspec/autorun"
152
+ require "kube/schema"
153
+
154
+ RSpec.describe Kube::Schema::Resource do
155
+ describe ".defaults" do
156
+ it "returns apiVersion and kind for a schema-bearing subclass" do
157
+ klass = Kube::Schema["Deployment"]
158
+ expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
159
+ end
160
+
161
+ it "returns correct apiVersion for core resources (no group)" do
162
+ klass = Kube::Schema["Pod"]
163
+ expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
164
+ end
165
+
166
+ it "returns correct apiVersion for grouped resources" do
167
+ klass = Kube::Schema["NetworkPolicy"]
168
+ expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "NetworkPolicy" })
169
+ end
170
+ end
171
+
172
+ describe "#initialize" do
173
+ it "accepts a hash" do
174
+ klass = Kube::Schema["Deployment"]
175
+ resource = klass.new("metadata" => { "name" => "my-deploy" })
176
+ expect(resource.to_h).to include(kind: "Deployment")
177
+ expect(resource.to_h[:metadata][:name]).to eq("my-deploy")
178
+ end
179
+
180
+ it "creates an empty resource when no arguments are given" do
181
+ klass = Kube::Schema["Deployment"]
182
+ resource = klass.new
183
+ expect(resource.to_h).to include(apiVersion: "apps/v1", kind: "Deployment")
184
+ end
185
+
186
+ it "accepts a block for DSL-style initialization" do
187
+ klass = Kube::Schema["Deployment"]
188
+ resource = klass.new {
189
+ metadata.name = "test"
190
+ }
191
+ expect(resource.to_h).to include(kind: "Deployment")
192
+ expect(resource.to_h[:metadata][:name]).to eq("test")
193
+ end
194
+ end
195
+
196
+ describe "#to_h" do
197
+ context "with a schema-bearing subclass" do
198
+ let(:klass) { Kube::Schema["Deployment"] }
199
+
200
+ it "automatically includes apiVersion and kind from defaults" do
201
+ resource = klass.new {
202
+ metadata.name = "test"
203
+ }
204
+ expect(resource.to_h[:apiVersion]).to eq("apps/v1")
205
+ expect(resource.to_h[:kind]).to eq("Deployment")
206
+ expect(resource.to_h[:metadata][:name]).to eq("test")
207
+ end
208
+
209
+ it "cannot override apiVersion or kind -- they are authoritative" do
210
+ resource = klass.new {
211
+ self.apiVersion = "apps/v1beta1"
212
+ self.kind = "NotADeployment"
213
+ }
214
+ expect(resource.to_h[:apiVersion]).to eq("apps/v1")
215
+ expect(resource.to_h[:kind]).to eq("Deployment")
216
+ end
217
+ end
218
+ end
219
+
220
+ describe "#==" do
221
+ it "considers two resources equal when their data matches" do
222
+ a = Kube::Schema["Pod"].new
223
+ b = Kube::Schema["Pod"].new
224
+ expect(a).to eq(b)
225
+ end
226
+
227
+ it "considers two resources unequal when their data differs" do
228
+ a = Kube::Schema["Pod"].new
229
+ b = Kube::Schema["Service"].new
230
+ expect(a).not_to eq(b)
231
+ end
232
+
233
+ it "is not equal to non-Resource objects" do
234
+ resource = Kube::Schema["Pod"].new
235
+ expect(resource).not_to eq({ "kind" => "Pod" })
236
+ end
237
+ end
238
+
239
+ describe "#valid?" do
240
+ context "with a schema-bearing subclass" do
241
+ let(:klass) { Kube::Schema["Deployment"] }
242
+
243
+ it "returns true for valid data (apiVersion/kind come from defaults)" do
244
+ resource = klass.new
245
+ expect(resource.valid?).to be true
246
+ end
247
+
248
+ it "returns false for data violating the schema" do
249
+ resource = klass.new {
250
+ spec.replicas = "not_a_number"
251
+ }
252
+ expect(resource.valid?).to be false
253
+ end
254
+ end
255
+ end
256
+
257
+ describe "#valid!" do
258
+ context "with a schema-bearing subclass" do
259
+ let(:klass) { Kube::Schema["Deployment"] }
260
+
261
+ it "returns true for valid data" do
262
+ resource = klass.new
263
+ expect(resource.valid!).to be true
264
+ end
265
+
266
+ it "raises ValidationError for data violating the schema" do
267
+ resource = klass.new {
268
+ spec.replicas = "not_a_number"
269
+ }
270
+ expect { resource.valid! }.to raise_error(Kube::ValidationError)
271
+ end
272
+
273
+ it "includes error details in the exception" do
274
+ resource = klass.new {
275
+ spec.replicas = "not_a_number"
276
+ }
277
+ expect { resource.valid! }.to raise_error(Kube::ValidationError, /Schema validation failed/)
278
+ end
279
+
280
+ it "shows the exact key path and value for type errors" do
281
+ resource = klass.new {
282
+ metadata.name = "web"
283
+ spec.replicas = "not_a_number"
284
+ }
285
+ expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
286
+ expect(error.message).to include('spec.replicas = "not_a_number" — expected integer, got String')
287
+ end
288
+ end
289
+
290
+ it "shows which required keys are missing" do
291
+ resource = klass.new {
292
+ metadata.name = "example"
293
+ spec.replicas = 1
294
+ spec.template.spec.containers = [{ name: "app", image: "ruby:latest" }]
295
+ }
296
+ expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
297
+ expect(error.message).to include("spec.selector is required but missing")
298
+ end
299
+ end
300
+
301
+ it "includes the resource kind in the error header" do
302
+ resource = klass.new {
303
+ metadata.name = "web"
304
+ spec.replicas = "bad"
305
+ }
306
+ expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
307
+ expect(error.message).to include("Schema validation failed for Deployment")
308
+ end
309
+ end
310
+
311
+ it "includes the resource name in the error header when available" do
312
+ resource = klass.new {
313
+ metadata.name = "my-app"
314
+ spec.replicas = "bad"
315
+ }
316
+ expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
317
+ expect(error.message).to include('Deployment "my-app"')
318
+ end
319
+ end
320
+
321
+ it "omits the resource name when metadata.name is not set" do
322
+ resource = klass.new {
323
+ spec.replicas = "bad"
324
+ }
325
+ expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
326
+ header = error.message.lines.find { |l| l.include?("Schema validation failed") }
327
+ expect(header).to include("Schema validation failed for Deployment")
328
+ expect(header).not_to match(/Deployment\s+"/) # no quoted name after kind
329
+ end
330
+ end
331
+
332
+ it "exposes the raw errors array" do
333
+ resource = klass.new {
334
+ spec.replicas = "not_a_number"
335
+ }
336
+ begin
337
+ resource.valid!
338
+ rescue Kube::ValidationError => e
339
+ expect(e.errors).to be_an(Array)
340
+ expect(e.errors).not_to be_empty
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ describe "#to_yaml" do
347
+ it "returns clean Kubernetes YAML" do
348
+ resource = Kube::Schema["Pod"].new
349
+ yaml = resource.to_yaml
350
+
351
+ expect(yaml).to include("kind: Pod")
352
+ expect(yaml).to include("apiVersion: v1")
353
+ expect(yaml).not_to include("BlackHoleStruct")
354
+ expect(yaml).not_to include("!ruby/object")
355
+ end
356
+
357
+ it "uses string keys, not symbol keys" do
358
+ resource = Kube::Schema["Pod"].new
359
+ yaml = resource.to_yaml
360
+
361
+ expect(yaml).not_to match(/:\w+:/)
362
+ expect(yaml).to include("kind: Pod")
363
+ end
364
+
365
+ it "produces parseable YAML that round-trips" do
366
+ resource = Kube::Schema["Pod"].new
367
+ parsed = YAML.safe_load(resource.to_yaml)
368
+
369
+ expect(parsed).to be_a(Hash)
370
+ expect(parsed["kind"]).to eq("Pod")
371
+ expect(parsed["apiVersion"]).to eq("v1")
372
+ end
373
+
374
+ it "raises ValidationError when the resource is invalid" do
375
+ klass = Kube::Schema["Deployment"]
376
+ resource = klass.new {
377
+ spec.replicas = "not_a_number"
378
+ }
379
+ expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
380
+ end
381
+
382
+ it "raises ValidationError for an incomplete Deployment missing selector" do
383
+ klass = Kube::Schema["Deployment"]
384
+ resource = klass.new {
385
+ metadata.namespace = "example"
386
+ metadata.name = "example-deployment"
387
+ spec.replicas = 1
388
+ spec.template.spec.containers = [
389
+ { name: "app", image: "ruby:latest" }
390
+ ]
391
+ }
392
+ expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
393
+ end
394
+
395
+ context "with a full Deployment (no manual apiVersion/kind)" do
396
+ let(:deployment) do
397
+ Kube::Schema["Deployment"].new {
398
+ metadata.name = "nginx-deployment"
399
+ metadata.namespace = "shopping-cart"
400
+ metadata.labels = { app: "nginx" }
401
+ spec.replicas = 3
402
+ spec.selector.matchLabels = { app: "nginx" }
403
+ spec.template.metadata.labels = { app: "nginx" }
404
+ spec.template.spec.containers = [
405
+ { name: "nginx", image: "nginx:1.19.5", ports: [{ containerPort: 80 }] }
406
+ ]
407
+ }
408
+ end
409
+
410
+ it "produces valid Kubernetes Deployment YAML" do
411
+ yaml = deployment.to_yaml
412
+ parsed = YAML.safe_load(yaml)
413
+
414
+ expect(parsed).to be_a(Hash)
415
+ expect(parsed["apiVersion"]).to eq("apps/v1")
416
+ expect(parsed["kind"]).to eq("Deployment")
417
+ expect(parsed["metadata"]["name"]).to eq("nginx-deployment")
418
+ expect(parsed["metadata"]["namespace"]).to eq("shopping-cart")
419
+ expect(parsed["metadata"]["labels"]).to eq({ "app" => "nginx" })
420
+ expect(parsed["spec"]["replicas"]).to eq(3)
421
+ expect(parsed["spec"]["selector"]["matchLabels"]).to eq({ "app" => "nginx" })
422
+ expect(parsed["spec"]["template"]["metadata"]["labels"]).to eq({ "app" => "nginx" })
423
+
424
+ containers = parsed["spec"]["template"]["spec"]["containers"]
425
+ expect(containers).to be_an(Array)
426
+ expect(containers.length).to eq(1)
427
+ expect(containers[0]["name"]).to eq("nginx")
428
+ expect(containers[0]["image"]).to eq("nginx:1.19.5")
429
+ expect(containers[0]["ports"]).to eq([{ "containerPort" => 80 }])
430
+ end
431
+
432
+ it "does not contain Ruby object serialization artifacts" do
433
+ yaml = deployment.to_yaml
434
+
435
+ expect(yaml).not_to include("!ruby/object")
436
+ expect(yaml).not_to include("BlackHoleStruct")
437
+ expect(yaml).not_to include("table:")
438
+ end
439
+
440
+ it "looks like real kubectl YAML output" do
441
+ yaml = deployment.to_yaml
442
+
443
+ expect(yaml).to include("apiVersion: apps/v1")
444
+ expect(yaml).to include("kind: Deployment")
445
+ expect(yaml).to include("name: nginx-deployment")
446
+ expect(yaml).to include("namespace: shopping-cart")
447
+ expect(yaml).to include("replicas: 3")
448
+ expect(yaml).to include("image: nginx:1.19.5")
449
+ expect(yaml).to include("containerPort: 80")
450
+ end
451
+
452
+ it "exactly matches real Kubernetes Deployment YAML" do
453
+ expected_yaml = <<~YAML
454
+ ---
455
+ apiVersion: apps/v1
456
+ kind: Deployment
457
+ metadata:
458
+ name: nginx-deployment
459
+ namespace: shopping-cart
460
+ labels:
461
+ app: nginx
462
+ spec:
463
+ replicas: 3
464
+ selector:
465
+ matchLabels:
466
+ app: nginx
467
+ template:
468
+ metadata:
469
+ labels:
470
+ app: nginx
471
+ spec:
472
+ containers:
473
+ - name: nginx
474
+ image: nginx:1.19.5
475
+ ports:
476
+ - containerPort: 80
477
+ YAML
478
+
479
+ expect(deployment.to_yaml).to eq(expected_yaml)
480
+ end
481
+
482
+ it "round-trips through YAML.safe_load" do
483
+ parsed = YAML.safe_load(deployment.to_yaml)
484
+ expect(parsed["apiVersion"]).to eq("apps/v1")
485
+ expect(parsed["kind"]).to eq("Deployment")
486
+ end
487
+ end
488
+ end
489
+
490
+ describe "Deployment schema validation against real Kubernetes YAML" do
491
+ let(:klass) { Kube::Schema["Deployment"] }
492
+
493
+ let(:incomplete_deployment) do
494
+ klass.new {
495
+ metadata.namespace = "example"
496
+ metadata.name = "example-deployment"
497
+ spec.replicas = 1
498
+ spec.template.spec.containers = [
499
+ { name: "app", image: "ruby:latest" }
500
+ ]
501
+ }
502
+ end
503
+
504
+ it "rejects an incomplete Deployment missing selector" do
505
+ expect(incomplete_deployment.valid?).to be false
506
+ end
507
+
508
+ it "reports specific validation errors for incomplete Deployment" do
509
+ expect { incomplete_deployment.valid! }.to raise_error(Kube::ValidationError) do |error|
510
+ expect(error.message).to include("spec.selector is required but missing")
511
+ end
512
+ end
513
+
514
+ it "refuses to serialize an incomplete Deployment to YAML" do
515
+ expect { incomplete_deployment.to_yaml }.to raise_error(Kube::ValidationError)
516
+ end
517
+
518
+ it "has apiVersion and kind from defaults even when incomplete" do
519
+ h = incomplete_deployment.to_h
520
+ expect(h[:apiVersion]).to eq("apps/v1")
521
+ expect(h[:kind]).to eq("Deployment")
522
+ end
523
+ end
524
+
525
+ describe "instantiation via Instance lookup" do
526
+ let(:klass) { Kube::Schema["Deployment"] }
527
+
528
+ it "returns a Resource instance from .new" do
529
+ resource = klass.new
530
+ expect(resource).to be_a(described_class)
531
+ end
532
+
533
+ it "supports block-based initialization" do
534
+ resource = klass.new {
535
+ metadata.name = "web"
536
+ metadata.namespace = "prod"
537
+ }
538
+ expect(resource.to_h[:metadata][:name]).to eq("web")
539
+ expect(resource.to_h[:metadata][:namespace]).to eq("prod")
540
+ end
541
+
542
+ it "has a schema attached to the class" do
543
+ expect(klass.schema).not_to be_nil
544
+ end
545
+
546
+ it "has defaults attached to the class" do
547
+ expect(klass.defaults).not_to be_nil
548
+ expect(klass.defaults["apiVersion"]).to eq("apps/v1")
549
+ expect(klass.defaults["kind"]).to eq("Deployment")
550
+ end
551
+ end
552
+ end
553
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Schema
5
- VERSION = "1.3.2"
5
+ VERSION = "1.3.5"
6
6
  end
7
7
  end
data/lib/kube/schema.rb CHANGED
@@ -48,11 +48,11 @@ module Kube
48
48
  # api_version: "cert-manager.io/v1"
49
49
  # )
50
50
  #
51
- # @example Register from a Hash
52
- # Kube::Schema.register("MyResource",
53
- # schema: { "type" => "object", "properties" => { ... } },
54
- # api_version: "example.com/v1"
55
- # )
51
+ # @example Register from Chart#crds
52
+ # chart.crds.each do |crd|
53
+ # s = crd.to_json_schema
54
+ # Kube::Schema.register(s[:kind], schema: s[:schema], api_version: s[:api_version])
55
+ # end
56
56
  #
57
57
  def register(kind, schema:, api_version:)
58
58
  require "json"
@@ -117,10 +117,26 @@ module Kube
117
117
  end
118
118
  end
119
119
 
120
- # Build a Resource from a hash.
121
- # Kube::Schema.parse(Kube::Schema["Deployment"].to_h) == Kube::Schema["Deployment"]
120
+ # Build a typed Resource from a raw hash.
121
+ #
122
+ # Looks up the "kind" key in the hash and resolves it to the
123
+ # correct Resource subclass via the schema registry. The hash
124
+ # may use string or symbol keys.
125
+ #
126
+ # Kube::Schema.parse("kind" => "Deployment", "apiVersion" => "apps/v1")
127
+ # Kube::Schema.parse(kind: "Pod", apiVersion: "v1", metadata: { name: "web" })
128
+ #
129
+ # @param hash [Hash] a Kubernetes resource hash with at least a "kind" key
130
+ # @return [Resource] a schema-validated Resource instance
131
+ # @raise [ArgumentError] if the hash is nil, not a Hash, or missing "kind"
122
132
  def parse(hash)
123
- raise NotImplementedError
133
+ raise ArgumentError, "Expected a Hash, got #{hash.class}" unless hash.is_a?(Hash)
134
+
135
+ kind = hash["kind"] || hash[:kind]
136
+ raise ArgumentError, "Hash must contain a \"kind\" key" if kind.nil?
137
+
138
+ resource_class = self[kind]
139
+ resource_class.new(hash)
124
140
  end
125
141
 
126
142
  # Available Kubernetes versions, read from the local schemas directory.
@@ -192,3 +208,152 @@ class BlackHoleStruct
192
208
  end
193
209
  end
194
210
  end
211
+
212
+ if __FILE__ == $0
213
+ require "bundler/setup"
214
+ require "rspec/autorun"
215
+
216
+ RSpec.describe Kube::Schema do
217
+ describe "::VERSION" do
218
+ it "is defined" do
219
+ expect(Kube::Schema::VERSION).not_to be_nil
220
+ end
221
+
222
+ it "is a valid semver string" do
223
+ expect(Kube::Schema::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
224
+ end
225
+ end
226
+
227
+ describe "::DEFAULT_VERSION" do
228
+ it "is a version present in schema_versions" do
229
+ expect(Kube::Schema.schema_versions).to include(Kube::Schema::DEFAULT_VERSION)
230
+ end
231
+ end
232
+
233
+ describe ".schema_versions" do
234
+ it "returns an array of version strings" do
235
+ versions = Kube::Schema.schema_versions
236
+ expect(versions).to be_an(Array)
237
+ expect(versions).not_to be_empty
238
+ expect(versions).to all(match(/\A\d+\.\d+/))
239
+ end
240
+
241
+ it "is sorted by Gem::Version" do
242
+ versions = Kube::Schema.schema_versions
243
+ sorted = versions.sort_by { |v| Gem::Version.new(v) }
244
+ expect(versions).to eq(sorted)
245
+ end
246
+
247
+ it "does not include a leading 'v' prefix" do
248
+ expect(Kube::Schema.schema_versions).to all(satisfy { |v| !v.start_with?("v") })
249
+ end
250
+ end
251
+
252
+ describe ".latest_version" do
253
+ it "returns the last element of schema_versions" do
254
+ expect(Kube::Schema.latest_version).to eq(Kube::Schema.schema_versions.last)
255
+ end
256
+ end
257
+
258
+ describe ".[]" do
259
+ context "with a version string" do
260
+ it "returns an Instance for a known version" do
261
+ instance = Kube::Schema["1.34"]
262
+ expect(instance).to be_a(Kube::Schema::Instance)
263
+ expect(instance.version).to eq("1.34")
264
+ end
265
+
266
+ it "caches Instance objects by version" do
267
+ a = Kube::Schema["1.34"]
268
+ b = Kube::Schema["1.34"]
269
+ expect(a).to be(b)
270
+ end
271
+
272
+ it "raises UnknownVersionError for an invalid version" do
273
+ expect { Kube::Schema["0.0.1"] }.to raise_error(Kube::UnknownVersionError)
274
+ end
275
+ end
276
+
277
+ context "with a resource name" do
278
+ it "returns a Class that subclasses Resource" do
279
+ klass = Kube::Schema["Deployment"]
280
+ expect(klass).to be_a(Class)
281
+ expect(klass).to be < Kube::Schema::Resource
282
+ end
283
+ end
284
+
285
+ context "with a 'v'-prefixed version string" do
286
+ it "raises IncorrectVersionFormat" do
287
+ expect { Kube::Schema["v1.34"] }.to raise_error(
288
+ Kube::IncorrectVersionFormat,
289
+ /Don't preface the version with a "v"/
290
+ )
291
+ end
292
+
293
+ it "suggests the correct format in the error message" do
294
+ expect { Kube::Schema["v1.34"] }.to raise_error(
295
+ Kube::IncorrectVersionFormat,
296
+ /Use Kube::Schema\["1\.34"\] instead/
297
+ )
298
+ end
299
+ end
300
+ end
301
+
302
+ describe ".parse" do
303
+ it "returns a typed Resource for a known kind" do
304
+ resource = Kube::Schema.parse("kind" => "Deployment", "apiVersion" => "apps/v1")
305
+ expect(resource).to be_a(Kube::Schema::Resource)
306
+ expect(resource.kind).to eq("Deployment")
307
+ expect(resource.apiVersion).to eq("apps/v1")
308
+ end
309
+
310
+ it "works with symbol keys" do
311
+ resource = Kube::Schema.parse(kind: "Pod", apiVersion: "v1", metadata: { name: "web" })
312
+ expect(resource).to be_a(Kube::Schema::Resource)
313
+ expect(resource.kind).to eq("Pod")
314
+ expect(resource.metadata.name).to eq("web")
315
+ end
316
+
317
+ it "returns a class backed by the correct schema" do
318
+ resource = Kube::Schema.parse("kind" => "Service", "apiVersion" => "v1")
319
+ expect(resource.class.schema).not_to be_nil
320
+ expect(resource.class.defaults).to eq({ "apiVersion" => "v1", "kind" => "Service" })
321
+ end
322
+
323
+ it "round-trips through to_h" do
324
+ original = Kube::Schema["Deployment"].new {
325
+ metadata.name = "web"
326
+ spec.replicas = 3
327
+ spec.selector.matchLabels = { app: "web" }
328
+ spec.template.metadata.labels = { app: "web" }
329
+ spec.template.spec.containers = [{ name: "web", image: "nginx" }]
330
+ }
331
+ parsed = Kube::Schema.parse(original.to_h)
332
+ expect(parsed.kind).to eq("Deployment")
333
+ expect(parsed.metadata.name).to eq("web")
334
+ end
335
+
336
+ it "raises ArgumentError for a non-Hash" do
337
+ expect { Kube::Schema.parse("not a hash") }.to raise_error(ArgumentError, /Expected a Hash/)
338
+ end
339
+
340
+ it "raises ArgumentError when kind is missing" do
341
+ expect { Kube::Schema.parse("apiVersion" => "v1") }.to raise_error(ArgumentError, /kind/)
342
+ end
343
+
344
+ it "raises RuntimeError for an unknown kind" do
345
+ expect { Kube::Schema.parse("kind" => "BogusKind", "apiVersion" => "v1") }.to raise_error(RuntimeError)
346
+ end
347
+ end
348
+
349
+ describe ".has_version?" do
350
+ it "returns true for a known version" do
351
+ expect(Kube::Schema.has_version?(Kube::Schema::DEFAULT_VERSION)).to be true
352
+ end
353
+
354
+ it "returns false for an unknown version" do
355
+ expect(Kube::Schema.has_version?("0.0.1")).to be false
356
+ end
357
+ end
358
+ end
359
+ end