chef-dk 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/lib/chef-dk/builtin_commands.rb +7 -0
  4. data/lib/chef-dk/command/env.rb +90 -0
  5. data/lib/chef-dk/command/export.rb +22 -1
  6. data/lib/chef-dk/command/generate.rb +1 -1
  7. data/lib/chef-dk/command/provision.rb +43 -0
  8. data/lib/chef-dk/command/push_archive.rb +126 -0
  9. data/lib/chef-dk/command/show_policy.rb +166 -0
  10. data/lib/chef-dk/command/verify.rb +58 -1
  11. data/lib/chef-dk/cookbook_omnifetch.rb +3 -2
  12. data/lib/chef-dk/exceptions.rb +27 -0
  13. data/lib/chef-dk/helpers.rb +29 -0
  14. data/lib/chef-dk/policyfile/chef_repo_cookbook_source.rb +8 -0
  15. data/lib/chef-dk/policyfile/chef_server_cookbook_source.rb +8 -0
  16. data/lib/chef-dk/policyfile/community_cookbook_source.rb +8 -0
  17. data/lib/chef-dk/policyfile/cookbook_locks.rb +76 -6
  18. data/lib/chef-dk/policyfile/dsl.rb +10 -5
  19. data/lib/chef-dk/policyfile/lister.rb +230 -0
  20. data/lib/chef-dk/policyfile/null_cookbook_source.rb +8 -0
  21. data/lib/chef-dk/policyfile_compiler.rb +35 -2
  22. data/lib/chef-dk/policyfile_lock.rb +43 -0
  23. data/lib/chef-dk/policyfile_services/clean_policies.rb +94 -0
  24. data/lib/chef-dk/policyfile_services/export_repo.rb +103 -16
  25. data/lib/chef-dk/policyfile_services/push_archive.rb +173 -0
  26. data/lib/chef-dk/policyfile_services/show_policy.rb +237 -0
  27. data/lib/chef-dk/service_exceptions.rb +21 -0
  28. data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +1 -0
  29. data/lib/chef-dk/skeletons/code_generator/files/default/repo/README.md +2 -40
  30. data/lib/chef-dk/skeletons/code_generator/recipes/app.rb +0 -2
  31. data/lib/chef-dk/skeletons/code_generator/templates/default/kitchen.yml.erb +2 -2
  32. data/lib/chef-dk/skeletons/code_generator/templates/default/recipe_spec.rb.erb +1 -1
  33. data/lib/chef-dk/version.rb +1 -1
  34. data/spec/unit/command/env_spec.rb +52 -0
  35. data/spec/unit/command/exec_spec.rb +2 -2
  36. data/spec/unit/command/export_spec.rb +13 -0
  37. data/spec/unit/command/provision_spec.rb +56 -0
  38. data/spec/unit/command/push_archive_spec.rb +153 -0
  39. data/spec/unit/command/show_policy_spec.rb +235 -0
  40. data/spec/unit/command/verify_spec.rb +1 -0
  41. data/spec/unit/helpers_spec.rb +68 -0
  42. data/spec/unit/policyfile/cookbook_locks_spec.rb +107 -1
  43. data/spec/unit/policyfile/lister_spec.rb +256 -0
  44. data/spec/unit/policyfile_demands_spec.rb +202 -10
  45. data/spec/unit/policyfile_evaluation_spec.rb +30 -4
  46. data/spec/unit/policyfile_lock_serialization_spec.rb +45 -0
  47. data/spec/unit/policyfile_services/clean_policies_spec.rb +236 -0
  48. data/spec/unit/policyfile_services/export_repo_spec.rb +99 -6
  49. data/spec/unit/policyfile_services/push_archive_spec.rb +345 -0
  50. data/spec/unit/policyfile_services/show_policy_spec.rb +839 -0
  51. metadata +139 -8
@@ -40,10 +40,13 @@ describe ChefDK::PolicyfileServices::ExportRepo do
40
40
 
41
41
  let(:force_export) { false }
42
42
 
43
+ let(:archive) { false }
44
+
43
45
  subject(:export_service) do
44
46
  described_class.new(policyfile: policyfile_rb_explicit_name,
45
47
  root_dir: working_dir,
46
48
  export_dir: export_dir,
49
+ archive: archive,
47
50
  force: force_export)
48
51
  end
49
52
 
@@ -154,6 +157,22 @@ E
154
157
  expect(export_service.policy_name).to eq("install-example")
155
158
  end
156
159
 
