kube_schema 1.11.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,6 +100,11 @@ module Kube
101
100
  other.is_a?(Resource) && to_h == other.to_h
102
101
  end
103
102
 
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.
106
+ def deconstruct_keys(keys) = to_h
107
+
104
108
  private
105
109
 
106
110
  def deep_compact(obj)
@@ -110,7 +114,9 @@ module Kube
110
114
  when Hash
111
115
  obj.each_with_object({}) do |(k, v), result|
112
116
  compacted = deep_compact(v)
113
- result[k] = compacted unless compacted.nil?
117
+ unless compacted.nil?
118
+ result[k] = compacted
119
+ end
114
120
  end
115
121
  when Array
116
122
  obj.map { |v| deep_compact(v) }
@@ -148,406 +154,396 @@ module Kube
148
154
  end
149
155
  end
150
156
 
151
- if __FILE__ == $0
152
- require "bundler/setup"
153
- require "rspec/autorun"
154
- require "kube/schema"
155
-
156
- RSpec.describe Kube::Schema::Resource do
157
- describe ".defaults" do
158
- it "returns apiVersion and kind for a schema-bearing subclass" do
159
- klass = Kube::Schema["Deployment"]
160
- expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
161
- end
162
157
 
163
- it "returns correct apiVersion for core resources (no group)" do
164
- klass = Kube::Schema["Pod"]
165
- expect(klass.defaults).to eq({ "apiVersion" => "v1", "kind" => "Pod" })
166
- 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" }
193
+ end
167
194
 
168
- it "returns correct apiVersion for grouped resources" do
169
- klass = Kube::Schema["NetworkPolicy"]
170
- expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "NetworkPolicy" })
171
- end
195
+ it "returns correct apiVersion for core resources (no group)" do
196
+ Kube::Schema["Pod"].defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
172
197
  end
173
198
 
174
- describe "#initialize" do
175
- it "accepts a hash" do
176
- klass = Kube::Schema["Deployment"]
177
- resource = klass.new("metadata" => { "name" => "my-deploy" })
178
- expect(resource.to_h).to include(kind: "Deployment")
179
- expect(resource.to_h[:metadata][:name]).to eq("my-deploy")
180
- end
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
181
203
 
182
- it "creates an empty resource when no arguments are given" do
183
- klass = Kube::Schema["Deployment"]
184
- resource = klass.new
185
- expect(resource.to_h).to include(apiVersion: "apps/v1", kind: "Deployment")
186
- 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
187
210
 
188
- it "accepts a block for DSL-style initialization" do
189
- klass = Kube::Schema["Deployment"]
190
- resource = klass.new {
191
- metadata.name = "test"
192
- }
193
- expect(resource.to_h).to include(kind: "Deployment")
194
- expect(resource.to_h[:metadata][:name]).to eq("test")
195
- 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"
196
215
  end
197
216
 
198
- describe "#to_h" do
199
- context "with a schema-bearing subclass" do
200
- let(:klass) { Kube::Schema["Deployment"] }
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
201
225
 
202
- it "automatically includes apiVersion and kind from defaults" do
203
- resource = klass.new {
204
- metadata.name = "test"
205
- }
206
- expect(resource.to_h[:apiVersion]).to eq("apps/v1")
207
- expect(resource.to_h[:kind]).to eq("Deployment")
208
- expect(resource.to_h[:metadata][:name]).to eq("test")
209
- 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
210
235
 
211
- it "cannot override apiVersion or kind -- they are authoritative" do
212
- resource = klass.new {
213
- self.apiVersion = "apps/v1beta1"
214
- self.kind = "NotADeployment"
215
- }
216
- expect(resource.to_h[:apiVersion]).to eq("apps/v1")
217
- expect(resource.to_h[:kind]).to eq("Deployment")
218
- end
219
- 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"
220
243
  end
244
+ end
221
245
 
222
- describe "#==" do
223
- it "considers two resources equal when their data matches" do
224
- a = Kube::Schema["Pod"].new
225
- b = Kube::Schema["Pod"].new
226
- expect(a).to eq(b)
227
- 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}}
228
250
 
229
- it "considers two resources unequal when their data differs" do
230
- a = Kube::Schema["Pod"].new
231
- b = Kube::Schema["Service"].new
232
- expect(a).not_to eq(b)
233
- end
251
+ deploy_name.should == "web"
252
+ end
234
253
 
235
- it "is not equal to non-Resource objects" do
236
- resource = Kube::Schema["Pod"].new
237
- expect(resource).not_to eq({ "kind" => "Pod" })
238
- 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)
239
257
  end
258
+ end
240
259
 
241
- describe "#valid?" do
242
- context "with a schema-bearing subclass" do
243
- 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
244
264
 
245
- it "returns true for valid data (apiVersion/kind come from defaults)" do
246
- resource = klass.new
247
- expect(resource.valid?).to be true
248
- 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
249
268
 
