Vendor the external cookbooks

Knife-Zero doesn't include Berkshelf support, so vendoring everything in
the repo is convenient again
This commit is contained in:
Greg Karékinian
2019-10-13 18:32:56 +02:00
parent aa66743166
commit 049d5dd006
1245 changed files with 100630 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
#
# 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.
#
module PoiseLanguages
autoload :Command, 'poise_languages/command'
autoload :Error, 'poise_languages/error'
autoload :Scl, 'poise_languages/scl'
autoload :Static, 'poise_languages/static'
autoload :System, 'poise_languages/system'
autoload :Utils, 'poise_languages/utils'
autoload :VERSION, 'poise_languages/version'
end

View File

@@ -0,0 +1,25 @@
#
# 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.
#
module PoiseLanguages
# A namespace for language-command-related stuff.
#
# @since 1.0.0
module Command
autoload :Mixin, 'poise_languages/command/mixin'
end
end

View File

@@ -0,0 +1,241 @@
#
# 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 'shellwords'
require 'poise'
require 'poise_languages/error'
require 'poise_languages/utils'
module PoiseLanguages
module Command
# A mixin for resources and providers that run language commands.
#
# @since 1.0.0
module Mixin
include Poise::Utils::ResourceProviderMixin
# A mixin for resources that run language commands. Also available as a
# parameterized mixin via `include PoiseLanguages::Command::Mixin::Resource(name)`.
#
# @example
# class MyLangThing
# include PoiseLanguages::Command::Mixin::Resource(:mylang)
# # ...
# end
module Resource
include Poise::Resource
poise_subresource(true)
private
# Implementation of the $name accessor.
#
# @api private
# @param name [Symbol] Language name.
# @param runtime [Symbol] Language runtime resource name.
# @param val [String, Chef::Resource, Poise::NOT_PASSED, nil] Accessor value.
# @return [String]
def language_command_runtime(name, runtime, default_binary, val=Poise::NOT_PASSED)
unless val == Poise::NOT_PASSED
path_arg = parent_arg = nil
# Figure out which property we are setting.
if val.is_a?(String)
# Check if it is a runtime resource.
begin
parent_arg = run_context.resource_collection.find("#{runtime}[#{val}]")
rescue Chef::Exceptions::ResourceNotFound
# Check if something looks like a path, defined as containing
# either / or \. While a single word could be a path, I think the
# UX win of better error messages should take priority.
might_be_path = val =~ %r{/|\\}
if might_be_path
Chef::Log.debug("[#{self}] #{runtime}[#{val}] not found, treating it as a path")
path_arg = val
else
# Surface the error up to the user.
raise
end
end
else
parent_arg = val
end
# Set both attributes.
send(:"parent_#{name}", parent_arg)
set_or_return(name, path_arg, kind_of: [String, NilClass])
else
# Getter behavior. Using the ivar directly is kind of gross but oh well.
instance_variable_get(:"@#{name}") || default_language_command_runtime(name, default_binary)
end
end
# Compute the path to the default runtime binary.
#
# @api private
# @param name [Symbol] Language name.
# @return [String]
def default_language_command_runtime(name, default_binary)
parent = send(:"parent_#{name}")
if parent
parent.send(:"#{name}_binary")
else
PoiseLanguages::Utils.which(default_binary || name.to_s)
end
end
# Inherit language parent from another resource.
#
# @api private
# @param name [Symbol] Language name.
# @param resource [Chef::Resource] Resource to inherit from.
# @return [void]
def language_command_runtime_from_parent(name, resource)
parent = resource.send(:"parent_#{name}")
if parent
send(:"parent_#{name}", parent)
else
path = resource.send(name)
if path
send(name, path)
end
end
end
module ClassMethods
# Configure this module or class for a specific language.
#
# @param name [Symbol] Language name.
# @param runtime [Symbol] Language runtime resource name.
# @param timeout [Boolean] Enable the timeout attribute.
# @param default_binary [String] Name of the default language binary.
# @return [void]
def language_command_mixin(name, runtime: :"#{name}_runtime", timeout: true, default_binary: nil)
# Create the parent attribute.
parent_attribute(name, type: runtime, optional: true)
# Timeout attribute for the shell_out wrappers in the provider.
attribute(:timeout, kind_of: Integer, default: 900) if timeout
# Create the main accessor for the parent/path.
define_method(name) do |val=Poise::NOT_PASSED|
language_command_runtime(name, runtime, default_binary, val)
end
# Create the method to inherit settings from another resource.
define_method(:"#{name}_from_parent") do |resource|
language_command_runtime_from_parent(name, resource)
end
private :"#{name}_from_parent"
end
def language_command_default_binary(val=Poise::NOT_PASSED)
@language_command_default_binary = val if val != Poise::NOT_PASSED
@language_command_default_binary
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
Poise::Utils.parameterized_module(self) {|*args| language_command_mixin(*args) }
end # /module Resource
# A mixin for providers that run language commands.
module Provider
include Poise::Utils::ShellOut
private
# Run a command using the configured language via `shell_out`.
#
# @api private
# @param name [Symbol] Language name.
# @param command_args [Array] Arguments to `shell_out`.
# @return [Mixlib::ShellOut]
def language_command_shell_out(name, *command_args, **options)
# Inject our environment variables if needed.
options[:environment] ||= {}
parent = new_resource.send(:"parent_#{name}")
if parent
options[:environment].update(parent.send(:"#{name}_environment"))
end
# Inject other options.
options[:timeout] ||= new_resource.timeout
# Find the actual binary to use. Raise an exception if we see false
# which happens if no parent resource is found, no explicit default
# binary was given, and which() fails to find a thing.
binary = new_resource.send(name)
raise Error.new("Unable to find a #{name} binary for command: #{command_args.is_a?(Array) ? Shellwords.shelljoin(command_args) : command_args}") unless binary
command = if command_args.length == 1 && command_args.first.is_a?(String)
# String mode, sigh.
"#{Shellwords.escape(binary)} #{command_args.first}"
else
# Array mode. Handle both ('one', 'two') and (['one', 'two']).
[binary] + command_args.flatten
end
Chef::Log.debug("[#{new_resource}] Running #{name} command: #{command.is_a?(Array) ? Shellwords.shelljoin(command) : command}")
# Run the command
poise_shell_out(command, options)
end
# Run a command using the configured language via `shell_out!`.
#
# @api private
# @param name [Symbol] Language name.
# @param command_args [Array] Arguments to `shell_out!`.
# @return [Mixlib::ShellOut]
def language_command_shell_out!(name, *command_args)
send(:"#{name}_shell_out", *command_args).tap(&:error!)
end
module ClassMethods
# Configure this module or class for a specific language.
#
# @param name [Symbol] Language name.
# @return [void]
def language_command_mixin(name)
define_method(:"#{name}_shell_out") do |*command_args|
language_command_shell_out(name, *command_args)
end
private :"#{name}_shell_out"
define_method(:"#{name}_shell_out!") do |*command_args|
language_command_shell_out!(name, *command_args)
end
private :"#{name}_shell_out!"
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
Poise::Utils.parameterized_module(self) {|*args| language_command_mixin(*args) }
end # /module Provider
Poise::Utils.parameterized_module(self) {|*args| language_command_mixin(*args) }
end # /module Mixin
end
end

