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
@@ -32,6 +32,14 @@ module ChefDK
32
32
  raise UnsupportedFeature, 'You must set a default_source in your Policyfile to download cookbooks without explicit sources'
33
33
  end
34
34
 
35
+ def null?
36
+ true
37
+ end
38
+
39
+ def desc
40
+ "null_cookbook_source"
41
+ end
42
+
35
43
  end
36
44
  end
37
45
  end
@@ -24,6 +24,7 @@ require 'chef-dk/policyfile/dsl'
24
24
  require 'chef-dk/policyfile_lock'
25
25
  require 'chef-dk/ui'
26
26
  require 'chef-dk/policyfile/reports/install'
27
+ require 'chef-dk/exceptions'
27
28
 
28
29
  module ChefDK
29
30
 
@@ -59,6 +60,8 @@ module ChefDK
59
60
  @dsl = Policyfile::DSL.new(storage_config)
60
61
  @artifact_server_cookbook_location_specs = {}
61
62
 
63
+ @merged_graph = nil
64
+
62
65
  @ui = ui || UI.null
63
66
  @install_report = Policyfile::Reports::Install.new(ui: @ui, policyfile_compiler: self)
64
67
  end
@@ -123,7 +126,11 @@ module ChefDK
123
126
  end
124
127
 
125
128
  def create_spec_for_cookbook(cookbook_name, version)
126
- source_options = default_source.source_options_for(cookbook_name, version)
129
+ matching_source = default_source.find { |s|
130
+ s.universe_graph.has_key?(cookbook_name)
131
+ }
132
+
133
+ source_options = matching_source.source_options_for(cookbook_name, version)
127
134
  spec = Policyfile::CookbookLocationSpecification.new(cookbook_name, "= #{version}", source_options, storage_config)
128
135
  @artifact_server_cookbook_location_specs[cookbook_name] = spec
129
136
  end
@@ -208,9 +215,21 @@ module ChefDK
208
215
  end
209
216
 
210
217
  def remote_artifacts_graph
211
- default_source.universe_graph
218
+ @merged_graph ||=
219
+ begin
220
+ conflicting_cb_names = []
221
+ merged = {}
222
+ default_source.each do |source|
223
+ merged.merge!(source.universe_graph) do |conflicting_cb_name, _old, _new|
224
+ conflicting_cb_names << conflicting_cb_name
225
+ end
226
+ end
227
+ handle_conflicting_cookbooks(conflicting_cb_names)
228
+ merged
229
+ end
212
230
  end
213
231
 
232
+
214
233
  def version_constraint_for(cookbook_name)
215
234
  if (cookbook_location_spec = cookbook_location_spec_for(cookbook_name)) and cookbook_location_spec.version_fixed?
216
235
  version = cookbook_location_spec.version
@@ -285,5 +304,19 @@ module ChefDK
285
304
  CookbookOmnifetch.storage_path
286
305
  end
287
306
 
307
+ def handle_conflicting_cookbooks(conflicting_cookbooks)
308
+ # ignore any cookbooks that have a source set.
309
+ cookbooks_wo_source = conflicting_cookbooks.select do |cookbook_name|
310
+ location_spec = cookbook_location_spec_for(cookbook_name)
311
+ location_spec.nil? || location_spec.source_options.empty?
312
+ end
313
+
314
+ if cookbooks_wo_source.empty?
315
+ nil
316
+ else
317
+ raise CookbookSourceConflict.new(cookbooks_wo_source, default_source)
318
+ end
319
+ end
320
+
288
321
  end
289
322
  end
@@ -261,6 +261,15 @@ module ChefDK
261
261
  self
262
262
  end
263
263
 
264
+ def build_from_archive(lock_data)
265
+ set_name_from_lock_data(lock_data)
266
+ set_run_list_from_lock_data(lock_data)
267
+ set_cookbook_locks_as_archives_from_lock_data(lock_data)
268
+ set_attributes_from_lock_data(lock_data)
269
+ set_solution_dependencies_from_lock_data(lock_data)
270
+ self
271
+ end
272
+
264
273
  def install_cookbooks
