# # Author:: Fletcher Nichol () # # Copyright (C) 2013, Fletcher Nichol # # 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 # # http://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 "shellwords" require "rbconfig" require_relative "../../errors" require_relative "../../logging" require_relative "../../shell_out" module Kitchen module Provisioner module Chef # Chef cookbook resolver that uses Policyfiles to calculate dependencies. # # @author Fletcher Nichol class Policyfile include Logging include ShellOut # Creates a new cookbook resolver. # # @param policyfile [String] path to a Policyfile # @param path [String] path in which to vendor the resulting # cookbooks # @param logger [Kitchen::Logger] a logger to use for output, defaults # to `Kitchen.logger` def initialize(policyfile, path, logger: Kitchen.logger, always_update: false) @policyfile = policyfile @path = path @logger = logger @always_update = always_update end # Loads the library code required to use the resolver. # # @param logger [Kitchen::Logger] a logger to use for output, defaults # to `Kitchen.logger` def self.load!(logger: Kitchen.logger) detect_chef_command!(logger) end # Performs the cookbook resolution and vendors the resulting cookbooks # in the desired path. def resolve info("Exporting cookbook dependencies from Policyfile #{path}...") run_command("chef export #{escape_path(policyfile)} #{escape_path(path)} --force") end # Runs `chef install` to determine the correct cookbook set and # generate the policyfile lock. def compile if File.exist?(lockfile) info("Installing cookbooks for Policyfile #{policyfile} using `chef install`") else info("Policy lock file doesn't exist, running `chef install` for "\ "Policyfile #{policyfile}...") end run_command("chef install #{escape_path(policyfile)}") if always_update info("Updating policy lock using `chef update`") run_command("chef update #{escape_path(policyfile)}") end end # Return the path to the lockfile corresponding to this policyfile. # # @return [String] def lockfile policyfile.gsub(/\.rb\Z/, ".lock.json") end private # @return [String] path to a Policyfile # @api private attr_reader :policyfile # @return [String] path in which to vendor the resulting cookbooks # @api private attr_reader :path # @return [Kitchen::Logger] a logger to use for output # @api private attr_reader :logger # @return [Boolean] If true, always update cookbooks in the policy. # @api private attr_reader :always_update # Escape spaces in a path in way that works with both Sh (Unix) and # Windows. # # @param path [String] Path to escape # @return [String] # @api private def escape_path(path) if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ # I know what you're thinking: "just use Shellwords.escape". That # method produces incorrect results on Windows with certain input # which would be a metacharacter in Sh but is not for one or more of # Windows command line parsing libraries. This covers the 99% case of # spaces in the path without breaking other stuff. if path =~ /[ \t\n\v"]/ "\"#{path.gsub(/[ \t\n\v\"\\]/) { |m| '\\' + m[0] }}\"" else path end else Shellwords.escape(path) end end class << self private # Ensure the `chef` command is in the path. # # @param logger [Kitchen::Logger] the logger to use # @raise [UserError] if the `chef` command is not in the PATH # @api private def detect_chef_command!(logger) unless ENV["PATH"].split(File::PATH_SEPARATOR).any? do |path| if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ # Windows could have different extentions: BAT, EXE or NONE %w{chef chef.exe chef.bat}.each do |bin| File.exist?(File.join(path, bin)) end else File.exist?(File.join(path, "chef")) end end logger.fatal("The `chef` executable cannot be found in your " \ "PATH. Ensure you have installed ChefDK or Chef Workstation " \ "from https://downloads.chef.io and that your PATH " \ "setting includes the path to the `chef` command.") raise UserError, "Could not find the chef executable in your PATH." end end end end end end end