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.
@@ -7,15 +7,13 @@ module Kube
7
7
  def initialize(hash = {}, &block)
8
8
  deep_symbolize_keys(self.class.defaults.to_h).then do |defaults|
9
9
 
10
- # You are NEVER allowed to change `apiVersion` or `kind`
11
- # Therefore, they are ONLY ever set from the self.defaults
12
- # property.
10
+ # apiVersion and kind are never user-settable.
11
+ # They come only from self.class.defaults.
13
12
  deep_symbolize_keys(hash).then do |symbolized|
14
13
  @data = defaults
15
14
 
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.
15
+ # Lift top-level properties, e.g. spec, metadata, roleRef.
16
+ # Any other attributes are dropped by design.
19
17
  self.class.schema_properties.each do |property|
20
18
  if symbolized.key?(property)
21
19
  @data[property] = symbolized.delete(property)
@@ -29,7 +27,8 @@ module Kube
29
27
  end
30
28
  end
31
29
 
32
- # Gets overridden by the factory in Kube::Schema::Instance
30
+ # Overridden by the factory in Kube::Schema::Instance.
31
+ # @returns [JSONSchemer::Schema, nil] the schema, once overridden.
33
32
  def self.schema
34
33
  raise "Kube::Schema::Resource should NOT be instanciated directly"
35
34
  end
@@ -38,8 +37,8 @@ module Kube
38
37
  raise "Kube::Schema::Resource should NOT be instanciated directly"
39
38
  end
40
39
 
41
- # Gets overridden by the factory in Kube::Schema::Instance.
42
- # Returns a frozen Hash like { "apiVersion" => "apps/v1", "kind" => "Deployment" }
40
+ # Overridden by the factory in Kube::Schema::Instance.
41
+ # @returns [Hash] a frozen Hash like { "apiVersion" => "apps/v1", "kind" => "Deployment" }.
43
42
  def self.defaults
44
43
  raise "Kube::Schema::Resource should NOT be instanciated directly"
45
44
  end
@@ -52,8 +51,9 @@ module Kube
52
51
  end
53
52
  end
54
53
 
55
- # Like #valid? but raises Kube::ValidationError with details on failure.
56
- # The error message includes the resource kind and name for context.
54
+ # Like #valid? but raises on failure.
55
+ # @returns [Boolean] true if the resource is valid.
56
+ # @raises [Kube::ValidationError] with details on failure.
57
57
  def valid!
58
58
  if self.class.schema.nil?
59
59
  true
@@ -71,9 +71,8 @@ module Kube
71
71
  end
72
72
  end
73
73
 
74
- # Returns the resource data as a Hash. Defaults (apiVersion, kind)
75
- # from the schema are authoritative and cannot be overridden --
76
- # they are facts derived from the GVK metadata.
74
+ # The resource data as a Hash; apiVersion/kind are authoritative.
75
+ # @returns [Hash] the resource data, symbol-keyed.
77
76
  def to_h
78
77
  defaults = self.class.defaults
79
78
  data = deep_compact(@data)
@@ -81,8 +80,7 @@ module Kube
81
80
 
82
81
  if defaults
83
82
  symbolized = deep_symbolize_keys(defaults)
84
- # Defaults go first (for key ordering), then user data minus
85
- # any attempts to override the authoritative keys.
83
+ # Defaults go first, for key ordering, then the rest of the data.
86
84
  symbolized.merge(data.reject { |k, _| symbolized.key?(k) })
87
85
  else
88
86
  data
@@ -90,7 +88,8 @@ module Kube
90
88
  end
91
89
 
92
90
  # Serializes to clean Kubernetes YAML.
93
- # Raises Kube::ValidationError if the resource is not valid.
91
+ # @returns [String] the resource as YAML.
92
+ # @raises [Kube::ValidationError] if the resource is not valid.
94
93
  def to_yaml
95
94
  if valid!
96
95
  deep_stringify_keys(to_h).to_yaml
@@ -101,9 +100,9 @@ module Kube
101
100
  other.is_a?(Resource) && to_h == other.to_h
102
101
  end
103
102
 
104
- # Ruby pattern matching: `resource => {metadata: {name:}}`.
105
- # to_h is already plain nested symbol-keyed Hashes, so nested
106
- # patterns work all the way down for free.
103
+ # Supports pattern matching, e.g. `resource => {metadata: {name:}}`.
104
+ # @parameter keys [Array(Symbol), nil] the keys requested by the pattern.
105
+ # @returns [Hash] the same nested Hash as #to_h.
107
106
  def deconstruct_keys(keys) = to_h