265
274
  # note: duplicates PolicyfileCompiler#ensure_cache_dir_exists
266
275
  ensure_cache_dir_exists
@@ -423,6 +432,22 @@ module ChefDK
423
432
  end
424
433
  end
425
434
 
435
+ def set_cookbook_locks_as_archives_from_lock_data(lock_data)
436
+ cookbook_lock_data = lock_data["cookbook_locks"]
437
+
438
+ if cookbook_lock_data.nil?
439
+ raise InvalidLockfile, "lockfile does not have a cookbook_locks attribute"
440
+ end
441
+
442
+ unless cookbook_lock_data.kind_of?(Hash)
443
+ raise InvalidLockfile, "lockfile's cookbook_locks attribute must be a Hash (JSON object). (got: #{cookbook_lock_data.inspect})"
444
+ end
445
+
446
+ lock_data["cookbook_locks"].each do |name, lock_info|
447
+ build_cookbook_lock_as_archive_from_lock_data(name, lock_info)
448
+ end
449
+ end
450
+
426
451
  def set_attributes_from_lock_data(lock_data)
427
452
  default_attr_data = lock_data["default_attributes"]
428
453
 
@@ -475,5 +500,23 @@ module ChefDK
475
500
  end
476
501
  end
477
502
 
503
+ def build_cookbook_lock_as_archive_from_lock_data(name, lock_info)
504
+ unless lock_info.kind_of?(Hash)
505
+ raise InvalidLockfile, "lockfile cookbook_locks entries must be a Hash (JSON object). (got: #{lock_info.inspect})"
506
+ end
507
+
508
+ if lock_info["cache_key"].nil?
509
+ local_cookbook = Policyfile::LocalCookbook.new(name, storage_config)
510
+ local_cookbook.build_from_lock_data(lock_info)
511
+ archived = Policyfile::ArchivedCookbook.new(local_cookbook, storage_config)
512
+ @cookbook_locks[name] = archived
513
+ else
514
+ cached_cookbook = Policyfile::CachedCookbook.new(name, storage_config)
515
+ cached_cookbook.build_from_lock_data(lock_info)
516
+ archived = Policyfile::ArchivedCookbook.new(cached_cookbook, storage_config)
517
+ @cookbook_locks[name] = archived
518
+ end
519
+ end
520
+
478
521
  end
479
522
  end
@@ -0,0 +1,94 @@
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 'chef-dk/service_exceptions'
19
+ require 'chef-dk/policyfile/lister'
20
+
21
+ module ChefDK
22
+ module PolicyfileServices
23
+ class CleanPolicies
24
+
25
+ Orphan = Struct.new(:policy_name, :revision_id)
26
+
27
+ attr_reader :chef_config
28
+ attr_reader :ui
29
+
30
+ def initialize(config: nil, ui: nil)
31
+ @chef_config = config
32
+ @ui = ui
33
+ end
34
+
35
+ def run
36
+ revisions_to_remove = orphaned_policies
37
+
38
+ if revisions_to_remove.empty?
39
+ ui.err("No policy revisions deleted")
40
+ return true
41
+ end
42
+
43
+ results = revisions_to_remove.map do |policy|
44
+ [ remove_policy(policy), policy ]
45
+ end
46
+
47
+ failures = results.select { |result, _policy| result.kind_of?(Exception) }
48
+
49
+ unless failures.empty?
50
+ details = failures.map do |result, policy|
51
+ "- #{policy.policy_name} (#{policy.revision_id}): #{result.class} #{result}"
52
+ end
53
+
54
+ message = "Failed to delete some policy revisions:\n" + details.join("\n") + "\n"
55
+
56
+ raise PolicyfileCleanError.new(message, nil)
57
+ end
58
+
59
+ true
60
+ end
61
+
62
+ def orphaned_policies
63
+ policy_lister.policies_by_name.keys.inject([]) do |orphans, policy_name|
64
+ orphans + policy_lister.orphaned_revisions(policy_name).map do |revision_id|
65
+ Orphan.new(policy_name, revision_id)
66
+ end
67
+ end
68
+ rescue => e
69
+ raise PolicyfileCleanError.new("Failed to list policies for cleaning.", e)
70
+ end
71
+
72
+ def policy_lister
73
+ @policy_lister ||= Policyfile::Lister.new(config: chef_config)
74
+ end
75
+
76
+ def http_client
77
+ @http_client ||= ChefDK::AuthenticatedHTTP.new(config.chef_server_url,
78
+ signing_key_filename: config.client_key,
79
+ client_name: config.node_name)
80
+ end
81
+
82
+ private
83
+
84
+ def remove_policy(policy)
85
+ ui.msg("DELETE #{policy.policy_name} #{policy.revision_id}")
86
+ http_client.delete("/policies/#{policy.policy_name}/revisions/#{policy.revision_id}")
87
+ :ok
88
+ rescue => e
89
+ e
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -16,6 +16,10 @@
16
16
  #
