class Kitchen::Provisioner::ChefBase

Common implementation details for Chef-related provisioners.

@author Fletcher Nichol <fnichol@nichol.ca>

Public Class Methods

new(config = {}) click to toggle source

Reads the local Chef::Config object (if present). We do this because we want to start bring Chef config and Chef Workstation config closer together. For example, we want to configure proxy settings in 1 location instead of 3 configuration files.

@param config [Hash] initial provided configuration

Calls superclass method Kitchen::Provisioner::Base::new
# File lib/kitchen/provisioner/chef_base.rb, line 235
def initialize(config = {})
  super(config)

  if defined?(ChefConfig::WorkstationConfigLoader)
    ChefConfig::WorkstationConfigLoader.new(config[:config_path]).load
  end
  # This exports any proxy config present in the Chef config to
  # appropriate environment variables, which Test Kitchen respects
  ChefConfig::Config.export_proxies if defined?(ChefConfig::Config.export_proxies)
end

Public Instance Methods

check_license() click to toggle source

(see Base#check_license)

# File lib/kitchen/provisioner/chef_base.rb, line 285
def check_license
  name = license_acceptance_id
  version = product_version
  debug("Checking if we need to prompt for license acceptance on product: #{name} version: #{version}.")

  acceptor = LicenseAcceptance::Acceptor.new(logger: Kitchen.logger, provided: config[:chef_license])
  if acceptor.license_required?(name, version)
    debug("License acceptance required for #{name} version: #{version}. Prompting")
    license_id = acceptor.id_from_mixlib(name)
    begin
      acceptor.check_and_persist(license_id, version.to_s)
    rescue LicenseAcceptance::LicenseNotAcceptedError => e
      error("Cannot converge without accepting the #{e.product.pretty_name} License. Set it in your kitchen.yml or using the CHEF_LICENSE environment variable")
      raise
    end
    config[:chef_license] ||= acceptor.acceptance_value
  end
end
create_sandbox() click to toggle source

(see Base#create_sandbox)

# File lib/kitchen/provisioner/chef_base.rb, line 305
def create_sandbox
  super
  sanity_check_sandbox_options!
  Chef::CommonSandbox.new(config, sandbox_path, instance).populate
end
doctor(state) click to toggle source
# File lib/kitchen/provisioner/chef_base.rb, line 246
def doctor(state)
  deprecated_config.each do |attr, msg|
    info("**** #{attr} deprecated\n#{msg}")
  end
end
init_command() click to toggle source

(see Base#init_command)

# File lib/kitchen/provisioner/chef_base.rb, line 312
def init_command
  dirs = %w{
    cookbooks data data_bags environments roles clients
    encrypted_data_bag_secret
  }.sort.map { |dir| remote_path_join(config[:root_path], dir) }

  vars = if powershell_shell?
           init_command_vars_for_powershell(dirs)
         else
           init_command_vars_for_bourne(dirs)
         end

  prefix_command(shell_code_from_file(vars, "chef_base_init_command"))
end
install_command() click to toggle source

(see Base#install_command)

# File lib/kitchen/provisioner/chef_base.rb, line 328
def install_command
  return unless config[:require_chef_omnibus] || config[:product_name]
  return if config[:product_name] && config[:install_strategy] == "skip"

  prefix_command(install_script_contents)
end
license_acceptance_id() click to toggle source

If the user has policyfiles we shell out to the `chef` executable, so need to ensure they have accepted the Chef Workstation license. Otherwise they just need the Chef Infra license.

@return [String] license id to prompt for acceptance

# File lib/kitchen/provisioner/chef_base.rb, line 273
def license_acceptance_id
  case
    when File.exist?(policyfile)
      "chef-workstation"
    when config[:product_name]
      config[:product_name]
    else
      "chef"
  end
end
product_version() click to toggle source

gives us the product version from either require_chef_omnibus or product_version If the non-default (true) value of require_chef_omnibus is present use that otherwise use config which defaults to :latest and is the actual default for chef provisioners

@return [String,Symbol,NilClass] version or nil if not applicable

# File lib/kitchen/provisioner/chef_base.rb, line 258
def product_version
  case config[:require_chef_omnibus]
  when FalseClass
    nil
  when TrueClass
    config[:product_version]
  else
    config[:require_chef_omnibus]
  end
end

Private Instance Methods

add_omnibus_directory_option() click to toggle source

Verify if the “omnibus_dir_option” has already been passed, if so we don't use the @driver.cache_directory

@api private

# File lib/kitchen/provisioner/chef_base.rb, line 363
def add_omnibus_directory_option
  cache_dir_option = "#{omnibus_dir_option} #{instance.driver.cache_directory}"
  if config[:chef_omnibus_install_options].nil?
    config[:chef_omnibus_install_options] = cache_dir_option
  elsif config[:chef_omnibus_install_options].match(/\s*#{omnibus_dir_option}\s*/).nil?
    config[:chef_omnibus_install_options] << " " << cache_dir_option
  end
end
berksfile() click to toggle source

@return [String] an absolute path to a Berksfile, relative to the

kitchen root

@api private

# File lib/kitchen/provisioner/chef_base.rb, line 383
def berksfile
  berksfile_basename = config[:berksfile_path] || config[:berksfile] || "Berksfile"
  File.expand_path(berksfile_basename, config[:kitchen_root])
end
chef_args(_config_filename) click to toggle source

Returns an Array of command line arguments for the chef client.

@return [Array<String>] an array of command line arguments @api private

# File lib/kitchen/provisioner/chef_base.rb, line 657
def chef_args(_config_filename)
  raise "You must override in sub classes!"
end
chef_cmd(base_cmd) click to toggle source

Gives the command used to run chef @api private

# File lib/kitchen/provisioner/chef_base.rb, line 672
def chef_cmd(base_cmd)
  if windows_os?
    separator = [
      "; if ($LastExitCode -ne 0) { ",
      "throw \"Command failed with exit code $LastExitCode.\" } ;",
    ].join
  else
    separator = " && "
  end
  chef_cmds(base_cmd).join(separator)
end
chef_cmds(base_cmd) click to toggle source

Gives an array of commands @api private

# File lib/kitchen/provisioner/chef_base.rb, line 686
def chef_cmds(base_cmd)
  cmds = []
  num_converges = config[:multiple_converge].to_i
  idempotency   = config[:enforce_idempotency]

  # Execute Chef Client n-1 times, without exiting
  (num_converges - 1).times do
    cmds << wrapped_chef_cmd(base_cmd, config_filename)
  end

  # Append another execution with Windows specific Exit code helper or (for
  # idempotency check) a specific config file which assures no changed resources.
  cmds << unless idempotency
            wrapped_chef_cmd(base_cmd, config_filename, append: last_exit_code)
          else
            wrapped_chef_cmd(base_cmd, "client_no_updated_resources.rb", append: last_exit_code)
          end
  cmds
end
config_filename() click to toggle source

Returns a filename for the configuration file defaults to client.rb

@return [String] a filename @api private

# File lib/kitchen/provisioner/chef_base.rb, line 666
def config_filename
  "client.rb"
end
default_config_rb() click to toggle source

Generates a Hash with default values for a solo.rb or client.rb Chef configuration file.

@return [Hash] a configuration hash @api private

# File lib/kitchen/provisioner/chef_base.rb, line 393
def default_config_rb # rubocop:disable Metrics/MethodLength
  root = config[:root_path].gsub("$env:TEMP", "\#{ENV['TEMP']\}")

  config_rb = {
    node_name: instance.name,
    checksum_path: remote_path_join(root, "checksums"),
    file_cache_path: remote_path_join(root, "cache"),
    file_backup_path: remote_path_join(root, "backup"),
    cookbook_path: [
      remote_path_join(root, "cookbooks"),
      remote_path_join(root, "site-cookbooks"),
    ],
    data_bag_path: remote_path_join(root, "data_bags"),
    environment_path: remote_path_join(root, "environments"),
    node_path: remote_path_join(root, "nodes"),
    role_path: remote_path_join(root, "roles"),
    client_path: remote_path_join(root, "clients"),
    user_path: remote_path_join(root, "users"),
    validation_key: remote_path_join(root, "validation.pem"),
    client_key: remote_path_join(root, "client.pem"),
    chef_server_url: "http://127.0.0.1:8889",
    encrypted_data_bag_secret: remote_path_join(
      root, "encrypted_data_bag_secret"
    ),
    treat_deprecation_warnings_as_errors: config[:deprecations_as_errors],
  }
  config_rb[:chef_license] = config[:chef_license] unless config[:chef_license].nil?
  config_rb
end
format_config_file(data) click to toggle source

Generates a rendered client.rb/solo.rb/knife.rb formatted file as a String.

@param data [Hash] a key/value pair hash of configuration @return [String] a rendered Chef config file as a String @api private

# File lib/kitchen/provisioner/chef_base.rb, line 429
def format_config_file(data)
  data.each.map do |attr, value|
    [attr, format_value(value)].join(" ")
  end.join("\n")
end
format_value(obj) click to toggle source

Converts a Ruby object to a String interpretation suitable for writing out to a client.rb/solo.rb/knife.rb file.

@param obj [Object] an object @return [String] a string representation @api private

# File lib/kitchen/provisioner/chef_base.rb, line 441
def format_value(obj)
  if obj.is_a?(String) && obj =~ /^:/
    obj
  elsif obj.is_a?(String)
    %{"#{obj.gsub(/\\/, "\\\\\\\\")}"}
  elsif obj.is_a?(Array)
    %{[#{obj.map { |i| format_value(i) }.join(", ")}]}
  else
    obj.inspect
  end
end
init_command_vars_for_bourne(dirs) click to toggle source

Generates the init command variables for Bourne shell-based platforms.

@param dirs [Array<String>] directories @return [String] shell variable lines @api private

# File lib/kitchen/provisioner/chef_base.rb, line 458
def init_command_vars_for_bourne(dirs)
  [
    shell_var("sudo_rm", sudo("rm")),
    shell_var("dirs", dirs.join(" ")),
    shell_var("root_path", config[:root_path]),
  ].join("\n")
end
init_command_vars_for_powershell(dirs) click to toggle source

Generates the init command variables for PowerShell-based platforms.

@param dirs [Array<String>] directories @return [String] shell variable lines @api private

# File lib/kitchen/provisioner/chef_base.rb, line 471
def init_command_vars_for_powershell(dirs)
  [
    %{$dirs = @(#{dirs.map { |d| %{"#{d}"} }.join(", ")})},
    shell_var("root_path", config[:root_path]),
  ].join("\n")
end
install_from_file(command) click to toggle source
# File lib/kitchen/provisioner/chef_base.rb, line 562
def install_from_file(command)
  install_file = "#{config[:root_path]}/chef-installer.sh"
  script = []
  script << "mkdir -p #{config[:root_path]}"
  script << "if [ $? -ne 0 ]; then"
  script << "  echo Kitchen config setting root_path: '#{config[:root_path]}' not creatable by regular user "
  script << "  exit 1"
  script << "fi"
  script << "cat > #{install_file} <<\"EOL\""
  script << command
  script << "EOL"
  script << "chmod +x #{install_file}"
  script << sudo(install_file)
  script.join("\n")
end
install_options() click to toggle source

@return [Hash] an option hash for the install commands @api private

# File lib/kitchen/provisioner/chef_base.rb, line 343
def install_options
  add_omnibus_directory_option if instance.driver.cache_directory
  project = /\s*-P (\w+)\s*/.match(config[:chef_omnibus_install_options])
  {
    omnibus_url: config[:chef_omnibus_url],
    project: project.nil? ? nil : project[1],
    install_flags: config[:chef_omnibus_install_options],
    sudo_command: sudo_command,
  }.tap do |opts|
    opts[:root] = config[:chef_omnibus_root] if config.key? :chef_omnibus_root
    %i{install_msi_url http_proxy https_proxy}.each do |key|
      opts[key] = config[key] if config.key? key
    end
  end
end
install_script_contents() click to toggle source

@return [String] contents of the install script @api private

# File lib/kitchen/provisioner/chef_base.rb, line 494
def install_script_contents
  # by default require_chef_omnibus is set to true. Check config[:product_name] first
  # so that we can use it if configured.
  if config[:product_name]
    script_for_product
  elsif config[:require_chef_omnibus]
    script_for_omnibus_version
  end
end
last_exit_code() click to toggle source
# File lib/kitchen/provisioner/chef_base.rb, line 337
def last_exit_code
  "; exit $LastExitCode" if powershell_shell?
end
load_needed_dependencies!() click to toggle source

Load cookbook dependency resolver code, if required.

(see Base#load_needed_dependencies!)

# File lib/kitchen/provisioner/chef_base.rb, line 481
def load_needed_dependencies!
  super
  if File.exist?(policyfile)
    debug("Policyfile found at #{policyfile}, using Policyfile to resolve cookbook dependencies")
    Chef::Policyfile.load!(logger: logger)
  elsif File.exist?(berksfile)
    debug("Berksfile found at #{berksfile}, using Berkshelf to resolve cookbook dependencies")
    Chef::Berkshelf.load!(logger: logger)
  end
end
omnibus_dir_option() click to toggle source

@return [String] Correct option per platform to specify the the

cache directory

@api private

# File lib/kitchen/provisioner/chef_base.rb, line 558
def omnibus_dir_option
  windows_os? ? "-download_directory" : "-d"
end
policyfile() click to toggle source

@return [String] an absolute path to a Policyfile, relative to the

kitchen root

@api private

# File lib/kitchen/provisioner/chef_base.rb, line 375
def policyfile
  policyfile_basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
  File.expand_path(policyfile_basename, config[:kitchen_root])
end
prepare_config_idempotency_check(data) click to toggle source

Writes a configuration file to the sandbox directory to check for idempotency of the run. @api private

# File lib/kitchen/provisioner/chef_base.rb, line 639
def prepare_config_idempotency_check(data)
  handler_filename = "chef-client-fail-if-update-handler.rb"
  source = File.join(
    File.dirname(__FILE__), %w{.. .. .. support }, handler_filename
  )
  FileUtils.cp(source, File.join(sandbox_path, handler_filename))
  File.open(File.join(sandbox_path, "client_no_updated_resources.rb"), "wb") do |file|
    file.write(format_config_file(data))
    file.write("\n\n")
    file.write("handler_file = File.join(File.dirname(__FILE__), '#{handler_filename}')\n")
    file.write "Chef::Config.from_file(handler_file)\n"
  end
end
prepare_config_rb() click to toggle source

Writes a configuration file to the sandbox directory. @api private

# File lib/kitchen/provisioner/chef_base.rb, line 622
def prepare_config_rb
  data = default_config_rb.merge(config[config_filename.tr(".", "_").to_sym])
  data = data.merge(named_run_list: config[:named_run_list]) if config[:named_run_list]

  info("Preparing #{config_filename}")
  debug("Creating #{config_filename} from #{data.inspect}")

  File.open(File.join(sandbox_path, config_filename), "wb") do |file|
    file.write(format_config_file(data))
  end

  prepare_config_idempotency_check(data) if config[:enforce_idempotency]
end
sanity_check_sandbox_options!() click to toggle source

@return [void] @raise [UserError] @api private

# File lib/kitchen/provisioner/chef_base.rb, line 601
def sanity_check_sandbox_options!
  if (config[:policyfile_path] || config[:policyfile]) && !File.exist?(policyfile)
    raise UserError, "policyfile_path set in config "\
      "(#{config[:policyfile_path]} could not be found. " \
      "Expected to find it at full path #{policyfile}."
  end
  if config[:berksfile_path] && !File.exist?(berksfile)
    raise UserError, "berksfile_path set in config "\
      "(#{config[:berksfile_path]} could not be found. " \
      "Expected to find it at full path #{berksfile}."
  end
  if File.exist?(policyfile) && !supports_policyfile?
    raise UserError, "policyfile detected, but provisioner " \
      "#{self.class.name} doesn't support Policyfiles. " \
      "Either use a different provisioner, or delete/rename " \
      "#{policyfile}."
  end
end
script_for_omnibus_version() click to toggle source

@return [String] contents of version based install script @api private

# File lib/kitchen/provisioner/chef_base.rb, line 580
def script_for_omnibus_version
  require "mixlib/install/script_generator"
  installer = Mixlib::Install::ScriptGenerator.new(
    config[:require_chef_omnibus], powershell_shell?, install_options
  )
  config[:chef_omnibus_root] = installer.root
  sudo(installer.install_command)
end
script_for_product() click to toggle source

@return [String] contents of product based install script @api private

# File lib/kitchen/provisioner/chef_base.rb, line 506
def script_for_product
  require "mixlib/install"
  installer = Mixlib::Install.new({
    product_name: config[:product_name],
    product_version: config[:product_version],
    channel: config[:channel].to_sym,
    install_command_options: {
      install_strategy: config[:install_strategy],
    },
  }.tap do |opts|
    opts[:shell_type] = :ps1 if powershell_shell?
    %i{platform platform_version architecture}.each do |key|
      opts[key] = config[key] if config[key]
    end

    unless windows_os?
      # omnitruck installer does not currently support a tmp dir option on windows
      opts[:install_command_options][:tmp_dir] = config[:root_path]
      opts[:install_command_options]["TMPDIR"] = config[:root_path]
    end

    if config[:download_url]
      opts[:install_command_options][:download_url_override] = config[:download_url]
      opts[:install_command_options][:checksum] = config[:checksum] if config[:checksum]
    end

    if instance.driver.cache_directory
      download_dir_option = windows_os? ? :download_directory : :cmdline_dl_dir
      opts[:install_command_options][download_dir_option] = instance.driver.cache_directory
    end

    proxies = {}.tap do |prox|
      %i{http_proxy https_proxy ftp_proxy no_proxy}.each do |key|
        prox[key] = config[key] if config[key]
      end

      # install.ps1 only supports http_proxy
      prox.delete_if { |p| %i{https_proxy ftp_proxy no_proxy}.include?(p) } if powershell_shell?
    end
    opts[:install_command_options].merge!(proxies)
  end)
  config[:chef_omnibus_root] = installer.root
  if powershell_shell?
    installer.install_command
  else
    install_from_file(installer.install_command)
  end
end
supports_policyfile?() click to toggle source

Hook used in subclasses to indicate support for policyfiles.

@abstract @return [Boolean] @api private

# File lib/kitchen/provisioner/chef_base.rb, line 594
def supports_policyfile?
  false
end
wrapped_chef_cmd(base_cmd, configfile, append: "") click to toggle source

Concatenate all arguments and wrap it with shell-specifics @api private

# File lib/kitchen/provisioner/chef_base.rb, line 708
def wrapped_chef_cmd(base_cmd, configfile, append: "")
  args = []

  args << base_cmd
  args << chef_args(configfile)
  args << append

  shell_cmd = args.flatten.join(" ")
  shell_cmd = shell_cmd.prepend(reload_ps1_path) if windows_os?

  prefix_command(wrap_shell_code(shell_cmd))
end