View File

@@ -0,0 +1,21 @@
#
# 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.
#
module PoiseLanguages
class Error < ::Exception
end
end

View File

@@ -0,0 +1,24 @@
#
# 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.
#
module PoiseLanguages
module Scl
autoload :Mixin, 'poise_languages/scl/mixin'
autoload :Resource, 'poise_languages/scl/resource'
autoload :Provider, 'poise_languages/scl/resource'
end
end

View File

@@ -0,0 +1,134 @@
#
# 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 'poise_languages/scl/resource'
module PoiseLanguages
module Scl
module Mixin
private
def install_scl_package
pkg = scl_package
poise_languages_scl options[:package_name] || pkg[:name] do
action :upgrade if options[:package_upgrade]
dev_package options[:dev_package] == true ? pkg[:devel_name] : options[:dev_package]
parent new_resource
version options[:package_version]
end
end
def uninstall_scl_package
install_scl_package.tap do |r|
r.action(:uninstall)
end
end
def scl_package
@scl_package ||= self.class.find_scl_package(node, options['version']).tap do |p|
raise PoiseLanguages::Error.new("No SCL repoistory package for #{node['platform']} #{node['platform_version']}") unless p
end
end
def scl_folder
::File.join('', 'opt', 'rh', scl_package[:name])
end
def scl_environment
parse_enable_file(::File.join(scl_folder, 'enable'))
end
# Parse an SCL enable file to extract the environment variables set in it.
#
# @param path [String] Path to the enable file.
# @return [Hash<String, String>]
def parse_enable_file(path, env={})
# Doesn't exist yet, so running Python will fail anyway. Just make sure
# it fails in the expected way.
return {} unless File.exist?(path)
# Yes, this is a bash parser in regex. Feel free to be mad at me.
IO.readlines(path).inject(env) do |memo, line|
if match = line.match(/^export (\w+)=(.*)$/)
memo[match[1]] = match[2].gsub(/\$(?:\{(\w+)(:\+:\$\{\w+\})?\}|(\w+))/) do
key = $1 || $3
value = (memo[key] || ENV[key]).to_s
value = ":#{value}" if $2 && !value.empty?
value
end
elsif match = line.match(/^\. scl_source enable (\w+)$/)
# Parse another file.
memo.update(parse_enable_file(::File.join('', 'opt', 'rh', match[1], 'enable'), memo))
end
memo
end
end
module ClassMethods
def provides_auto?(node, resource)
# They don't build 32-bit versions for these and only for RHEL/CentOS.
# TODO: What do I do about Fedora and/or Amazon?
return false unless node['kernel']['machine'] == 'x86_64' && node.platform?('redhat', 'centos')
version = inversion_options(node, resource)['version']
!!find_scl_package(node, version)
end
# Set some default inversion provider options. Package name can't get
# a default value here because that would complicate the handling of
# {system_package_candidates}.
#
# @api private
def default_inversion_options(node, resource)
super.merge({
# Install dev headers?
dev_package: true,
# Manual overrides for package name and/or version.
package_name: nil,
package_version: nil,
# Set to true to use action :upgrade on system packages.
package_upgrade: false,
})
end
def find_scl_package(node, version)
platform_version = ::Gem::Version.create(node['platform_version'])
# Filter out anything that doesn't match this EL version.
candidate_packages = scl_packages.select {|p| p[:platform_version].satisfied_by?(platform_version) }
# Find something with a prefix match on the Python version.
candidate_packages.find {|p| p[:version].start_with?(version) }
end
private
def scl_packages
@scl_packages ||= []
end
def scl_package(version, name, devel_name=nil, platform_version='>= 6.0')
scl_packages << {version: version, name: name, devel_name: devel_name, platform_version: ::Gem::Requirement.create(platform_version)}
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,159 @@
#
# 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 Scl
# A `poise_language_scl` resource to manage installing a language from
# SCL packages. This is an internal implementation detail of
# poise-languages.
#
# @api private
# @since 1.0
# @provides poise_languages_scl
# @action install
# @action uninstall
class Resource < Chef::Resource
include Poise
provides(:poise_languages_scl)
actions(:install, :upgrade, :uninstall)
# @!attribute package_name
# Name of the SCL package for the language.
# @return [String]
attribute(:package_name, kind_of: String, name_attribute: true)
# @!attribute dev_package
# Name of the -devel package with headers and whatnot.
# @return [String, nil]
attribute(:dev_package, kind_of: [String, NilClass])
# @!attribute version
# Version of the SCL package(s) to install. If unset, follows the same
# semantics as the core `package` resource.
# @return [String, nil]
attribute(: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)
end
# The default provider for `poise_languages_scl`.
#
# @api private
# @since 1.0
# @see Resource
# @provides poise_languages_scl
class Provider < Chef::Provider
include Poise
provides(:poise_languages_scl)
# The `install` action for the `poise_languages_scl` resource.
#
# @return [void]
def action_install
notifying_block do
install_scl_repo
flush_yum_cache
install_scl_package(:install)
install_scl_devel_package(:install) if new_resource.dev_package
end
end
# The `upgrade` action for the `poise_languages_scl` resource.
#
# @return [void]
def action_upgrade
notifying_block do
install_scl_repo
flush_yum_cache
install_scl_package(:upgrade)
install_scl_devel_package(:upgrade) if new_resource.dev_package
end
end
# The `uninstall` action for the `poise_languages_scl` resource.
#
# @return [void]
def action_uninstall
notifying_block do
uninstall_scl_devel_package if new_resource.dev_package
uninstall_scl_package
end
end
private
def install_scl_repo
if node.platform?('redhat')
# Set up the real RHSCL subscription.
# NOTE: THIS IS NOT TESTED BECAUSE REDHAT DOESN'T OFFER ANY WAY TO DO
# AUTOMATED TESTING. IF YOU USE REDHAT AND THIS BREAKS, PLEASE LET ME
# KNOW BY FILING A GITHUB ISSUE AT http://github.com/poise/poise-languages/issues/new.
repo_name = "rhel-server-rhscl-#{node['platform_version'][0]}-rpms"
execute "subscription-manager repos --enable #{repo_name}" do
not_if { shell_out!('subscription-manager repos --list-enabled').stdout.include?(repo_name) }
end
else
package 'centos-release-scl-rh' do
# Using upgrade here because changes very very rare and always
# important when they happen. If this breaks your prod infra, I'm
# sorry :-(
action :upgrade
retries 5
end
end
end
def flush_yum_cache
ruby_block 'flush_yum_cache' do
block do
# Equivalent to flush_cache after: true
Chef::Provider::Package::Yum::YumCache.instance.reload
end
end
end
def install_scl_package(action)
package new_resource.package_name do
action action
retries 5
version new_resource.version
end
end
def install_scl_devel_package(action)
package new_resource.dev_package do
action action
retries 5
version new_resource.version
end
end
def uninstall_scl_package
install_scl_package(:remove)
end
def uninstall_scl_devel_package
install_scl_devel_package(:remove)
end
end
end
end

