255 lines
9.5 KiB
Ruby
255 lines
9.5 KiB
Ruby
#
|
|
# Copyright 2015-2017, Noah Kantrowitz
|
|
#
|
|
# 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 'chef/resource'
|
|
require 'chef/provider'
|
|
require 'poise'
|
|
|
|
|
|
module PoiseLanguages
|
|
module System
|
|
# A `poise_language_system` resource to manage installing a language from
|
|
# system packages. This is an internal implementation detail of
|
|
# poise-languages.
|
|
#
|
|
# @api private
|
|
# @since 1.0
|
|
# @provides poise_languages_system
|
|
# @action install
|
|
# @action upgrade
|
|
# @action uninstall
|
|
class Resource < Chef::Resource
|
|
include Poise
|
|
provides(:poise_languages_system)
|
|
actions(:install, :upgrade, :uninstall)
|
|
|
|
# @!attribute package_name
|
|
# Name of the main package for the language.
|
|
# @return [String]
|
|
attribute(:package_name, kind_of: String, name_attribute: true)
|
|
# @!attribute dev_package
|
|
# Name of the development headers package, or false to disable
|
|
# installing headers. By default computed from {package_name}.
|
|
# @return [String, false]
|
|
attribute(:dev_package, kind_of: [String, FalseClass], default: lazy { default_dev_package })
|
|
# @!attribute dev_package_overrides
|
|
# A hash of override names for dev packages that don't match the normal
|
|
# naming scheme.
|
|
# @return [Hash<String, String>]
|
|
attribute(:dev_package_overrides, kind_of: Hash, default: lazy { {} })
|
|
# @!attribute package_version
|
|
# Version of the package(s) to install. This is distinct from {version},
|
|
# and is the specific version package version, not the language version.
|
|
# By default this is unset meaning the latest version will be used.
|
|
# @return [String, nil]
|
|
attribute(:package_version, kind_of: [String, NilClass])
|
|
# @!attribute parent
|
|
# Resource for the language runtime. Used only for messages.
|
|
# @return [Chef::Resource]
|
|
attribute(:parent, kind_of: Chef::Resource, required: true)
|
|
# @!attributes version
|
|
# Language version prefix. This prefix determines which version of the
|
|
# language to install, following prefix matching rules.
|
|
# @return [String]
|
|
attribute(:version, kind_of: String, default: '')
|
|
|
|
# Compute the default package name for the development headers.
|
|
#
|
|
# @return [String]
|
|
def default_dev_package
|
|
# Check for an override.
|
|
return dev_package_overrides[package_name] if dev_package_overrides.include?(package_name)
|
|
suffix = node.value_for_platform_family(debian: '-dev', rhel: '-devel', fedora: '-devel')
|
|
# Platforms like Arch and Gentoo don't need this anyway. I've got no
|
|
# clue how Amazon Linux does this.
|
|
if suffix
|
|
package_name + suffix
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
# The default provider for `poise_languages_system`.
|
|
#
|
|
# @api private
|
|
# @since 1.0
|
|
# @see Resource
|
|
# @provides poise_languages_system
|
|
class Provider < Chef::Provider
|
|
include Poise
|
|
provides(:poise_languages_system)
|
|
|
|
# The `install` action for the `poise_languages_system` resource.
|
|
#
|
|
# @return [void]
|
|
def action_install
|
|
notifying_block do
|
|
install_packages
|
|
run_action_hack
|
|
end
|
|
end
|
|
|
|
# The `upgrade` action for the `poise_languages_system` resource.
|
|
#
|
|
# @return [void]
|
|
def action_upgrade
|
|
notifying_block do
|
|
upgrade_packages
|
|
run_action_hack
|
|
end
|
|
end
|
|
|
|
# The `uninstall` action for the `poise_languages_system` resource.
|
|
#
|
|
# @return [void]
|
|
def action_uninstall
|
|
notifying_block do
|
|
uninstall_packages
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Install the needed language packages.
|
|
#
|
|
# @api private
|
|
# @return [Array<Chef::Resource>]
|
|
def install_packages
|
|
packages = {new_resource.package_name => new_resource.package_version}
|
|
# If we are supposed to install the dev package, grab it using the same
|
|
# version as the main package.
|
|
if new_resource.dev_package
|
|
packages[new_resource.dev_package] = new_resource.package_version
|
|
end
|
|
Chef::Log.debug("[#{new_resource.parent}] Building package resource using #{packages.inspect}.")
|
|
|
|
# Check for multi-package support.
|
|
package_resource_class = Chef::Resource.resource_for_node(:package, node)
|
|
package_provider_class = package_resource_class.new('multipackage_check', run_context).provider_for_action(:install)
|
|
package_resources = if package_provider_class.respond_to?(:use_multipackage_api?) && package_provider_class.use_multipackage_api?
|
|
package packages.keys do
|
|
version packages.values
|
|
end
|
|
else
|
|
# Fallback for non-multipackage.
|
|
packages.map do |pkg_name, pkg_version|
|
|
package pkg_name do
|
|
version pkg_version
|
|
end
|
|
end
|
|
end
|
|
|
|
# Apply some settings to all of the resources.
|
|
Array(package_resources).each do |res|
|
|
res.retries(5)
|
|
res.define_singleton_method(:apply_action_hack?) { true }
|
|
end
|
|
end
|
|
|
|
# Upgrade the needed language packages.
|
|
#
|
|
# @api private
|
|
# @return [Array<Chef::Resource>]
|
|
def upgrade_packages
|
|
install_packages.each do |res|
|
|
res.action(:upgrade)
|
|
end
|
|
end
|
|
|
|
# Uninstall the needed language packages.
|
|
#
|
|
# @api private
|
|
# @return [Array<Chef::Resource>]
|
|
def uninstall_packages
|
|
install_packages.each do |res|
|
|
res.action(node.platform_family?('debian') ? :purge : :remove)
|
|
end
|
|
end
|
|
|
|
# Run the requested action for all package resources. This exists because
|
|
# we inject our version check in to the provider directly and I want to
|
|
# only run the provider action once for performance. It is otherwise
|
|
# mostly a stripped down version of Chef::Resource#run_action.
|
|
#
|
|
# @param action [Symbol] Action to run on all package resources.
|
|
# @return [void]
|
|
def run_action_hack
|
|
# If new_resource.package_version is set, skip this madness.
|
|
return if new_resource.package_version
|
|
|
|
# Process every resource in the current collection, which is bounded
|
|
# by notifying_block.
|
|
run_context.resource_collection.each do |resource|
|
|
# Only apply to things we tagged above.
|
|
next unless resource.respond_to?(:apply_action_hack?) && resource.apply_action_hack?
|
|
|
|
Array(resource.action).each do |action|
|
|
# Reset it so we have a clean baseline.
|
|
resource.updated_by_last_action(false)
|
|
# Grab the provider.
|
|
provider = resource.provider_for_action(action)
|
|
provider.action = action
|
|
# Inject our check for the candidate version. This will actually
|
|
# get run during run_action below.
|
|
patch_load_current_resource!(provider, new_resource.version)
|
|
# Run our action.
|
|
Chef::Log.debug("[#{new_resource.parent}] Running #{provider} with #{action}")
|
|
provider.run_action(action)
|
|
# Check updated flag.
|
|
new_resource.updated_by_last_action(true) if resource.updated_by_last_action?
|
|
end
|
|
|
|
# Make sure the resource doesn't run again when notifying_block ends.
|
|
resource.action(:nothing)
|
|
end
|
|
end
|
|
|
|
# Hack a provider object to run our verification code.
|
|
#
|
|
# @param provider [Chef::Provider] Provider object to patch.
|
|
# @param version [String] Language version prefix to check for.
|
|
# @return [void]
|
|
def patch_load_current_resource!(provider, version)
|
|
# Create a closure module and inject it.
|
|
provider.extend Module.new {
|
|
# Patch load_current_resource to run our verification logic after
|
|
# the normal code.
|
|
define_method(:load_current_resource) do
|
|
super().tap do |_|
|
|
each_package do |package_name, new_version, current_version, candidate_version|
|
|
# In Chef 12.14+, candidate_version is a Chef::Decorator::Lazy object
|
|
# so we need the nil? check to see if the object being proxied is
|
|
# nil (i.e. there is no version).
|
|
unless candidate_version && (!candidate_version.nil?) && (!candidate_version.empty?) && candidate_version.start_with?(version)
|
|
# Don't display a wonky error message if there is no candidate.
|
|
candidate_label = if candidate_version && (!candidate_version.nil?) && (!candidate_version.empty?)
|
|
candidate_version
|
|
else
|
|
candidate_version.inspect
|
|
end
|
|
raise PoiseLanguages::Error.new("Package #{package_name} would install #{candidate_label}, which does not match #{version.empty? ? version.inspect : version}. Please set the package_name or package_version provider options.")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|