This repository has been archived by the owner on Jul 14, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 170
Export Policyfile as Chef Zero-compatible repo #249
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
dbffcf9
Add Export Service
danielsdeleo e1ef32c
Add export CLI
danielsdeleo 7764970
Add force option to policyfile export
danielsdeleo f352ab0
Add --force option to export command
danielsdeleo 406e8c5
Add tests for export CLI
danielsdeleo 9e7ed80
Remove TODO for de-duplicating data item code.
danielsdeleo 222b1f9
Set CLI option to boolean
danielsdeleo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# | ||
# Copyright:: Copyright (c) 2014 Chef Software Inc. | ||
# License:: Apache License, Version 2.0 | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
require 'chef-dk/command/base' | ||
require 'chef-dk/ui' | ||
require 'chef-dk/policyfile_services/export_repo' | ||
|
||
module ChefDK | ||
module Command | ||
|
||
class Export < Base | ||
|
||
banner(<<-E) | ||
Usage: chef export [ POLICY_FILE ] DESTINATION_DIRECTORY [options] | ||
|
||
`chef export` creates a Chef Zero compatible Chef repository containing the | ||
cookbooks described in a Policyfile.lock.json. Once the exported repo is copied | ||
to the target machine, you can apply the policy to the machine with | ||
`chef-client -z`. You will need at least the following config: | ||
|
||
use_policyfile true | ||
deployment_group '$POLICY_NAME-local' | ||
versioned_cookbooks true | ||
|
||
The Policyfile feature is incomplete and beta quality. See our detailed README | ||
for more information. | ||
|
||
https://github.com/opscode/chef-dk/blob/master/POLICYFILE_README.md | ||
|
||
Options: | ||
|
||
E | ||
|
||
option :force, | ||
short: "-f", | ||
long: "--force", | ||
description: "If the DESTINATION_DIRECTORY is not empty, remove its content.", | ||
default: false, | ||
boolean: true | ||
|
||
option :debug, | ||
short: "-D", | ||
long: "--debug", | ||
description: "Enable stacktraces and other debug output", | ||
default: false | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added it to the
|
||
attr_reader :policyfile_relative_path | ||
attr_reader :export_dir | ||
|
||
attr_accessor :ui | ||
|
||
def initialize(*args) | ||
super | ||
@push = nil | ||
@ui = nil | ||
@policyfile_relative_path = nil | ||
@export_dir = nil | ||
@chef_config = nil | ||
@ui = UI.new | ||
end | ||
|
||
def run(params = []) | ||
return 1 unless apply_params!(params) | ||
export_service.run | ||
ui.msg("Exported policy '#{export_service.policyfile_lock.name}' to #{export_dir}") | ||
0 | ||
rescue ExportDirNotEmpty => e | ||
ui.err("ERROR: " + e.message) | ||
ui.err("Use --force to force export") | ||
1 | ||
rescue PolicyfileServiceError => e | ||
handle_error(e) | ||
1 | ||
end | ||
|
||
def debug? | ||
!!config[:debug] | ||
end | ||
|
||
def chef_config | ||
return @chef_config if @chef_config | ||
Chef::WorkstationConfigLoader.new(config[:config_file]).load | ||
@chef_config = Chef::Config | ||
end | ||
|
||
def export_service | ||
@export_service ||= PolicyfileServices::ExportRepo.new(policyfile: policyfile_relative_path, | ||
export_dir: export_dir, | ||
root_dir: Dir.pwd, | ||
force: config[:force]) | ||
end | ||
|
||
def handle_error(error) | ||
ui.err("Error: #{error.message}") | ||
if error.respond_to?(:reason) | ||
ui.err("Reason: #{error.reason}") | ||
ui.err("") | ||
ui.err(error.extended_error_info) if debug? | ||
ui.err(error.cause.backtrace.join("\n")) if debug? | ||
end | ||
end | ||
|
||
def apply_params!(params) | ||
remaining_args = parse_options(params) | ||
case remaining_args.size | ||
when 1 | ||
@export_dir = remaining_args[0] | ||
when 2 | ||
@policyfile_relative_path, @export_dir = remaining_args | ||
else | ||
ui.err(banner) | ||
return false | ||
end | ||
true | ||
end | ||
|
||
end | ||
end | ||
end | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
# | ||
# Copyright:: Copyright (c) 2014 Chef Software Inc. | ||
# License:: Apache License, Version 2.0 | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
require 'fileutils' | ||
|
||
require 'chef-dk/service_exceptions' | ||
require 'chef-dk/policyfile_lock' | ||
require 'chef-dk/policyfile/storage_config' | ||
|
||
module ChefDK | ||
module PolicyfileServices | ||
|
||
class ExportRepo | ||
|
||
# Policy groups provide namespaces for policies so that a Chef Server can | ||
# have multiple active iterations of a policy at once, but we don't need | ||
# this when serving a single exported policy via Chef Zero, so hardcode | ||
# it to a "well known" value: | ||
POLICY_GROUP = 'local'.freeze | ||
|
||
include Policyfile::StorageConfigDelegation | ||
|
||
attr_reader :storage_config | ||
attr_reader :root_dir | ||
attr_reader :export_dir | ||
|
||
def initialize(policyfile: nil, export_dir: nil, root_dir: nil, force: false) | ||
@root_dir = root_dir | ||
@export_dir = File.expand_path(export_dir) | ||
@force_export = force | ||
|
||
@policy_data = nil | ||
@policyfile_lock = nil | ||
|
||
policyfile_rel_path = policyfile || "Policyfile.rb" | ||
policyfile_full_path = File.expand_path(policyfile_rel_path, root_dir) | ||
@storage_config = Policyfile::StorageConfig.new.use_policyfile(policyfile_full_path) | ||
end | ||
|
||
def run | ||
assert_lockfile_exists! | ||
assert_export_dir_empty! | ||
|
||
validate_lockfile | ||
write_updated_lockfile | ||
export | ||
end | ||
|
||
def policy_data | ||
@policy_data ||= FFI_Yajl::Parser.parse(IO.read(policyfile_lock_expanded_path)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems like this should also be moved to the |
||
rescue => error | ||
raise PolicyfileExportRepoError.new("Error reading lockfile #{policyfile_lock_expanded_path}", error) | ||
end | ||
|
||
def policyfile_lock | ||
@policyfile_lock || validate_lockfile | ||
end | ||
|
||
def export | ||
create_repo_structure | ||
copy_cookbooks | ||
create_policyfile_data_item | ||
rescue => error | ||
msg = "Failed to export policy (in #{policyfile_filename}) to #{export_dir}" | ||
raise PolicyfileExportRepoError.new(msg, error) | ||
end | ||
|
||
private | ||
|
||
def create_repo_structure | ||
FileUtils.rm_rf(export_dir) | ||
FileUtils.mkdir_p(export_dir) | ||
FileUtils.mkdir_p(File.join(export_dir, "cookbooks")) | ||
FileUtils.mkdir_p(File.join(export_dir, "data_bags", "policyfiles")) | ||
end | ||
|
||
def copy_cookbooks | ||
policyfile_lock.cookbook_locks.each do |name, lock| | ||
copy_cookbook(lock) | ||
end | ||
end | ||
|
||
def copy_cookbook(lock) | ||
dirname = "#{lock.name}-#{lock.dotted_decimal_identifier}" | ||
export_path = File.join(export_dir, "cookbooks", dirname) | ||
metadata_rb_path = File.join(export_path, "metadata.rb") | ||
FileUtils.cp_r(lock.cookbook_path, export_path) | ||
FileUtils.rm_f(metadata_rb_path) | ||
metadata = lock.cookbook_version.metadata | ||
metadata.version(lock.dotted_decimal_identifier) | ||
|
||
metadata_json_path = File.join(export_path, "metadata.json") | ||
|
||
File.open(metadata_json_path, "wb+") do |f| | ||
f.print(FFI_Yajl::Encoder.encode(metadata.to_hash, pretty: true )) | ||
end | ||
end | ||
|
||
def create_policyfile_data_item | ||
policy_id = "#{policyfile_lock.name}-#{POLICY_GROUP}" | ||
item_path = File.join(export_dir, "data_bags", "policyfiles", "#{policy_id}.json") | ||
|
||
lock_data = policyfile_lock.to_lock.dup | ||
|
||
lock_data["id"] = policy_id | ||
|
||
data_item = { | ||
"id" => policy_id, | ||
"name" => "data_bag_item_policyfiles_#{policy_id}", | ||
"data_bag" => "policyfiles", | ||
"raw_data" => lock_data, | ||
# we'd prefer to leave this out, but the "compatibility mode" | ||
# implementation in chef-client relies on magical class inflation | ||
"json_class" => "Chef::DataBagItem" | ||
} | ||
|
||
File.open(item_path, "wb+") do |f| | ||
f.print(FFI_Yajl::Encoder.encode(data_item, pretty: true )) | ||
end | ||
end | ||
|
||
def validate_lockfile | ||
return @policyfile_lock if @policyfile_lock | ||
@policyfile_lock = ChefDK::PolicyfileLock.new(storage_config).build_from_lock_data(policy_data) | ||
# TODO: enumerate any cookbook that have been updated | ||
@policyfile_lock.validate_cookbooks! | ||
@policyfile_lock | ||
rescue PolicyfileExportRepoError | ||
raise | ||
rescue => error | ||
raise PolicyfileExportRepoError.new("Invalid lockfile data", error) | ||
end | ||
|
||
def write_updated_lockfile | ||
File.open(policyfile_lock_expanded_path, "wb+") do |f| | ||
f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true )) | ||
end | ||
end | ||
|
||
def assert_lockfile_exists! | ||
unless File.exist?(policyfile_lock_expanded_path) | ||
raise LockfileNotFound, "No lockfile at #{policyfile_lock_expanded_path} - you need to run `install` before `push`" | ||
end | ||
end | ||
|
||
def assert_export_dir_empty! | ||
entries = Dir.glob(File.join(export_dir, "*")) | ||
if !force_export? && !entries.empty? | ||
raise ExportDirNotEmpty, "Export dir (#{export_dir}) not empty. Refusing to export." | ||
end | ||
end | ||
|
||
def force_export? | ||
@force_export | ||
end | ||
|
||
end | ||
|
||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused - shouldn't this cause an error because the method call is being completed with
)
before the multi-line-string delimiter is reached?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby's grammar allows you to pass a heredoc string to a method using this syntax. It's definitely a little weird looking.
BTW, I think the syntax you'd expect actually doesn't work, e.g., I think this isn't valid:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha - The syntax I am familiar with doesn't use parens