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.
- 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
|