class ChefCLI::PolicyfileServices::ExportRepo
def archive?
def archive? @archive end
def archive_file_location
def archive_file_location return nil unless archive? filename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}.tgz" File.join(export_dir, filename) end
def assert_export_dir_clean!
def assert_export_dir_clean! if !force_export? && !conflicting_fs_entries.empty? && !archive? msg = "Export dir (#{export_dir}) not clean. Refusing to export. (Conflicting files: #{conflicting_fs_entries.join(", ")})" raise ExportDirNotEmpty, msg end end
def assert_lockfile_exists!
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 chefignore_for(cookbook_path)
def chefignore_for(cookbook_path) Chef::Cookbook::Chefignore.new(File.join(cookbook_path, "chefignore")) end
def client_rb_staging_path
def client_rb_staging_path File.join(dot_chef_staging_dir, "config.rb") end
def conflicting_fs_entries
def conflicting_fs_entries Dir.glob(File.join(cookbook_artifacts_dir, "*")) + Dir.glob(File.join(policies_dir, "*")) + Dir.glob(File.join(policy_groups_dir, "*")) + Dir.glob(File.join(export_dir, "Policyfile.lock.json")) end
def cookbook_artifacts_dir
def cookbook_artifacts_dir File.join(export_dir, "cookbook_artifacts") end
def cookbook_artifacts_staging_dir
def cookbook_artifacts_staging_dir File.join(staging_dir, "cookbook_artifacts") end
def cookbook_files_to_copy(cookbook_path)
def cookbook_files_to_copy(cookbook_path) cookbook = cookbook_loader_for(cookbook_path).cookbook_version root = Pathname.new(cookbook.root_dir) cookbook.all_files.map do |full_path| Pathname.new(full_path).relative_path_from(root).to_s end end
def cookbook_loader_for(cookbook_path)
def cookbook_loader_for(cookbook_path) loader = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore_for(cookbook_path)) loader.load! loader end
def copy_cookbook(lock)
def copy_cookbook(lock) dirname = "#{lock.name}-#{lock.identifier}" export_path = File.join(staging_dir, "cookbook_artifacts", dirname) metadata_rb_path = File.join(export_path, "metadata.rb") FileUtils.mkdir(export_path) unless File.directory?(export_path) copy_unignored_cookbook_files(lock, export_path) FileUtils.rm_f(metadata_rb_path) if lock.cookbook_version.nil? ui.msg "Unable to get the cookbook version/metadata for #{lock}" end metadata = lock.cookbook_version.metadata 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 copy_cookbooks
def copy_cookbooks policyfile_lock.cookbook_locks.each do |name, lock| copy_cookbook(lock) end end
def copy_policyfile_lock
def copy_policyfile_lock File.open(lockfile_staging_path, "wb+") do |f| f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true )) end end
def copy_unignored_cookbook_files(lock, export_path)
def copy_unignored_cookbook_files(lock, export_path) cookbook_files_to_copy(lock.cookbook_path).each do |rel_path| full_source_path = File.join(lock.cookbook_path, rel_path) full_dest_path = File.join(export_path, rel_path) dest_dirname = File.dirname(full_dest_path) FileUtils.mkdir_p(dest_dirname) unless File.directory?(dest_dirname) FileUtils.cp(full_source_path, full_dest_path) end end
def create_archive
def create_archive Dir.chdir(staging_dir) do targets = Find.find(".").collect { |e| e } Mixlib::Archive.new(archive_file_location).create(targets, gzip: true) end end
def create_client_rb
def create_client_rb File.open(client_rb_staging_path, "wb+") do |f| f.print( <<~CONFIG ) ### Chef Infra Client Configuration ### # The settings in this file will configure chef to apply the exported policy in # this directory. To use it, run: # # chef-client -z # policy_name '#{policy_name}' policy_group '#{policy_group}' use_policyfile true policy_document_native_api true # In order to use this repo, you need a version of Chef Infra Client and Chef Zero # that supports policyfile "native mode" APIs: current_version = Gem::Version.new(Chef::VERSION) unless Gem::Requirement.new(">= 12.7").satisfied_by?(current_version) puts("!" * 80) puts(<<-MESSAGE) This Chef Repo requires features introduced in Chef Infra Client 12.7, but you are using Chef \#{Chef::VERSION}. Please upgrade to Chef Infra Client 12.7 or later. MESSAGE puts("!" * 80) exit!(1) end CONFIG end end
def create_policy_group_repo_item
def create_policy_group_repo_item data = { "policies" => { policyfile_lock.name => { "revision_id" => policyfile_lock.revision_id, }, }, } File.open(policy_group_repo_item_path, "wb+") do |f| f.print(FFI_Yajl::Encoder.encode(data, pretty: true )) end end
def create_policyfile_repo_item
def create_policyfile_repo_item File.open(policyfile_repo_item_path, "wb+") do |f| f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true )) end end
def create_readme_md
def create_readme_md File.open(readme_staging_path, "wb+") do |f| f.print( <<~README ) # Exported Chef Infra Repository for Policy '#{policy_name}' Policy revision: #{policyfile_lock.revision_id} This directory contains all the cookbooks and configuration necessary for Chef to converge a system using this exported policy. To converge a system with the exported policy, use a privileged account to run `chef-client -z` from the directory containing the exported policy. ## Contents: ### Policyfile.lock.json A copy of the exported policy, used by the `chef push-archive` command. ### .chef/config.rb A configuration file for Chef Infra Client. This file configures Chef Infra Client to use the correct `policy_name` and `policy_group` for this exported repository. Chef Infra Client will use this configuration automatically if you've set your working directory properly. ### cookbook_artifacts/ All of the cookbooks required by the policy will be stored in this directory. ### policies/ A different copy of the exported policy, used by the `chef-client` command. ### policy_groups/ Policy groups are used by Chef Infra Server to manage multiple revisions of the same policy. The default "local" policy is recommended for export use since there can be no different revisions when not utilizing a server. README end end
def create_repo_structure
def create_repo_structure FileUtils.mkdir_p(export_dir) FileUtils.mkdir_p(dot_chef_staging_dir) FileUtils.mkdir_p(cookbook_artifacts_staging_dir) FileUtils.mkdir_p(policies_staging_dir) FileUtils.mkdir_p(policy_groups_staging_dir) end
def dot_chef_dir
def dot_chef_dir File.join(export_dir, ".chef") end
def dot_chef_staging_dir
def dot_chef_staging_dir File.join(staging_dir, ".chef") end
def export
def export with_staging_dir do create_repo_structure copy_cookbooks create_policyfile_repo_item create_policy_group_repo_item copy_policyfile_lock create_client_rb create_readme_md if archive? create_archive else mv_staged_repo end end rescue => error msg = "Failed to export policy (in #{policyfile_filename}) to #{export_dir}" raise PolicyfileExportRepoError.new(msg, error) end
def force_export?
def force_export? @force_export end
def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false, policy_group: nil)
def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false, policy_group: nil) @root_dir = root_dir @export_dir = File.expand_path(export_dir) @archive = archive @force_export = force @ui = UI.new @policy_data = nil @policyfile_lock = nil @policy_group = policy_group @policy_group ||= "local".freeze 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) @staging_dir = nil end
def lockfile_staging_path
def lockfile_staging_path File.join(staging_dir, "Policyfile.lock.json") end
def mv_staged_repo
def mv_staged_repo # If we got here, either these dirs are empty/don't exist or force is # set to true. FileUtils.rm_rf(cookbook_artifacts_dir) FileUtils.rm_rf(policies_dir) FileUtils.rm_rf(policy_groups_dir) FileUtils.rm_rf(dot_chef_dir) FileUtils.mv(cookbook_artifacts_staging_dir, export_dir) FileUtils.mv(policies_staging_dir, export_dir) FileUtils.mv(policy_groups_staging_dir, export_dir) FileUtils.mv(lockfile_staging_path, export_dir) FileUtils.mv(dot_chef_staging_dir, export_dir) FileUtils.mv(readme_staging_path, export_dir) end
def policies_dir
def policies_dir File.join(export_dir, "policies") end
def policies_staging_dir
def policies_staging_dir File.join(staging_dir, "policies") end
def policy_data
def policy_data @policy_data ||= FFI_Yajl::Parser.parse(IO.read(policyfile_lock_expanded_path)) rescue => error raise PolicyfileExportRepoError.new("Error reading lockfile #{policyfile_lock_expanded_path}", error) end
def policy_group_repo_item_path
def policy_group_repo_item_path File.join(staging_dir, "policy_groups", "#{policy_group}.json") end
def policy_groups_dir
def policy_groups_dir File.join(export_dir, "policy_groups") end
def policy_groups_staging_dir
def policy_groups_staging_dir File.join(staging_dir, "policy_groups") end
def policy_name
def policy_name policyfile_lock.name end
def policyfile_lock
def policyfile_lock @policyfile_lock || validate_lockfile end
def policyfile_repo_item_path
def policyfile_repo_item_path basename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}" File.join(staging_dir, "policies", "#{basename}.json") end
def readme_staging_path
def readme_staging_path File.join(staging_dir, "README.md") end
def run
def run assert_lockfile_exists! assert_export_dir_clean! validate_lockfile write_updated_lockfile export end
def staging_dir
def staging_dir @staging_dir end
def validate_lockfile
def validate_lockfile return @policyfile_lock if @policyfile_lock @policyfile_lock = ChefCLI::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 with_staging_dir
def with_staging_dir require "securerandom" unless defined?(SecureRandom) random_string = SecureRandom.hex(2) path = "chef-export-#{random_string}" Dir.mktmpdir(path) do |d| begin @staging_dir = d yield ensure @staging_dir = nil end end end
def write_updated_lockfile
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