class Chef::Provider::Package::Chocolatey
Constants
- CHOCO_MISSING_MSG
- PATHFINDING_POWERSHELL_COMMAND
Public Instance Methods
Source
# File lib/chef/provider/package/chocolatey.rb, line 168 def cache_is_valid? return false if @@choco_config.nil? || (actual_config != @@choco_config) true end
This checks that the repo list has not changed between now and when we last checked the cache
Source
# File lib/chef/provider/package/chocolatey.rb, line 83 def candidate_version @candidate_version ||= build_candidate_versions 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
Source
# File lib/chef/provider/package/chocolatey.rb, line 144 def check_resource_semantics!; end
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.
Source
# File lib/chef/provider/package/chocolatey.rb, line 189 def collect_package_requests(ignore_list: [], walk_resource_tree: false) return ["*"] if new_resource.bulk_query || Chef::Config[:always_use_bulk_chocolatey_package_list] if walk_resource_tree # Get to the root of the resource collection rc = run_context.parent_run_context || run_context rc = rc.parent_run_context while rc.parent_run_context package_collection = package_name_array package_collection += nested_package_resources(rc.resource_collection) else package_collection = package_name_array end # downcase the array and uniq. sorted for easier testing... package_collection.uniq.sort.filter { |pkg| !ignore_list.include?(pkg) } end
Find the set of packages to ask the chocolatey server about
if walk_resource_tree is true, this finds all of the packages that we have referenced anywhere in our recipes - this is so we can attempt to query them all in a single transaction. However, currently we don’t do that - see the comment on available_packages
for details of the why, but the TL;DR is that the public chocolatey servers do not support ‘or` type queries properly.
If walk_resource_tree is false, we don’t do any of that - we just filter the package list based on cache data. This is the default due to reasons explained in the comment on available_packages
- the goal is to eventually turn this back on, hence the default false parameter here.
@return [Array] List of chocolatey packages referenced in the run list
Source
# File lib/chef/provider/package/chocolatey.rb, line 64 def define_resource_requirements super # The check that Chocolatey is installed is in #choco_exe. # Chocolatey source property points to an alternate feed # and not a package specific alternate source like other providers # so we want to assert candidates exist for the alternate source requirements.assert(:upgrade, :install) do |a| a.assertion { candidates_exist_for_all_uninstalled? } a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{packages_missing_candidates.join(", ")}") a.whyrun("Assuming a repository that offers #{packages_missing_candidates.join(", ")} would have been configured") end end
Chef::Provider::Package#define_resource_requirements
Source
# File lib/chef/provider/package/chocolatey.rb, line 146 def get_choco_version # We need a different way to get the version than by simply calling "choco --version". # If the license file is installed (for business customers) but not the Chocolatey.Extension (because you're using the choco resource to install it) # then you get a license error. This method bypasses that by getting the version from the exe directly instead of invoking it. # deprecated: @get_choco_version ||= powershell_exec!("#{choco_exe} --version").result @get_choco_version ||= powershell_exec!("Get-ItemProperty #{choco_exe} | select-object -expandproperty versioninfo | select-object -expandproperty productversion").result end
Source
# File lib/chef/provider/package/chocolatey.rb, line 91 def install_package(names, versions) name_versions_to_install = desired_name_versions.select { |n, v| lowercase_names(names).include?(n) } name_nil_versions = name_versions_to_install.select { |n, v| v.nil? } name_has_versions = name_versions_to_install.compact # 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
Install multiple packages via choco.exe
@param names [Array<String>] array of package names to install @param versions [Array<String>] array of versions to install
Source
# File lib/chef/provider/package/chocolatey.rb, line 162 def invalidate_cache @@choco_config = nil end
invalidate cache for testing purposes
Source
# File lib/chef/provider/package/chocolatey.rb, line 53 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) # Ensure that we have a working chocolatey executable - this used to be # covered off by loading the resource, but since that's no longer required, # we're going to put a quick check here to fail early! choco_exe current_resource end
Responsible for building the current_resource.
@return [Chef::Resource::ChocolateyPackage] the current_resource
Choco does not have dpkg’s distinction between purge and remove
Source
# File lib/chef/provider/package/chocolatey.rb, line 155 def query_command return "list" if Gem::Dependency.new("", "< 1.4.0").match?("", get_choco_version) "search" end
Choco V2 uses ‘Search’ for remote repositories and ‘List’ for local packages
Source
# File lib/chef/provider/package/chocolatey.rb, line 135 def remove_package(names, versions) choco_command("uninstall", "-y", cmd_args(include_source: false), *names) end
Remove multiple packages via choco.exe
@param names [Array<String>] array of package names to install @param versions [Array<String>] array of versions to install
Source
# File lib/chef/provider/package/chocolatey.rb, line 113 def upgrade_package(names, versions) name_versions_to_install = desired_name_versions.select { |n, v| lowercase_names(names).include?(n) } name_nil_versions = name_versions_to_install.select { |n, v| v.nil? } name_has_versions = name_versions_to_install.compact # 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
Upgrade multiple packages via choco.exe
@param names [Array<String>] array of package names to install @param versions [Array<String>] array of versions to install
Private Instance Methods
Source
# File lib/chef/provider/package/chocolatey.rb, line 221 def actual_config config_path = ::File.join("#{choco_install_path}", "config", "chocolatey.config") if ::File.exist?(config_path) return ::File.read(config_path) end nil end
Cache the configuration in order to ensure that we can check our package cache is valid for a run
Source
# File lib/chef/provider/package/chocolatey.rb, line 327 def available_packages return @available_packages unless @available_packages.nil? # @available_packages is per object - each resource is an object, meaning if you # have a LOT of chocolatey package installs, then this quickly gets very slow. # So we use @@choco_available_packages instead - BUT it's important to ensure that # the cache is valid before you do this. There are two cache items that can change: # a) the sources - we check this with cache_is_valid? if cache_is_valid? && @@choco_available_packages.is_a?(Hash) && @@choco_available_packages[new_resource.list_options] # Ensure we have the package names, or else double check... need_redo = false package_name_array.each do |pkg| need_redo = true unless @@choco_available_packages[new_resource.list_options][pkg.downcase] end return @@choco_available_packages[new_resource.list_options] unless need_redo end if new_resource.list_options Chef::Log.info("Fetching chocolatey package list with options #{new_resource.list_options.inspect}") else Chef::Log.info("Fetching chocolatey package list") end # Only reset the array if the cache is invalid - if we're just augmenting it, don't # clear it @@choco_available_packages = {} if @@choco_available_packages.nil? || !cache_is_valid? if @@choco_available_packages[new_resource.list_options].nil? @@choco_available_packages[new_resource.list_options] = {} end # This would previously grab 25 packages at a time, which previously worked - however, # upstream changed and it turns out this was only working by accident - see # https://github.com/chocolatey/choco/issues/2116 for this. So the TL;DR ends up # being that this can be re-enabled when the chocolatey server actually supports an # or operator. So it makes sense to leave the logic here for this split, while we # work with upstream to get this to be a working feature there # # Foot guns: there is a --id-starts-with for chocolatey, which you'd think would work, # but that actually fails on public chocolatey as well, because it seems to do the filtering # locally. Which means it too will omit a lot of results (this is also corroborated by # the 2116 issue above). # # collect_package_requests, however, continues to be useful here because it filters # the already cached things from the list. However, for now it will no longer walk the # resource tree until 2116 can be sorted out. When we regain that ability, we should # re-evaluate this, since it does save a LOT of API requests! collect_package_requests( ignore_list: @@choco_available_packages[new_resource.list_options].keys ).each do |pkg_set| available_versions = begin cmd = [ query_command, "-r" ] # Chocolatey doesn't actually take a wildcard for this query, however # it will return all packages when using '*' as a query unless pkg_set == "*" cmd << pkg_set end cmd += common_options cmd.push( new_resource.list_options ) if new_resource.list_options Chef::Log.debug("Choco List Command: #{cmd}") raw = parse_list_output(*cmd) raw.keys.each_with_object({}) do |name, available| available[name] = desired_name_versions[name] || raw[name] end end @@choco_available_packages[new_resource.list_options].merge!(available_versions) end # Mark the cache as valid, with the required metadata set_package_cache # Why both? So when we fail to find a package once, we don't try on every # retry, even though it would be reasonable to do so if queried in another # resource (because the chocolatey configuration may well have changed!) @available_packages = @@choco_available_packages[new_resource.list_options] end
Available packages in chocolatey as a Hash of names mapped to versions If pinning a package to a specific version, filter out all non matching versions (names are downcased for case-insensitive matching)
@return [Hash] name-to-version mapping of available packages
Source
# File lib/chef/provider/package/chocolatey.rb, line 276 def build_candidate_versions new_resource.package_name.map do |package_name| available_packages[package_name.downcase] end 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
Source
# File lib/chef/provider/package/chocolatey.rb, line 286 def build_current_versions new_resource.package_name.map do |package_name| installed_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
Source
# File lib/chef/provider/package/chocolatey.rb, line 268 def choco_command(*args) shell_out!(choco_exe, *args, returns: new_resource.returns) 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!
Source
# File lib/chef/provider/package/chocolatey.rb, line 239 def choco_exe @choco_exe ||= begin # if this check is in #define_resource_requirements, it won't get # run before choco.exe gets called from #load_current_resource. exe_path = ::File.join(choco_install_path, "choco.exe") raise Chef::Exceptions::MissingLibrary, CHOCO_MISSING_MSG unless ::File.exist?(exe_path) exe_path end end
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
Source
# File lib/chef/provider/package/chocolatey.rb, line 251 def choco_install_path @choco_install_path ||= begin result = powershell_exec!(PATHFINDING_POWERSHELL_COMMAND).result result = "" if result.empty? result end end
lets us mock out an incorrect value for testing.
Source
# File lib/chef/provider/package/chocolatey.rb, line 259 def choco_lib_path ::File.join(choco_install_path, "lib") end
Source
# File lib/chef/provider/package/chocolatey.rb, line 316 def cmd_args(include_source: true) cmd_args = new_resource.options.is_a?(String) ? command_line_to_argv_w_helper(new_resource.options) : Array(new_resource.options) cmd_args += common_options(include_source: include_source) cmd_args end
Helper to construct optional args out of new_resource
@param include_source [Boolean] should the source parameter be added @return [String] options from new_resource or empty string
Source
# File lib/chef/provider/package/chocolatey.rb, line 486 def common_options(include_source: true) args = [] args.push( [ "-source", new_resource.source ] ) if new_resource.source && include_source args.push( [ "--user", new_resource.user ] ) if new_resource.user args.push( [ "--password", new_resource.password ]) if new_resource.password args end
Source
# File lib/chef/provider/package/chocolatey.rb, line 296 def desired_name_versions desired_versions = new_resource.version || new_resource.package_name.map { nil } Hash[*lowercase_names(new_resource.package_name).zip(desired_versions).flatten] 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
Source
# File lib/chef/provider/package/chocolatey.rb, line 495 def fetch_package_versions(base_dir, target_dirs, targets) pkg_versions = {} targets = [targets] if targets.is_a?(String) target_dirs.each do |dir| pkg_versions.merge!(get_pkg_data(::File.join(base_dir, dir))) # return early if we found the single package version we were looking for return pkg_versions if targets.length == 1 && pkg_versions[targets.first] end pkg_versions end
Fetch the local package versions from chocolatey
Source
# File lib/chef/provider/package/chocolatey.rb, line 454 def get_local_pkg_dirs(base_dir) return [] unless Dir.exist?(base_dir) Dir.entries(base_dir).select do |dir| ::File.directory?(::File.join(base_dir, dir)) && !dir.start_with?(".") end end
Grab the nupkg folder list
Source
# File lib/chef/provider/package/chocolatey.rb, line 508 def get_pkg_data(path) t = ::File.join(path, "*.nupkg").gsub("\\", "/") targets = Dir.glob(t) # Extract package version from the first nuspec file in this nupkg targets.each do |target| Zip::File.open(target) do |zip_file| zip_file.each do |entry| next unless entry.name.end_with?(".nuspec") f = entry.get_input_stream doc = REXML::Document.new(f.read.to_s) f.close id = doc.elements["package/metadata/id"] version = doc.elements["package/metadata/version"] return { id.text.to_s.downcase => version.text } if id && version end end end {} rescue StandardError => e Chef::Log.warn("Failed to get package info for #{path}: #{e}") {} end
Grab the locally installed packages from the nupkg list rather than shelling out to chocolatey
Source
# File lib/chef/provider/package/chocolatey.rb, line 412 def installed_packages # Logic here must be either use_choco_list is false _and_ always_use_choco_list is # falsy, since the global overrides the local if new_resource.use_choco_list == false && !Chef::Config[:always_use_choco_list] installed_packages_via_disk else installed_packages_via_choco end end
Installed packages in chocolatey as a Hash of names mapped to versions (names are downcased for case-insensitive matching). Depending on the user preference, we get these either from the local database, or from the choco list command
@return [Hash] name-to-version mapping of installed packages
Source
# File lib/chef/provider/package/chocolatey.rb, line 425 def installed_packages_via_choco @installed_packages ||= Hash[*parse_list_output("list", "-l", "-r").flatten] @installed_packages end
Beginning with Choco 2.0, “list” returns local packages only while “search” returns packages from external package sources
@return [Hash] name-to-version mapping of installed packages
Source
# File lib/chef/provider/package/chocolatey.rb, line 435 def installed_packages_via_disk @installed_packages ||= begin targets = new_resource.name target_dirs = [] # If we're using a single package name, have it at the head of the list # so we can get more performance. In either case, the # array is filled by the call to `get_local_pkg_dirs` below - but # that contains all possible package folders, and so we push our # guess to the front as an optimization. target_dirs << targets.first.downcase if targets.length == 1 if targets.is_a?(String) target_dirs << targets.downcase end target_dirs += get_local_pkg_dirs(choco_lib_path) fetch_package_versions(choco_lib_path, target_dirs, targets) end end
Return packages sourced from the local disk - because this doesn’t have shell out overhead, this ends up being a significant performance win vs calling choco list
@return [Hash] name-to-version mapping of installed packages
Source
# File lib/chef/provider/package/chocolatey.rb, line 482 def lowercase_names(names) names.map(&:downcase) end
Helper to downcase all names in an array
@param names [Array] original mixed case names @return [Array] same names in lower case
Source
# File lib/chef/provider/package/chocolatey.rb, line 301 def nested_package_resources(res) package_collection = [] res.each do |child_res| package_collection += nested_package_resources(child_res.resources) next unless child_res.is_a?(Chef::Resource::ChocolateyPackage) package_collection += child_res.package_name.flatten end package_collection end
Source
# File lib/chef/provider/package/chocolatey.rb, line 467 def parse_list_output(*args) hash = {} choco_command(*args).stdout.each_line do |line| next if line.start_with?("Chocolatey v") name, version = line.split("|") hash[name.downcase] = version&.chomp end hash end
Helper to convert choco.exe list output to a Hash (names are downcased for case-insensitive matching)
@param cmd [String] command to run @return [Hash] list output converted to ruby Hash
Source
# File lib/chef/provider/package/chocolatey.rb, line 231 def set_package_cache @@choco_config = actual_config end
update the validity of the package cache
Source
# File lib/chef/provider/package/chocolatey.rb, line 208 def version_compare(v1, v2) if v1 == "latest" || v2 == "latest" return 0 end gem_v1 = Gem::Version.new(v1) gem_v2 = Gem::Version.new(v2) gem_v1 <=> gem_v2 end