lib/kitchen/provisioner/chef_target.rb
# # Author:: Thomas Heinen (<thomas.heinen@gmail.com>) # # Copyright (C) 2023, Thomas Heinen # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require_relative "chef_infra" module Kitchen module Provisioner # Chef Target provisioner with enterprise gem delegation support. # # This provisioner will automatically detect and use kitchen-chef-enterprise # or kitchen-cinc if they are installed, providing a seamless upgrade path # for enterprise Chef features. # # @author Thomas Heinen <thomas.heinen@gmail.com> class ChefTarget < ChefInfra # Factory method that returns the appropriate provisioner implementation. # If an enterprise gem (kitchen-chef-enterprise or kitchen-cinc) is available, # delegate to its implementation. Otherwise, use the standard implementation. # # @param config [Hash] configuration hash # @return [ChefTarget] provisioner instance def self.new(config = {}) enterprise_gem = ChefBase.enterprise_gem_available? if enterprise_gem begin omnibus_chef_class = self require "#{enterprise_gem}/provisioner/chef_target" enterprise_class = Kitchen::Provisioner.const_get(:ChefTarget) if enterprise_class != omnibus_chef_class if config[:instance] && config[:instance].respond_to?(:logger) config[:instance].logger.info("Using #{enterprise_gem} implementation of ChefTarget provisioner") end return enterprise_class.allocate.tap { |instance| instance.send(:initialize, config) } end rescue LoadError, NameError => e if config[:instance] && config[:instance].respond_to?(:logger) config[:instance].logger.debug("Could not load enterprise provisioner, using kitchen-omnibus-chef: #{e.message}") end end end allocate.tap { |instance| instance.send(:initialize, config) } end MIN_VERSION_REQUIRED = "19.0.0".freeze class ChefVersionTooLow < UserError; end class ChefClientNotFound < UserError; end class RequireTrainTransport < UserError; end default_config :install_strategy, "none" default_config :sudo, true def install_command; ""; end def init_command; ""; end def prepare_command; ""; end def chef_args(client_rb_filename) # Dummy execution to initialize and test remote connection connection = instance.remote_exec("echo Connection established") check_transport(connection) check_local_chef_client instance_name = instance.name credentials_file = File.join(kitchen_basepath, ".kitchen", instance_name + ".ini") File.write(credentials_file, connection.credentials_file) super.push( "--target #{instance_name}", "--credentials #{credentials_file}" ) end def check_transport(connection) debug("Checking for active transport") unless connection.respond_to? "train_uri" error("Chef Target Mode provisioner requires a Train-based transport like kitchen-transport-train") raise RequireTrainTransport.new("No Train transport") end debug("Kitchen transport responds to train_uri function call, as required") end def check_local_chef_client debug("Checking for chef-client version") begin client_version = `chef-client -v`.chop.split(":")[-1] rescue Errno::ENOENT => e error("Error determining Chef Infra version: #{e.exception.message}") raise ChefClientNotFound.new("Need chef-client installed locally") end minimum_version = Gem::Version.new(MIN_VERSION_REQUIRED) installed_version = Gem::Version.new(client_version) if installed_version < minimum_version error("Found Chef Infra version #{installed_version}, but require #{minimum_version} for Target Mode") raise ChefVersionTooLow.new("Need version #{MIN_VERSION_REQUIRED} or higher") end debug("Chef Infra found and version constraints match") end def kitchen_basepath instance.driver.config[:kitchen_root] end def create_sandbox super # Change config.rb to point to the local sandbox path, not to /tmp/kitchen config[:root_path] = sandbox_path prepare_config_rb end def call(state) remote_connection = instance.transport.connection(state) config[:uploads].to_h.each do |locals, remote| debug("Uploading #{Array(locals).join(", ")} to #{remote}") remote_connection.upload(locals.to_s, remote) end # no installation create_sandbox # no prepare command # Stream output to logger require "open3" Open3.popen2e(run_command) do |_stdin, output, _thread| output.each { |line| logger << line } end info("Downloading files from #{instance.to_str}") config[:downloads].to_h.each do |remotes, local| debug("Downloading #{Array(remotes).join(", ")} to #{local}") remote_connection.download(remotes, local) end debug("Download complete") rescue Kitchen::Transport::TransportFailed => ex raise ActionFailed, ex.message ensure cleanup_sandbox end end end end