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.
@@ -4,30 +4,26 @@ require "yaml"
4
4
 
5
5
  module Kube
6
6
  module Schema
7
- # A flat, ordered collection of Kubernetes resources.
8
- #
9
- # Manifest is Enumerable over Resource objects. It cannot be nested —
10
- # appending one Manifest into another extracts and flattens its resources.
7
+ # A flat, ordered, Enumerable collection of Kubernetes resources.
11
8
  #
9
+ # @example
12
10
  # manifest = Kube::Schema::Manifest.new
13
11
  # manifest << deployment_resource
14
12
  # manifest << another_manifest # flattened automatically
15
13
  # puts manifest.to_yaml # multi-document YAML output
16
14
  #
17
- # File I/O:
15
+ # @example File I/O
18
16
  # manifest = Kube::Schema::Manifest.open("cluster.yaml")
19
17
  # manifest << new_resource
20
18
  # manifest.write
21
- #
22
19
  class Manifest
23
20
  include Enumerable
24
21
 
25
22
  attr_reader :filename
26
23
 
27
24
  # Create a new Manifest, optionally seeded with resources.
28
- #
29
- # @param resources [Array<Resource, Manifest>] initial resources to include
30
- # @param filename [String, nil] optional filename for file-backed manifests
25
+ # @parameter resources [Array(Resource, Manifest)] initial resources to include.
26
+ # @parameter filename [String, nil] an optional filename for file-backed manifests.
31
27
  def initialize(*resources, filename: nil)
32
28
  @resources = []
33
29
  @filename = filename
@@ -35,14 +31,9 @@ module Kube
35
31
  resources.each { |r| self << r }
36
32
  end
37
33
 
38
- # Append a resource, manifest, or array of resources.
39
- #
40
- # - Resource objects are appended directly.
41
- # - Manifest objects are flattened — their resources are extracted.
42
- # - Arrays are iterated and each element is appended.
43
- #
44
- # @param item [Resource, Manifest, Array]
45
- # @return [self]
34
+ # Append a resource, manifest, or array of either.
35
+ # @parameter item [Resource, Manifest, Array] what to append.
36
+ # @returns [self] appended; a Manifest is flattened, an Array iterated.
46
37
  def <<(item)
47
38
  case item
48
39
  when Manifest
@@ -60,94 +51,86 @@ module Kube
60
51
  self
61
52
  end
62
53
 
63
- # @yield [Resource] each resource in insertion order
54
+ # @yields {|resource| ...} each resource in insertion order.
64
55
  def each(&block)
65
56
  @resources.each(&block)
66
57
  end
67
58
 
68
59
  # Number of resources in the manifest.
60
+ # @returns [Integer] the resource count.
69
61
  def size
70
62
  @resources.size
71
63
  end
72
64
  alias_method :length, :size
73
65
 
74
- # Returns the manifest as multi-document YAML, separated by "---".
75
- # This is the standard format for Kubernetes manifest files.
76
- #
77
- # @return [String]
66
+ # The manifest as "---"-separated multi-document YAML.
67
+ # @returns [String] the standard Kubernetes manifest file format.
78
68
  def to_yaml
79
69
  @resources.map { |r| r.to_yaml }.join("")
80
70
  end
81
71
 
82
72
  # Returns an array of resource hashes.
83
- #
84
- # @return [Array<Hash>]
73
+ # @returns [Array(Hash)] a copy of the resources, in order.
85
74
  def to_a
86
75
  @resources.dup
87
76
  end
88
77
 
89
- # Ruby pattern matching: `manifest => [Kube::Cluster["Deployment"], *]`.
78
+ # Supports pattern matching, e.g. `manifest => [resource, *]`.
79
+ # @returns [Array(Hash)] the same array as #to_a.
90
80
  def deconstruct = to_a
91
81
 
92
- # -------------------------------------------------------------------
93
- # File I/O
94
- # -------------------------------------------------------------------
82
+ # File I/O.
95
83
 
96
- # Parse a YAML string containing one or more Kubernetes resource
97
- # documents and return a Manifest populated with typed Resource objects.
98
- #
99
- # Each document's "kind" is resolved via Kube::Schema.parse to
100
- # produce the correct Resource subclass (e.g. Deployment, Service).
101
- # Documents without a recognized "kind" fall back to a bare Resource.
84
+ # Parse multi-document YAML into a Manifest of typed Resources.
102
85
  #