17
17
 
18
18
  require 'fileutils'
19
+ require 'tmpdir'
20
+ require 'zlib'
21
+
22
+ require 'archive/tar/minitar'
19
23
 
20
24
  require 'chef-dk/service_exceptions'
21
25
  require 'chef-dk/policyfile_lock'
@@ -38,9 +42,10 @@ module ChefDK
38
42
  attr_reader :root_dir
39
43
  attr_reader :export_dir
40
44
 
41
- def initialize(policyfile: nil, export_dir: nil, root_dir: nil, force: false)
45
+ def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false)
42
46
  @root_dir = root_dir
43
47
  @export_dir = File.expand_path(export_dir)
48
+ @archive = archive
44
49
  @force_export = force
45
50
 
46
51
  @policy_data = nil
@@ -49,6 +54,12 @@ module ChefDK
49
54
  policyfile_rel_path = policyfile || "Policyfile.rb"
50
55
  policyfile_full_path = File.expand_path(policyfile_rel_path, root_dir)
51
56
  @storage_config = Policyfile::StorageConfig.new.use_policyfile(policyfile_full_path)
57
+
58
+ @staging_dir = nil
59
+ end
60
+
61
+ def archive?
62
+ @archive
52
63
  end
53
64
 
54
65
  def policy_name
@@ -74,10 +85,24 @@ module ChefDK
74
85
  @policyfile_lock || validate_lockfile
75
86
  end
76
87
 
88
+ def archive_file_location
89
+ return nil unless archive?
90
+ filename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}.tgz"
91
+ File.join(export_dir, filename)
92
+ end
93
+
77
94
  def export
78
- create_repo_structure
79
- copy_cookbooks
80
- create_policyfile_data_item
95
+ with_staging_dir do
96
+ create_repo_structure
97
+ copy_cookbooks
98
+ create_policyfile_data_item
99
+ copy_policyfile_lock
100
+ if archive?
101
+ create_archive
102
+ else
103
+ mv_staged_repo
104
+ end
105
+ end
81
106
  rescue => error
82
107
  msg = "Failed to export policy (in #{policyfile_filename}) to #{export_dir}"
83
108
  raise PolicyfileExportRepoError.new(msg, error)
@@ -85,13 +110,35 @@ module ChefDK
85
110
 
86
111
  private
87
112
 
