lib/puppet/provider/package/brew.rb (182 lines of code) (raw):
require 'puppet/provider/package'
Puppet::Type.type(:package).provide(:brew, :parent => Puppet::Provider::Package) do
desc 'Package management using HomeBrew on OSX'
confine :operatingsystem => :darwin
has_feature :installable
has_feature :uninstallable
has_feature :upgradeable
has_feature :versionable
has_feature :install_options
commands :brew => ENV.fetch('PUPPET_HOMEBREW_COMMAND', 'brew')
commands :stat => '/usr/bin/stat'
def self.execute(cmd, failonfail = false, combine = false)
brew_cmd = command(:brew)
owner = stat('-nf', '%Uu', brew_cmd).to_i
group = stat('-nf', '%Ug', brew_cmd).to_i
home = Etc.getpwuid(owner).dir
if owner == 0
raise Puppet::ExecutionFailure, "Homebrew does not support installations owned by the \"root\" user. Please check the permissions of #{brew_cmd}"
end
# the uid and gid can only be set if running as root
if Process.uid == 0
uid = owner
gid = group
else
uid = nil
gid = nil
end
custom_env = {'HOME' => home}
custom_env['HOMEBREW_CHANGE_ARCH_TO_ARM'] = '1' if Facter.value(:has_arm64)
cmd = ["arch", "-arm64"].append(cmd) if Facter.value(:has_arm64)
if Puppet.features.bundled_environment?
Bundler.with_clean_env do
super(cmd, :uid => uid, :gid => gid, :combine => combine,
:custom_environment => custom_env, :failonfail => failonfail)
end
else
super(cmd, :uid => uid, :gid => gid, :combine => combine,
:custom_environment => custom_env, :failonfail => failonfail)
end
end
def self.instances
package_list.collect { |hash| new(hash) }
end
def execute(*args)
# This does not return exit codes in puppet <3.4.0
# See https://projects.puppetlabs.com/issues/2538
self.class.execute(*args)
end
def fix_checksum(files)
begin
for file in files
File.delete(file)
end
rescue Errno::ENOENT
Puppet.warning "Could not remove mismatched checksum files #{files}"
end
raise Puppet::ExecutionFailure, "Checksum error for package #{name} in files #{files}"
end
def resource_name
if @resource[:name].match(/^https?:\/\//)
@resource[:name]
else
@resource[:name].downcase
end
end
def install_name
should = @resource[:ensure].downcase
case should
when true, false, Symbol
resource_name
else
"#{resource_name}@#{should}"
end
end
def install_options
Array(resource[:install_options]).flatten.compact
end
def latest
cmd_line = [command(:brew), :info, '--json', resource_name]
cmd_output = execute(cmd_line)
data = JSON.parse(cmd_output, symbolize_names: true)
if data.count < 1
uppet.debug "Package #{options[:justme]} not found"
end
if data.count > 1
Puppet.warning "Multiple matches for package #{options[:justme]} - using first one found"
end
pkg_data = data[0]
Puppet.debug "Found package #{pkg_data[:name]}"
return pkg_data[:versions][:stable]
end
def query
self.class.package_list(:justme => resource_name)
end
def do_install
begin
output = execute([command(:brew), :install, install_name, *install_options], :failonfail => true)
if output =~ /sha256 checksum/
Puppet.debug "Fixing checksum error..."
mismatched = output.match(/Already downloaded: (.*)/).captures
fix_checksum(mismatched)
end
rescue Puppet::ExecutionFailure => detail
raise Puppet::Error, "Could not install package: #{detail}"
end
end
def do_upgrade
begin
execute([command(:brew), :upgrade, resource_name], :failonfail => true)
rescue Puppet::ExecutionFailure => detail
raise Puppet::Error, "Could not upgrade package: #{detail}"
end
end
def install
begin
Puppet.debug "Looking for #{install_name} package..."
execute([command(:brew), :info, install_name], :failonfail => true)
rescue Puppet::ExecutionFailure
raise Puppet::Error, "Could not find package: #{install_name}"
end
Puppet.debug "Package found, installing..."
do_install
end
def uninstall
begin
Puppet.debug "Uninstalling #{resource_name}"
execute([command(:brew), :uninstall, resource_name], :failonfail => true)
rescue Puppet::ExecutionFailure => detail
raise Puppet::Error, "Could not uninstall package: #{detail}"
end
end
def update
if query
Puppet.debug "Upgrading #{resource_name}"
do_upgrade
else
Puppet.debug "Installing #{resource_name}"
do_install
end
end
def self.package_list(options={})
Puppet.debug "Listing installed packages"
cmd_line = [command(:brew), :list, '--versions']
if options[:justme]
cmd_line += [ options[:justme] ]
end
begin
cmd_output = execute(cmd_line)
rescue Puppet::ExecutionFailure => detail
raise Puppet::Error, "Could not list packages: #{detail}"
end
# Exclude extraneous lines from stdout that interfere with the parsing
# logic below. These look like they should be on stderr anyway based
# on comparison to other output on stderr. homebrew bug?
re_excludes = Regexp.union([
/^==>.*/,
/^Tapped \d+ formulae.*/,
])
lines = cmd_output.lines.delete_if { |line| line.match(re_excludes) }
if options[:justme]
if lines.empty?
Puppet.debug "Package #{options[:justme]} not installed"
return nil
else
if lines.length > 1
Puppet.warning "Multiple matches for package #{options[:justme]} - using first one found"
end
line = lines.shift
Puppet.debug "Found package #{line}"
return name_version_split(line)
end
else
return lines.map{ |l| name_version_split(l) }
end
end
def self.name_version_split(line)
if line =~ (/^(\S+)\s+(.+)/)
{
:name => $1,
:ensure => $2,
:provider => :brew
}
else
Puppet.warning "Could not match #{line}"
nil
end
end
end