86
+ # @example
103
87
  # yaml = `helm template my-release bitnami/nginx`
104
88
  # manifest = Kube::Schema::Manifest.parse(yaml)
105
89
  # manifest.first.class #=> Kube::Schema::Resource (Deployment subclass)
106
90
  #
107
- # @param yaml_string [String] multi-document YAML
108
- # @return [Manifest]
91
+ # @parameter yaml_string [String] multi-document YAML.
92
+ # @returns [Manifest] the parsed resources.
109
93
  def self.parse(yaml_string)
110
- docs = if YAML.respond_to?(:safe_load_stream)
111
- YAML.safe_load_stream(yaml_string, permitted_classes: [Symbol])
112
- else
113
- YAML.load_stream(yaml_string)
114
- end
94
+ if YAML.respond_to?(:safe_load_stream)
95
+ docs = YAML.safe_load_stream(yaml_string, permitted_classes: [Symbol])
96
+ else
97
+ docs = YAML.load_stream(yaml_string)
98
+ end
115
99
 
116
100
  resources = docs.compact.map { |doc| parse_doc(doc) }
117
101
  new(*resources)
118
102
  end
119
103
 
120
- # Read a YAML file containing one or more Kubernetes resource documents
121
- # and return a Manifest populated with typed Resource objects.
104
+ # Read a YAML file into a Manifest of typed Resources.
122
105
  #
106
+ # @example
123
107
  # manifest = Kube::Schema::Manifest.open("deploy.yaml")
124
108
  # manifest.count #=> 3
125
109
  # manifest.filename #=> "deploy.yaml"
126
110
  #
127
- # @param path [String] path to a YAML file
128
- # @return [Manifest]
111
+ # @parameter path [String] path to a YAML file.
112
+ # @returns [Manifest] the parsed resources, with filename set.
129
113
  def self.open(path)
130
114
  contents = File.read(path)
131
- docs = if YAML.respond_to?(:safe_load_stream)
132
- YAML.safe_load_stream(contents, permitted_classes: [Symbol])
133
- else
134
- # Ruby < 3.1 fallback
135
- YAML.load_stream(contents)
136
- end
115
+ if YAML.respond_to?(:safe_load_stream)
116
+ docs = YAML.safe_load_stream(contents, permitted_classes: [Symbol])
117
+ else
118
+ docs = YAML.load_stream(contents)
119
+ end
137
120
 
138
121
  resources = docs.compact.map { |doc| parse_doc(doc) }
139
122
  new(*resources, filename: path)
140
123
  end
141
124
 
142
125
  # Write the manifest to a file as multi-document YAML.
143
- #
144
- # @param path [String, nil] destination path; defaults to the filename
145
- # the manifest was opened from.
146
- # @return [String] the path written to
147
- # @raise [ArgumentError] if no path is given and no filename is set
126
+ # @parameter path [String, nil] destination path, defaulting to #filename.
127
+ # @returns [String] the path written to.
128
+ # @raises [ArgumentError] if no path is given and no filename is set.
148
129
  def write(path = nil)
149
130
  path ||= @filename
150
- raise ArgumentError, "No filename set. Pass a path to #write or use Manifest.open." if path.nil?
131
+ if path.nil?
132
+ raise ArgumentError, "No filename set. Pass a path to #write or use Manifest.open."
133
+ end
151
134
 
152
135
  File.write(path, to_yaml)
153
136
  @filename = path
@@ -155,10 +138,9 @@ module Kube
155
138
  end
156
139
 
157
140
  # Parse a single YAML document hash into a typed Resource.
158
- #
159
- # @param doc [Hash] a parsed YAML document
160
- # @return [Resource]
161
- # @raise [RuntimeError] if the kind is not recognized
141
+ # @parameter doc [Hash] a parsed YAML document.
142
+ # @returns [Resource] the typed resource.
143
+ # @raises [RuntimeError] if the kind is not recognized.
162
144
  def self.parse_doc(doc)
163
145
  Kube::Schema.parse(doc)
164
146
  end
