class Kitchen::Provisioner::ChefBase

@author Fletcher Nichol <fnichol@nichol.ca>
Common implementation details for Chef-related provisioners.

def self.enterprise_gem_available?

Other tags:
    Api: - private

Returns:
  • (String, nil) - the name of the enterprise gem if available
def self.enterprise_gem_available?
  @enterprise_gem_checked ||= false
  return @enterprise_gem if @enterprise_gem_checked
  @enterprise_gem_checked = true
  enterprise_gem = begin
                     # Try kitchen-chef-enterprise first (Progress Chef Enterprise)
                     if Gem::Specification.find_by_name("kitchen-chef-enterprise")
                       "kitchen-chef-enterprise"
                     end
                   rescue Gem::LoadError
                     nil
                   end
  cinc_gem = nil
  if enterprise_gem.nil?
    cinc_gem = begin
                 # Fall back to kitchen-cinc (Cinc Project)
                 if Gem::Specification.find_by_name("kitchen-cinc")
                   "kitchen-cinc"
                 end
               rescue Gem::LoadError
                 nil
               end
  end
  @enterprise_gem = enterprise_gem || cinc_gem
end

def add_omnibus_directory_option

Other tags:
    Api: - private
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

def berksfile

Other tags:
    Api: - private

Returns:
  • (String) - an absolute path to a Berksfile, relative to the
def berksfile
  berksfile_basename = config[:berksfile_path] || config[:berksfile] || "Berksfile"
  File.expand_path(berksfile_basename, config[:kitchen_root])
end

def check_license

