class ChefDK::PolicyfileServices::ExportRepo

Constants

POLICY_GROUP

Policy groups provide namespaces for policies so that a Chef Infra 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:

Attributes

export_dir[R]
root_dir[R]
storage_config[R]

Public Class Methods

new(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 48
def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false)
  @root_dir = root_dir
  @export_dir = File.expand_path(export_dir)
  @archive = archive
  @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)

  @staging_dir = nil
end

Public Instance Methods

archive?() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 64
def archive?
  @archive
end
archive_file_location() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 91
def archive_file_location
  return nil unless archive?

  filename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}.tgz"
  File.join(export_dir, filename)
end
export() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 98
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
policy_data() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 81
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
policy_name() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 68
def policy_name
  policyfile_lock.name
end
policyfile_lock() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 87
def policyfile_lock
  @policyfile_lock || validate_lockfile
end
run() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 72
def run
  assert_lockfile_exists!
  assert_export_dir_clean!

  validate_lockfile
  write_updated_lockfile
  export
end

Private Instance Methods

assert_export_dir_clean!() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 347
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
assert_lockfile_exists!() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 341
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
chefignore_for(cookbook_path) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 200
def chefignore_for(cookbook_path)
  Chef::Cookbook::Chefignore.new(File.join(cookbook_path, "chefignore"))
end
client_rb_staging_path() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 410
def client_rb_staging_path
  File.join(dot_chef_staging_dir, "config.rb")
end
conflicting_fs_entries() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 358
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
cookbook_artifacts_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 365
def cookbook_artifacts_dir
  File.join(export_dir, "cookbook_artifacts")
end
cookbook_artifacts_staging_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 394
def cookbook_artifacts_staging_dir
  File.join(staging_dir, "cookbook_artifacts")
end
cookbook_files_to_copy(cookbook_path) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 184
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
cookbook_loader_for(cookbook_path) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 194
def cookbook_loader_for(cookbook_path)
  loader = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore_for(cookbook_path))
  loader.load!
  loader
end
copy_cookbook(lock) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 158
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)
  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
copy_cookbooks() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 152
def copy_cookbooks
  policyfile_lock.cookbook_locks.each do |name, lock|
    copy_cookbook(lock)
  end
end
copy_policyfile_lock() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 224
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
copy_unignored_cookbook_files(lock, export_path) click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 174
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
create_archive() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 133
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
create_client_rb() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 230
      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 'local'

            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
create_policy_group_repo_item() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 210
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
create_policyfile_repo_item() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 204
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
create_readme_md() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 263
      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. However, exported policies contain only a single policy revision, so
            this policy group name is hardcoded to "local" and should not be changed.

          README
        end
      end
create_repo_structure() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 144
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
dot_chef_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 377
def dot_chef_dir
  File.join(export_dir, ".chef")
end
dot_chef_staging_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 390
def dot_chef_staging_dir
  File.join(staging_dir, ".chef")
end
force_export?() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 354
def force_export?
  @force_export
end
lockfile_staging_path() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 406
def lockfile_staging_path
  File.join(staging_dir, "Policyfile.lock.json")
end
mv_staged_repo() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 306
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
policies_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 369
def policies_dir
  File.join(export_dir, "policies")
end
policies_staging_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 398
def policies_staging_dir
  File.join(staging_dir, "policies")
end
policy_group_repo_item_path() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 386
def policy_group_repo_item_path
  File.join(staging_dir, "policy_groups", "local.json")
end
policy_groups_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 373
def policy_groups_dir
  File.join(export_dir, "policy_groups")
end
policy_groups_staging_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 402
def policy_groups_staging_dir
  File.join(staging_dir, "policy_groups")
end
policyfile_repo_item_path() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 381
def policyfile_repo_item_path
  basename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}"
  File.join(staging_dir, "policies", "#{basename}.json")
end
readme_staging_path() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 414
def readme_staging_path
  File.join(staging_dir, "README.md")
end
staging_dir() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 140
def staging_dir
  @staging_dir
end
validate_lockfile() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 322
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
with_staging_dir() { || ... } click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 120
def with_staging_dir
  p = Process.pid
  t = Time.new.utc.strftime("%Y%m%d%H%M%S")
  Dir.mktmpdir("chefdk-export-#{p}-#{t}") do |d|
    begin
      @staging_dir = d
      yield
    ensure
      @staging_dir = nil
    end
  end
end
write_updated_lockfile() click to toggle source
# File lib/chef-dk/policyfile_services/export_repo.rb, line 335
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