@@ -167,7 +149,8 @@ module Kube
167
149
  private
168
150
 
169
151
  # Deep-stringify keys for clean YAML output.
170
- # (Hash#to_yaml with symbol keys produces `:key:` which is ugly.)
152
+ # @parameter obj [Object] a Hash, Array, or scalar to stringify.
153
+ # @returns [Object] the same structure with string keys.
171
154
  def deep_stringify(obj)
172
155
  case obj
173
156
  when Hash
@@ -184,340 +167,318 @@ module Kube
184
167
  end
185
168
  end
186
169
 
187
- if __FILE__ == $0
188
- require "bundler/setup"
189
- require "rspec/autorun"
190
- require "kube/schema"
191
- require "tmpdir"
192
-
193
- RSpec.describe Kube::Schema::Manifest do
194
- let(:resource_a) { Kube::Schema["Deployment"].new }
195
- let(:resource_b) { Kube::Schema["Service"].new }
196
- let(:resource_c) { Kube::Schema["Namespace"].new }
197
-
198
- describe "#initialize" do
199
- it "creates an empty manifest with no arguments" do
200
- manifest = described_class.new
201
- expect(manifest.count).to eq(0)
202
- end
203
170
 
204
- it "accepts resources as arguments" do
205
- manifest = described_class.new(resource_a, resource_b)
206
- expect(manifest.count).to eq(2)
207
- end
171
+ __END__
208
172
 
209
- it "accepts a filename keyword argument" do
210
- manifest = described_class.new(filename: "/tmp/test.yaml")
211
- expect(manifest.filename).to eq("/tmp/test.yaml")
212
- end
213
-
214
- it "flattens manifests passed as arguments" do
215
- inner = described_class.new(resource_a, resource_b)
216
- outer = described_class.new(inner, resource_c)
217
- expect(outer.count).to eq(3)
218
- end
219
- end
220
-
221
- describe "#<<" do
222
- subject(:manifest) { described_class.new }
223
-
224
- it "appends a Resource" do
225
- manifest << resource_a
226
- expect(manifest.count).to eq(1)
227
- expect(manifest.first).to eq(resource_a)
228
- end
229
-
230
- it "returns self for chaining" do
231
- result = manifest << resource_a
232
- expect(result).to be(manifest)
233
- end
173
+ require "kube/schema"
174
+ require "tmpdir"
175
+ require "fileutils"
234
176
 
235
- it "flattens a Manifest (cannot be nested)" do
236
- other = described_class.new(resource_a, resource_b)
237
- manifest << other
238
- expect(manifest.count).to eq(2)
239
- expect(manifest.to_a).to contain_exactly(resource_a, resource_b)
240
- end
177
+ manifest_class = Kube::Schema::Manifest
178
+ resource_a = Kube::Schema["Deployment"].new
179
+ resource_b = Kube::Schema["Service"].new
180
+ resource_c = Kube::Schema["Namespace"].new
241
181
 
242
- it "flattens an Array of Resources" do
243
- manifest << [resource_a, resource_b]
244
- expect(manifest.count).to eq(2)
245
- end
182
+ describe "Kube::Schema::Manifest" do
183
+ describe "#initialize" do
184
+ it "creates an empty manifest with no arguments" do
185
+ manifest_class.new.count.should == 0
186
+ end
246
187
 
247
- it "raises ArgumentError for a Hash" do
248
- expect { manifest << { "kind" => "Pod" } }.to raise_error(ArgumentError, /Expected a Kube::Schema::Resource/)
249
- end
188
+ it "accepts resources as arguments" do
189
+ manifest_class.new(resource_a, resource_b).count.should == 2
190
+ end
250
191
 
251
- it "raises ArgumentError for a String" do
252
- expect { manifest << "not a resource" }.to raise_error(ArgumentError)
253
- end
192
+ it "accepts a filename keyword argument" do
193
+ manifest_class.new(filename: "/tmp/test.yaml").filename.should == "/tmp/test.yaml"
194
+ end
254
195
 
255
- it "supports chaining multiple appends" do
256
- manifest << resource_a << resource_b << resource_c
257
- expect(manifest.count).to eq(3)
258
- end
196
+ it "flattens manifests passed as arguments" do
197
+ inner = manifest_class.new(resource_a, resource_b)
198
+ manifest_class.new(inner, resource_c).count.should == 3
259
199
  end