(see Base#check_license)
def check_license
  unless config[:download_url]
    warn(
      <<~WARNING
        ==============================================================================================================
        \e[1m\e[93m!!!WARNING!!! kitchen-omnibus-chef is deprecated\e[0m
        Omnitruck downloads are being shutdown for specific Chef Infra Client versions and will stop working entirely
        in the future. This kitchen-omnibus-chef gem is also not compatible with infra-client 19+ new habitat
        based installation method.
        For Chef customers it is recommended to switch to using the new kitchen-chef-enterprise plugin found with
        chef-test-kitchen-enterprise and bundled in chef-workstation 26.x+.
        Please refer to this blog for schedule of which chef-client versions and when they will be affected:
        https://www.chef.io/blog/decoding-the-change-progress-chef-is-moving-to-licensed-downloads
        For non Chef customers or community users it is recommended to switch to the new kitchen-cinc plugin and cinc
        provisioners like cinc_infra.
        ==============================================================================================================
      WARNING
    )
  end
  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

def chef_args(_config_filename)

Other tags:
    Api: - private

Returns:
  • (Array) - an array of command line arguments
def chef_args(_config_filename)
  raise "You must override in sub classes!"
end

def chef_cmd(base_cmd)

Other tags:
    Api: - private
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

def chef_cmds(base_cmd)

Other tags:
    Api: - private
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

def config_filename

Other tags:
    Api: - private

Returns:
  • (String) - a filename
def config_filename
  "client.rb"
end

def create_sandbox

(see Base#create_sandbox)
def create_sandbox
  super
  sanity_check_sandbox_options!
  Chef::CommonSandbox.new(config, sandbox_path, instance).populate
end

def default_config_rb # rubocop:disable Metrics/MethodLength

Other tags:
    Api: - private

Returns:
  • (Hash) - a configuration hash
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

def doctor(state)

def doctor(state)
  deprecated_config = instance.driver.instance_variable_get(:@deprecated_config)
  deprecated_config.each do |attr, msg|
    info("**** #{attr} deprecated\n#{msg}")
  end
end

def format_config_file(data)

Other tags:
    Api: - private

Returns:
  • (String) - a rendered Chef config file as a String

Parameters:
  • data (Hash) -- a key/value pair hash of configuration
def format_config_file(data)
  data.each.map do |attr, value|
    [attr, format_value(value)].join(" ")
  end.join("\n")
end

def format_value(obj)

Other tags:
    Api: - private

Returns:
  • (String) - a string representation

Parameters:
  • obj (Object) -- an object
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

def init_command

(see Base#init_command)
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

def init_command_vars_for_bourne(dirs)

Other tags:
    Api: - private

Returns:
  • (String) - shell variable lines

Parameters:
  • dirs (Array) -- directories
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

def init_command_vars_for_powershell(dirs)

Other tags:
    Api: - private

Returns:
  • (String) - shell variable lines

Parameters:
  • dirs (Array) -- directories
def init_command_vars_for_powershell(dirs)
  [
    %{$dirs = @(#{dirs.map { |d| %{"#{d}"} }.join(", ")})},
    shell_var("root_path", config[:root_path]),
  ].join("\n")
end

def initialize(config = {})

Parameters:
  • config (Hash) -- initial provided configuration
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

def install_command

(see Base#install_command)
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

def install_from_file(command)

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

def install_options

Other tags:
    Api: - private

Returns:
  • (Hash) - an option hash for the install commands
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:,
  }.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

def install_script_contents

Other tags:
    Api: - private

Returns:
  • (String) - contents of the install script
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

def last_exit_code

def last_exit_code
  "; exit $LastExitCode" if powershell_shell?
end

def license_acceptance_id

Returns:
  • (String) - license id to prompt for acceptance
def license_acceptance_id
  case
    when File.exist?(policyfile) && (config[:product_name].nil? || config[:product_name].start_with?("chef"))
      "chef-workstation"
    when config[:product_name]
      config[:product_name]
    else
      "chef"
  end
end

def load_needed_dependencies!

(see Base#load_needed_dependencies!)

Load cookbook dependency resolver code, if required.
def load_needed_dependencies!
  super
  if File.exist?(policyfile)
    debug("Policyfile found at #{policyfile}, using Policyfile to resolve cookbook dependencies")
    Chef::Policyfile.load!(logger:)
  elsif File.exist?(berksfile)
    debug("Berksfile found at #{berksfile}, using Berkshelf to resolve cookbook dependencies")
    Chef::Berkshelf.load!(logger:)
  end
end

def omnibus_dir_option

Other tags:
    Api: - private

Returns:
  • (String) - Correct option per platform to specify the the
def omnibus_dir_option
  windows_os? ? "-download_directory" : "-d"
end

def policyfile

Other tags:
    Api: - private

Returns:
  • (String) - an absolute path to a Policyfile, relative to the
def policyfile
  policyfile_basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
  File.expand_path(policyfile_basename, config[:kitchen_root])
end

def prepare_config_idempotency_check(data)

Other tags:
    Api: - private
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

def prepare_config_rb

Other tags:
    Api: - private
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

def product_version

Returns:
  • (String, Symbol, NilClass) - version or nil if not applicable
def product_version
  case config[:require_chef_omnibus]
  when FalseClass
    nil
  when TrueClass
    config[:product_version]
  else
    config[:require_chef_omnibus]
  end
end

def sanity_check_sandbox_options!

Other tags:
    Api: - private

Raises:
  • (UserError) -

Returns:
  • (void) -
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

def script_for_omnibus_version

Other tags:
    Api: - private

Returns:
  • (String) - contents of version based install script
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

def script_for_product

Other tags:
    Api: - private

Returns:
  • (String) - contents of product based install script
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 config[:chef_license_key]
      opts[:install_command_options][:license_id] = config[:chef_license_key]
    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

def shell_code_from_file(vars, file)

Other tags:
    Api: - private

Returns:
  • (String) - command

Parameters:
  • file (String) -- file basename (without extension) containing
  • vars (String) -- shell variables, as a String
def shell_code_from_file(vars, file)
  src_file = File.join(
    File.dirname(__FILE__),
    %w{.. .. .. support},
    file + (powershell_shell? ? ".ps1" : ".sh")
  )
  wrap_shell_code([vars, "", File.read(src_file)].join("\n"))
end

def supports_policyfile?

Other tags:
    Api: - private

Returns:
  • (Boolean) -

Other tags:
    Abstract: -
def supports_policyfile?
  false
end

def wrapped_chef_cmd(base_cmd, configfile, append: "")

Other tags:
    Api: - private
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