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