Skip to content
This repository has been archived by the owner on Jul 14, 2021. It is now read-only.

Export Policyfile as Chef Zero-compatible repo #249

Merged
merged 7 commits into from
Dec 3, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/chef-dk/builtin_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@

c.builtin "push", :Push, desc: "Push a local policy lock to a policy group on the server"

c.builtin "export", :Export, desc: "Export a policy lock as a Chef Zero code repo"

c.builtin "verify", :Verify, desc: "Test the embedded ChefDK applications"
end
135 changes: 135 additions & 0 deletions lib/chef-dk/command/export.rb
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)
Copy link
Contributor

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?

Copy link
Contributor Author

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:

method_call(<<-HERE_DOC
some text
HERE_DOC)

Copy link
Contributor

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

method_call <<-HERE_DOC
some text
HERE_DOC

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs boolean: true here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to the force option, but I don't know if it actually does anything. When I added some debug code to print the values of the mixlib-cli config object, here's what I get:

$ bin/chef export -D
{:config=>{:force=>false, :debug=>true}}
$ bin/chef export   
{:config=>{:force=>false, :debug=>false}}

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


175 changes: 175 additions & 0 deletions lib/chef-dk/policyfile_services/export_repo.rb
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like this should also be moved to the PolicyFileLock class eventually

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

6 changes: 6 additions & 0 deletions lib/chef-dk/service_exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class PolicyfileNotFound < PolicyfileServiceError
class LockfileNotFound < PolicyfileServiceError
end

class ExportDirNotEmpty < PolicyfileServiceError
end

class PolicyfileNestedException < PolicyfileServiceError

attr_reader :cause
Expand Down Expand Up @@ -73,5 +76,8 @@ class PolicyfileInstallError < PolicyfileNestedException
class PolicyfilePushError < PolicyfileNestedException
end

class PolicyfileExportRepoError < PolicyfileNestedException
end

end

Loading