chef-dk 0.6.2 → 0.7.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.
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