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/manifest.rb
CHANGED
|
@@ -180,3 +180,326 @@ module Kube
|
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
|
+
|
|
184
|
+
if __FILE__ == $0
|
|
185
|
+
require "bundler/setup"
|
|
186
|
+
require "rspec/autorun"
|
|
187
|
+
require "kube/schema"
|
|
188
|
+
require "tmpdir"
|
|
189
|
+
|
|
190
|
+
RSpec.describe Kube::Schema::Manifest do
|
|
191
|
+
let(:resource_a) { Kube::Schema["Deployment"].new }
|
|
192
|
+
let(:resource_b) { Kube::Schema["Service"].new }
|
|
193
|
+
let(:resource_c) { Kube::Schema["Namespace"].new }
|
|
194
|
+
|
|
195
|
+
describe "#initialize" do
|
|
196
|
+
it "creates an empty manifest with no arguments" do
|
|
197
|
+
manifest = described_class.new
|
|
198
|
+
expect(manifest.count).to eq(0)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "accepts resources as arguments" do
|
|
202
|
+
manifest = described_class.new(resource_a, resource_b)
|
|
203
|
+
expect(manifest.count).to eq(2)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "accepts a filename keyword argument" do
|
|
207
|
+
manifest = described_class.new(filename: "/tmp/test.yaml")
|
|
208
|
+
expect(manifest.filename).to eq("/tmp/test.yaml")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "flattens manifests passed as arguments" do
|
|
212
|
+
inner = described_class.new(resource_a, resource_b)
|
|
213
|
+
outer = described_class.new(inner, resource_c)
|
|
214
|
+
expect(outer.count).to eq(3)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
describe "#<<" do
|
|
219
|
+
subject(:manifest) { described_class.new }
|
|
220
|
+
|
|
221
|
+
it "appends a Resource" do
|
|
222
|
+
manifest << resource_a
|
|
223
|
+
expect(manifest.count).to eq(1)
|
|
224
|
+
expect(manifest.first).to eq(resource_a)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "returns self for chaining" do
|
|
228
|
+
result = manifest << resource_a
|
|
229
|
+
expect(result).to be(manifest)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "flattens a Manifest (cannot be nested)" do
|
|
233
|
+
other = described_class.new(resource_a, resource_b)
|
|
234
|
+
manifest << other
|
|
235
|
+
expect(manifest.count).to eq(2)
|
|
236
|
+
expect(manifest.to_a).to contain_exactly(resource_a, resource_b)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "flattens an Array of Resources" do
|
|
240
|
+
manifest << [resource_a, resource_b]
|
|
241
|
+
expect(manifest.count).to eq(2)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it "raises ArgumentError for a Hash" do
|
|
245
|
+
expect { manifest << { "kind" => "Pod" } }.to raise_error(ArgumentError, /Expected a Kube::Schema::Resource/)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "raises ArgumentError for a String" do
|
|
249
|
+
expect { manifest << "not a resource" }.to raise_error(ArgumentError)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "supports chaining multiple appends" do
|
|
253
|
+
manifest << resource_a << resource_b << resource_c
|
|
254
|
+
expect(manifest.count).to eq(3)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe "Enumerable" do
|
|
259
|
+
subject(:manifest) { described_class.new(resource_a, resource_b, resource_c) }
|
|
260
|
+
|
|
261
|
+
it "includes Enumerable" do
|
|
262
|
+
expect(described_class).to include(Enumerable)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "yields resources in insertion order via #each" do
|
|
266
|
+
resources = []
|
|
267
|
+
manifest.each { |r| resources << r }
|
|
268
|
+
expect(resources).to eq([resource_a, resource_b, resource_c])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "supports #map" do
|
|
272
|
+
kinds = manifest.map { |r| r.to_h[:kind] }
|
|
273
|
+
expect(kinds).to eq(["Deployment", "Service", "Namespace"])
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "supports #select" do
|
|
277
|
+
services = manifest.select { |r| r.to_h[:kind] == "Service" }
|
|
278
|
+
expect(services.length).to eq(1)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "supports #first and #last" do
|
|
282
|
+
expect(manifest.first).to eq(resource_a)
|
|
283
|
+
# Enumerable doesn't provide #last by default, but #to_a does
|
|
284
|
+
expect(manifest.to_a.last).to eq(resource_c)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "#size / #length" do
|
|
289
|
+
it "returns the number of resources" do
|
|
290
|
+
manifest = described_class.new(resource_a, resource_b)
|
|
291
|
+
expect(manifest.size).to eq(2)
|
|
292
|
+
expect(manifest.length).to eq(2)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "returns 0 for an empty manifest" do
|
|
296
|
+
manifest = described_class.new
|
|
297
|
+
expect(manifest.size).to eq(0)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe "#to_a" do
|
|
302
|
+
it "returns a copy of the internal resources array" do
|
|
303
|
+
manifest = described_class.new(resource_a, resource_b)
|
|
304
|
+
arr = manifest.to_a
|
|
305
|
+
expect(arr).to eq([resource_a, resource_b])
|
|
306
|
+
|
|
307
|
+
# Verify it's a copy, not the internal array
|
|
308
|
+
arr << resource_c
|
|
309
|
+
expect(manifest.count).to eq(2)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
describe "#to_yaml" do
|
|
314
|
+
it "returns multi-document YAML" do
|
|
315
|
+
manifest = described_class.new(resource_a, resource_b)
|
|
316
|
+
yaml = manifest.to_yaml
|
|
317
|
+
|
|
318
|
+
expect(yaml).to include("---")
|
|
319
|
+
expect(yaml).to include("kind: Deployment")
|
|
320
|
+
expect(yaml).to include("kind: Service")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it "uses string keys (not symbol keys) in output" do
|
|
324
|
+
manifest = described_class.new(resource_a)
|
|
325
|
+
yaml = manifest.to_yaml
|
|
326
|
+
|
|
327
|
+
# Should NOT contain Ruby symbol syntax like `:kind:`
|
|
328
|
+
expect(yaml).not_to match(/:\w+:/)
|
|
329
|
+
expect(yaml).to include("kind: Deployment")
|
|
330
|
+
expect(yaml).to include("apiVersion: apps/v1")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "returns empty string for an empty manifest" do
|
|
334
|
+
manifest = described_class.new
|
|
335
|
+
expect(manifest.to_yaml).to eq("")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "produces parseable YAML that round-trips" do
|
|
339
|
+
manifest = described_class.new(resource_a, resource_b)
|
|
340
|
+
yaml_output = manifest.to_yaml
|
|
341
|
+
docs = if YAML.respond_to?(:safe_load_stream)
|
|
342
|
+
YAML.safe_load_stream(yaml_output)
|
|
343
|
+
else
|
|
344
|
+
YAML.load_stream(yaml_output)
|
|
345
|
+
end
|
|
346
|
+
expect(docs.length).to eq(2)
|
|
347
|
+
expect(docs[0]["kind"]).to eq("Deployment")
|
|
348
|
+
expect(docs[1]["kind"]).to eq("Service")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
describe ".parse" do
|
|
353
|
+
it "parses a single-document YAML string" do
|
|
354
|
+
yaml = { "kind" => "Pod", "apiVersion" => "v1", "metadata" => { "name" => "test" } }.to_yaml
|
|
355
|
+
manifest = described_class.parse(yaml)
|
|
356
|
+
|
|
357
|
+
expect(manifest.count).to eq(1)
|
|
358
|
+
expect(manifest.first).to be_a(Kube::Schema::Resource)
|
|
359
|
+
expect(manifest.first.kind).to eq("Pod")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "parses a multi-document YAML string" do
|
|
363
|
+
yaml = [
|
|
364
|
+
{ "kind" => "Deployment", "apiVersion" => "apps/v1" },
|
|
365
|
+
{ "kind" => "Service", "apiVersion" => "v1" }
|
|
366
|
+
].map(&:to_yaml).join("")
|
|
367
|
+
|
|
368
|
+
manifest = described_class.parse(yaml)
|
|
369
|
+
expect(manifest.count).to eq(2)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
it "returns typed Resource subclasses" do
|
|
373
|
+
yaml = { "kind" => "Deployment", "apiVersion" => "apps/v1", "metadata" => { "name" => "web" } }.to_yaml
|
|
374
|
+
manifest = described_class.parse(yaml)
|
|
375
|
+
|
|
376
|
+
resource = manifest.first
|
|
377
|
+
expect(resource.class.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
|
|
378
|
+
expect(resource.kind).to eq("Deployment")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it "skips nil documents (empty YAML docs)" do
|
|
382
|
+
yaml = "---\nkind: Pod\napiVersion: v1\n---\n---\nkind: Service\napiVersion: v1\n"
|
|
383
|
+
manifest = described_class.parse(yaml)
|
|
384
|
+
expect(manifest.count).to eq(2)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it "raises for unknown kinds" do
|
|
388
|
+
yaml = { "kind" => "UnknownCRD", "apiVersion" => "custom.io/v1" }.to_yaml
|
|
389
|
+
expect { described_class.parse(yaml) }.to raise_error(RuntimeError)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it "does not set a filename" do
|
|
393
|
+
yaml = { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml
|
|
394
|
+
manifest = described_class.parse(yaml)
|
|
395
|
+
expect(manifest.filename).to be_nil
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it "returns an empty manifest for empty YAML" do
|
|
399
|
+
manifest = described_class.parse("---\n")
|
|
400
|
+
expect(manifest.count).to eq(0)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
describe ".open" do
|
|
405
|
+
let(:tmpdir) { Dir.mktmpdir("manifest_test") }
|
|
406
|
+
let(:yaml_path) { File.join(tmpdir, "resources.yaml") }
|
|
407
|
+
|
|
408
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
409
|
+
|
|
410
|
+
it "reads a single-document YAML file" do
|
|
411
|
+
File.write(yaml_path, { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml)
|
|
412
|
+
|
|
413
|
+
manifest = described_class.open(yaml_path)
|
|
414
|
+
expect(manifest.count).to eq(1)
|
|
415
|
+
expect(manifest.first).to be_a(Kube::Schema::Resource)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "reads a multi-document YAML file" do
|
|
419
|
+
content = [
|
|
420
|
+
{ "kind" => "Deployment", "apiVersion" => "apps/v1" },
|
|
421
|
+
{ "kind" => "Service", "apiVersion" => "v1" }
|
|
422
|
+
].map(&:to_yaml).join("")
|
|
423
|
+
|
|
424
|
+
File.write(yaml_path, content)
|
|
425
|
+
|
|
426
|
+
manifest = described_class.open(yaml_path)
|
|
427
|
+
expect(manifest.count).to eq(2)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it "sets the filename" do
|
|
431
|
+
File.write(yaml_path, { "kind" => "Pod" }.to_yaml)
|
|
432
|
+
|
|
433
|
+
manifest = described_class.open(yaml_path)
|
|
434
|
+
expect(manifest.filename).to eq(yaml_path)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
it "skips nil documents (empty YAML docs)" do
|
|
438
|
+
File.write(yaml_path, "---\nkind: Pod\n---\n---\nkind: Service\n")
|
|
439
|
+
|
|
440
|
+
manifest = described_class.open(yaml_path)
|
|
441
|
+
expect(manifest.count).to eq(2)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
describe "#write" do
|
|
446
|
+
let(:tmpdir) { Dir.mktmpdir("manifest_test") }
|
|
447
|
+
|
|
448
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
449
|
+
|
|
450
|
+
it "writes to the given path" do
|
|
451
|
+
path = File.join(tmpdir, "output.yaml")
|
|
452
|
+
manifest = described_class.new(resource_a)
|
|
453
|
+
|
|
454
|
+
manifest.write(path)
|
|
455
|
+
expect(File.exist?(path)).to be true
|
|
456
|
+
|
|
457
|
+
content = File.read(path)
|
|
458
|
+
expect(content).to include("kind: Deployment")
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it "writes to the stored filename when no path is given" do
|
|
462
|
+
path = File.join(tmpdir, "stored.yaml")
|
|
463
|
+
manifest = described_class.new(resource_a, filename: path)
|
|
464
|
+
|
|
465
|
+
manifest.write
|
|
466
|
+
expect(File.exist?(path)).to be true
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "updates the filename after writing to a new path" do
|
|
470
|
+
path = File.join(tmpdir, "new.yaml")
|
|
471
|
+
manifest = described_class.new(resource_a)
|
|
472
|
+
|
|
473
|
+
manifest.write(path)
|
|
474
|
+
expect(manifest.filename).to eq(path)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it "returns the path written to" do
|
|
478
|
+
path = File.join(tmpdir, "result.yaml")
|
|
479
|
+
manifest = described_class.new(resource_a)
|
|
480
|
+
|
|
481
|
+
result = manifest.write(path)
|
|
482
|
+
expect(result).to eq(path)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "raises ArgumentError when no path is available" do
|
|
486
|
+
manifest = described_class.new(resource_a)
|
|
487
|
+
expect { manifest.write }.to raise_error(ArgumentError, /No filename set/)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
it "round-trips through write and open" do
|
|
491
|
+
path = File.join(tmpdir, "roundtrip.yaml")
|
|
492
|
+
original = described_class.new(resource_a, resource_b)
|
|
493
|
+
|
|
494
|
+
original.write(path)
|
|
495
|
+
loaded = described_class.open(path)
|
|
496
|
+
|
|
497
|
+
expect(loaded.count).to eq(original.count)
|
|
498
|
+
|
|
499
|
+
content = File.read(path)
|
|
500
|
+
expect(content).to include("kind: Deployment")
|
|
501
|
+
expect(content).to include("kind: Service")
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|