kube_schema 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +4 -4
- data/bin/increment-version +2 -0
- data/bin/test +4 -6
- data/lib/kube/errors.rb +450 -0
- data/lib/kube/monkey_patches.rb +13 -0
- data/lib/kube/schema/instance.rb +100 -0
- data/lib/kube/schema/manifest.rb +323 -0
- data/lib/kube/schema/resource.rb +406 -0
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +149 -0
- data/schemas/crd-definitions.json +220478 -595
- metadata +1 -1
data/lib/kube/schema/resource.rb
CHANGED
|
@@ -145,3 +145,409 @@ module Kube
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
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
|
data/lib/kube/schema/version.rb
CHANGED
data/lib/kube/schema.rb
CHANGED
|
@@ -208,3 +208,152 @@ class BlackHoleStruct
|
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
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
|