108
107
 
109
108
  private
@@ -115,7 +114,9 @@ module Kube
115
114
  when Hash
116
115
  obj.each_with_object({}) do |(k, v), result|
117
116
  compacted = deep_compact(v)
118
- result[k] = compacted unless compacted.nil?
117
+ unless compacted.nil?
118
+ result[k] = compacted
119
+ end
119
120
  end
120
121
  when Array
121
122
  obj.map { |v| deep_compact(v) }
@@ -153,442 +154,396 @@ module Kube
153
154
  end
154
155
  end
155
156
 
156
- if __FILE__ == $0
157
- require "bundler/setup"
158
- require "rspec/autorun"
159
- require "kube/schema"
160
-
161
- RSpec.describe Kube::Schema::Resource do
162
- describe ".defaults" do
163
- it "returns apiVersion and kind for a schema-bearing subclass" do
164
- klass = Kube::Schema["Deployment"]
165
- expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
166
- end
167
157
 
168
- it "returns correct apiVersion for core resources (no group)" do
169
- klass = Kube::Schema["Pod"]
170
- expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
171
- end
172
-
173
- it "returns correct apiVersion for grouped resources" do
174
- klass = Kube::Schema["NetworkPolicy"]
175
- expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "NetworkPolicy" })
176
- end
158
+ __END__
159
+
160
+ require "kube/schema"
161
+
162
+ deployment_class = lambda { Kube::Schema["Deployment"] }
163
+
164
+ full_deployment = lambda {
165
+ Kube::Schema["Deployment"].new {
166
+ metadata.name = "nginx-deployment"
167
+ metadata.namespace = "shopping-cart"
168
+ metadata.labels = { app: "nginx" }
169
+ spec.replicas = 3
170
+ spec.selector.matchLabels = { app: "nginx" }
171
+ spec.template.metadata.labels = { app: "nginx" }
172
+ spec.template.spec.containers = [
173
+ { name: "nginx", image: "nginx:1.19.5", ports: [{ containerPort: 80 }] }
174
+ ]
175
+ }
176
+ }
177
+
178
+ incomplete_deployment = lambda {
179
+ Kube::Schema["Deployment"].new {
180
+ metadata.namespace = "example"
181
+ metadata.name = "example-deployment"
182
+ spec.replicas = 1
183
+ spec.template.spec.containers = [
184
+ { name: "app", image: "ruby:latest" }
185
+ ]
186
+ }
187
+ }
188
+
189
+ describe "Kube::Schema::Resource" do
190
+ describe ".defaults" do
191
+ it "returns apiVersion and kind for a schema-bearing subclass" do
192
+ Kube::Schema["Deployment"].defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
177
193
  end
178
194
 
179
- describe "#initialize" do
180
- it "accepts a hash" do
181
- klass = Kube::Schema["Deployment"]
182
- resource = klass.new("metadata" => { "name" => "my-deploy" })
183
- expect(resource.to_h).to include(kind: "Deployment")
184
- expect(resource.to_h[:metadata][:name]).to eq("my-deploy")
185
- end
186
-
187
- it "creates an empty resource when no arguments are given" do
188
- klass = Kube::Schema["Deployment"]
189
- resource = klass.new
190
- expect(resource.to_h).to include(apiVersion: "apps/v1", kind: "Deployment")
191
- end
192
-
193
- it "accepts a block for DSL-style initialization" do
194
- klass = Kube::Schema["Deployment"]
195
- resource = klass.new {
196
- metadata.name = "test"
197
- }
198
- expect(resource.to_h).to include(kind: "Deployment")
199
- expect(resource.to_h[:metadata][:name]).to eq("test")
200
- end
195
+ it "returns correct apiVersion for core resources (no group)" do
196
+ Kube::Schema["Pod"].defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
201
197
  end
202
198
 
203
- describe "#to_h" do
204
- context "with a schema-bearing subclass" do
205
- let(:klass) { Kube::Schema["Deployment"] }
199
+ it "returns correct apiVersion for grouped resources" do
200
+ Kube::Schema["NetworkPolicy"].defaults.should == { "apiVersion" => "networking.k8s.io/v1", "kind" => "NetworkPolicy" }
201
+ end
202
+ end
206
203
 
