From 02bbb1aeef2dcf720967ac476002877344e88f80 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Fri, 8 Jan 2016 12:14:20 -0800 Subject: chocolatey multipackage provider --- lib/chef/provider/package/chocolatey.rb | 230 ++++++++++++++++++++++++++++++++ lib/chef/providers.rb | 3 +- lib/chef/resource/chocolatey_package.rb | 37 +++++ lib/chef/resources.rb | 3 +- 4 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 lib/chef/provider/package/chocolatey.rb create mode 100644 lib/chef/resource/chocolatey_package.rb (limited to 'lib') diff --git a/lib/chef/provider/package/chocolatey.rb b/lib/chef/provider/package/chocolatey.rb new file mode 100644 index 0000000000..3f52370939 --- /dev/null +++ b/lib/chef/provider/package/chocolatey.rb @@ -0,0 +1,230 @@ +# +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# 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/provider/package' +require 'chef/resource/chocolatey_package' +require 'chef/mixin/powershell_out' + +class Chef + class Provider + class Package + class Chocolatey < Chef::Provider::Package + include Chef::Mixin::PowershellOut + + provides :chocolatey_package, os: "windows" + + # Declare that our arguments should be arrays + use_multipackage_api + + # Responsible for building the current_resource. + # + # @return [Chef::Resource::ChocolateyPackage] the current_resource + def load_current_resource + @current_resource = Chef::Resource::ChocolateyPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(build_current_versions) + current_resource + end + + # Lazy initializer for candidate_version. A nil value means that there is no candidate + # version and the package is not installable (generally an error). + # + # @return [Array] list of candidate_versions indexed same as new_resource.package_name/version + def candidate_version + @candidate_version ||= build_candidate_versions + end + + # Install multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def install_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| names.include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("install -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("install -y", cmd_args, *cmd_names) + end + end + + # Upgrade multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def upgrade_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| names.include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("upgrade -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("upgrade -y", cmd_args, *cmd_names) + end + end + + # Remove multiple packages via choco.exe + # + # @param names [Array] array of package names to install + # @param versions [Array] array of versions to install + def remove_package(names, versions) + choco_command("uninstall -y", cmd_args, *names) + end + + # Support :uninstall as an action in order for users to easily convert + # from the `chocolatey` provider in the cookbook. It is, however, + # already deprecated. + def action_uninstall + Chef::Log.deprecation "The use of action :uninstall on the chocolatey_package provider is deprecated, please use :remove" + action_remove + end + + # Choco does not have dpkg's distinction between purge and remove + alias_method :purge_package, :remove_package + + # Override the superclass check. The semantics for our new_resource.source is not files to + # install from, but like the rubygem provider's sources which are more like repos. + def check_resource_semantics! + end + + private + + # Magic to find where chocolatey is installed in the system, and to + # return the full path of choco.exe + # + # @return [String] full path of choco.exe + def choco_exe + @choco_exe ||= + ::File.join( + powershell_out!( + "[System.Environment]::GetEnvironmentVariable('ChocolateyInstall', 'MACHINE')" + ).stdout.chomp, + 'bin', + 'choco.exe' + ) + end + + # Helper to dispatch a choco command through shell_out using the timeout + # set on the new resource, with nice command formatting. + # + # @param args [String] variable number of string arguments + # @return [Mixlib::ShellOut] object returned from shell_out! + def choco_command(*args) + shell_out_with_timeout!(args_to_string(choco_exe, *args)) + end + + # Use the available_packages Hash helper to create an array suitable for + # using in candidate_version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_candidate_versions + new_resource.package_name.map do |package_name| + available_packages[package_name.downcase] + end + end + + # Use the installed_packages Hash helper to create an array suitable for + # using in current_resource.version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_current_versions + new_resource.package_name.map do |package_name| + installed_packages[package_name.downcase] + end + end + + # Helper to construct Hash of names-to-versions, requested on the new_resource. + # If new_resource.version is nil, then all values will be nil. + # + # @return [Hash] Mapping of requested names to versions + def desired_name_versions + desired_versions = new_resource.version || new_resource.package_name.map { nil } + Hash[*new_resource.package_name.zip(desired_versions).flatten] + end + + # Helper to construct optional args out of new_resource + # + # @return [String] options from new_resource or empty string + def cmd_args + cmd_args = [ new_resource.options ] + cmd_args.push( "-source #{new_resource.source}" ) if new_resource.source + args_to_string(*cmd_args) + end + + # Helper to nicely convert variable string args into a single command line. It + # will compact nulls or empty strings and join arguments with single spaces, without + # introducing any double-spaces for missing args. + # + # @param args [String] variable number of string arguments + # @return [String] nicely concatenated string or empty string + def args_to_string(*args) + args.reject {|i| i.nil? || i == "" }.join(" ") + end + + # Available packages in chocolatey as a Hash of names mapped to versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of available packages + def available_packages + @available_packages ||= + begin + cmd = [ "list -r #{package_name_array.join ' '}" ] + cmd.push( "-source #{new_resource.source}" ) if new_resource.source + parse_list_output(*cmd) + end + end + + # Installed packages in chocolatey as a Hash of names mapped to versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of installed packages + def installed_packages + @installed_packages ||= parse_list_output("list -l -r") + end + + # Helper to convert choco.exe list output to a Hash + # (names are downcased for case-insenstive matching) + # + # @param cmd [String] command to run + # @return [String] list output converted to ruby Hash + def parse_list_output(*args) + hash = {} + choco_command(*args).stdout.each_line do |line| + name, version = line.split('|') + hash[name.downcase] = version.chomp + end + hash + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index f5e7a0f989..b4eeabd48d 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2010 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,6 +57,7 @@ require 'chef/provider/whyrun_safe_ruby_block' require 'chef/provider/env/windows' require 'chef/provider/package/apt' +require 'chef/provider/package/chocolatey' require 'chef/provider/package/dpkg' require 'chef/provider/package/easy_install' require 'chef/provider/package/freebsd/port' diff --git a/lib/chef/resource/chocolatey_package.rb b/lib/chef/resource/chocolatey_package.rb new file mode 100644 index 0000000000..2e29c1722e --- /dev/null +++ b/lib/chef/resource/chocolatey_package.rb @@ -0,0 +1,37 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# 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/package' + +class Chef + class Resource + class ChocolateyPackage < Chef::Resource::Package + + provides :chocolatey_package, os: "windows" + + def initialize(name, run_context=nil) + super + @resource_name = :chocolatey_package + end + + property :package_name, [String, Array], coerce: proc { |x| [x].flatten } + + property :version, [String, Array], coerce: proc { |x| [x].flatten } + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index f699d95ace..cc5b9be8c6 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2010 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ require 'chef/resource/batch' require 'chef/resource/breakpoint' require 'chef/resource/cookbook_file' require 'chef/resource/chef_gem' +require 'chef/resource/chocolatey_package' require 'chef/resource/cron' require 'chef/resource/csh' require 'chef/resource/deploy' -- cgit v1.2.1