88
- def create_repo_structure
89
- FileUtils.rm_rf(cookbooks_dir)
90
- FileUtils.rm_rf(policyfiles_data_bag_dir)
113
+ def with_staging_dir
114
+ p = Process.pid
115
+ t = Time.new.utc.strftime("%Y%m%d%H%M%S")
116
+ Dir.mktmpdir("chefdk-export-#{p}-#{t}") do |d|
117
+ begin
118
+ @staging_dir = d
119
+ yield
120
+ ensure
121
+ @staging_dir = nil
122
+ end
123
+ end
124
+ end
125
+
126
+ def create_archive
127
+ Zlib::GzipWriter.open(archive_file_location) do |gz_file|
128
+ Dir.chdir(staging_dir) do
129
+ Archive::Tar::Minitar.pack(".", gz_file)
130
+ end
131
+ end
132
+ end
91
133
 
134
+ def staging_dir
135
+ @staging_dir
136
+ end
137
+
138
+ def create_repo_structure
92
139
  FileUtils.mkdir_p(export_dir)
93
- FileUtils.mkdir_p(cookbooks_dir)
94
- FileUtils.mkdir_p(policyfiles_data_bag_dir)
140
+ FileUtils.mkdir_p(cookbooks_staging_dir)
141
+ FileUtils.mkdir_p(policyfiles_data_bag_staging_dir)
95
142
  end
96
143
 
97
144
  def copy_cookbooks
@@ -102,7 +149,7 @@ module ChefDK
102
149
 
103
150
  def copy_cookbook(lock)
104
151
  dirname = "#{lock.name}-#{lock.dotted_decimal_identifier}"
105
- export_path = File.join(export_dir, "cookbooks", dirname)
152
+ export_path = File.join(staging_dir, "cookbooks", dirname)
106
153
  metadata_rb_path = File.join(export_path, "metadata.rb")
107
154
  FileUtils.cp_r(lock.cookbook_path, export_path)
108
155
  FileUtils.rm_f(metadata_rb_path)
@@ -117,9 +164,6 @@ module ChefDK
117
164
  end
118
165
 
119
166
  def create_policyfile_data_item
120
- policy_id = "#{policyfile_lock.name}-#{POLICY_GROUP}"
121
- item_path = File.join(export_dir, "data_bags", "policyfiles", "#{policy_id}.json")
122
-
123
167
  lock_data = policyfile_lock.to_lock.dup
124
168
 
125
169
  lock_data["id"] = policy_id
@@ -139,6 +183,24 @@ module ChefDK
139
183
  end
140
184
  end
141
185
 
186
+ def copy_policyfile_lock
187
+ File.open(lockfile_staging_path, "wb+") do |f|
188
+ f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
189
+ end
190
+ end
191
+
192
+ def mv_staged_repo
193
+ # If we got here, either these dirs are empty/don't exist or force is
194
+ # set to true.
195
+ FileUtils.rm_rf(cookbooks_dir)
196
+ FileUtils.rm_rf(policyfiles_data_bag_dir)
197
+
198
+ FileUtils.mv(cookbooks_staging_dir, export_dir)
199
+ FileUtils.mkdir_p(export_data_bag_dir)
200
+ FileUtils.mv(policyfiles_data_bag_staging_dir, export_data_bag_dir)
201
+ FileUtils.mv(lockfile_staging_path, export_dir)
202
+ end
203
+
142
204
  def validate_lockfile
143
205
  return @policyfile_lock if @policyfile_lock
144
206
  @policyfile_lock = ChefDK::PolicyfileLock.new(storage_config).build_from_lock_data(policy_data)
@@ -164,7 +226,7 @@ module ChefDK
164
226
  end
165
227
 
166
228
  def assert_export_dir_clean!
167
- if !force_export? && !conflicting_fs_entries.empty?
229
+ if !force_export? && !conflicting_fs_entries.empty? && !archive?
168
230
  msg = "Export dir (#{export_dir}) not clean. Refusing to export. (Conflicting files: #{conflicting_fs_entries.join(', ')})"
169
231
  raise ExportDirNotEmpty, msg
170
232
  end
@@ -176,15 +238,40 @@ module ChefDK
176
238
 