250
- it "returns false for data violating the schema" do
251
- resource = klass.new {
252
- spec.replicas = "not_a_number"
253
- }
254
- expect(resource.valid?).to be false
255
- end
256
- end
269
+ it "is not equal to non-Resource objects" do
270
+ Kube::Schema["Pod"].new.should.not == { "kind" => "Pod" }
257
271
  end
272
+ end
258
273
 
259
- describe "#valid!" do
260
- context "with a schema-bearing subclass" do
261
- 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
262
278
 
263
- it "returns true for valid data" do
264
- resource = klass.new
265
- expect(resource.valid!).to be true
266
- 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
267
286
 
268
- it "raises ValidationError for data violating the schema" do
269
- resource = klass.new {
270
- spec.replicas = "not_a_number"
271
- }
272
- expect { resource.valid! }.to raise_error(Kube::ValidationError)
273
- 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
274
291
 
275
- it "includes error details in the exception" do
276
- resource = klass.new {
277
- spec.replicas = "not_a_number"
278
- }
279
- expect { resource.valid! }.to raise_error(Kube::ValidationError, /Schema validation failed/)
280
- 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
281
298
 
282
- it "shows the exact key path and value for type errors" do
283
- resource = klass.new {
284
- metadata.name = "web"
285
- spec.replicas = "not_a_number"
286
- }
287
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
288
- expect(error.message).to include('spec.replicas = "not_a_number" — expected integer, got String')
289
- end
290
- 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
291
306
 
292
- it "shows which required keys are missing" do
293
- resource = klass.new {
294
- metadata.name = "example"
295
- spec.replicas = 1
296
- spec.template.spec.containers = [{ name: "app", image: "ruby:latest" }]
297
- }
298
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
299
- expect(error.message).to include("spec.selector is required but missing")
300
- end
301
- 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
302
315
 
303
- it "includes the resource kind in the error header" do
304
- resource = klass.new {
305
- metadata.name = "web"
306
- spec.replicas = "bad"
307
- }
308
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
309
- expect(error.message).to include("Schema validation failed for Deployment")
310
- end
311
- 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
312
325
 
313
- it "includes the resource name in the error header when available" do
314
- resource = klass.new {
315
- metadata.name = "my-app"
316
- spec.replicas = "bad"
317
- }
318
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
319
- expect(error.message).to include('Deployment "my-app"')
320
- end
321
- 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
322
334
 