207
- it "automatically includes apiVersion and kind from defaults" do
208
- resource = klass.new {
209
- metadata.name = "test"
210
- }
211
- expect(resource.to_h[:apiVersion]).to eq("apps/v1")
212
- expect(resource.to_h[:kind]).to eq("Deployment")
213
- expect(resource.to_h[:metadata][:name]).to eq("test")
214
- end
204
+ describe "#initialize" do
205
+ it "accepts a hash" do
206
+ resource = Kube::Schema["Deployment"].new("metadata" => { "name" => "my-deploy" })
207
+ resource.to_h[:kind].should == "Deployment"
208
+ resource.to_h[:metadata][:name].should == "my-deploy"
209
+ end
215
210
 
216
- it "cannot override apiVersion or kind -- they are authoritative" do
217
- resource = klass.new {
218
- self.apiVersion = "apps/v1beta1"
219
- self.kind = "NotADeployment"
220
- }
221
- expect(resource.to_h[:apiVersion]).to eq("apps/v1")
222
- expect(resource.to_h[:kind]).to eq("Deployment")
223
- end
224
- end
211
+ it "creates an empty resource when no arguments are given" do
212
+ resource = Kube::Schema["Deployment"].new
213
+ resource.to_h[:apiVersion].should == "apps/v1"
214
+ resource.to_h[:kind].should == "Deployment"
225
215
  end
226
216
 
227
- describe "pattern matching" do
228
- it "destructures nested keys" do
229
- resource = Kube::Schema["Deployment"].new { metadata.name = "web" }
230
- resource => {kind: "Deployment", metadata: {name: String => name}}
217
+ it "accepts a block for DSL-style initialization" do
218
+ resource = Kube::Schema["Deployment"].new {
219
+ metadata.name = "test"
220
+ }
221
+ resource.to_h[:kind].should == "Deployment"
222
+ resource.to_h[:metadata][:name].should == "test"
223
+ end
224
+ end
231
225
 
232
- expect(name).to eq("web")
233
- end
226
+ describe "#to_h with a schema-bearing subclass" do
227
+ it "automatically includes apiVersion and kind from defaults" do
228
+ resource = deployment_class.().new {
229
+ metadata.name = "test"
230
+ }
231
+ resource.to_h[:apiVersion].should == "apps/v1"
232
+ resource.to_h[:kind].should == "Deployment"
233
+ resource.to_h[:metadata][:name].should == "test"
234
+ end
234
235
 
235
- it "does not match when a nested value differs" do
236
- resource = Kube::Schema["Deployment"].new { metadata.name = "web" }
237
- expect { resource => {metadata: {name: "other"}} }.to raise_error(NoMatchingPatternError)
238
- end
236
+ it "cannot override apiVersion or kind -- they are authoritative" do
237
+ resource = deployment_class.().new {
238
+ self.apiVersion = "apps/v1beta1"
239
+ self.kind = "NotADeployment"
240
+ }
241
+ resource.to_h[:apiVersion].should == "apps/v1"
242
+ resource.to_h[:kind].should == "Deployment"
239
243
  end
244
+ end
240
245
 
241
- describe "#==" do
242
- it "considers two resources equal when their data matches" do
243
- a = Kube::Schema["Pod"].new
244
- b = Kube::Schema["Pod"].new
245
- expect(a).to eq(b)
246
- end
246
+ describe "pattern matching" do
247
+ it "destructures nested keys" do
248
+ resource = Kube::Schema["Deployment"].new { metadata.name = "web" }
249
+ resource => {kind: "Deployment", metadata: {name: String => deploy_name}}
247
250
 
248
- it "considers two resources unequal when their data differs" do
249
- a = Kube::Schema["Pod"].new
250
- b = Kube::Schema["Service"].new
251
- expect(a).not_to eq(b)
252
- end
251
+ deploy_name.should == "web"
252
+ end
253
253
 
254
- it "is not equal to non-Resource objects" do
255
- resource = Kube::Schema["Pod"].new
256
- expect(resource).not_to eq({ "kind" => "Pod" })
257
- end
254
+ it "does not match when a nested value differs" do
255
+ resource = Kube::Schema["Deployment"].new { metadata.name = "web" }
256
+ lambda { resource => {metadata: {name: "other"}} }.should.raise(NoMatchingPatternError)
258
257
  end
258
+ end
259
259
 
260
- describe "#valid?" do
261
- context "with a schema-bearing subclass" do
262
- let(:klass) { Kube::Schema["Deployment"] }
260
+ describe "#==" do
261
+ it "considers two resources equal when their data matches" do
262
+ Kube::Schema["Pod"].new.should == Kube::Schema["Pod"].new
263
+ end
263
264
 