160
+ context "when using archive mode" do
161
+
162
+ let(:archive) { true }
163
+
164
+ # TODO: also support a full file name
165
+ context "when the given 'export_dir' is a directory" do
166
+
167
+ it "sets the archive file location to $policy_name-$revision.tgz" do
168
+ expected = File.join(export_dir, "install-example-60e5ad638dce219d8f87d589463ec4a9884007ba5e2adbb4c0a7021d67204f1a.tgz")
169
+ expect(export_service.archive_file_location).to eq(expected)
170
+ end
171
+
172
+ end
173
+
174
+ end
175
+
157
176
  describe "writing updates to the policyfile lock" do
158
177
 
159
178
  let(:updated_lockfile_io) { StringIO.new }
@@ -169,7 +188,7 @@ E
169
188
 
170
189
  context "copying the cookbooks to the export dir" do
171
190
 
172
- context "when the export dir is empty" do
191
+ shared_examples_for "successful_export" do
173
192
  before do
174
193
  allow(export_service.policyfile_lock).to receive(:validate_cookbooks!).and_return(true)
175
194
  export_service.run
@@ -220,6 +239,23 @@ E
220
239
  expect(data_item_json["id"]).to eq("install-example-local")
221
240
  end
222
241
 
242
+ it "copies the policyfile lock in standard format to Policyfile.lock.json" do
243
+ policyfile_lock_path = File.join(export_dir, "Policyfile.lock.json")
244
+ policyfile_lock_data = FFI_Yajl::Parser.parse(IO.read(policyfile_lock_path))
245
+ expected_lock_data = export_service.policyfile_lock.to_lock
246
+
247
+ # stringify keys in source_options
248
+ path = expected_lock_data["cookbook_locks"]["local-cookbook"]["source_options"].delete(:path)
249
+ expected_lock_data["cookbook_locks"]["local-cookbook"]["source_options"]["path"] = path
250
+
251
+ expect(policyfile_lock_data).to eq(expected_lock_data)
252
+ end
253
+
254
+ end
255
+
256
+ context "when the export dir is empty" do
257
+
258
+ include_examples "successful_export"
223
259
  end
224
260
 
225
261
  context "When an error occurs creating the export" do
@@ -253,14 +289,15 @@ E
253
289
  end
254
290
 
255
291
  it "ignores the non-conflicting content and exports" do
256
- export_service.run
257
-
258
292
  expect(File).to exist(file_in_export_dir)
259
293
  expect(File).to exist(extra_data_bag_item)
260
294
 
261
295
  expect(File).to be_directory(File.join(export_dir, "cookbooks"))
262
296
  expect(File).to be_directory(File.join(export_dir, "data_bags"))
263
297
  end
298
+
299
+ include_examples "successful_export"
300
+
264
301
  end
265
302
 
266
303
  context "When the export dir has conflicting content" do
@@ -275,6 +312,8 @@ E
275
312
 
276
313
  let(:extra_policyfile_data_item) { File.join(policyfiles_data_bag_dir, "leftover-policy.json") }
277
314
 
315
+ let(:conflicting_policyfile_lock) { File.join(export_dir, "Policyfile.lock.json") }
316
+
278
317
  before do
279
318
  FileUtils.mkdir_p(export_dir)
280
319
  FileUtils.mkdir_p(cookbooks_dir)
@@ -282,10 +321,11 @@ E
282
321
  File.open(non_conflicting_file_in_export_dir, "wb+") { |f| f.print "some random cruft" }
283
322
  File.open(file_in_cookbooks_dir, "wb+") { |f| f.print "some random cruft" }
284
323
  File.open(extra_policyfile_data_item, "wb+") { |f| f.print "some random cruft" }
324
+ File.open(conflicting_policyfile_lock, "wb+") { |f| f.print "some random cruft" }
285
325
  end
286
326
 
287
327
  it "raises a PolicyfileExportRepoError" do
288
- message = "Export dir (#{export_dir}) not clean. Refusing to export. (Conflicting files: #{file_in_cookbooks_dir}, #{extra_policyfile_data_item})"
328
+ message = "Export dir (#{export_dir}) not clean. Refusing to export. (Conflicting files: #{file_in_cookbooks_dir}, #{extra_policyfile_data_item}, #{conflicting_policyfile_lock})"
289
329
  expect { export_service.run }.to raise_error(ChefDK::ExportDirNotEmpty, message)
290
330
  expect(File).to exist(non_conflicting_file_in_export_dir)
291
331
  expect(File).to exist(file_in_cookbooks_dir)
@@ -310,9 +350,62 @@ E
310
350
 
311
351
  end
312
352
 
313
- end
353
+ end # When the export dir has conflicting content
314
354
 