200
+ end
260
201
 
261
- describe "Enumerable" do
262
- subject(:manifest) { described_class.new(resource_a, resource_b, resource_c) }
202
+ describe "#<<" do
203
+ it "appends a Resource" do
204
+ manifest = manifest_class.new
205
+ manifest << resource_a
206
+ manifest.count.should == 1
207
+ manifest.first.should == resource_a
208
+ end
263
209
 
264
- it "includes Enumerable" do
265
- expect(described_class).to include(Enumerable)
266
- end
210
+ it "returns self for chaining" do
211
+ manifest = manifest_class.new
212
+ (manifest << resource_a).should.be.identical_to manifest
213
+ end
267
214
 
268
- it "yields resources in insertion order via #each" do
269
- resources = []
270
- manifest.each { |r| resources << r }
271
- expect(resources).to eq([resource_a, resource_b, resource_c])
272
- end
215
+ it "flattens a Manifest (cannot be nested)" do
216
+ manifest = manifest_class.new
217
+ manifest << manifest_class.new(resource_a, resource_b)
218
+ manifest.count.should == 2
219
+ manifest.to_a.should == [resource_a, resource_b]
220
+ end
273
221
 
274
- it "supports #map" do
275
- kinds = manifest.map { |r| r.to_h[:kind] }
276
- expect(kinds).to eq(["Deployment", "Service", "Namespace"])
277
- end
222
+ it "flattens an Array of Resources" do
223
+ manifest = manifest_class.new
224
+ manifest << [resource_a, resource_b]
225
+ manifest.count.should == 2
226
+ end
278
227
 
279
- it "supports #select" do
280
- services = manifest.select { |r| r.to_h[:kind] == "Service" }
281
- expect(services.length).to eq(1)
282
- end
228
+ it "raises ArgumentError for a Hash" do
229
+ manifest = manifest_class.new
230
+ error = lambda { manifest << { "kind" => "Pod" } }.should.raise(ArgumentError)
231
+ error.message.should.match(/Expected a Kube::Schema::Resource/)
232
+ end
283
233
 
284
- it "supports #first and #last" do
285
- expect(manifest.first).to eq(resource_a)
286
- # Enumerable doesn't provide #last by default, but #to_a does
287
- expect(manifest.to_a.last).to eq(resource_c)
288
- end
234
+ it "raises ArgumentError for a String" do
235
+ manifest = manifest_class.new
236
+ lambda { manifest << "not a resource" }.should.raise(ArgumentError)
289
237
  end
290
238
 
291
- describe "#size / #length" do
292
- it "returns the number of resources" do
293
- manifest = described_class.new(resource_a, resource_b)
294
- expect(manifest.size).to eq(2)
295
- expect(manifest.length).to eq(2)
296
- end
239
+ it "supports chaining multiple appends" do
240
+ manifest = manifest_class.new
241
+ manifest << resource_a << resource_b << resource_c
242
+ manifest.count.should == 3
243
+ end
244
+ end
297
245
 
298
- it "returns 0 for an empty manifest" do
299
- manifest = described_class.new
300
- expect(manifest.size).to eq(0)
301
- end
246
+ describe "Enumerable" do
247
+ it "includes Enumerable" do
248
+ manifest_class.should.include Enumerable
302
249
  end
303
250
 
304
- describe "pattern matching" do
305
- it "destructures into an array of resources" do
306
- manifest = described_class.new(resource_a, resource_b)
307
- manifest => [Kube::Schema::Resource => first, *rest]
251
+ it "yields resources in insertion order via #each" do
252
+ resources = []
253
+ manifest_class.new(resource_a, resource_b, resource_c).each { |r| resources << r }
254
+ resources.should == [resource_a, resource_b, resource_c]
255
+ end
308
256
 
309
- expect(first).to eq(resource_a)
310
- expect(rest).to eq([resource_b])
311
- end
257
+ it "supports #map" do
258
+ manifest = manifest_class.new(resource_a, resource_b, resource_c)
259
+ manifest.map { |r| r.to_h[:kind] }.should == ["Deployment", "Service", "Namespace"]
260
+ end
312
261
 