264
- it "returns true for valid data (apiVersion/kind come from defaults)" do
265
- resource = klass.new
266
- expect(resource.valid?).to be true
267
- end
265
+ it "considers two resources unequal when their data differs" do
266
+ Kube::Schema["Pod"].new.should.not == Kube::Schema["Service"].new
267
+ end
268
268
 
269
- it "returns false for data violating the schema" do
270
- resource = klass.new {
271
- spec.replicas = "not_a_number"
272
- }
273
- expect(resource.valid?).to be false
274
- end
275
- end
269
+ it "is not equal to non-Resource objects" do
270
+ Kube::Schema["Pod"].new.should.not == { "kind" => "Pod" }
276
271
  end
272
+ end
277
273
 
278
- describe "#valid!" do
279
- context "with a schema-bearing subclass" do
280
- let(:klass) { Kube::Schema["Deployment"] }
274
+ describe "#valid? with a schema-bearing subclass" do
275
+ it "returns true for valid data (apiVersion/kind come from defaults)" do
276
+ deployment_class.().new.valid?.should.be.true
277
+ end
281
278
 
282
- it "returns true for valid data" do
283
- resource = klass.new
284
- expect(resource.valid!).to be true
285
- end
279
+ it "returns false for data violating the schema" do
280
+ resource = deployment_class.().new {
281
+ spec.replicas = "not_a_number"
282
+ }
283
+ resource.valid?.should.be.false
284
+ end
285
+ end
286
286
 
287
- it "raises ValidationError for data violating the schema" do
288
- resource = klass.new {
289
- spec.replicas = "not_a_number"
290
- }
291
- expect { resource.valid! }.to raise_error(Kube::ValidationError)
292
- end
287
+ describe "#valid! with a schema-bearing subclass" do
288
+ it "returns true for valid data" do
289
+ deployment_class.().new.valid!.should.be.true
290
+ end
293
291
 
294
- it "includes error details in the exception" do
295
- resource = klass.new {
296
- spec.replicas = "not_a_number"
297
- }
298
- expect { resource.valid! }.to raise_error(Kube::ValidationError, /Schema validation failed/)
299
- end
292
+ it "raises ValidationError for data violating the schema" do
293
+ resource = deployment_class.().new {
294
+ spec.replicas = "not_a_number"
295
+ }
296
+ lambda { resource.valid! }.should.raise(Kube::ValidationError)
297
+ end
300
298
 
301
- it "shows the exact key path and value for type errors" do
302
- resource = klass.new {
303
- metadata.name = "web"
304
- spec.replicas = "not_a_number"
305
- }
306
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
307
- expect(error.message).to include('spec.replicas = "not_a_number" — expected integer, got String')
308
- end
309
- end
299
+ it "includes error details in the exception" do
300
+ resource = deployment_class.().new {
301
+ spec.replicas = "not_a_number"
302
+ }
303
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
304
+ error.message.should.match(/Schema validation failed/)
305
+ end
310
306
 
311
- it "shows which required keys are missing" do
312
- resource = klass.new {
313
- metadata.name = "example"
314
- spec.replicas = 1
315
- spec.template.spec.containers = [{ name: "app", image: "ruby:latest" }]
316
- }
317
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
318
- expect(error.message).to include("spec.selector is required but missing")
319
- end
320
- end
307
+ it "shows the exact key path and value for type errors" do
308
+ resource = deployment_class.().new {
309
+ metadata.name = "web"
310
+ spec.replicas = "not_a_number"
311
+ }
312
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
313
+ error.message.should.include 'spec.replicas = "not_a_number" — expected integer, got String'
314
+ end
321
315
 
322
- it "includes the resource kind in the error header" do
323
- resource = klass.new {
324
- metadata.name = "web"
325
- spec.replicas = "bad"
326
- }
327
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
328
- expect(error.message).to include("Schema validation failed for Deployment")
329
- end
330
- end
316
+ it "shows which required keys are missing" do
317
+ resource = deployment_class.().new {
318
+ metadata.name = "example"
319
+ spec.replicas = 1
320
+ spec.template.spec.containers = [{ name: "app", image: "ruby:latest" }]
321
+ }
322
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
323
+ error.message.should.include "spec.selector is required but missing"
324
+ end
331
325
 