177
239
  def conflicting_fs_entries
178
240
  Dir.glob(File.join(cookbooks_dir, "*")) +
179
- Dir.glob(File.join(policyfiles_data_bag_dir, "*"))
241
+ Dir.glob(File.join(policyfiles_data_bag_dir, "*")) +
242
+ Dir.glob(File.join(export_dir, "Policyfile.lock.json"))
180
243
  end
181
244
 
182
245
  def cookbooks_dir
183
246
  File.join(export_dir, "cookbooks")
184
247
  end
185
248
 
249
+ def export_data_bag_dir
250
+ File.join(export_dir, "data_bags")
251
+ end
252
+
186
253
  def policyfiles_data_bag_dir
187
- File.join(export_dir, "data_bags", "policyfiles")
254
+ File.join(export_data_bag_dir, "policyfiles")
255
+ end
256
+
257
+ def policy_id
258
+ "#{policyfile_lock.name}-#{POLICY_GROUP}"
259
+ end
260
+
261
+ def item_path
262
+ File.join(staging_dir, "data_bags", "policyfiles", "#{policy_id}.json")
263
+ end
264
+
265
+ def cookbooks_staging_dir
266
+ File.join(staging_dir, "cookbooks")
267
+ end
268
+
269
+ def policyfiles_data_bag_staging_dir
270
+ File.join(staging_dir, "data_bags", "policyfiles")
271
+ end
272
+
273
+ def lockfile_staging_path
274
+ File.join(staging_dir, "Policyfile.lock.json")
188
275
  end
189
276
 
190
277
  end
