class Kitchen::Provisioner::ChefBase
Common implementation details for Chef-related provisioners.
@author Fletcher Nichol <fnichol@nichol.ca>
Public Class Methods
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
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
(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
(see Base#create_sandbox
)
Kitchen::Provisioner::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
# 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
(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
(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
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
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
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
@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
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
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
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
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
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
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
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
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
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
# 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
@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
@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
# File lib/kitchen/provisioner/chef_base.rb, line 337 def last_exit_code "; exit $LastExitCode" if powershell_shell? end
Load cookbook dependency resolver code, if required.
(see Base#load_needed_dependencies!
)
Kitchen::Configurable#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
@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
@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
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
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
@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
@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
@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
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
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