332
- it "includes the resource name in the error header when available" do
333
- resource = klass.new {
334
- metadata.name = "my-app"
335
- spec.replicas = "bad"
336
- }
337
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
338
- expect(error.message).to include('Deployment "my-app"')
339
- end
340
- end
326
+ it "includes the resource kind in the error header" do
327
+ resource = deployment_class.().new {
328
+ metadata.name = "web"
329
+ spec.replicas = "bad"
330
+ }
331
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
332
+ error.message.should.include "Schema validation failed for Deployment"
333
+ end
341
334
 
342
- it "omits the resource name when metadata.name is not set" do
343
- resource = klass.new {
344
- spec.replicas = "bad"
345
- }
346
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
347
- header = error.message.lines.find { |l| l.include?("Schema validation failed") }
348
- expect(header).to include("Schema validation failed for Deployment")
349
- expect(header).not_to match(/Deployment\s+"/) # no quoted name after kind
350
- end
351
- end
335
+ it "includes the resource name in the error header when available" do
336
+ resource = deployment_class.().new {
337
+ metadata.name = "my-app"
338
+ spec.replicas = "bad"
339
+ }
340
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
341
+ error.message.should.include 'Deployment "my-app"'
342
+ end
352
343
 
353
- it "exposes the raw errors array" do
354
- resource = klass.new {
355
- spec.replicas = "not_a_number"
356
- }
357
- begin
358
- resource.valid!
359
- rescue Kube::ValidationError => e
360
- expect(e.errors).to be_an(Array)
361
- expect(e.errors).not_to be_empty
362
- end
363
- end
364
- end
344
+ it "omits the resource name when metadata.name is not set" do
345
+ resource = deployment_class.().new {
346
+ spec.replicas = "bad"
347
+ }
348
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
349
+ header = error.message.lines.find { |l| l.include?("Schema validation failed") }
350
+ header.should.include "Schema validation failed for Deployment"
351
+ header.should.not.match(/Deployment\s+"/) # no quoted name after kind
365
352
  end
366
353
 
367
- describe "#to_yaml" do
368
- it "returns clean Kubernetes YAML" do
369
- resource = Kube::Schema["Pod"].new
370
- yaml = resource.to_yaml
354
+ it "exposes the raw errors array" do
355
+ resource = deployment_class.().new {
356
+ spec.replicas = "not_a_number"
357
+ }
358
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
359
+ error.errors.should.be.kind_of Array
360
+ error.errors.should.not.be.empty
361
+ end
362
+ end
371
363
 
372
- expect(yaml).to include("kind: Pod")
373
- expect(yaml).to include("apiVersion: v1")
374
- expect(yaml).not_to include("!ruby/object")
375
- end
364
+ describe "#to_yaml" do
365
+ it "returns clean Kubernetes YAML" do
366
+ yaml = Kube::Schema["Pod"].new.to_yaml
376
367
 
377
- it "uses string keys, not symbol keys" do
378
- resource = Kube::Schema["Pod"].new
379
- yaml = resource.to_yaml
368
+ yaml.should.include "kind: Pod"
369
+ yaml.should.include "apiVersion: v1"
370
+ yaml.should.not.include "!ruby/object"
371
+ end
380
372
 
381
- expect(yaml).not_to match(/:\w+:/)
382
- expect(yaml).to include("kind: Pod")
383
- end
373
+ it "uses string keys, not symbol keys" do
374
+ yaml = Kube::Schema["Pod"].new.to_yaml
384
375
 
385
- it "produces parseable YAML that round-trips" do
386
- resource = Kube::Schema["Pod"].new
387
- parsed = YAML.safe_load(resource.to_yaml)
376
+ yaml.should.not.match(/:\w+:/)
377
+ yaml.should.include "kind: Pod"
378
+ end
388
379
 
389
- expect(parsed).to be_a(Hash)
390
- expect(parsed["kind"]).to eq("Pod")
391
- expect(parsed["apiVersion"]).to eq("v1")
392
- end
380
+ it "produces parseable YAML that round-trips" do
381
+ parsed = YAML.safe_load(Kube::Schema["Pod"].new.to_yaml)
393
382
 
394
- it "raises ValidationError when the resource is invalid" do
395
- klass = Kube::Schema["Deployment"]
396
- resource = klass.new {
397
- spec.replicas = "not_a_number"
398
- }
399
- expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
400
- end
383
+ parsed.should.be.kind_of Hash
384
+ parsed["kind"].should == "Pod"
385
+ parsed["apiVersion"].should == "v1"
386
+ end
401
387
 
402
- it "raises ValidationError for an incomplete Deployment missing selector" do
403
- klass = Kube::Schema["Deployment"]
404
- resource = klass.new {
405
- metadata.namespace = "example"
406
- metadata.name = "example-deployment"
407
- spec.replicas = 1
408
- spec.template.spec.containers = [
409
- { name: "app", image: "ruby:latest" }
410
- ]
411
- }
412
- expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
413
- end
388
+ it "raises ValidationError when the resource is invalid" do
389
+ resource = Kube::Schema["Deployment"].new {
390
+ spec.replicas = "not_a_number"
391
+ }
392
+ lambda { resource.to_yaml }.should.raise(Kube::ValidationError)
393
+ end
414
394
 
415
- context "with a full Deployment (no manual apiVersion/kind)" do
416
- let(:deployment) do
417
- Kube::Schema["Deployment"].new {
418
- metadata.name = "nginx-deployment"
419
- metadata.namespace = "shopping-cart"
420
- metadata.labels = { app: "nginx" }
421
- spec.replicas = 3
422
- spec.selector.matchLabels = { app: "nginx" }
423
- spec.template.metadata.labels = { app: "nginx" }
424
- spec.template.spec.containers = [
425
- { name: "nginx", image: "nginx:1.19.5", ports: [{ containerPort: 80 }] }
426
- ]
427
- }
428
- end
395
+ it "raises ValidationError for an incomplete Deployment missing selector" do
396
+ resource = incomplete_deployment.()
397
+ lambda { resource.to_yaml }.should.raise(Kube::ValidationError)
398
+ end
399
+ end
429
400
 
430
- it "produces valid Kubernetes Deployment YAML" do
431
- yaml = deployment.to_yaml
432
- parsed = YAML.safe_load(yaml)
433
-
434
- expect(parsed).to be_a(Hash)
435
- expect(parsed["apiVersion"]).to eq("apps/v1")
436
- expect(parsed["kind"]).to eq("Deployment")
437
- expect(parsed["metadata"]["name"]).to eq("nginx-deployment")
438
- expect(parsed["metadata"]["namespace"]).to eq("shopping-cart")
439
- expect(parsed["metadata"]["labels"]).to eq({ "app" => "nginx" })
440
- expect(parsed["spec"]["replicas"]).to eq(3)
441
- expect(parsed["spec"]["selector"]["matchLabels"]).to eq({ "app" => "nginx" })
442
- expect(parsed["spec"]["template"]["metadata"]["labels"]).to eq({ "app" => "nginx" })
443
-
444
- containers = parsed["spec"]["template"]["spec"]["containers"]
445
- expect(containers).to be_an(Array)
446
- expect(containers.length).to eq(1)
447
- expect(containers[0]["name"]).to eq("nginx")
448
- expect(containers[0]["image"]).to eq("nginx:1.19.5")
449
- expect(containers[0]["ports"]).to eq([{ "containerPort" => 80 }])
450
- end
401
+ describe "#to_yaml with a full Deployment (no manual apiVersion/kind)" do
402
+ it "produces valid Kubernetes Deployment YAML" do
403
+ parsed = YAML.safe_load(full_deployment.().to_yaml)
404
+
405
+ parsed.should.be.kind_of Hash
406
+ parsed["apiVersion"].should == "apps/v1"
407
+ parsed["kind"].should == "Deployment"
408
+ parsed["metadata"]["name"].should == "nginx-deployment"
409
+ parsed["metadata"]["namespace"].should == "shopping-cart"
410
+ parsed["metadata"]["labels"].should == { "app" => "nginx" }
411
+ parsed["spec"]["replicas"].should == 3
412
+ parsed["spec"]["selector"]["matchLabels"].should == { "app" => "nginx" }
413
+ parsed["spec"]["template"]["metadata"]["labels"].should == { "app" => "nginx" }
414
+
415
+ containers = parsed["spec"]["template"]["spec"]["containers"]
416
+ containers.should.be.kind_of Array
417
+ containers.length.should == 1
418
+ containers[0]["name"].should == "nginx"
419
+ containers[0]["image"].should == "nginx:1.19.5"
420
+ containers[0]["ports"].should == [{ "containerPort" => 80 }]
421
+ end
451
422
 
452
- it "does not contain Ruby object serialization artifacts" do
453
- yaml = deployment.to_yaml
423
+ it "does not contain Ruby object serialization artifacts" do
424
+ yaml = full_deployment.().to_yaml
454
425
 
455
- expect(yaml).not_to include("!ruby/object")
456
- expect(yaml).not_to include("table:")
457
- end
426
+ yaml.should.not.include "!ruby/object"
427
+ yaml.should.not.include "table:"
428
+ end
458
429
 
459
- it "looks like real kubectl YAML output" do
460
- yaml = deployment.to_yaml
430
+ it "looks like real kubectl YAML output" do
431
+ yaml = full_deployment.().to_yaml
461
432
 
462
- expect(yaml).to include("apiVersion: apps/v1")
463
- expect(yaml).to include("kind: Deployment")
464
- expect(yaml).to include("name: nginx-deployment")
465
- expect(yaml).to include("namespace: shopping-cart")
466
- expect(yaml).to include("replicas: 3")
467
- expect(yaml).to include("image: nginx:1.19.5")
468
- expect(yaml).to include("containerPort: 80")
469
- end
433
+ yaml.should.include "apiVersion: apps/v1"
434
+ yaml.should.include "kind: Deployment"
435
+ yaml.should.include "name: nginx-deployment"
436
+ yaml.should.include "namespace: shopping-cart"
437
+ yaml.should.include "replicas: 3"
438
+ yaml.should.include "image: nginx:1.19.5"
439
+ yaml.should.include "containerPort: 80"
440
+ end
470
441
 
471
- it "exactly matches real Kubernetes Deployment YAML" do
472
- expected_yaml = <<~YAML
473
- ---
474
- apiVersion: apps/v1
475
- kind: Deployment
442
+ it "exactly matches real Kubernetes Deployment YAML" do
443
+ expected_yaml = <<~YAML
444
+ ---
445
+ apiVersion: apps/v1
446
+ kind: Deployment
447
+ metadata:
448
+ name: nginx-deployment
449
+ namespace: shopping-cart
450
+ labels:
451
+ app: nginx
452
+ spec:
453
+ replicas: 3
454
+ selector:
455
+ matchLabels:
456
+ app: nginx
457
+ template:
476
458
  metadata:
477
- name: nginx-deployment
478
- namespace: shopping-cart
479
459
  labels:
480
460
  app: nginx
481
461
  spec:
482
- replicas: 3
483
- selector:
484
- matchLabels:
485
- app: nginx
486
- template:
487
- metadata:
488
- labels:
489
- app: nginx
490
- spec:
491
- containers:
492
- - name: nginx
493
- image: nginx:1.19.5
494
- ports:
495
- - containerPort: 80
496
- YAML
497
-
498
- expect(deployment.to_yaml).to eq(expected_yaml)
499
- end
500
-
501
- it "round-trips through YAML.safe_load" do
502
- parsed = YAML.safe_load(deployment.to_yaml)
503
- expect(parsed["apiVersion"]).to eq("apps/v1")
504
- expect(parsed["kind"]).to eq("Deployment")
505
- end
506
- end
462
+ containers:
463
+ - name: nginx
464
+ image: nginx:1.19.5
465
+ ports:
466
+ - containerPort: 80
467
+ YAML
468
+
469
+ full_deployment.().to_yaml.should == expected_yaml
507
470
  end
508
471
 
509
- describe "Deployment schema validation against real Kubernetes YAML" do
510
- let(:klass) { Kube::Schema["Deployment"] }
511
-
512
- let(:incomplete_deployment) do
513
- klass.new {
514
- metadata.namespace = "example"
515
- metadata.name = "example-deployment"
516
- spec.replicas = 1
517
- spec.template.spec.containers = [
518
- { name: "app", image: "ruby:latest" }
519
- ]
520
- }
521
- end
522
-
523
- it "rejects an incomplete Deployment missing selector" do
524
- expect(incomplete_deployment.valid?).to be false
525
- end
526
-
527
- it "reports specific validation errors for incomplete Deployment" do
528
- expect { incomplete_deployment.valid! }.to raise_error(Kube::ValidationError) do |error|
529
- expect(error.message).to include("spec.selector is required but missing")
530
- end
531
- end
472
+ it "round-trips through YAML.safe_load" do
473
+ parsed = YAML.safe_load(full_deployment.().to_yaml)
474
+ parsed["apiVersion"].should == "apps/v1"
475
+ parsed["kind"].should == "Deployment"
476
+ end
477
+ end
532
478
 
533
- it "refuses to serialize an incomplete Deployment to YAML" do
534
- expect { incomplete_deployment.to_yaml }.to raise_error(Kube::ValidationError)
535
- end
479
+ describe "Deployment schema validation against real Kubernetes YAML" do
480
+ it "rejects an incomplete Deployment missing selector" do
481
+ incomplete_deployment.().valid?.should.be.false
482
+ end
536
483
 
537
- it "has apiVersion and kind from defaults even when incomplete" do
538
- h = incomplete_deployment.to_h
539
- expect(h[:apiVersion]).to eq("apps/v1")
540
- expect(h[:kind]).to eq("Deployment")
541
- end
484
+ it "reports specific validation errors for incomplete Deployment" do
485
+ resource = incomplete_deployment.()
486
+ error = lambda { resource.valid! }.should.raise(Kube::ValidationError)
487
+ error.message.should.include "spec.selector is required but missing"
542
488
  end
543
489
 
544
- describe "pattern matching" do
545
- let(:deployment) do
546
- Kube::Schema["Deployment"].new {
547
- metadata.name = "some-resource"
548
- metadata.namespace = "prod"
549
- }
550
- end
490
+ it "refuses to serialize an incomplete Deployment to YAML" do
491
+ resource = incomplete_deployment.()
492
+ lambda { resource.to_yaml }.should.raise(Kube::ValidationError)
493
+ end
551
494
 
552
- it "destructures with a nested hash pattern" do
553
- deployment => {kind: String => kind, metadata: {name:, namespace:}}
554
- expect([kind, name, namespace]).to eq(["Deployment", "some-resource", "prod"])
555
- end
495
+ it "has apiVersion and kind from defaults even when incomplete" do
496
+ h = incomplete_deployment.().to_h
497
+ h[:apiVersion].should == "apps/v1"
498
+ h[:kind].should == "Deployment"
499
+ end
500
+ end
556
501
 
557
- it "matches in a case/in" do
558
- matched = case deployment
559
- in {metadata: {name: "some-resource"}} then true
560
- else false
561
- end
562
- expect(matched).to be true
563
- end
502
+ describe "pattern matching on a named resource" do
503
+ it "destructures with a nested hash pattern" do
504
+ resource = Kube::Schema["Deployment"].new {
505
+ metadata.name = "some-resource"
506
+ metadata.namespace = "prod"
507
+ }
508
+ resource => {kind: String => kind, metadata: {name: deploy_name, namespace: deploy_namespace}}
509
+ [kind, deploy_name, deploy_namespace].should == ["Deployment", "some-resource", "prod"]
564
510
  end
565
511
 
566
- describe "instantiation via Instance lookup" do
567
- let(:klass) { Kube::Schema["Deployment"] }
512
+ it "matches in a case/in" do
513
+ resource = Kube::Schema["Deployment"].new {
514
+ metadata.name = "some-resource"
515
+ metadata.namespace = "prod"
516
+ }
517
+ matched = case resource
518
+ in {metadata: {name: "some-resource"}} then true
519
+ else false
520
+ end
521
+ matched.should.be.true
522
+ end
523
+ end
568
524
 
569
- it "returns a Resource instance from .new" do
570
- resource = klass.new
571
- expect(resource).to be_a(described_class)
572
- end
525
+ describe "instantiation via Instance lookup" do
526
+ it "returns a Resource instance from .new" do
527
+ deployment_class.().new.should.be.kind_of Kube::Schema::Resource
528
+ end
573
529
 
574
- it "supports block-based initialization" do
575
- resource = klass.new {
576
- metadata.name = "web"
577
- metadata.namespace = "prod"
578
- }
579
- expect(resource.to_h[:metadata][:name]).to eq("web")
580
- expect(resource.to_h[:metadata][:namespace]).to eq("prod")
581
- end
530
+ it "supports block-based initialization" do
531
+ resource = deployment_class.().new {
532
+ metadata.name = "web"
533
+ metadata.namespace = "prod"
534
+ }
535
+ resource.to_h[:metadata][:name].should == "web"
536
+ resource.to_h[:metadata][:namespace].should == "prod"
537
+ end
582
538
 
583
- it "has a schema attached to the class" do
584
- expect(klass.schema).not_to be_nil
585
- end
539
+ it "has a schema attached to the class" do
540
+ deployment_class.().schema.should.not.be.nil
541
+ end
586
542
 
587
- it "has defaults attached to the class" do
588
- expect(klass.defaults).not_to be_nil
589
- expect(klass.defaults["apiVersion"]).to eq("apps/v1")
590
- expect(klass.defaults["kind"]).to eq("Deployment")
591
- end
543
+ it "has defaults attached to the class" do
544
+ deployment_class.().defaults.should.not.be.nil
545
+ deployment_class.().defaults["apiVersion"].should == "apps/v1"
546
+ deployment_class.().defaults["kind"].should == "Deployment"
592
547
  end
593
548
  end
594
549
  end