View File

@@ -0,0 +1,34 @@
#
# 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 'poise/utils'
module PoiseLanguages
# Helpers for installing languages from static archives.
#
# @since 1.1.0
module Static
autoload :Mixin, 'poise_languages/static/mixin'
autoload :Resource, 'poise_languages/static/resource'
autoload :Provider, 'poise_languages/static/resource'
Poise::Utils.parameterized_module(self) do |opts|
require 'poise_languages/static/mixin'
include PoiseLanguages::Static::Mixin(opts)
end
end
end

View File

@@ -0,0 +1,144 @@
#
# 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 'poise_languages/static/resource'
module PoiseLanguages
module Static
# Mixin for language providers to install from static archives.
#
# @since 1.1.0
module Mixin
private
def install_static
url = static_url
poise_languages_static static_folder do
source url
strip_components options['strip_components']
end
end
def uninstall_static
install_static.tap do |r|
r.action(:uninstall)
end
end
def static_folder
options['path'] || ::File.join('', 'opt', "#{self.class.static_name}-#{options['static_version']}")
end
def static_url
options['url'] % static_url_variables
end
def static_url_variables
{
version: options['static_version'],
kernel: node['kernel']['name'].downcase,
machine: node['kernel']['machine'],
machine_label: self.class.static_machine_label_wrapper(node, new_resource),
}
end
module ClassMethods
attr_accessor :static_name
attr_accessor :static_versions
attr_accessor :static_machines
attr_accessor :static_url
attr_accessor :static_strip_components
attr_accessor :static_retries
def provides_auto?(node, resource)
# Check that the version starts with our project name and the machine
# we are on is supported.
resource.version.to_s =~ /^#{static_name}(-|$)/ && static_machines.include?(static_machine_label_wrapper(node, resource))
end
# Set some default inversion provider options. Package name can't get
# a default value here because that would complicate the handling of
# {system_package_candidates}.
#
# @api private
def default_inversion_options(node, resource)
super.merge({
# Path to install the package. Defaults to /opt/name-version.
path: nil,
# Number of times to retry failed downloads.
retries: static_retries,
# Full version number for use in interpolation.
static_version: static_version(node, resource),
# Value to pass to tar --strip-components.
strip_components: static_strip_components,
# URL template to download from.
url: static_url,
})
end
def static_options(name: nil, versions: [], machines: %w{linux-i686 linux-x86_64}, url: nil, strip_components: 1, retries: 5)
raise PoiseLanguages::Error.new("Static archive URL is required, on #{self}") unless url
self.static_name = name || provides.to_s
self.static_versions = versions
self.static_machines = Set.new(machines)
self.static_url = url
self.static_strip_components = strip_components
self.static_retries = retries
end
def static_version(node, resource)
raw_version = resource.version.to_s.gsub(/^#{static_name}(-|$)/, '')
if static_versions.include?(raw_version)
raw_version
else
# Prefix match or just use the given version number if not found.
# This allow mild future proofing in some cases.
static_versions.find {|v| v.start_with?(raw_version) } || raw_version
end
end
def static_machine_label(node, _resource=nil)
"#{node['kernel']['name'].downcase}-#{node['kernel']['machine']}"
end
# Wrapper for {#static_machine_label} because I need to add an argument.
# This preserves backwards compat.
#
# @api private
def static_machine_label_wrapper(node, resource)
args = [node]
arity = method(:static_machine_label).arity
args << resource if arity > 1 || arity < 0
static_machine_label(*args)
end
def included(klass)
super
klass.extend ClassMethods
end
end
extend ClassMethods
Poise::Utils.parameterized_module(self) do |opts|
static_options(opts)
end
end
end
end

View File

@@ -0,0 +1,139 @@
#
# 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 Static
# A `poise_languages_static` resource to manage installing a language from
# static binary archives. This is an internal implementation detail of
# poise-languages.
#
# @api private
# @since 1.1.0
# @provides poise_languages_static
# @action install
# @action uninstall
class Resource < Chef::Resource
include Poise
provides(:poise_languages_static)
actions(:install, :uninstall)
# @!attribute path
# Directory to install to.
# @return [String]
attribute(:path, kind_of: String, name_attribute: true)
# @!attribute download_retries
# Number of times to retry failed downloads. Defaults to 5.
# @return [Integer]
attribute(:download_retries, kind_of: Integer, default: 5)
# @!attribute source
# URL to download from.
# @return [String]
attribute(:source, kind_of: String, required: true)
# @!attribute strip_components
# Value to pass to tar --strip-components.
# @return [String, Integer, nil]
attribute(:strip_components, kind_of: [String, Integer, NilClass], default: 1)
def cache_path
@cache_path ||= ::File.join(Chef::Config[:file_cache_path], source.split(/\//).last)
end
end
# The default provider for `poise_languages_static`.
#
# @api private
# @since 1.0
# @see Resource
# @provides poise_languages_static
class Provider < Chef::Provider
include Poise
provides(:poise_languages_static)
# The `install` action for the `poise_languages_static` resource.
#
# @return [void]
def action_install
notifying_block do
download_archive
create_directory
# Unpack is handled as a notification from download_archive.
end
end
# The `uninstall` action for the `poise_languages_static` resource.
#
# @return [void]
def action_uninstall
notifying_block do
delete_archive
delete_directory
end
end
private
def create_directory
unpack_resource = unpack_archive
directory new_resource.path do
user 0
group 0
mode '755'
notifies :unpack, unpack_resource, :immediately
end
end
def download_archive
unpack_resource = unpack_archive
remote_file new_resource.cache_path do
source new_resource.source
owner 0
group 0
mode '644'
notifies :unpack, unpack_resource, :immediately if ::File.exist?(new_resource.path)
retries new_resource.download_retries
end
end
def unpack_archive
@unpack_archive ||= poise_archive new_resource.cache_path do
# Run via notification from #download_archive and #create_directory.
action :nothing
destination new_resource.path
strip_components new_resource.strip_components
end
end
def delete_archive
file new_resource.cache_path do
action :delete
end
end
def delete_directory
directory new_resource.path do
action :delete
recursive true
end
end
end
end
end

View File

@@ -0,0 +1,24 @@
#
# 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.
#
module PoiseLanguages
module System
autoload :Mixin, 'poise_languages/system/mixin'
autoload :Resource, 'poise_languages/system/resource'
autoload :Provider, 'poise_languages/system/resource'
end
end

View File

@@ -0,0 +1,170 @@
#
# 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 'poise_languages/system/resource'
module PoiseLanguages
module System
module Mixin
private
# Install a language using system packages.
#
# @api public
# @return [PoiseLanguages::System::Resource]
def install_system_packages(&block)
dev_package_overrides = system_dev_package_overrides
poise_languages_system system_package_name do
# Otherwise use the default install action.
action(:upgrade) if options['package_upgrade']
parent new_resource
# Don't pass true because we want the default computed behavior for that.
dev_package options['dev_package'] unless options['dev_package'] == true
dev_package_overrides dev_package_overrides
package_version options['package_version'] if options['package_version']
version options['version']
instance_exec(&block) if block
end
end
# Uninstall a language using system packages.
#
# @api public
# @return [PoiseLanguages::System::Resource]
def uninstall_system_packages(&block)
install_system_packages.tap do |r|
r.action(:uninstall)
r.instance_exec(&block) if block
end
end
# Compute all possible package names for a given language version. Must be
# implemented by mixin users. Versions are expressed as prefixes so ''
# matches all versions, '2' matches 2.x.
#
# @abstract
# @api public
# @param version [String] Language version prefix.
# @return [Array<String>]
def system_package_candidates(version)
raise NotImplementedError
end
# Compute the default package name for the base package for this language.
#
# @api public
# @return [String]
def system_package_name
# If we have an override, just use that.
return options['package_name'] if options['package_name']
# Look up all packages for this language on this platform.
system_packages = self.class.packages && node.value_for_platform(self.class.packages)
if !system_packages && self.class.default_package
Chef::Log.debug("[#{new_resource}] No known packages for #{node['platform']} #{node['platform_version']}, defaulting to '#{self.class.default_package}'.") if self.class.packages
system_packages = Array(self.class.default_package)
end
# Find the first value on system_package_candidates that is in system_packages.
system_package_candidates(options['version'].to_s).each do |name|
return name if system_packages.include?(name)
end
# No valid candidate. Sad trombone.
raise PoiseLanguages::Error.new("Unable to find a candidate package for version #{options['version'].to_s.inspect}. Please set package_name provider option for #{new_resource}.")
end
# A hash mapping package names to their override dev package name.
#
# @api public
# @return [Hash<String, String>]
def system_dev_package_overrides
{}
end
module ClassMethods
# Install this as a default provider if nothing else matched. Might not
# work, but worth a try at least for unknown platforms. Windows is a
# whole different story, and OS X might work sometimes so at least try.
#
# @api private
def provides_auto?(node, resource)
!node.platform_family?('windows')
end
# Set some default inversion provider options. Package name can't get
# a default value here because that would complicate the handling of
# {system_package_candidates}.
#
# @api private
def default_inversion_options(node, resource)
super.merge({
# Install dev headers?
dev_package: true,
# Manual overrides for package name and/or version.
package_name: nil,
package_version: nil,
# Set to true to use action :upgrade on system packages.
package_upgrade: false,
})
end
# @overload packages()
# Return a hash formatted for value_for_platform returning an Array
# of package names.
# @return [Hash]
# @overload packages(default_package, packages)
# Define what system packages are available for this language on each
# platform.
# @param default_package [String] Default package name for platforms
# not otherwise defined.
# @param [Hash] Hash formatted for value_for_platform returning an
# Array of package names.
# @return [Hash]
def packages(default_package=nil, packages=nil)
self.default_package(default_package) if default_package
if packages
@packages = packages
end
@packages
end
# @overload default_package()
# Return the default package name for platforms not otherwise defined.
# @return [String]
# @overload default_package(name)
# Set the default package name for platforms not defined in {packages}.
# @param name [String] Package name.
# @return [String]
def default_package(name=nil)
if name
@default_package = name
end
@default_package
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,254 @@
#
# 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). The `\d+:` is for RPM epoch prefixes.
unless candidate_version && (!candidate_version.nil?) && (!candidate_version.empty?) && candidate_version =~ /^(\d+:)?#{Regexp.escape(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

View File

@@ -0,0 +1,68 @@
#
# 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 'shellwords'
require 'poise_languages/utils/which'
module PoiseLanguages
module Utils
include Which
extend self
# Default whitelist for {#shelljoin}.
SHELLJOIN_WHITELIST = [/^2?[><]/]
# An improved version of Shellwords.shelljoin that doesn't escape a few
# things.
#
# @param cmd [Array<String>] Command array to join.
# @param whitelist [Array<Regexp>] Array of patterns to whitelist.
# @return [String]
def shelljoin(cmd, whitelist: SHELLJOIN_WHITELIST)
cmd.map do |str|
if whitelist.any? {|pat| str =~ pat }
str
else
Shellwords.shellescape(str)
end
end.join(' ')
end
# Convert the executable in a string or array command to an absolute path.
#
# @param cmd [String, Array<String>] Command to fix up.
# @param path [String, nil] Replacement $PATH for executable lookup.
# @return [String, Array<String>]
def absolute_command(cmd, path: nil)
was_array = cmd.is_a?(Array)
cmd = if was_array
cmd.dup
else
Shellwords.split(cmd)
end
# Don't try to touch anything if the first value looks like a flag or a path.
if cmd.first && !cmd.first.start_with?('-') && !cmd.first.include?(::File::SEPARATOR)
# If which returns false, just leave it I guess.
cmd[0] = which(cmd.first, path: path) || cmd.first
end
cmd = shelljoin(cmd) unless was_array
cmd
end
end
end

View File

@@ -0,0 +1,51 @@
#
# 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.
#
module PoiseLanguages
module Utils
# Replacement module for Chef::Mixin::Which with a slight improvement.
#
# @since 1.0.0
# @see Which#which
module Which
extend self
# A replacement for Chef::Mixin::Which#which that allows using something
# other than an environment variable if needed.
#
# @param cmd [String] Executable to search for.
# @param extra_path [Array<String>] Extra directories to always search.
# @param path [String, nil] Replacement $PATH value.
# @return [String, false]
def which(cmd, extra_path: %w{/bin /usr/bin /sbin /usr/sbin}, path: nil)
# If it was already absolute, just return that.
return cmd if cmd =~ /^(\/|([a-z]:)?\\)/i
# Allow passing something other than the real env var.
path ||= ENV['PATH']
# Based on Chef::Mixin::Which#which
# Copyright 2010-2017, Chef Softare, Inc.
paths = path.split(File::PATH_SEPARATOR) + extra_path
paths.each do |candidate_path|
filename = ::File.join(candidate_path, cmd)
return filename if ::File.executable?(filename)
end
false
end
end
end
end

View File

@@ -0,0 +1,20 @@
#
# 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.
#
module PoiseLanguages
VERSION = '2.1.2'
end