313
- it "enforces arity" do
314
- manifest = described_class.new(resource_a, resource_b)
315
- expect { manifest => [Kube::Schema::Resource] }.to raise_error(NoMatchingPatternError)
316
- end
262
+ it "supports #select" do
263
+ manifest = manifest_class.new(resource_a, resource_b, resource_c)
264
+ manifest.select { |r| r.to_h[:kind] == "Service" }.length.should == 1
317
265
  end
318
266
 
319
- describe "#to_a" do
320
- it "returns a copy of the internal resources array" do
321
- manifest = described_class.new(resource_a, resource_b)
322
- arr = manifest.to_a
323
- expect(arr).to eq([resource_a, resource_b])
267
+ it "supports #first and #last" do
268
+ manifest = manifest_class.new(resource_a, resource_b, resource_c)
269
+ manifest.first.should == resource_a
270
+ # Enumerable doesn't provide #last by default, but #to_a does
271
+ manifest.to_a.last.should == resource_c
272
+ end
273
+ end
324
274
 
325
- # Verify it's a copy, not the internal array
326
- arr << resource_c
327
- expect(manifest.count).to eq(2)
328
- end
275
+ describe "#size / #length" do
276
+ it "returns the number of resources" do
277
+ manifest = manifest_class.new(resource_a, resource_b)
278
+ manifest.size.should == 2
279
+ manifest.length.should == 2
329
280
  end
330
281
 
331
- describe "#to_yaml" do
332
- it "returns multi-document YAML" do
333
- manifest = described_class.new(resource_a, resource_b)
334
- yaml = manifest.to_yaml
282
+ it "returns 0 for an empty manifest" do
283
+ manifest_class.new.size.should == 0
284
+ end
285
+ end
335
286
 
336
- expect(yaml).to include("---")
337
- expect(yaml).to include("kind: Deployment")
338
- expect(yaml).to include("kind: Service")
339
- end
287
+ describe "pattern matching" do
288
+ it "destructures into an array of resources" do
289
+ manifest = manifest_class.new(resource_a, resource_b)
290
+ manifest => [Kube::Schema::Resource => head, *rest]
340
291
 
341
- it "uses string keys (not symbol keys) in output" do
342
- manifest = described_class.new(resource_a)
343
- yaml = manifest.to_yaml
292
+ head.should == resource_a
293
+ rest.should == [resource_b]
294
+ end
344
295
 
345
- # Should NOT contain Ruby symbol syntax like `:kind:`
346
- expect(yaml).not_to match(/:\w+:/)
347
- expect(yaml).to include("kind: Deployment")
348
- expect(yaml).to include("apiVersion: apps/v1")
349
- end
296
+ it "enforces arity" do
297
+ manifest = manifest_class.new(resource_a, resource_b)
298
+ lambda { manifest => [Kube::Schema::Resource] }.should.raise(NoMatchingPatternError)
299
+ end
300
+ end
350
301
 
351
- it "returns empty string for an empty manifest" do
352
- manifest = described_class.new
353
- expect(manifest.to_yaml).to eq("")
354
- end
302
+ describe "#to_a" do
303
+ it "returns a copy of the internal resources array" do
304
+ manifest = manifest_class.new(resource_a, resource_b)
305
+ arr = manifest.to_a
306
+ arr.should == [resource_a, resource_b]
355
307
 
356
- it "produces parseable YAML that round-trips" do
357
- manifest = described_class.new(resource_a, resource_b)
358
- yaml_output = manifest.to_yaml
359
- docs = if YAML.respond_to?(:safe_load_stream)
360
- YAML.safe_load_stream(yaml_output)
361
- else
362
- YAML.load_stream(yaml_output)
363
- end
364
- expect(docs.length).to eq(2)
365
- expect(docs[0]["kind"]).to eq("Deployment")
366
- expect(docs[1]["kind"]).to eq("Service")
367
- end
308
+ # Verify it's a copy, not the internal array
309
+ arr << resource_c
310
+ manifest.count.should == 2
368
311
  end
312
+ end
369
313
 
