kube_schema 1.3.9 → 1.4.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/Gemfile.lock +1 -1
- data/bin/copy-schemas-over +2 -0
- data/bin/create-kubevirt-x-values +54 -0
- data/examples/sub_specs.rb +412 -0
- data/lib/kube/schema/instance.rb +106 -0
- data/lib/kube/schema/resource.rb +2 -0
- data/lib/kube/schema/sub_spec.rb +485 -0
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +1 -0
- data/schemas/kubevirt-definitions.json +143 -143
- metadata +4 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kube
|
|
4
|
+
module Schema
|
|
5
|
+
# A lightweight, schema-validated wrapper for non-resource Kubernetes
|
|
6
|
+
# definitions — things like Container, ContainerPort, Volume, Probe,
|
|
7
|
+
# etc. that live inside resource specs but have no apiVersion/kind.
|
|
8
|
+
#
|
|
9
|
+
# SubSpec validates against the OpenAPI JSON Schema definition and
|
|
10
|
+
# produces a plain Hash via #to_h, suitable for embedding directly
|
|
11
|
+
# inside Resource specs.
|
|
12
|
+
#
|
|
13
|
+
# container = Kube::Schema::SubSpec["Container"].new {
|
|
14
|
+
# name = "app"
|
|
15
|
+
# image = "nginx:1.27"
|
|
16
|
+
# ports = [{ containerPort: 80 }]
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# container.valid? # => true
|
|
20
|
+
# container.to_h # => { name: "app", image: "nginx:1.27", ... }
|
|
21
|
+
#
|
|
22
|
+
# SubSpec instances auto-coerce when placed inside a Resource —
|
|
23
|
+
# no explicit .to_h is needed:
|
|
24
|
+
#
|
|
25
|
+
# spec.template.spec.containers = [container]
|
|
26
|
+
#
|
|
27
|
+
class SubSpec
|
|
28
|
+
|
|
29
|
+
def initialize(hash = {}, &block)
|
|
30
|
+
deep_symbolize_keys(hash).then do |symbolized|
|
|
31
|
+
@data = {}
|
|
32
|
+
|
|
33
|
+
self.class.schema_properties.each do |property|
|
|
34
|
+
if symbolized.key?(property)
|
|
35
|
+
@data[property] = symbolized.delete(property)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if block_given?
|
|
41
|
+
@data.instance_exec(&block)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Gets overridden by the factory in Kube::Schema::Instance
|
|
46
|
+
def self.schema
|
|
47
|
+
raise "Kube::Schema::SubSpec should NOT be instantiated directly"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.schema_properties
|
|
51
|
+
raise "Kube::Schema::SubSpec should NOT be instantiated directly"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.definition_name
|
|
55
|
+
raise "Kube::Schema::SubSpec should NOT be instantiated directly"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def valid?
|
|
59
|
+
if self.class.schema.nil?
|
|
60
|
+
true
|
|
61
|
+
else
|
|
62
|
+
self.class.schema.valid?(deep_stringify_keys(to_h))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Like #valid? but raises Kube::ValidationError with details on failure.
|
|
67
|
+
def valid!
|
|
68
|
+
if self.class.schema.nil?
|
|
69
|
+
true
|
|
70
|
+
else
|
|
71
|
+
data = deep_stringify_keys(to_h)
|
|
72
|
+
errors = self.class.schema.validate(data).to_a
|
|
73
|
+
|
|
74
|
+
unless errors.empty?
|
|
75
|
+
raise Kube::ValidationError.new(errors,
|
|
76
|
+
kind: self.class.definition_name,
|
|
77
|
+
manifest: data
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the sub-spec data as a plain Hash.
|
|
86
|
+
def to_h
|
|
87
|
+
data = deep_compact(@data)
|
|
88
|
+
data.reject { |_, v| v.is_a?(Hash) && v.empty? }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ==(other)
|
|
92
|
+
other.is_a?(SubSpec) && to_h == other.to_h
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Look up a sub-spec definition by short name.
|
|
96
|
+
#
|
|
97
|
+
# Kube::Schema::SubSpec["Container"]
|
|
98
|
+
# Kube::Schema::SubSpec["ContainerPort"]
|
|
99
|
+
# Kube::Schema::SubSpec["Volume"]
|
|
100
|
+
#
|
|
101
|
+
class << self
|
|
102
|
+
def [](name)
|
|
103
|
+
version = Schema.schema_version || Schema::DEFAULT_VERSION
|
|
104
|
+
instance = Schema[version]
|
|
105
|
+
instance.sub_spec(name)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def deep_compact(obj)
|
|
112
|
+
case obj
|
|
113
|
+
when Hash
|
|
114
|
+
obj.each_with_object({}) do |(k, v), result|
|
|
115
|
+
compacted = deep_compact(v)
|
|
116
|
+
result[k] = compacted unless compacted.nil?
|
|
117
|
+
end
|
|
118
|
+
when Array
|
|
119
|
+
obj.map { |v| deep_compact(v) }
|
|
120
|
+
else
|
|
121
|
+
obj
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def deep_stringify_keys(obj)
|
|
126
|
+
case obj
|
|
127
|
+
when Hash
|
|
128
|
+
obj.each_with_object({}) do |(k, v), result|
|
|
129
|
+
result[k.to_s] = deep_stringify_keys(v)
|
|
130
|
+
end
|
|
131
|
+
when Array
|
|
132
|
+
obj.map { |v| deep_stringify_keys(v) }
|
|
133
|
+
else
|
|
134
|
+
obj
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def deep_symbolize_keys(obj)
|
|
139
|
+
case obj
|
|
140
|
+
when Hash
|
|
141
|
+
obj.each_with_object({}) do |(k, v), result|
|
|
142
|
+
result[k.to_sym] = deep_symbolize_keys(v)
|
|
143
|
+
end
|
|
144
|
+
when Array
|
|
145
|
+
obj.map { |v| deep_symbolize_keys(v) }
|
|
146
|
+
else
|
|
147
|
+
obj
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if __FILE__ == $0
|
|
155
|
+
require "bundler/setup"
|
|
156
|
+
require "rspec/autorun"
|
|
157
|
+
require "kube/schema"
|
|
158
|
+
|
|
159
|
+
RSpec.describe Kube::Schema::SubSpec do
|
|
160
|
+
describe ".[]" do
|
|
161
|
+
it "returns a Class that subclasses SubSpec" do
|
|
162
|
+
klass = described_class["Container"]
|
|
163
|
+
expect(klass).to be_a(Class)
|
|
164
|
+
expect(klass).to be < described_class
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "caches sub-spec classes by name" do
|
|
168
|
+
a = described_class["Container"]
|
|
169
|
+
b = described_class["Container"]
|
|
170
|
+
expect(a).to be(b)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "resolves ContainerPort" do
|
|
174
|
+
klass = described_class["ContainerPort"]
|
|
175
|
+
expect(klass).to be < described_class
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "resolves Probe" do
|
|
179
|
+
klass = described_class["Probe"]
|
|
180
|
+
expect(klass).to be < described_class
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "resolves Volume" do
|
|
184
|
+
klass = described_class["Volume"]
|
|
185
|
+
expect(klass).to be < described_class
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "resolves EnvVar" do
|
|
189
|
+
klass = described_class["EnvVar"]
|
|
190
|
+
expect(klass).to be < described_class
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "resolves ResourceRequirements" do
|
|
194
|
+
klass = described_class["ResourceRequirements"]
|
|
195
|
+
expect(klass).to be < described_class
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "resolves a full definition key" do
|
|
199
|
+
klass = described_class["io.k8s.api.core.v1.Container"]
|
|
200
|
+
expect(klass).to be < described_class
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "raises for an unknown definition" do
|
|
204
|
+
expect { described_class["ThisDoesNotExist999"] }.to raise_error(RuntimeError, /No definition found/)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe ".definition_name" do
|
|
209
|
+
it "returns the short name used to look up the sub-spec" do
|
|
210
|
+
klass = described_class["Container"]
|
|
211
|
+
expect(klass.definition_name).to eq("Container")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
describe ".schema" do
|
|
216
|
+
it "has a schema attached" do
|
|
217
|
+
klass = described_class["Container"]
|
|
218
|
+
expect(klass.schema).not_to be_nil
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe ".schema_properties" do
|
|
223
|
+
it "lists known properties as symbols" do
|
|
224
|
+
klass = described_class["Container"]
|
|
225
|
+
props = klass.schema_properties
|
|
226
|
+
expect(props).to include(:name, :image, :ports, :env, :resources)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe "#initialize" do
|
|
231
|
+
let(:klass) { described_class["Container"] }
|
|
232
|
+
|
|
233
|
+
it "accepts a hash" do
|
|
234
|
+
sub = klass.new(name: "app", image: "nginx")
|
|
235
|
+
expect(sub.to_h).to include(name: "app", image: "nginx")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "accepts string keys" do
|
|
239
|
+
sub = klass.new("name" => "app", "image" => "nginx")
|
|
240
|
+
expect(sub.to_h).to include(name: "app", image: "nginx")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "creates an empty sub-spec when no arguments are given" do
|
|
244
|
+
sub = klass.new
|
|
245
|
+
expect(sub.to_h).to eq({})
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "accepts a block for DSL-style initialization" do
|
|
249
|
+
sub = klass.new {
|
|
250
|
+
self.name = "app"
|
|
251
|
+
self.image = "nginx:1.27"
|
|
252
|
+
}
|
|
253
|
+
expect(sub.to_h).to include(name: "app", image: "nginx:1.27")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "supports nested DSL" do
|
|
257
|
+
sub = klass.new {
|
|
258
|
+
self.name = "app"
|
|
259
|
+
self.image = "nginx"
|
|
260
|
+
self.resources.requests = { cpu: "100m", memory: "128Mi" }
|
|
261
|
+
self.resources.limits = { cpu: "500m", memory: "256Mi" }
|
|
262
|
+
}
|
|
263
|
+
expect(sub.to_h[:resources][:requests]).to eq({ cpu: "100m", memory: "128Mi" })
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "ignores unknown properties" do
|
|
267
|
+
sub = klass.new(name: "app", bogus_field: "ignored")
|
|
268
|
+
expect(sub.to_h).to eq({ name: "app" })
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
describe "accessor methods" do
|
|
273
|
+
let(:klass) { described_class["Container"] }
|
|
274
|
+
|
|
275
|
+
it "defines reader methods for schema properties" do
|
|
276
|
+
sub = klass.new(name: "app", image: "nginx")
|
|
277
|
+
expect(sub.name).to eq("app")
|
|
278
|
+
expect(sub.image).to eq("nginx")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "returns nil for unset properties" do
|
|
282
|
+
sub = klass.new(name: "app")
|
|
283
|
+
expect(sub.image).to be_nil
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe "#valid?" do
|
|
288
|
+
let(:klass) { described_class["Container"] }
|
|
289
|
+
|
|
290
|
+
it "returns true for valid data" do
|
|
291
|
+
sub = klass.new(name: "app", image: "nginx")
|
|
292
|
+
expect(sub.valid?).to be true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "returns false for data violating the schema" do
|
|
296
|
+
sub = klass.new(name: "app", ports: "not_an_array")
|
|
297
|
+
expect(sub.valid?).to be false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "returns false when required fields are missing" do
|
|
301
|
+
# Container requires 'name'
|
|
302
|
+
sub = klass.new(image: "nginx")
|
|
303
|
+
expect(sub.valid?).to be false
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
describe "#valid!" do
|
|
308
|
+
let(:klass) { described_class["Container"] }
|
|
309
|
+
|
|
310
|
+
it "returns true for valid data" do
|
|
311
|
+
sub = klass.new(name: "app")
|
|
312
|
+
expect(sub.valid!).to be true
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "raises ValidationError for invalid data" do
|
|
316
|
+
sub = klass.new(image: "nginx")
|
|
317
|
+
expect { sub.valid! }.to raise_error(Kube::ValidationError)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "includes the definition name in the error" do
|
|
321
|
+
sub = klass.new(image: "nginx")
|
|
322
|
+
expect { sub.valid! }.to raise_error(Kube::ValidationError, /Container/)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it "shows which required keys are missing" do
|
|
326
|
+
sub = klass.new(image: "nginx")
|
|
327
|
+
expect { sub.valid! }.to raise_error(Kube::ValidationError) do |error|
|
|
328
|
+
expect(error.message).to include("name is required but missing")
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe "#to_h" do
|
|
334
|
+
let(:klass) { described_class["Container"] }
|
|
335
|
+
|
|
336
|
+
it "returns a plain Hash" do
|
|
337
|
+
sub = klass.new(name: "app", image: "nginx")
|
|
338
|
+
h = sub.to_h
|
|
339
|
+
expect(h).to be_a(Hash)
|
|
340
|
+
expect(h).to eq({ name: "app", image: "nginx" })
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "strips empty sub-hashes" do
|
|
344
|
+
sub = klass.new(name: "app")
|
|
345
|
+
expect(sub.to_h).to eq({ name: "app" })
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "does NOT include apiVersion or kind" do
|
|
349
|
+
sub = klass.new(name: "app")
|
|
350
|
+
expect(sub.to_h).not_to have_key(:apiVersion)
|
|
351
|
+
expect(sub.to_h).not_to have_key(:kind)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it "preserves nested structure" do
|
|
355
|
+
sub = klass.new {
|
|
356
|
+
self.name = "app"
|
|
357
|
+
self.image = "nginx"
|
|
358
|
+
self.ports = [{ containerPort: 80 }]
|
|
359
|
+
}
|
|
360
|
+
expect(sub.to_h[:ports]).to eq([{ containerPort: 80 }])
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
describe "#==" do
|
|
365
|
+
let(:klass) { described_class["Container"] }
|
|
366
|
+
|
|
367
|
+
it "considers two sub-specs equal when their data matches" do
|
|
368
|
+
a = klass.new(name: "app", image: "nginx")
|
|
369
|
+
b = klass.new(name: "app", image: "nginx")
|
|
370
|
+
expect(a).to eq(b)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it "considers two sub-specs unequal when their data differs" do
|
|
374
|
+
a = klass.new(name: "app", image: "nginx")
|
|
375
|
+
b = klass.new(name: "other", image: "nginx")
|
|
376
|
+
expect(a).not_to eq(b)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it "is not equal to a plain Hash" do
|
|
380
|
+
sub = klass.new(name: "app")
|
|
381
|
+
expect(sub).not_to eq({ name: "app" })
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
describe "auto-coercion in Resource" do
|
|
386
|
+
it "auto-coerces SubSpec instances inside Resource arrays" do
|
|
387
|
+
container = described_class["Container"].new {
|
|
388
|
+
self.name = "app"
|
|
389
|
+
self.image = "nginx:1.27"
|
|
390
|
+
self.ports = [{ containerPort: 80 }]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
deploy = Kube::Schema["Deployment"].new {
|
|
394
|
+
metadata.name = "web"
|
|
395
|
+
spec.replicas = 1
|
|
396
|
+
spec.selector.matchLabels = { app: "web" }
|
|
397
|
+
spec.template.metadata.labels = { app: "web" }
|
|
398
|
+
spec.template.spec.containers = [container]
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
h = deploy.to_h
|
|
402
|
+
containers = h[:spec][:template][:spec][:containers]
|
|
403
|
+
expect(containers).to be_an(Array)
|
|
404
|
+
expect(containers.first).to be_a(Hash)
|
|
405
|
+
expect(containers.first[:name]).to eq("app")
|
|
406
|
+
expect(containers.first[:image]).to eq("nginx:1.27")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it "produces valid YAML with auto-coerced SubSpec" do
|
|
410
|
+
container = described_class["Container"].new {
|
|
411
|
+
self.name = "nginx"
|
|
412
|
+
self.image = "nginx:1.27"
|
|
413
|
+
self.ports = [{ containerPort: 80 }]
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
deploy = Kube::Schema["Deployment"].new {
|
|
417
|
+
metadata.name = "web"
|
|
418
|
+
spec.replicas = 1
|
|
419
|
+
spec.selector.matchLabels = { app: "web" }
|
|
420
|
+
spec.template.metadata.labels = { app: "web" }
|
|
421
|
+
spec.template.spec.containers = [container]
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
yaml = deploy.to_yaml
|
|
425
|
+
parsed = YAML.safe_load(yaml)
|
|
426
|
+
expect(parsed["spec"]["template"]["spec"]["containers"].first["name"]).to eq("nginx")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
it "handles multiple SubSpec instances in an array" do
|
|
430
|
+
app = described_class["Container"].new(name: "app", image: "app:latest")
|
|
431
|
+
sidecar = described_class["Container"].new(name: "sidecar", image: "sidecar:latest")
|
|
432
|
+
|
|
433
|
+
deploy = Kube::Schema["Deployment"].new {
|
|
434
|
+
metadata.name = "web"
|
|
435
|
+
spec.replicas = 1
|
|
436
|
+
spec.selector.matchLabels = { app: "web" }
|
|
437
|
+
spec.template.metadata.labels = { app: "web" }
|
|
438
|
+
spec.template.spec.containers = [app, sidecar]
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
containers = deploy.to_h[:spec][:template][:spec][:containers]
|
|
442
|
+
expect(containers.size).to eq(2)
|
|
443
|
+
expect(containers.map { |c| c[:name] }).to eq(["app", "sidecar"])
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it "works mixed with plain hashes" do
|
|
447
|
+
container = described_class["Container"].new(name: "typed", image: "typed:latest")
|
|
448
|
+
|
|
449
|
+
deploy = Kube::Schema["Deployment"].new {
|
|
450
|
+
metadata.name = "web"
|
|
451
|
+
spec.replicas = 1
|
|
452
|
+
spec.selector.matchLabels = { app: "web" }
|
|
453
|
+
spec.template.metadata.labels = { app: "web" }
|
|
454
|
+
spec.template.spec.containers = [
|
|
455
|
+
container,
|
|
456
|
+
{ name: "plain", image: "plain:latest" }
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
containers = deploy.to_h[:spec][:template][:spec][:containers]
|
|
461
|
+
expect(containers.map { |c| c[:name] }).to eq(["typed", "plain"])
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
describe "disambiguation" do
|
|
466
|
+
it "prefers stable API versions over beta/alpha" do
|
|
467
|
+
# MatchCondition exists in v1, v1alpha1, and v1beta1
|
|
468
|
+
klass = described_class["MatchCondition"]
|
|
469
|
+
expect(klass).to be < described_class
|
|
470
|
+
expect(klass.schema).not_to be_nil
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
describe "Instance#list_definitions" do
|
|
475
|
+
it "returns a sorted array of short definition names" do
|
|
476
|
+
instance = Kube::Schema::Instance.new("1.34")
|
|
477
|
+
defs = instance.list_definitions
|
|
478
|
+
expect(defs).to be_an(Array)
|
|
479
|
+
expect(defs).not_to be_empty
|
|
480
|
+
expect(defs).to include("Container", "ContainerPort", "Probe", "Volume")
|
|
481
|
+
expect(defs).to eq(defs.sort)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
data/lib/kube/schema/version.rb
CHANGED
data/lib/kube/schema.rb
CHANGED