315
- end
355
+ context "when archive mode is enabled" do
356
+
357
+ let(:archive) { true }
358
+
359
+ let(:expected_archive_path) do
360
+ File.join(export_dir, "install-example-60e5ad638dce219d8f87d589463ec4a9884007ba5e2adbb4c0a7021d67204f1a.tgz")
361
+ end
362
+
363
+ it "exports the repo as a tgz archive" do
364
+ expect(File).to exist(expected_archive_path)
365
+ end
366
+
367
+ include_examples "successful_export" do
368
+
369
+ # explode the tarball so the assertions can find the files
370
+ before do
371
+ Zlib::GzipReader.open(expected_archive_path) do |gz_file|
372
+ tar = Archive::Tar::Minitar::Input.new(gz_file)
373
+ tar.each do |e|
374
+ tar.extract_entry(export_dir, e)
375
+ end
376
+ end
377
+ end
378
+
379
+ end
380
+
381
+ context "when the target dir has a cookbooks or data_bags dir" do
382
+
383
+ let(:cookbooks_dir) { File.join(export_dir, "cookbooks") }
384
+
385
+ let(:file_in_cookbooks_dir) { File.join(cookbooks_dir, "some_random_cruft") }
386
+
387
+ let(:policyfiles_data_bag_dir) { File.join(export_dir, "data_bags", "policyfiles") }
388
+
389
+ let(:extra_policyfile_data_item) { File.join(policyfiles_data_bag_dir, "leftover-policy.json") }
390
+
391
+ before do
392
+ FileUtils.mkdir_p(export_dir)
393
+ FileUtils.mkdir_p(cookbooks_dir)
394
+ FileUtils.mkdir_p(policyfiles_data_bag_dir)
395
+ File.open(file_in_cookbooks_dir, "wb+") { |f| f.print "some random cruft" }
396
+ File.open(extra_policyfile_data_item, "wb+") { |f| f.print "some random cruft" }
397
+ end
398
+
399
+ it "exports successfully" do
400
+ expect { export_service.run }.to_not raise_error
401
+ expect(File).to exist(expected_archive_path)
402
+ end
403
+
404
+ end
405
+
406
+ end # when archive mode is enabled
407
+
408
+ end # copying the cookbooks to the export dir
316
409
  end
317
410
 
318
411
  end