370
- describe ".parse" do
371
- it "parses a single-document YAML string" do
372
- yaml = { "kind" => "Pod", "apiVersion" => "v1", "metadata" => { "name" => "test" } }.to_yaml
373
- manifest = described_class.parse(yaml)
314
+ describe "#to_yaml" do
315
+ it "returns multi-document YAML" do
316
+ yaml = manifest_class.new(resource_a, resource_b).to_yaml
374
317
 
375
- expect(manifest.count).to eq(1)
376
- expect(manifest.first).to be_a(Kube::Schema::Resource)
377
- expect(manifest.first.kind).to eq("Pod")
378
- end
318
+ yaml.should.include "---"
319
+ yaml.should.include "kind: Deployment"
320
+ yaml.should.include "kind: Service"
321
+ end
379
322
 
380
- it "parses a multi-document YAML string" do
381
- yaml = [
382
- { "kind" => "Deployment", "apiVersion" => "apps/v1" },
383
- { "kind" => "Service", "apiVersion" => "v1" }
384
- ].map(&:to_yaml).join("")
323
+ it "uses string keys (not symbol keys) in output" do
324
+ yaml = manifest_class.new(resource_a).to_yaml
385
325
 
386
- manifest = described_class.parse(yaml)
387
- expect(manifest.count).to eq(2)
388
- end
326
+ # Should NOT contain Ruby symbol syntax like `:kind:`
327
+ yaml.should.not.match(/:\w+:/)
328
+ yaml.should.include "kind: Deployment"
329
+ yaml.should.include "apiVersion: apps/v1"
330
+ end
389
331
 
390
- it "returns typed Resource subclasses" do
391
- yaml = { "kind" => "Deployment", "apiVersion" => "apps/v1", "metadata" => { "name" => "web" } }.to_yaml
392
- manifest = described_class.parse(yaml)
332
+ it "returns empty string for an empty manifest" do
333
+ manifest_class.new.to_yaml.should == ""
334
+ end
393
335
 
394
- resource = manifest.first
395
- expect(resource.class.defaults).to eq({ "apiVersion" => "apps/v1", "kind" => "Deployment" })
396
- expect(resource.kind).to eq("Deployment")
397
- end
336
+ it "produces parseable YAML that round-trips" do
337
+ yaml_output = manifest_class.new(resource_a, resource_b).to_yaml
338
+ docs = if YAML.respond_to?(:safe_load_stream)
339
+ YAML.safe_load_stream(yaml_output)
340
+ else
341
+ YAML.load_stream(yaml_output)
342
+ end
343
+ docs.length.should == 2
344
+ docs[0]["kind"].should == "Deployment"
345
+ docs[1]["kind"].should == "Service"
346
+ end
347
+ end
398
348
 
399
- it "skips nil documents (empty YAML docs)" do
400
- yaml = "---\nkind: Pod\napiVersion: v1\n---\n---\nkind: Service\napiVersion: v1\n"
401
- manifest = described_class.parse(yaml)
402
- expect(manifest.count).to eq(2)
403
- end
349
+ describe ".parse" do
350
+ it "parses a single-document YAML string" do
351
+ yaml = { "kind" => "Pod", "apiVersion" => "v1", "metadata" => { "name" => "test" } }.to_yaml
352
+ manifest = manifest_class.parse(yaml)
404
353
 
405
- it "raises for unknown kinds" do
406
- yaml = { "kind" => "UnknownCRD", "apiVersion" => "custom.io/v1" }.to_yaml
407
- expect { described_class.parse(yaml) }.to raise_error(RuntimeError)
408
- end
354
+ manifest.count.should == 1
355
+ manifest.first.should.be.kind_of Kube::Schema::Resource
356
+ manifest.first.kind.should == "Pod"
357
+ end
409
358
 
