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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.github/FUNDING.yml +3 -0
- data/.rubocop.yml +226 -5984
- data/CHANGELOG.md +110 -3
- data/DEPRECATIONS.md +172 -0
- data/Gemfile +13 -3
- data/Gemfile.lock +41 -31
- data/RELEASE.md +161 -0
- data/Rakefile +1 -1
- data/bin/rubocop +3 -0
- data/bin/test +2 -9
- data/cops/begin_block_assignment.rb +34 -0
- data/cops/comment_structure.rb +307 -0
- data/cops/constant_matches_path.rb +142 -0
- data/cops/multiline_method_arguments.rb +89 -0
- data/cops/no_conditional_assignment.rb +186 -0
- data/cops/no_console_logging.rb +62 -0
- data/cops/no_modifier_if.rb +74 -0
- data/cops/no_return.rb +152 -0
- data/cops/support/prism_source.rb +234 -0
- data/cops/use_tap.rb +100 -0
- data/flake.lock +3 -3
- data/flake.nix +22 -15
- data/gemset.nix +142 -73
- data/kube_schema.gemspec +4 -2
- data/lib/kube/errors.rb +652 -645
- data/lib/kube/monkey_patches.rb +5 -6
- data/lib/kube/schema/instance.rb +344 -363
- data/lib/kube/schema/lazy.rb +370 -0
- data/lib/kube/schema/manifest.rb +286 -325
- data/lib/kube/schema/resource.rb +348 -393
- data/lib/kube/schema/sub_spec.rb +261 -293
- data/lib/kube/schema.rb +173 -172
- data/lib/{kube/schema → kube_schema}/version.rb +1 -1
- data/lib/kube_schema.rb +6 -0
- data/readme.md +5 -0
- metadata +49 -9
- data/bin/increment-version +0 -45
- data/bin/release-gem +0 -32
- data/bin/tag-version +0 -19
- data/lib/kube/schema/version.rb.erb +0 -7
data/lib/kube/schema/resource.rb
CHANGED
|
@@ -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
|
-
#
|
|
11
|
-
#
|
|
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
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
42
|
-
#
|
|
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
|
|
56
|
-
#
|
|
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
|
-
#
|
|
75
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
368
|
+
yaml.should.include "kind: Pod"
|
|
369
|
+
yaml.should.include "apiVersion: v1"
|
|
370
|
+
yaml.should.not.include "!ruby/object"
|
|
371
|
+
end
|
|
380
372
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
end
|
|
373
|
+
it "uses string keys, not symbol keys" do
|
|
374
|
+
yaml = Kube::Schema["Pod"].new.to_yaml
|
|
384
375
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
376
|
+
yaml.should.not.match(/:\w+:/)
|
|
377
|
+
yaml.should.include "kind: Pod"
|
|
378
|
+
end
|
|
388
379
|
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
|
|
423
|
+
it "does not contain Ruby object serialization artifacts" do
|
|
424
|
+
yaml = full_deployment.().to_yaml
|
|
454
425
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
426
|
+
yaml.should.not.include "!ruby/object"
|
|
427
|
+
yaml.should.not.include "table:"
|
|
428
|
+
end
|
|
458
429
|
|
|
459
|
-
|
|
460
|
-
|
|
430
|
+
it "looks like real kubectl YAML output" do
|
|
431
|
+
yaml = full_deployment.().to_yaml
|
|
461
432
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
539
|
+
it "has a schema attached to the class" do
|
|
540
|
+
deployment_class.().schema.should.not.be.nil
|
|
541
|
+
end
|
|
586
542
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|