@@ -0,0 +1,345 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2015 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'spec_helper'
19
+ require 'chef-dk/policyfile_services/push_archive'
20
+
21
+ describe ChefDK::PolicyfileServices::PushArchive do
22
+
23
+ FileToTar = Struct.new(:name, :content)
24
+
25
+ def create_archive
26
+ Zlib::GzipWriter.open(archive_file_path) do |gz_file|
27
+ Archive::Tar::Minitar::Writer.open(gz_file) do |tar|
28
+
29
+
30
+ archive_dirs.each do |dir|
31
+ tar.mkdir(dir, mode: 0755)
32
+ end
33
+
34
+ archive_files.each do |file|
35
+ name = file.name
36
+ content = file.content
37
+ size = content.bytesize
38
+ tar.add_file_simple(name, mode: 0644, size: size) { |f| f.write(content) }
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+
45
+ let(:valid_lockfile) do
46
+ <<-E
47
+ {
48
+ "name": "install-example",
49
+ "run_list": [
50
+ "recipe[local-cookbook::default]"
51
+ ],
52
+ "cookbook_locks": {
53
+ "local-cookbook": {
54
+ "version": "2.3.4",
55
+ "identifier": "fab501cfaf747901bd82c1bc706beae7dc3a350c",
56
+ "dotted_decimal_identifier": "70567763561641081.489844270461035.258281553147148",
57
+ "source": "project-cookbooks/local-cookbook",
58
+ "cache_key": null,
59
+ "scm_info": null,
60
+ "source_options": {
61
+ "path": "project-cookbooks/local-cookbook"
62
+ }
63
+ }
64
+ },
65
+ "default_attributes": {},
66
+ "override_attributes": {},
67
+ "solution_dependencies": {
68
+ "Policyfile": [
69
+ [
70
+ "local-cookbook",
71
+ ">= 0.0.0"
72
+ ]
73
+ ],
74
+ "dependencies": {
75
+ "local-cookbook (2.3.4)": [
76
+
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ E
82
+ end
83
+
84
+ let(:archive_files) { [] }
85
+
86
+ let(:archive_dirs) { [] }
87
+
88
+ let(:working_dir) do
89
+ path = File.join(tempdir, "policyfile_services_test_working_dir")
90
+ Dir.mkdir(path)
91
+ path
92
+ end
93
+
94
+ let(:archive_file_name) { "example-policy-abc123.tgz" }
95
+
96
+ let(:archive_file_path) { File.join(working_dir, archive_file_name) }
97
+
98
+ let(:policy_group) { "dev-cluster-1" }
99
+
100
+ let(:config) do
101
+ double("Chef::Config",
102
+ chef_server_url: "https://localhost:10443",
103
+ client_key: "/path/to/client/key.pem",
104
+ node_name: "deuce",
105
+ policy_document_native_api: true)
106
+ end
107
+
108
+ let(:ui) { TestHelpers::TestUI.new }
109
+
110
+ subject(:push_archive_service) do
111
+ described_class.new(archive_file: archive_file_name,
112
+ policy_group: policy_group,
113
+ root_dir: working_dir,
114
+ ui: ui,
115
+ config: config)
116
+ end
117
+
118
+ it "has an archive file" do
119
+ expect(push_archive_service.archive_file).to eq(archive_file_name)
120
+ expect(push_archive_service.archive_file_path).to eq(archive_file_path)
121
+ end
122
+
123
+ it "configures an HTTP client" do
124
+ expect(ChefDK::AuthenticatedHTTP).to receive(:new).with("https://localhost:10443",
125
+ signing_key_filename: "/path/to/client/key.pem",
126
+ client_name: "deuce")
127
+ push_archive_service.http_client
128
+ end
129
+
130
+ context "with an invalid archive" do
131
+
132
+ let(:exception) do
133
+ begin
134
+ push_archive_service.run
135
+ rescue ChefDK::PolicyfilePushArchiveError => e
136
+ e
137
+ else
138
+ nil
139
+ end
140
+ end
141
+
142
+ let(:exception_cause) { exception.cause }
143
+
144
+ context "when the archive is malformed/corrupted/etc" do
145
+
146
+ context "when the archive file doesn't exist" do
147
+
148
+ it "errors out" do
149
+ expect(exception).to_not be_nil
150
+ expect(exception.message).to eq("Failed to publish archived policy")
151
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
152
+ expect(exception_cause.message).to eq("Archive file #{archive_file_path} not found")
153
+ end
154
+ end
155
+
156
+ context "when the archive is not a gzip file" do
157
+
158
+ before do
159
+ FileUtils.touch(archive_file_path)
160
+ end
161
+
162
+ it "errors out" do
163
+ expect(exception).to_not be_nil
164
+ expect(exception.message).to eq("Failed to publish archived policy")
165
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
166
+ expect(exception_cause.message).to eq("Archive file #{archive_file_path} could not be unpacked. not in gzip format")
167
+ end
168
+
169
+ end
170
+
171
+ context "when the archive is a gzip file of a garbage file" do
172
+
173
+ before do
174
+ Zlib::GzipWriter.open(archive_file_path) do |gz_file|
175
+ gz_file << "lol this isn't a tar file"
176
+ end
177
+ end
178
+
179
+ it "errors out" do
180
+ expect(exception).to_not be_nil
181
+ expect(exception.message).to eq("Failed to publish archived policy")
182
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
183
+ expect(exception_cause.message).to eq("Archive file #{archive_file_path} could not be unpacked. Tar archive looks corrupt.")
184
+ end
185
+ end
186
+
187
+
188
+ context "when the archive is a gzip file of a very malformed tar archive" do
189
+
190
+ before do
191
+ Zlib::GzipWriter.open(archive_file_path) do |gz_file|
192
+ gz_file << "\0\0\0\0\0"
193
+ end
194
+ end
195
+
196
+ it "errors out" do
197
+ expect(exception).to_not be_nil
198
+ expect(exception.message).to eq("Failed to publish archived policy")
199
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
200
+ expect(exception_cause.message).to eq("Archive file #{archive_file_path} could not be unpacked. Tar archive looks corrupt.")
201
+ end
202
+ end
203
+ end
204
+
205
+ context "when the archive is well-formed but has invalid content" do
206
+
207
+ before do
208
+ create_archive
209
+ end
210
+
211
+ context "when the archive is missing Policyfile.lock.json" do
212
+
213
+ let(:archive_files) { [ FileToTar.new("empty.txt", "") ] }
214
+
215
+ it "errors out" do
216
+ expect(exception).to_not be_nil
217
+ expect(exception.message).to eq("Failed to publish archived policy")
218
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
219
+ expect(exception_cause.message).to eq("Archive does not contain a Policyfile.lock.json")
220
+ end
221
+
222
+ end
223
+
224
+ context "when the archive has no cookbooks/ directory" do
225
+
226
+ let(:archive_files) { [ FileToTar.new("Policyfile.lock.json", "") ] }
227
+
228
+ it "errors out" do
229
+ expect(exception).to_not be_nil
230
+ expect(exception.message).to eq("Failed to publish archived policy")
231
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
232
+ expect(exception_cause.message).to eq("Archive does not contain a cookbooks directory")
233
+ end
234
+
235
+ end
236
+
237
+ context "when the archive has the correct files but the lockfile is invalid" do
238
+
239
+ let(:archive_dirs) { ["cookbooks"] }
240
+
241
+ let(:archive_files) { [ FileToTar.new("Policyfile.lock.json", lockfile_content) ] }
242
+
243
+ context "when the lockfile has invalid JSON" do
244
+
245
+ let(:lockfile_content) { ":::" }
246
+
247
+ it "errors out" do
248
+ expect(exception).to_not be_nil
249
+ expect(exception.message).to eq("Failed to publish archived policy")
250
+ expect(exception_cause).to be_a(FFI_Yajl::ParseError)
251
+ end
252
+
253
+ end
254
+
255
+ context "when the lockfile is semantically invalid" do
256
+
257
+ let(:lockfile_content) { '{ }' }
258
+
259
+ it "errors out" do
260
+ expect(exception).to_not be_nil
261
+ expect(exception.message).to eq("Failed to publish archived policy")
262
+ expect(exception_cause).to be_a(ChefDK::InvalidLockfile)
263
+ end
264
+
265
+ end
266
+
267
+
268
+ context "when the archive does not have all the necessary cookbooks" do
269
+
270
+ let(:lockfile_content) { valid_lockfile }
271
+
272
+ it "errors out" do
273
+ expect(exception).to_not be_nil
274
+ expect(exception.message).to eq("Failed to publish archived policy")
275
+ expect(exception_cause).to be_a(ChefDK::InvalidPolicyArchive)
276
+
277
+ msg = "Archive does not have all cookbooks required by the Policyfile.lock. Missing cookbooks: 'local-cookbook'."
278
+ expect(exception_cause.message).to eq(msg)
279
+ end
280
+
281
+ end
282
+ end
283
+ end
284
+
285
+ end
286
+
287
+ context "with a valid archive" do
288
+
289
+ let(:lockfile_content) { valid_lockfile }
290
+
291
+ let(:cookbook_name) { "local-cookbook" }
292
+
293
+ let(:dotted_decimal_identifier) { "70567763561641081.489844270461035.258281553147148" }
294
+
295
+ let(:cookbook_dir) { File.join("cookbooks", "#{cookbook_name}-#{dotted_decimal_identifier}") }
296
+
297
+ let(:recipes_dir) { File.join(cookbook_dir, "recipes") }
298
+
299
+ let(:archive_dirs) { ["cookbooks", cookbook_dir, recipes_dir] }
300
+
301
+ let(:archive_files) do
302
+ [
303
+ FileToTar.new("Policyfile.lock.json", lockfile_content),
304
+ FileToTar.new(File.join(cookbook_dir, "metadata.rb"), "name 'local-cookbook'"),
305
+ FileToTar.new(File.join(recipes_dir, "default.rb"), "puts 'hello'")
306
+ ]
307
+ end
308
+
309
+ let(:http_client) { instance_double(ChefDK::AuthenticatedHTTP) }
310
+
311
+ let(:uploader) { instance_double(ChefDK::Policyfile::Uploader) }
312
+
313
+ before do
314
+ expect(push_archive_service).to receive(:http_client).and_return(http_client)
315
+
316
+ expect(ChefDK::Policyfile::Uploader).to receive(:new).
317
+ # TODO: need more verification that the policyfile.lock is right (?)
318
+ with(an_instance_of(ChefDK::PolicyfileLock), policy_group, http_client: http_client, ui: ui, policy_document_native_api: true).
319
+ and_return(uploader)
320
+
321
+ create_archive
322
+ end
323
+
324
+ describe "when the upload is successful" do
325
+
326
+ it "uploads the cookbooks and lockfile" do
327
+ expect(uploader).to receive(:upload)
328
+ push_archive_service.run
329
+ end
330
+
331
+ end
332
+
333
+ describe "when the upload fails" do
334
+
335
+ it "raises a nested error" do
336
+ expect(uploader).to receive(:upload).and_raise("an error")
337
+ expect { push_archive_service.run }.to raise_error(ChefDK::PolicyfilePushArchiveError)
338
+ end
339
+
340
+ end
341
+
342
+ end
343
+
344
+ end
345
+