410
- it "does not set a filename" do
411
- yaml = { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml
412
- manifest = described_class.parse(yaml)
413
- expect(manifest.filename).to be_nil
414
- end
359
+ it "parses a multi-document YAML string" do
360
+ yaml = [
361
+ { "kind" => "Deployment", "apiVersion" => "apps/v1" },
362
+ { "kind" => "Service", "apiVersion" => "v1" }
363
+ ].map(&:to_yaml).join("")
415
364
 
416
- it "returns an empty manifest for empty YAML" do
417
- manifest = described_class.parse("---\n")
418
- expect(manifest.count).to eq(0)
419
- end
365
+ manifest_class.parse(yaml).count.should == 2
420
366
  end
421
367
 
422
- describe ".open" do
423
- let(:tmpdir) { Dir.mktmpdir("manifest_test") }
424
- let(:yaml_path) { File.join(tmpdir, "resources.yaml") }
368
+ it "returns typed Resource subclasses" do
369
+ yaml = { "kind" => "Deployment", "apiVersion" => "apps/v1", "metadata" => { "name" => "web" } }.to_yaml
370
+ resource = manifest_class.parse(yaml).first
425
371
 
426
- after { FileUtils.rm_rf(tmpdir) }
372
+ resource.class.defaults.should == { "apiVersion" => "apps/v1", "kind" => "Deployment" }
373
+ resource.kind.should == "Deployment"
374
+ end
427
375
 
428
- it "reads a single-document YAML file" do
429
- File.write(yaml_path, { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml)
376
+ it "skips nil documents (empty YAML docs)" do
377
+ yaml = "---\nkind: Pod\napiVersion: v1\n---\n---\nkind: Service\napiVersion: v1\n"
378
+ manifest_class.parse(yaml).count.should == 2
379
+ end
430
380
 
431
- manifest = described_class.open(yaml_path)
432
- expect(manifest.count).to eq(1)
433
- expect(manifest.first).to be_a(Kube::Schema::Resource)
434
- end
381
+ it "raises for unknown kinds" do
382
+ yaml = { "kind" => "UnknownCRD", "apiVersion" => "custom.io/v1" }.to_yaml
383
+ lambda { manifest_class.parse(yaml) }.should.raise(RuntimeError)
384
+ end
435
385
 
436
- it "reads a multi-document YAML file" do
437
- content = [
438
- { "kind" => "Deployment", "apiVersion" => "apps/v1" },
439
- { "kind" => "Service", "apiVersion" => "v1" }
440
- ].map(&:to_yaml).join("")
386
+ it "does not set a filename" do
387
+ yaml = { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml
388
+ manifest_class.parse(yaml).filename.should.be.nil
389
+ end
441
390
 
442
- File.write(yaml_path, content)
391
+ it "returns an empty manifest for empty YAML" do
392
+ manifest_class.parse("---\n").count.should == 0
393
+ end
394
+ end
443
395
 
444
- manifest = described_class.open(yaml_path)
445
- expect(manifest.count).to eq(2)
446
- end
396
+ describe ".open" do
397
+ before { @tmpdir = Dir.mktmpdir("manifest_test") }
398
+ after { FileUtils.rm_rf(@tmpdir) }
447
399
 
448
- it "sets the filename" do
449
- File.write(yaml_path, { "kind" => "Pod" }.to_yaml)
400
+ it "reads a single-document YAML file" do
401
+ path = File.join(@tmpdir, "resources.yaml")
402
+ File.write(path, { "kind" => "Pod", "apiVersion" => "v1" }.to_yaml)
450
403
 
451
- manifest = described_class.open(yaml_path)
452
- expect(manifest.filename).to eq(yaml_path)
453
- end
404
+ manifest = manifest_class.open(path)
405
+ manifest.count.should == 1
406
+ manifest.first.should.be.kind_of Kube::Schema::Resource
407
+ end
454
408
 
455
- it "skips nil documents (empty YAML docs)" do
456
- File.write(yaml_path, "---\nkind: Pod\n---\n---\nkind: Service\n")
409
+ it "reads a multi-document YAML file" do
410
+ path = File.join(@tmpdir, "resources.yaml")
411
+ File.write(path, [
412
+ { "kind" => "Deployment", "apiVersion" => "apps/v1" },
413
+ { "kind" => "Service", "apiVersion" => "v1" }
414
+ ].map(&:to_yaml).join(""))
457
415
 
458
- manifest = described_class.open(yaml_path)
459
- expect(manifest.count).to eq(2)
460
- end
416
+ manifest_class.open(path).count.should == 2
461
417
  end
462
418
 
463
- describe "#write" do
464
- let(:tmpdir) { Dir.mktmpdir("manifest_test") }
419
+ it "sets the filename" do
420
+ path = File.join(@tmpdir, "resources.yaml")
421
+ File.write(path, { "kind" => "Pod" }.to_yaml)
465
422
 
466
- after { FileUtils.rm_rf(tmpdir) }
423
+ manifest_class.open(path).filename.should == path
424
+ end
467
425
 
468
- it "writes to the given path" do
469
- path = File.join(tmpdir, "output.yaml")
470
- manifest = described_class.new(resource_a)
426
+ it "skips nil documents (empty YAML docs)" do
427
+ path = File.join(@tmpdir, "resources.yaml")
428
+ File.write(path, "---\nkind: Pod\n---\n---\nkind: Service\n")
471
429
 
472
- manifest.write(path)
473
- expect(File.exist?(path)).to be true
430
+ manifest_class.open(path).count.should == 2
431
+ end
432
+ end
474
433
 
475
- content = File.read(path)
476
- expect(content).to include("kind: Deployment")
477
- end
434
+ describe "#write" do
435
+ before { @tmpdir = Dir.mktmpdir("manifest_test") }
436
+ after { FileUtils.rm_rf(@tmpdir) }
478
437
 
479
- it "writes to the stored filename when no path is given" do
480
- path = File.join(tmpdir, "stored.yaml")
481
- manifest = described_class.new(resource_a, filename: path)
438
+ it "writes to the given path" do
439
+ path = File.join(@tmpdir, "output.yaml")
440
+ manifest_class.new(resource_a).write(path)
482
441
 
483
- manifest.write
484
- expect(File.exist?(path)).to be true
485
- end
442
+ File.exist?(path).should.be.true
443
+ File.read(path).should.include "kind: Deployment"
444
+ end
486
445
 
487
- it "updates the filename after writing to a new path" do
488
- path = File.join(tmpdir, "new.yaml")
489
- manifest = described_class.new(resource_a)
446
+ it "writes to the stored filename when no path is given" do
447
+ path = File.join(@tmpdir, "stored.yaml")
448
+ manifest_class.new(resource_a, filename: path).write
490
449
 
491
- manifest.write(path)
492
- expect(manifest.filename).to eq(path)
493
- end
450
+ File.exist?(path).should.be.true
451
+ end
494
452
 
495
- it "returns the path written to" do
496
- path = File.join(tmpdir, "result.yaml")
497
- manifest = described_class.new(resource_a)
453
+ it "updates the filename after writing to a new path" do
454
+ path = File.join(@tmpdir, "new.yaml")
455
+ manifest = manifest_class.new(resource_a)
498
456
 
499
- result = manifest.write(path)
500
- expect(result).to eq(path)
501
- end
457
+ manifest.write(path)
458
+ manifest.filename.should == path
459
+ end
502
460
 
503
- it "raises ArgumentError when no path is available" do
504
- manifest = described_class.new(resource_a)
505
- expect { manifest.write }.to raise_error(ArgumentError, /No filename set/)
506
- end
461
+ it "returns the path written to" do
462
+ path = File.join(@tmpdir, "result.yaml")
463
+ manifest_class.new(resource_a).write(path).should == path
464
+ end
507
465
 
508
- it "round-trips through write and open" do
509
- path = File.join(tmpdir, "roundtrip.yaml")
510
- original = described_class.new(resource_a, resource_b)
466
+ it "raises ArgumentError when no path is available" do
467
+ manifest = manifest_class.new(resource_a)
468
+ error = lambda { manifest.write }.should.raise(ArgumentError)
469
+ error.message.should.match(/No filename set/)
470
+ end
511
471
 
512
- original.write(path)
513
- loaded = described_class.open(path)
472
+ it "round-trips through write and open" do
473
+ path = File.join(@tmpdir, "roundtrip.yaml")
474
+ original = manifest_class.new(resource_a, resource_b)
514
475
 
515
- expect(loaded.count).to eq(original.count)
476
+ original.write(path)
477
+ manifest_class.open(path).count.should == original.count
516
478
 
517
- content = File.read(path)
518
- expect(content).to include("kind: Deployment")
519
- expect(content).to include("kind: Service")
520
- end
479
+ content = File.read(path)
480
+ content.should.include "kind: Deployment"
481
+ content.should.include "kind: Service"
521
482
  end
522
483
  end
523
484
  end