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.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/lib/chef-dk/builtin_commands.rb +7 -0
- data/lib/chef-dk/command/env.rb +90 -0
- data/lib/chef-dk/command/export.rb +22 -1
- data/lib/chef-dk/command/generate.rb +1 -1
- data/lib/chef-dk/command/provision.rb +43 -0
- data/lib/chef-dk/command/push_archive.rb +126 -0
- data/lib/chef-dk/command/show_policy.rb +166 -0
- data/lib/chef-dk/command/verify.rb +58 -1
- data/lib/chef-dk/cookbook_omnifetch.rb +3 -2
- data/lib/chef-dk/exceptions.rb +27 -0
- data/lib/chef-dk/helpers.rb +29 -0
- data/lib/chef-dk/policyfile/chef_repo_cookbook_source.rb +8 -0
- data/lib/chef-dk/policyfile/chef_server_cookbook_source.rb +8 -0
- data/lib/chef-dk/policyfile/community_cookbook_source.rb +8 -0
- data/lib/chef-dk/policyfile/cookbook_locks.rb +76 -6
- data/lib/chef-dk/policyfile/dsl.rb +10 -5
- data/lib/chef-dk/policyfile/lister.rb +230 -0
- data/lib/chef-dk/policyfile/null_cookbook_source.rb +8 -0
- data/lib/chef-dk/policyfile_compiler.rb +35 -2
- data/lib/chef-dk/policyfile_lock.rb +43 -0
- data/lib/chef-dk/policyfile_services/clean_policies.rb +94 -0
- data/lib/chef-dk/policyfile_services/export_repo.rb +103 -16
- data/lib/chef-dk/policyfile_services/push_archive.rb +173 -0
- data/lib/chef-dk/policyfile_services/show_policy.rb +237 -0
- data/lib/chef-dk/service_exceptions.rb +21 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +1 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/README.md +2 -40
- data/lib/chef-dk/skeletons/code_generator/recipes/app.rb +0 -2
- data/lib/chef-dk/skeletons/code_generator/templates/default/kitchen.yml.erb +2 -2
- data/lib/chef-dk/skeletons/code_generator/templates/default/recipe_spec.rb.erb +1 -1
- data/lib/chef-dk/version.rb +1 -1
- data/spec/unit/command/env_spec.rb +52 -0
- data/spec/unit/command/exec_spec.rb +2 -2
- data/spec/unit/command/export_spec.rb +13 -0
- data/spec/unit/command/provision_spec.rb +56 -0
- data/spec/unit/command/push_archive_spec.rb +153 -0
- data/spec/unit/command/show_policy_spec.rb +235 -0
- data/spec/unit/command/verify_spec.rb +1 -0
- data/spec/unit/helpers_spec.rb +68 -0
- data/spec/unit/policyfile/cookbook_locks_spec.rb +107 -1
- data/spec/unit/policyfile/lister_spec.rb +256 -0
- data/spec/unit/policyfile_demands_spec.rb +202 -10
- data/spec/unit/policyfile_evaluation_spec.rb +30 -4
- data/spec/unit/policyfile_lock_serialization_spec.rb +45 -0
- data/spec/unit/policyfile_services/clean_policies_spec.rb +236 -0
- data/spec/unit/policyfile_services/export_repo_spec.rb +99 -6
- data/spec/unit/policyfile_services/push_archive_spec.rb +345 -0
- data/spec/unit/policyfile_services/show_policy_spec.rb +839 -0
- 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
|
-
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|
89
|
-
|
90
|
-
|
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(
|
94
|
-
FileUtils.mkdir_p(
|
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(
|
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(
|
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
|