323
- it "omits the resource name when metadata.name is not set" do
324
- resource = klass.new {
325
- spec.replicas = "bad"
326
- }
327
- expect { resource.valid! }.to raise_error(Kube::ValidationError) do |error|
328
- header = error.message.lines.find { |l| l.include?("Schema validation failed") }
329
- expect(header).to include("Schema validation failed for Deployment")
330
- expect(header).not_to match(/Deployment\s+"/) # no quoted name after kind
331
- end
332
- 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
333
343
 
334
- it "exposes the raw errors array" do
335
- resource = klass.new {
336
- spec.replicas = "not_a_number"
337
- }
338
- begin
339
- resource.valid!
340
- rescue Kube::ValidationError => e
341
- expect(e.errors).to be_an(Array)
342
- expect(e.errors).not_to be_empty
343
- end
344
- end
345
- 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
346
352
  end
347
353
 
348
- describe "#to_yaml" do
349
- it "returns clean Kubernetes YAML" do
350
- resource = Kube::Schema["Pod"].new
351
- 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
352
363
 
353
- expect(yaml).to include("kind: Pod")
354
- expect(yaml).to include("apiVersion: v1")
355
- expect(yaml).not_to include("!ruby/object")
356
- end
364
+ describe "#to_yaml" do
365
+ it "returns clean Kubernetes YAML" do
366
+ yaml = Kube::Schema["Pod"].new.to_yaml
357
367
 
358
- it "uses string keys, not symbol keys" do
359
- resource = Kube::Schema["Pod"].new
360
- 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
361
372
 
362
- expect(yaml).not_to match(/:\w+:/)
363
- expect(yaml).to include("kind: Pod")
364
- end
373
+ it "uses string keys, not symbol keys" do
374
+ yaml = Kube::Schema["Pod"].new.to_yaml
365
375
 
366
- it "produces parseable YAML that round-trips" do
367
- resource = Kube::Schema["Pod"].new
368
- parsed = YAML.safe_load(resource.to_yaml)
376
+ yaml.should.not.match(/:\w+:/)
377
+ yaml.should.include "kind: Pod"
378
+ end
369
379
 
370
- expect(parsed).to be_a(Hash)
371
- expect(parsed["kind"]).to eq("Pod")
372
- expect(parsed["apiVersion"]).to eq("v1")
373
- end
380
+ it "produces parseable YAML that round-trips" do
381
+ parsed = YAML.safe_load(Kube::Schema["Pod"].new.to_yaml)
374
382
 
375
- it "raises ValidationError when the resource is invalid" do
376
- klass = Kube::Schema["Deployment"]
377
- resource = klass.new {
378
- spec.replicas = "not_a_number"
379
- }
380
- expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
381
- end
383
+ parsed.should.be.kind_of Hash
384
+ parsed["kind"].should == "Pod"
385
+ parsed["apiVersion"].should == "v1"
386
+ end
382
387
 
383
- it "raises ValidationError for an incomplete Deployment missing selector" do
384
- klass = Kube::Schema["Deployment"]
385
- resource = klass.new {
386
- metadata.namespace = "example"
387
- metadata.name = "example-deployment"
388
- spec.replicas = 1
389
- spec.template.spec.containers = [
390
- { name: "app", image: "ruby:latest" }
391
- ]
392
- }
393
- expect { resource.to_yaml }.to raise_error(Kube::ValidationError)
394
- 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
395
394
 
396
- context "with a full Deployment (no manual apiVersion/kind)" do
397
- let(:deployment) do
398
- Kube::Schema["Deployment"].new {
399
- metadata.name = "nginx-deployment"
400
- metadata.namespace = "shopping-cart"
401
- metadata.labels = { app: "nginx" }
402
- spec.replicas = 3
403
- spec.selector.matchLabels = { app: "nginx" }
404
- spec.template.metadata.labels = { app: "nginx" }
405
- spec.template.spec.containers = [
406
- { name: "nginx", image: "nginx:1.19.5", ports: [{ containerPort: 80 }] }
407
- ]
408
- }
409
- 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
410
400
 
411
- it "produces valid Kubernetes Deployment YAML" do
412
- yaml = deployment.to_yaml
413
- parsed = YAML.safe_load(yaml)
414
-
415
- expect(parsed).to be_a(Hash)
416
- expect(parsed["apiVersion"]).to eq("apps/v1")
417
- expect(parsed["kind"]).to eq("Deployment")
418
- expect(parsed["metadata"]["name"]).to eq("nginx-deployment")
419
- expect(parsed["metadata"]["namespace"]).to eq("shopping-cart")
420
- expect(parsed["metadata"]["labels"]).to eq({ "app" => "nginx" })
421
- expect(parsed["spec"]["replicas"]).to eq(3)
422
- expect(parsed["spec"]["selector"]["matchLabels"]).to eq({ "app" => "nginx" })
423
- expect(parsed["spec"]["template"]["metadata"]["labels"]).to eq({ "app" => "nginx" })
424
-
425
- containers = parsed["spec"]["template"]["spec"]["containers"]
426
- expect(containers).to be_an(Array)
427
- expect(containers.length).to eq(1)
428
- expect(containers[0]["name"]).to eq("nginx")
429
- expect(containers[0]["image"]).to eq("nginx:1.19.5")
430
- expect(containers[0]["ports"]).to eq([{ "containerPort" => 80 }])
431
- 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
432
422
 
433
- it "does not contain Ruby object serialization artifacts" do
434
- yaml = deployment.to_yaml
423
+ it "does not contain Ruby object serialization artifacts" do
424
+ yaml = full_deployment.().to_yaml
435
425
 
436
- expect(yaml).not_to include("!ruby/object")
437
- expect(yaml).not_to include("table:")
438
- end
426
+ yaml.should.not.include "!ruby/object"
427
+ yaml.should.not.include "table:"
428
+ end
439
429
 
440
- it "looks like real kubectl YAML output" do
441
- yaml = deployment.to_yaml
430
+ it "looks like real kubectl YAML output" do
431
+ yaml = full_deployment.().to_yaml
442
432
 
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
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
451
441
 
452
- it "exactly matches real Kubernetes Deployment YAML" do
453
- expected_yaml = <<~YAML
454
- ---
455
- apiVersion: apps/v1
456
- 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:
457
458
  metadata:
458
- name: nginx-deployment
459
- namespace: shopping-cart
460
459
  labels:
461
460
  app: nginx
462
461
  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
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
488
470
  end
489
471
 
490
- describe "Deployment schema validation against real Kubernetes YAML" do
491
- let(:klass) { Kube::Schema["Deployment"] }
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
492
478
 
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
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
503
483
 
504
- it "rejects an incomplete Deployment missing selector" do
505
- expect(incomplete_deployment.valid?).to be false
506
- 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"
488
+ end
507
489
 
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
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
513
494
 
514
- it "refuses to serialize an incomplete Deployment to YAML" do
515
- expect { incomplete_deployment.to_yaml }.to raise_error(Kube::ValidationError)
516
- 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
517
501
 
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
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"]
523
510
  end
524
511
 
525
- describe "instantiation via Instance lookup" do
526
- 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
527
524
 
528
- it "returns a Resource instance from .new" do
529
- resource = klass.new
530
- expect(resource).to be_a(described_class)
531
- 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
532
529
 
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
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
541
538
 
542
- it "has a schema attached to the class" do
543
- expect(klass.schema).not_to be_nil
544
- end
539
+ it "has a schema attached to the class" do
540
+ deployment_class.().schema.should.not.be.nil
541
+ end
545
542
 
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
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"
551
547
  end
552
548
  end
553
549
  end