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/instance.rb
CHANGED
|
@@ -5,24 +5,20 @@ require "json_schemer"
|
|
|
5
5
|
|
|
6
6
|
module Kube
|
|
7
7
|
module Schema
|
|
8
|
-
#
|
|
9
|
-
# Lazily loads the Swagger JSON once per version (shared across instances)
|
|
10
|
-
# and builds resource classes by kind.
|
|
8
|
+
# One Kubernetes version's OpenAPI schema.
|
|
11
9
|
#
|
|
10
|
+
# @example
|
|
12
11
|
# instance = Kube::Schema::Instance.new("1.34")
|
|
13
12
|
# instance["Deployment"] # => Resource subclass
|
|
14
13
|
# instance["NetworkPolicy"] # => Resource subclass
|
|
15
14
|
# instance.list_resources # => sorted array of kind strings
|
|
16
|
-
#
|
|
17
15
|
class Instance
|
|
18
16
|
attr_reader :version
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
# share the parsed Swagger schema.
|
|
22
|
-
@schemers = {}
|
|
18
|
+
@catalogs = {}
|
|
23
19
|
|
|
24
20
|
class << self
|
|
25
|
-
attr_reader :
|
|
21
|
+
attr_reader :catalogs
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
def initialize(version)
|
|
@@ -40,16 +36,14 @@ module Kube
|
|
|
40
36
|
|
|
41
37
|
# Look up a resource by kind or full GVK string.
|
|
42
38
|
#
|
|
43
|
-
#
|
|
44
|
-
# instance["Deployment"]
|
|
45
|
-
# instance["apps/v1/Deployment"]
|
|
46
|
-
# instance["v1/Pod"]
|
|
47
|
-
# instance["networking.k8s.io/v1/Ingress"]
|
|
48
|
-
#
|
|
49
|
-
# Returns a class that inherits from Kube::Schema::Resource.
|
|
39
|
+
# @example
|
|
40
|
+
# instance["Deployment"] # kind-only lookup
|
|
41
|
+
# instance["apps/v1/Deployment"] # group/version/kind
|
|
42
|
+
# instance["v1/Pod"] # version/kind, empty group
|
|
43
|
+
# instance["networking.k8s.io/v1/Ingress"] # fully qualified
|
|
50
44
|
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
45
|
+
# @parameter input [String] a kind, or a "group/version/kind" string.
|
|
46
|
+
# @returns [Class] a class that inherits from Kube::Schema::Resource.
|
|
53
47
|
def [](input)
|
|
54
48
|
@resource_classes[input] ||= begin
|
|
55
49
|
if input.include?("/")
|
|
@@ -69,64 +63,66 @@ module Kube
|
|
|
69
63
|
# Custom schemas take precedence on full-GVK lookups too.
|
|
70
64
|
custom = find_custom_entry_by_gvk(group, version, kind)
|
|
71
65
|
if custom
|
|
72
|
-
|
|
66
|
+
entry = nil
|
|
67
|
+
else
|
|
68
|
+
entry = find_gvk_entry_by_full_gvk(group, version, kind)
|
|
73
69
|
end
|
|
74
|
-
|
|
75
|
-
entry = find_gvk_entry_by_full_gvk(group, version, kind)
|
|
76
70
|
else
|
|
77
71
|
# Kind-only lookup — custom schemas take precedence.
|
|
78
72
|
custom = find_custom_entry(input)
|
|
79
73
|
if custom
|
|
80
|
-
|
|
74
|
+
entry = nil
|
|
75
|
+
else
|
|
76
|
+
entry = find_gvk_entry(input)
|
|
81
77
|
end
|
|
82
|
-
|
|
83
|
-
entry = find_gvk_entry(input)
|
|
84
78
|
end
|
|
85
79
|
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
if custom
|
|
81
|
+
build_resource_class(Lazy::Source.spilled(catalog, custom[:entry]), custom[:defaults])
|
|
82
|
+
else
|
|
83
|
+
if entry.nil?
|
|
84
|
+
raise "No resource schema found for #{input.inspect}!" \
|
|
85
|
+
"\nUse #list_resources to see available kinds for v#{@version}."
|
|
86
|
+
end
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
ref_schema = Lazy::Source.indexed(catalog, entry[:definition_key])
|
|
89
|
+
build_resource_class(ref_schema, entry[:defaults].freeze)
|
|
90
|
+
end
|
|
93
91
|
end
|
|
94
92
|
end
|
|
95
93
|
|
|
96
|
-
# All
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# @return [Array<String>] sorted kind names
|
|
94
|
+
# All resource kinds for this version, plus custom ones.
|
|
95
|
+
# @returns [Array(String)] sorted kind names.
|
|
100
96
|
def list_resources
|
|
101
97
|
custom_kinds = Schema.custom_schemas.values.map { |v| v[:defaults]["kind"] }
|
|
102
98
|
(gvk_index.keys + custom_kinds).uniq.sort
|
|
103
99
|
end
|
|
104
100
|
|
|
105
|
-
# All API groups
|
|
106
|
-
# registered via Kube::Schema.register. The core API group is the
|
|
107
|
-
# empty string.
|
|
101
|
+
# All API groups for this version, plus custom ones.
|
|
108
102
|
#
|
|
103
|
+
# @example
|
|
109
104
|
# instance.api_groups # => ["", "apps", "argoproj.io", "batch", ...]
|
|
110
105
|
#
|
|
111
|
-
# @
|
|
106
|
+
# @returns [Array(String)] sorted, deduplicated group names.
|
|
112
107
|
def api_groups
|
|
113
108
|
custom_groups = Schema.custom_schemas.keys.filter_map do |key|
|
|
114
109
|
parts = key.split("/")
|
|
115
|
-
|
|
110
|
+
if parts.size == 3
|
|
111
|
+
parts.first
|
|
112
|
+
end
|
|
116
113
|
end
|
|
117
114
|
(gvk_index.values.flatten.map { |entry| entry[:group] } + custom_groups).uniq.sort
|
|
118
115
|
end
|
|
119
116
|
|
|
120
|
-
# Look up a sub-spec definition by short name
|
|
121
|
-
# "ContainerPort", "Volume", "Probe"). Returns a class that
|
|
122
|
-
# inherits from Kube::Schema::SubSpec.
|
|
123
|
-
#
|
|
124
|
-
# Also accepts a full definition key like
|
|
125
|
-
# "io.k8s.api.core.v1.Container" for disambiguation.
|
|
117
|
+
# Look up a sub-spec definition by short or full name.
|
|
126
118
|
#
|
|
119
|
+
# @example
|
|
127
120
|
# instance.sub_spec("Container") # => SubSpec subclass
|
|
128
121
|
# instance.sub_spec("ContainerPort") # => SubSpec subclass
|
|
122
|
+
# instance.sub_spec("io.k8s.api.core.v1.Container") # disambiguated
|
|
129
123
|
#
|
|
124
|
+
# @parameter name [String] a short name or a full definition key.
|
|
125
|
+
# @returns [Class] a class that inherits from Kube::Schema::SubSpec.
|
|
130
126
|
def sub_spec(name)
|
|
131
127
|
@sub_spec_classes[name] ||= begin
|
|
132
128
|
definition_key = find_definition_key(name)
|
|
@@ -136,31 +132,23 @@ module Kube
|
|
|
136
132
|
"\nUse #list_definitions to see available definitions for v#{version}."
|
|
137
133
|
end
|
|
138
134
|
|
|
139
|
-
ref_schema =
|
|
135
|
+
ref_schema = Lazy::Source.indexed(catalog, definition_key)
|
|
140
136
|
build_sub_spec_class(ref_schema, name)
|
|
141
137
|
end
|
|
142
138
|
end
|
|
143
139
|
|
|
144
|
-
# All
|
|
145
|
-
#
|
|
146
|
-
# @return [Array<String>] sorted, deduplicated short names
|
|
140
|
+
# All definition short names for this version.
|
|
141
|
+
# @returns [Array(String)] sorted, deduplicated short names.
|
|
147
142
|
def list_definitions
|
|
148
|
-
|
|
143
|
+
catalog.keys
|
|
149
144
|
.map { |k| k.split(".").last }
|
|
150
145
|
.uniq.sort
|
|
151
146
|
end
|
|
152
147
|
|
|
153
148
|
private
|
|
154
149
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
# After loading the base Swagger JSON, merges in any extra definition
|
|
159
|
-
# files found in the schemas directory (e.g. crd-definitions.json,
|
|
160
|
-
# loft-definitions.json). These files are flat JSON objects where keys
|
|
161
|
-
# are definition names and values are OpenAPI v2 schema objects.
|
|
162
|
-
def schemer
|
|
163
|
-
self.class.schemers[@version] ||= begin
|
|
150
|
+
def catalog
|
|
151
|
+
self.class.catalogs[@version] ||= begin
|
|
164
152
|
path = File.join(SCHEMAS_DIR, "v#{version}.json")
|
|
165
153
|
|
|
166
154
|
unless File.exist?(path)
|
|
@@ -169,70 +157,54 @@ module Kube
|
|
|
169
157
|
"\nUse `Kube::Schema.schema_versions` to get a list."
|
|
170
158
|
end
|
|
171
159
|
|
|
172
|
-
|
|
160
|
+
extra = Dir.glob(File.join(SCHEMAS_DIR, "*-definitions.json")).sort
|
|
173
161
|
|
|
174
|
-
|
|
175
|
-
# base schema so CRD and aggregated-API types (e.g. loft,
|
|
176
|
-
# gateway-api) are available alongside built-in k8s types.
|
|
177
|
-
Dir.glob(File.join(SCHEMAS_DIR, "*-definitions.json")).each do |defs_path|
|
|
178
|
-
extra = JSON.parse(File.read(defs_path))
|
|
179
|
-
schema["definitions"] ||= {}
|
|
180
|
-
schema["definitions"].merge!(extra)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Kubernetes OpenAPI v2 defines IntOrString as "type": "string"
|
|
184
|
-
# with "format": "int-or-string". This is a limitation of
|
|
185
|
-
# OpenAPI v2 which cannot express union types. Patch the
|
|
186
|
-
# definition so JSONSchemer accepts both integers and strings.
|
|
187
|
-
if (int_or_str = schema.dig("definitions", "io.k8s.apimachinery.pkg.util.intstr.IntOrString"))
|
|
188
|
-
int_or_str["type"] = ["string", "integer"]
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
JSONSchemer.schema(schema)
|
|
162
|
+
Lazy::Index.build([path] + extra)
|
|
192
163
|
end
|
|
193
164
|
end
|
|
194
165
|
|
|
195
|
-
# Builds a map from kind to full GVK entry
|
|
166
|
+
# Builds a map from kind to full GVK entry.
|
|
167
|
+
# @returns [Hash] kind => Array of entry hashes, keyed by GVK.
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
196
170
|
# {
|
|
197
|
-
# "Deployment" => {
|
|
171
|
+
# "Deployment" => [{
|
|
198
172
|
# definition_key: "io.k8s.api.apps.v1.Deployment",
|
|
199
173
|
# group: "apps",
|
|
200
174
|
# version: "v1",
|
|
201
175
|
# kind: "Deployment",
|
|
202
176
|
# defaults: { "apiVersion" => "apps/v1", "kind" => "Deployment" }
|
|
203
|
-
# }
|
|
204
|
-
# "Pod" => {
|
|
205
|
-
# definition_key: "io.k8s.api.core.v1.Pod",
|
|
206
|
-
# group: "",
|
|
207
|
-
# version: "v1",
|
|
208
|
-
# kind: "Pod",
|
|
209
|
-
# defaults: { "apiVersion" => "v1", "kind" => "Pod" }
|
|
210
|
-
# },
|
|
211
|
-
# ...
|
|
177
|
+
# }]
|
|
212
178
|
# }
|
|
213
179
|
def gvk_index
|
|
214
180
|
@gvk_index ||= begin
|
|
215
181
|
index = Hash.new { |h, k| h[k] = [] }
|
|
216
182
|
|
|
217
|
-
|
|
218
|
-
gvks =
|
|
219
|
-
|
|
183
|
+
catalog.entries.each do |key, entry|
|
|
184
|
+
gvks = entry.gvks
|
|
185
|
+
unless gvks
|
|
186
|
+
next
|
|
187
|
+
end
|
|
220
188
|
|
|
221
189
|
gvks.each do |gvk|
|
|
222
190
|
group = gvk["group"].to_s
|
|
223
191
|
version = gvk["version"]
|
|
224
192
|
kind = gvk["kind"]
|
|
225
|
-
|
|
193
|
+
if group.empty?
|
|
194
|
+
api_version = version
|
|
195
|
+
else
|
|
196
|
+
api_version = "#{group}/#{version}"
|
|
197
|
+
end
|
|
226
198
|
|
|
227
199
|
index[kind] << {
|
|
228
200
|
definition_key: key,
|
|
229
|
-
group:
|
|
230
|
-
version:
|
|
231
|
-
kind:
|
|
232
|
-
defaults:
|
|
201
|
+
group: group,
|
|
202
|
+
version: version,
|
|
203
|
+
kind: kind,
|
|
204
|
+
defaults: {
|
|
233
205
|
"apiVersion" => api_version,
|
|
234
|
-
"kind"
|
|
235
|
-
}
|
|
206
|
+
"kind" => kind,
|
|
207
|
+
},
|
|
236
208
|
}
|
|
237
209
|
end
|
|
238
210
|
end
|
|
@@ -241,50 +213,66 @@ module Kube
|
|
|
241
213
|
end
|
|
242
214
|
end
|
|
243
215
|
|
|
244
|
-
# Find a GVK entry by kind name
|
|
245
|
-
#
|
|
216
|
+
# Find a GVK entry by kind name, case-insensitively.
|
|
217
|
+
# @parameter kind [String] the kind name to look up.
|
|
218
|
+
# @returns [Hash, nil] the first matching entry, if any.
|
|
246
219
|
def find_gvk_entry(kind)
|
|
247
220
|
entries = gvk_index[kind]
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
221
|
+
if entries && !entries.empty?
|
|
222
|
+
entries.first
|
|
223
|
+
else
|
|
224
|
+
match = gvk_index.find { |k, _v| k.downcase == kind.downcase }
|
|
225
|
+
match && match[1].first
|
|
252
226
|
end
|
|
253
|
-
|
|
254
|
-
nil
|
|
255
227
|
end
|
|
256
228
|
|
|
257
229
|
# Find a GVK entry by exact group, version, and kind.
|
|
258
|
-
#
|
|
230
|
+
# @parameter group [String] the API group, empty for core.
|
|
231
|
+
# @parameter version [String] the API version, e.g. "v1".
|
|
232
|
+
# @parameter kind [String] the kind name.
|
|
233
|
+
# @returns [Hash, nil] the matching entry, if any.
|
|
259
234
|
def find_gvk_entry_by_full_gvk(group, version, kind)
|
|
260
235
|
entries = gvk_index[kind]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
236
|
+
if entries.nil? || entries.empty?
|
|
237
|
+
nil
|
|
238
|
+
else
|
|
239
|
+
entries.find do |entry|
|
|
240
|
+
entry[:group] == group && entry[:version] == version
|
|
241
|
+
end
|
|
265
242
|
end
|
|
266
243
|
end
|
|
267
244
|
|
|
268
|
-
# Find a custom schema entry by kind
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
# Returns the { schema:, defaults: } hash or nil.
|
|
245
|
+
# Find a custom schema entry by kind, case-insensitively.
|
|
246
|
+
# @parameter kind [String] the kind name to look up.
|
|
247
|
+
# @returns [Hash, nil] the first-registered { entry:, defaults: } hash.
|
|
272
248
|
def find_custom_entry(kind)
|
|
273
249
|
registry = Schema.custom_schemas
|
|
274
|
-
registry.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
250
|
+
exact = registry.values.find { |v| v[:defaults]["kind"] == kind }
|
|
251
|
+
if exact
|
|
252
|
+
exact
|
|
253
|
+
else
|
|
254
|
+
registry.values.find { |v| v[:defaults]["kind"].casecmp?(kind) }
|
|
255
|
+
end
|
|
278
256
|
end
|
|
279
257
|
|
|
280
|
-
# Find a custom schema entry by
|
|
281
|
-
#
|
|
258
|
+
# Find a custom schema entry by group, version, and kind.
|
|
259
|
+
# @parameter group [String] the API group, empty for core.
|
|
260
|
+
# @parameter version [String] the API version, e.g. "v1".
|
|
261
|
+
# @parameter kind [String] the kind name.
|
|
262
|
+
# @returns [Hash, nil] the { entry:, defaults: } hash, if any.
|
|
282
263
|
def find_custom_entry_by_gvk(group, version, kind)
|
|
283
|
-
|
|
264
|
+
if group.empty?
|
|
265
|
+
api_version = version
|
|
266
|
+
else
|
|
267
|
+
api_version = "#{group}/#{version}"
|
|
268
|
+
end
|
|
284
269
|
Schema.custom_schemas["#{api_version}/#{kind}"]
|
|
285
270
|
end
|
|
286
271
|
|
|
287
|
-
# Build a Resource subclass from a
|
|
272
|
+
# Build a Resource subclass from a schema and defaults.
|
|
273
|
+
# @parameter schema_instance [JSONSchemer::Schema] the resource's schema.
|
|
274
|
+
# @parameter defaults [Hash] the frozen apiVersion/kind defaults.
|
|
275
|
+
# @returns [Class] a Resource subclass bound to the schema.
|
|
288
276
|
def build_resource_class(schema_instance, defaults)
|
|
289
277
|
Class.new(::Kube::Schema::Resource) do
|
|
290
278
|
@schema = schema_instance
|
|
@@ -311,38 +299,45 @@ module Kube
|
|
|
311
299
|
end
|
|
312
300
|
end
|
|
313
301
|
|
|
314
|
-
# Resolve a short name
|
|
315
|
-
#
|
|
302
|
+
# Resolve a short name to its full definition key.
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# find_definition_key("Container") # => "io.k8s.api.core.v1.Container"
|
|
316
306
|
#
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
# 2. Short-name match on last segment
|
|
320
|
-
# 3. Prefer stable API versions (v1 > v1beta1 > v1alpha1)
|
|
307
|
+
# @parameter name [String] a short name or a full definition key.
|
|
308
|
+
# @returns [String, nil] a full key, preferring stable API versions.
|
|
321
309
|
def find_definition_key(name)
|
|
322
|
-
definitions =
|
|
310
|
+
definitions = catalog.entries
|
|
323
311
|
|
|
324
312
|
# Exact full-key match
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
313
|
+
if definitions.key?(name)
|
|
314
|
+
name
|
|
315
|
+
else
|
|
316
|
+
candidates = definitions.keys.select { |k| k.split(".").last == name }
|
|
317
|
+
if candidates.empty?
|
|
318
|
+
nil
|
|
319
|
+
else
|
|
320
|
+
if candidates.size == 1
|
|
321
|
+
candidates.first
|
|
322
|
+
else
|
|
323
|
+
candidates.min_by { |k|
|
|
324
|
+
version_segment = k.split(".")[-2].to_s
|
|
325
|
+
case version_segment
|
|
326
|
+
when /\Av\d+\z/ then [0, version_segment]
|
|
327
|
+
when /beta/ then [1, version_segment]
|
|
328
|
+
when /alpha/ then [2, version_segment]
|
|
329
|
+
else [3, version_segment]
|
|
330
|
+
end
|
|
331
|
+
}
|
|
332
|
+
end
|
|
340
333
|
end
|
|
341
|
-
|
|
334
|
+
end
|
|
342
335
|
end
|
|
343
336
|
|
|
344
|
-
# Build a SubSpec subclass from a
|
|
345
|
-
#
|
|
337
|
+
# Build a SubSpec subclass from a schema and definition name.
|
|
338
|
+
# @parameter schema_instance [JSONSchemer::Schema] the definition's schema.
|
|
339
|
+
# @parameter definition_name [String] the name shown in error messages.
|
|
340
|
+
# @returns [Class] a SubSpec subclass bound to the schema.
|
|
346
341
|
def build_sub_spec_class(schema_instance, definition_name)
|
|
347
342
|
Class.new(::Kube::Schema::SubSpec) do
|
|
348
343
|
@schema = schema_instance
|
|
@@ -367,8 +362,8 @@ module Kube
|
|
|
367
362
|
end
|
|
368
363
|
end
|
|
369
364
|
|
|
370
|
-
#
|
|
371
|
-
#
|
|
365
|
+
# Invalidates cached resource classes after a schema registration.
|
|
366
|
+
# @returns [void] clears the resource class cache.
|
|
372
367
|
def clear_resource_cache!
|
|
373
368
|
@resource_classes.clear
|
|
374
369
|
end
|
|
@@ -376,266 +371,252 @@ module Kube
|
|
|
376
371
|
end
|
|
377
372
|
end
|
|
378
373
|
|
|
379
|
-
if __FILE__ == $0
|
|
380
|
-
require "bundler/setup"
|
|
381
|
-
require "rspec/autorun"
|
|
382
|
-
require "kube/schema"
|
|
383
374
|
|
|
384
|
-
|
|
385
|
-
subject(:instance) { described_class.new("1.34") }
|
|
375
|
+
__END__
|
|
386
376
|
|
|
387
|
-
|
|
388
|
-
it "stores the version" do
|
|
389
|
-
expect(instance.version).to eq("1.34")
|
|
390
|
-
end
|
|
377
|
+
require "kube/schema"
|
|
391
378
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
379
|
+
instance_class = Kube::Schema::Instance
|
|
380
|
+
instance = instance_class.new("1.34")
|
|
381
|
+
|
|
382
|
+
describe "Kube::Schema::Instance" do
|
|
383
|
+
describe "#initialize" do
|
|
384
|
+
it "stores the version" do
|
|
385
|
+
instance.version.should == "1.34"
|
|
395
386
|
end
|
|
396
387
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
expect(klass).to be < Kube::Schema::Resource
|
|
402
|
-
end
|
|
388
|
+
it "raises UnknownVersionError for a non-version string" do
|
|
389
|
+
lambda { instance_class.new("not-a-version") }.should.raise(Kube::UnknownVersionError)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
403
392
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
393
|
+
describe "#[]" do
|
|
394
|
+
it "returns a Class that subclasses Resource" do
|
|
395
|
+
klass = instance["Deployment"]
|
|
396
|
+
klass.should.be.kind_of Class
|
|
397
|
+
klass.should.be < Kube::Schema::Resource
|
|
398
|
+
end
|
|
409
399
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
400
|
+
it "caches resource classes by key" do
|
|
401
|
+
instance["Deployment"].should.be.identical_to instance["Deployment"]
|
|
402
|
+
end
|
|
413
403
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
end
|
|
404
|
+
it "raises for an unknown resource" do
|
|
405
|
+
error = lambda { instance["ThisDoesNotExist999"] }.should.raise(RuntimeError)
|
|
406
|
+
error.message.should.match(/No resource schema found/)
|
|
407
|
+
end
|
|
419
408
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
end
|
|
409
|
+
it "returns different classes for different resources" do
|
|
410
|
+
instance["Deployment"].should.not == instance["Service"]
|
|
411
|
+
end
|
|
424
412
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
expect(a).to be < Kube::Schema::Resource
|
|
429
|
-
expect(b).to be < Kube::Schema::Resource
|
|
430
|
-
end
|
|
413
|
+
it "attaches a schema to the resource class" do
|
|
414
|
+
instance["Deployment"].schema.should.not.be.nil
|
|
415
|
+
end
|
|
431
416
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
expect(resource.to_h[:apiVersion]).to eq("apps/v1")
|
|
437
|
-
expect(resource.to_h[:kind]).to eq("Deployment")
|
|
438
|
-
expect(resource.to_h[:metadata][:name]).to eq("test")
|
|
439
|
-
end
|
|
417
|
+
it "is case-insensitive" do
|
|
418
|
+
instance["Deployment"].should.be < Kube::Schema::Resource
|
|
419
|
+
instance["deployment"].should.be < Kube::Schema::Resource
|
|
420
|
+
end
|
|
440
421
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
422
|
+
it "produces resources that can be instantiated with the block DSL" do
|
|
423
|
+
resource = instance["Deployment"].new {
|
|
424
|
+
metadata.name = "test"
|
|
425
|
+
}
|
|
426
|
+
resource.to_h[:apiVersion].should == "apps/v1"
|
|
427
|
+
resource.to_h[:kind].should == "Deployment"
|
|
428
|
+
resource.to_h[:metadata][:name].should == "test"
|
|
429
|
+
end
|
|
445
430
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
end
|
|
431
|
+
it "attaches defaults (apiVersion and kind) to the resource class" do
|
|
432
|
+
instance["Deployment"].defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
|
|
433
|
+
end
|
|
450
434
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
expect(klass.defaults).to eq({ "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" })
|
|
454
|
-
end
|
|
435
|
+
it "derives correct apiVersion for core resources (empty group)" do
|
|
436
|
+
instance["Pod"].defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
|
|
455
437
|
end
|
|
456
438
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
expect(klass.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
|
|
462
|
-
end
|
|
439
|
+
it "derives correct apiVersion for grouped resources" do
|
|
440
|
+
instance["Ingress"].defaults.should == { "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" }
|
|
441
|
+
end
|
|
442
|
+
end
|
|
463
443
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
444
|
+
describe "#[] with full GVK string" do
|
|
445
|
+
it "resolves apps/v1/Deployment" do
|
|
446
|
+
klass = instance["apps/v1/Deployment"]
|
|
447
|
+
klass.should.be < Kube::Schema::Resource
|
|
448
|
+
klass.defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
|
|
449
|
+
end
|
|
469
450
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
451
|
+
it "resolves v1/Pod (core resource, empty group)" do
|
|
452
|
+
klass = instance["v1/Pod"]
|
|
453
|
+
klass.should.be < Kube::Schema::Resource
|
|
454
|
+
klass.defaults.should == { "apiVersion" => "v1", "kind" => "Pod" }
|
|
455
|
+
end
|
|
475
456
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
457
|
+
it "resolves networking.k8s.io/v1/Ingress" do
|
|
458
|
+
klass = instance["networking.k8s.io/v1/Ingress"]
|
|
459
|
+
klass.should.be < Kube::Schema::Resource
|
|
460
|
+
klass.defaults.should == { "apiVersion" => "networking.k8s.io/v1", "kind" => "Ingress" }
|
|
461
|
+
end
|
|
481
462
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
463
|
+
it "resolves kubevirt.io/v1/VirtualMachine" do
|
|
464
|
+
klass = instance["kubevirt.io/v1/VirtualMachine"]
|
|
465
|
+
klass.should.be < Kube::Schema::Resource
|
|
466
|
+
klass.defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" }
|
|
467
|
+
end
|
|
485
468
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
469
|
+
it "raises for invalid GVK format (too many slashes)" do
|
|
470
|
+
error = lambda { instance["a/b/c/d"] }.should.raise(RuntimeError)
|
|
471
|
+
error.message.should.match(/Invalid GVK format/)
|
|
472
|
+
end
|
|
489
473
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
expect(a).to be(b)
|
|
494
|
-
end
|
|
474
|
+
it "raises for non-existent GVK" do
|
|
475
|
+
error = lambda { instance["fake.io/v99/Blah"] }.should.raise(RuntimeError)
|
|
476
|
+
error.message.should.match(/No resource schema found/)
|
|
495
477
|
end
|
|
496
478
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
kinds = instance.list_resources
|
|
500
|
-
expect(kinds).to be_an(Array)
|
|
501
|
-
expect(kinds).not_to be_empty
|
|
502
|
-
expect(kinds).to include("Deployment", "Service", "Namespace", "Pod")
|
|
503
|
-
expect(kinds).to eq(kinds.sort)
|
|
504
|
-
end
|
|
479
|
+
it "caches GVK lookups" do
|
|
480
|
+
instance["apps/v1/Deployment"].should.be.identical_to instance["apps/v1/Deployment"]
|
|
505
481
|
end
|
|
482
|
+
end
|
|
506
483
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
484
|
+
describe "#list_resources" do
|
|
485
|
+
it "returns a sorted array of kind strings" do
|
|
486
|
+
kinds = instance.list_resources
|
|
487
|
+
kinds.should.be.kind_of Array
|
|
488
|
+
kinds.should.not.be.empty
|
|
489
|
+
["Deployment", "Service", "Namespace", "Pod"].each { |kind| kinds.should.include kind }
|
|
490
|
+
kinds.should == kinds.sort
|
|
491
|
+
end
|
|
492
|
+
end
|
|
514
493
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
494
|
+
describe "#api_groups" do
|
|
495
|
+
it "returns a sorted array of group names including the core group" do
|
|
496
|
+
groups = instance.api_groups
|
|
497
|
+
groups.should.be.kind_of Array
|
|
498
|
+
groups.should == groups.sort
|
|
499
|
+
groups.should.include ""
|
|
500
|
+
end
|
|
518
501
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
502
|
+
it "includes built-in groups, including ones without dots" do
|
|
503
|
+
["apps", "batch", "rbac.authorization.k8s.io"].each { |group| instance.api_groups.should.include group }
|
|
504
|
+
end
|
|
522
505
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
schema: { "type" => "object" },
|
|
526
|
-
api_version: "tradeportal.ai/v1")
|
|
527
|
-
expect(instance.api_groups).to include("tradeportal.ai")
|
|
528
|
-
ensure
|
|
529
|
-
Kube::Schema.reset_custom_schemas!
|
|
530
|
-
end
|
|
506
|
+
it "includes groups from the merged CRD definition bundles" do
|
|
507
|
+
["kubevirt.io", "argoproj.io"].each { |group| instance.api_groups.should.include group }
|
|
531
508
|
end
|
|
532
509
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
510
|
+
it "includes groups from registered custom schemas" do
|
|
511
|
+
Kube::Schema.register("TenantVmClaim",
|
|
512
|
+
schema: { "type" => "object" },
|
|
513
|
+
api_version: "tradeportal.ai/v1")
|
|
514
|
+
instance.api_groups.should.include "tradeportal.ai"
|
|
515
|
+
ensure
|
|
516
|
+
Kube::Schema.reset_custom_schemas!
|
|
517
|
+
end
|
|
518
|
+
end
|
|
539
519
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
520
|
+
describe "KubeVirt schemas" do
|
|
521
|
+
it "resolves VirtualMachine" do
|
|
522
|
+
klass = instance["VirtualMachine"]
|
|
523
|
+
klass.should.be.kind_of Class
|
|
524
|
+
klass.should.be < Kube::Schema::Resource
|
|
525
|
+
end
|
|
544
526
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
527
|
+
it "has correct defaults for VirtualMachine" do
|
|
528
|
+
instance["VirtualMachine"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachine" }
|
|
529
|
+
end
|
|
548
530
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
end
|
|
531
|
+
it "includes VirtualMachine in list_resources" do
|
|
532
|
+
instance.list_resources.should.include "VirtualMachine"
|
|
533
|
+
end
|
|
553
534
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
end
|
|
535
|
+
it "resolves VirtualMachineInstance" do
|
|
536
|
+
instance["VirtualMachineInstance"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "VirtualMachineInstance" }
|
|
537
|
+
end
|
|
558
538
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
expect(resource.to_h[:apiVersion]).to eq("kubevirt.io/v1")
|
|
564
|
-
expect(resource.to_h[:kind]).to eq("VirtualMachine")
|
|
565
|
-
expect(resource.to_h[:metadata][:name]).to eq("test-vm")
|
|
566
|
-
end
|
|
539
|
+
it "resolves KubeVirt" do
|
|
540
|
+
instance["KubeVirt"].defaults.should == { "apiVersion" => "kubevirt.io/v1", "kind" => "KubeVirt" }
|
|
541
|
+
end
|
|
567
542
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
spec.template.spec.domain.memory = { guest: "1Gi" }
|
|
576
|
-
spec.template.spec.domain.resources = { requests: { memory: "1Gi" } }
|
|
577
|
-
spec.template.spec.volumes = [
|
|
578
|
-
{ name: "rootdisk", containerDisk: { image: "registry.example.com/vm:latest" } }
|
|
579
|
-
]
|
|
580
|
-
}
|
|
581
|
-
expect { resource.to_yaml }.not_to raise_error
|
|
582
|
-
end
|
|
543
|
+
it "can instantiate a VirtualMachine with the block DSL" do
|
|
544
|
+
resource = instance["VirtualMachine"].new {
|
|
545
|
+
metadata.name = "test-vm"
|
|
546
|
+
}
|
|
547
|
+
resource.to_h[:apiVersion].should == "kubevirt.io/v1"
|
|
548
|
+
resource.to_h[:kind].should == "VirtualMachine"
|
|
549
|
+
resource.to_h[:metadata][:name].should == "test-vm"
|
|
583
550
|
end
|
|
584
551
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
552
|
+
it "validates a VirtualMachine without $ref resolution errors" do
|
|
553
|
+
resource = instance["VirtualMachine"].new {
|
|
554
|
+
metadata.name = "test-vm"
|
|
555
|
+
spec.runStrategy = "Always"
|
|
556
|
+
spec.template.metadata.labels = { "kubevirt.io/vm": "test-vm" }
|
|
557
|
+
spec.template.spec.domain.cpu = { cores: 1 }
|
|
558
|
+
spec.template.spec.domain.devices = {}
|
|
559
|
+
spec.template.spec.domain.memory = { guest: "1Gi" }
|
|
560
|
+
spec.template.spec.domain.resources = { requests: { memory: "1Gi" } }
|
|
561
|
+
spec.template.spec.volumes = [
|
|
562
|
+
{ name: "rootdisk", containerDisk: { image: "registry.example.com/vm:latest" } }
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
lambda { resource.to_yaml }.should.not.raise(StandardError)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
591
568
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
569
|
+
describe "CloudnativePG schemas" do
|
|
570
|
+
it "resolves Cluster by kind" do
|
|
571
|
+
klass = instance["Cluster"]
|
|
572
|
+
klass.should.be < Kube::Schema::Resource
|
|
573
|
+
klass.defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" }
|
|
574
|
+
end
|
|
597
575
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
576
|
+
it "resolves Cluster by full GVK string" do
|
|
577
|
+
klass = instance["postgresql.cnpg.io/v1/Cluster"]
|
|
578
|
+
klass.should.be < Kube::Schema::Resource
|
|
579
|
+
klass.defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Cluster" }
|
|
580
|
+
end
|
|
601
581
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
end
|
|
582
|
+
it "includes Cluster in list_resources" do
|
|
583
|
+
instance.list_resources.should.include "Cluster"
|
|
584
|
+
end
|
|
606
585
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
end
|
|
586
|
+
it "resolves Backup" do
|
|
587
|
+
instance["postgresql.cnpg.io/v1/Backup"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Backup" }
|
|
588
|
+
end
|
|
611
589
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
end
|
|
590
|
+
it "resolves ScheduledBackup" do
|
|
591
|
+
instance["postgresql.cnpg.io/v1/ScheduledBackup"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "ScheduledBackup" }
|
|
592
|
+
end
|
|
616
593
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
metadata.name = "pg-cluster"
|
|
620
|
-
metadata.namespace = "databases"
|
|
621
|
-
spec.instances = 3
|
|
622
|
-
spec.storage.size = "10Gi"
|
|
623
|
-
}
|
|
624
|
-
expect(resource.to_h[:apiVersion]).to eq("postgresql.cnpg.io/v1")
|
|
625
|
-
expect(resource.to_h[:kind]).to eq("Cluster")
|
|
626
|
-
expect(resource.to_h[:metadata][:name]).to eq("pg-cluster")
|
|
627
|
-
expect(resource.to_h[:spec][:instances]).to eq(3)
|
|
628
|
-
end
|
|
594
|
+
it "resolves Pooler" do
|
|
595
|
+
instance["postgresql.cnpg.io/v1/Pooler"].defaults.should == { "apiVersion" => "postgresql.cnpg.io/v1", "kind" => "Pooler" }
|
|
629
596
|
end
|
|
630
597
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
598
|
+
it "can instantiate a Cluster with the block DSL" do
|
|
599
|
+
resource = instance["postgresql.cnpg.io/v1/Cluster"].new {
|
|
600
|
+
metadata.name = "pg-cluster"
|
|
601
|
+
metadata.namespace = "databases"
|
|
602
|
+
spec.instances = 3
|
|
603
|
+
spec.storage.size = "10Gi"
|
|
604
|
+
}
|
|
605
|
+
resource.to_h[:apiVersion].should == "postgresql.cnpg.io/v1"
|
|
606
|
+
resource.to_h[:kind].should == "Cluster"
|
|
607
|
+
resource.to_h[:metadata][:name].should == "pg-cluster"
|
|
608
|
+
resource.to_h[:spec][:instances].should == 3
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
describe "class-level catalog cache" do
|
|
613
|
+
it "shares the catalog across instances of the same version" do
|
|
614
|
+
a = instance_class.new("1.34")
|
|
615
|
+
b = instance_class.new("1.34")
|
|
616
|
+
|
|
617
|
+
a["Deployment"].should.be < Kube::Schema::Resource
|
|
618
|
+
b["Deployment"].should.be < Kube::Schema::Resource
|
|
619
|
+
instance_class.catalogs["1.34"].should.be.kind_of Kube::Schema::Lazy::Index
|
|
639
620
|
end
|
|
640
621
|
end
|
|
641
622
|
end
|