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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.github/FUNDING.yml +3 -0
- data/.rubocop.yml +226 -5984
- data/CHANGELOG.md +127 -2
- 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 +42 -5
- data/flake.nix +28 -21
- data/gemset.nix +983 -0
- 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 +289 -310
- data/lib/kube/schema/resource.rb +352 -356
- 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 +50 -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,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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
yaml.should.include "kind: Pod"
|
|
369
|
+
yaml.should.include "apiVersion: v1"
|
|
370
|
+
yaml.should.not.include "!ruby/object"
|
|
371
|
+
end
|
|
361
372
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
end
|
|
373
|
+
it "uses string keys, not symbol keys" do
|
|
374
|
+
yaml = Kube::Schema["Pod"].new.to_yaml
|
|
365
375
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
376
|
+
yaml.should.not.match(/:\w+:/)
|
|
377
|
+
yaml.should.include "kind: Pod"
|
|
378
|
+
end
|
|
369
379
|
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
423
|
+
it "does not contain Ruby object serialization artifacts" do
|
|
424
|
+
yaml = full_deployment.().to_yaml
|
|
435
425
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
426
|
+
yaml.should.not.include "!ruby/object"
|
|
427
|
+
yaml.should.not.include "table:"
|
|
428
|
+
end
|
|
439
429
|
|
|
440
|
-
|
|
441
|
-
|
|
430
|
+
it "looks like real kubectl YAML output" do
|
|
431
|
+
yaml = full_deployment.().to_yaml
|
|
442
432
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
539
|
+
it "has a schema attached to the class" do
|
|
540
|
+
deployment_class.().schema.should.not.be.nil
|
|
541
|
+
end
|
|
545
542
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|