@@ -0,0 +1,173 @@
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 'zlib'
19
+ require 'archive/tar/minitar'
20
+
21
+ require 'chef-dk/service_exceptions'
22
+ require 'chef-dk/policyfile_lock'
23
+ require 'chef-dk/authenticated_http'
24
+ require 'chef-dk/policyfile/uploader'
25
+
26
+ module ChefDK
27
+ module PolicyfileServices
28
+ class PushArchive
29
+
30
+ USTAR_INDICATOR = "ustar\0".force_encoding(Encoding::ASCII_8BIT).freeze
31
+
32
+ attr_reader :archive_file
33
+ attr_reader :policy_group
34
+ attr_reader :root_dir
35
+ attr_reader :ui
36
+ attr_reader :config
37
+
38
+ attr_reader :policyfile_lock
39
+
40
+
41
+ def initialize(archive_file: nil, policy_group: nil, root_dir: nil, ui: nil, config: nil)
42
+ @archive_file = archive_file
43
+ @policy_group = policy_group
44
+ @root_dir = root_dir || Dir.pwd
45
+ @ui = ui
46
+ @config = config
47
+
48
+ @policyfile_lock = nil
49
+ end
50
+
51
+ def archive_file_path
52
+ File.expand_path(archive_file, root_dir)
53
+ end
54
+
55
+ def run
56
+ unless File.exist?(archive_file_path)
57
+ raise InvalidPolicyArchive, "Archive file #{archive_file_path} not found"
58
+ end
59
+ stage_unpacked_archive do |staging_dir|
60
+ read_policyfile_lock(staging_dir)
61
+
62
+ uploader.upload
63
+ end
64
+
65
+ rescue => e
66
+ raise PolicyfilePushArchiveError.new("Failed to publish archived policy", e)
67
+ end
68
+
69
+ # @api private
70
+ def uploader
71
+ ChefDK::Policyfile::Uploader.new(policyfile_lock, policy_group,
72
+ ui: ui,
73
+ http_client: http_client,
74
+ policy_document_native_api: config.policy_document_native_api)
75
+ end
76
+
77
+ # @api private
78
+ def http_client
79
+ @http_client ||= ChefDK::AuthenticatedHTTP.new(config.chef_server_url,
80
+ signing_key_filename: config.client_key,
81
+ client_name: config.node_name)
82
+ end
83
+
84
+ private
85
+
86
+ def read_policyfile_lock(staging_dir)
87
+ policyfile_lock_path = File.join(staging_dir, "Policyfile.lock.json")
88
+
89
+ unless File.exist?(policyfile_lock_path)
90
+ raise InvalidPolicyArchive, "Archive does not contain a Policyfile.lock.json"
91
+ end
92
+
93
+ unless File.directory?(File.join(staging_dir, "cookbooks"))
94
+ raise InvalidPolicyArchive, "Archive does not contain a cookbooks directory"
95
+ end
96
+
97
+
98
+ policy_data = load_policy_data(policyfile_lock_path)
99
+ storage_config = Policyfile::StorageConfig.new.use_policyfile_lock(policyfile_lock_path)
100
+ @policyfile_lock = ChefDK::PolicyfileLock.new(storage_config).build_from_archive(policy_data)
101
+
102
+ missing_cookbooks = policyfile_lock.cookbook_locks.select do |name, lock|
103
+ !lock.installed?
104
+ end
105
+
106
+ unless missing_cookbooks.empty?
107
+ message = "Archive does not have all cookbooks required by the Policyfile.lock. " +
108
+ "Missing cookbooks: '#{missing_cookbooks.keys.join('", "')}'."
109
+ raise InvalidPolicyArchive, message
110
+ end
111
+
112
+ end
113
+
114
+ def load_policy_data(policyfile_lock_path)
115
+ FFI_Yajl::Parser.parse(IO.read(policyfile_lock_path))
116
+ end
117
+
118
+ def stage_unpacked_archive
119
+ p = Process.pid
120
+ t = Time.new.utc.strftime("%Y%m%d%H%M%S")
121
+ Dir.mktmpdir("chefdk-push-archive-#{p}-#{t}") do |staging_dir|
122
+ unpack_to(staging_dir)
123
+ yield staging_dir
124
+ end
125
+
126
+ end
127
+
128
+ def unpack_to(staging_dir)
129
+ Zlib::GzipReader.open(archive_file_path) do |gz_file|
130
+ untar_to(gz_file, staging_dir)
131
+ end
132
+
133
+ # untar_to can raise InvalidPolicyArchive, let it through
134
+ rescue InvalidPolicyArchive
135
+ raise
136
+ rescue => e
137
+ raise InvalidPolicyArchive, "Archive file #{archive_file_path} could not be unpacked. #{e}"
138
+ end
139
+
140
+ def untar_to(tar_file, staging_dir)
141
+ # Minitar doesn't do much input checking, so if you feed it a
142
+ # garbage-enough file it will just do weird things and blow up. For
143
+ # example, if tar_file is just a bunch of nul characters, then tar will
144
+ # try to open a file named '.'; if you give it some random string that
145
+ # fits in the size of the filename header, it will create that file.
146
+ #
147
+ # Tar archives that we create via `chef export -a` and probably
148
+ # everything else we might encounter should be in ustar format. For
149
+ # such a tar file, bytes 257-263 should be "ustar\0", so we use this as
150
+ # a sanity check.
151
+ # https://en.wikipedia.org/wiki/Tar_(computing)
152
+
153
+ first_tar_header = tar_file.read(512)
154
+ ustar_indicator = first_tar_header[257, 6]
155
+
156
+ unless ustar_indicator == USTAR_INDICATOR
157
+ raise InvalidPolicyArchive, "Archive file #{archive_file_path} could not be unpacked. Tar archive looks corrupt."
158
+ end
159
+
160
+ # "undo" read of the first 512 bytes
161
+ tar_file.rewind
162
+
163
+ Archive::Tar::Minitar::Input.open(tar_file) do |stream|
164
+ stream.each do |entry|
165
+ stream.extract_entry(staging_dir, entry)
166
+ end
167
+ end
168
+ end
169
+
170
+
171
+ end
172
+ end
173
+ end