Puppet Function: complyadm::download_file

Defined in:
lib/puppet/functions/complyadm/download_file.rb
Function type:
Ruby 4.x API

Overview

complyadm::download_file(String[1] $source, String[1] $destination, Boltlib::TargetSpec $targets, Optional[Hash[String[1], Any]] $options, Optional[Boolean] $omit_src_dir)ResultSet

Downloads the given file or directory from the given set of targets and saves it to a directory matching the target’s name under the given destination directory. Returns the result from each download. This does nothing if the list of targets is empty.

This function is largely a copy of:

https://github.com/puppetlabs/bolt/blob/8f7d5ea3ef49dadc5e166d5d802d091abc4b02bc/bolt-modules/boltlib/lib/puppet/functions/download_file.rb

but fixes several problems we ran into:

1. It expected a relative path so we would need to deal with both relative and absolute paths which is a pain.
2. The function was destructive so if you call download_file multiple times within a plan with the same destination, files would be
   deleted. Not overwritten, just straight up deleted.
3. There is no way to omit the source directory when you're copying a directory from a target so you end up with an unwanted directory.

> Note: Not available in apply block

Examples:

Download a file from multiple Linux targets to a destination directory

download_file('/etc/ssh/ssh_config', '~/Downloads', $targets)

Parameters:

  • source (String[1])

    The absolute path to the file or directory on the target(s).

  • destination (String[1])

    The absolute path to the destination directory on the local system.

  • targets (Boltlib::TargetSpec)

    A pattern identifying zero or more targets. See get_targets for accepted patterns.

  • options (Optional[Hash[String[1], Any]])

    A hash of additional options.

  • use_absolute_destination

    When downloading a directory, use the absolute path specified by the

  • omit_src_dir (Optional[Boolean])

Options Hash (options):

  • _catch_errors (Boolean)

    Whether to catch raised errors.

  • _run_as (String)

    User to run as using privilege escalation.

Returns:

  • (ResultSet)

    A list of results, one entry per target, with the path to the downloaded file under the ‘path` key.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/puppet/functions/complyadm/download_file.rb', line 21

Puppet::Functions.create_function(:'complyadm::download_file') do
  # Download a file or directory.
  # @param source The absolute path to the file or directory on the target(s).
  # @param destination The absolute path to the destination directory on the local system.
  # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
  # @param options A hash of additional options.
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
  # @option options [String] _run_as User to run as using privilege escalation.
  # @param use_absolute_destination When downloading a directory, use the absolute path specified by the
  # 'destination' param so the source directory is excluded along with the default target name directory.
  # @return A list of results, one entry per target, with the path to the downloaded file under the
  #         `path` key.
  # @example Download a file from multiple Linux targets to a destination directory
  #   download_file('/etc/ssh/ssh_config', '~/Downloads', $targets)
  dispatch :download_file do
    param 'String[1]', :source
    param 'String[1]', :destination
    param 'Boltlib::TargetSpec', :targets
    optional_param 'Hash[String[1], Any]', :options
    optional_param 'Boolean', :omit_src_dir
    return_type 'ResultSet'
  end

  def download_file(source, destination, targets, options = {}, omit_src_dir = false)
    unless Puppet[:tasks]
      raise Puppet::ParseErrorWithIssue
        .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'download_file')
    end

    options = options.select { |opt| opt.start_with?('_') }.transform_keys { |k| k.sub(%r{^_}, '').to_sym }
    executor = Puppet.lookup(:bolt_executor)
    inventory = Puppet.lookup(:bolt_inventory)

    if (destination = destination.strip).empty?
      raise Bolt::ValidationError, 'Destination cannot be an empty string'
    end

    unless (destination = Pathname.new(destination)).absolute?
      raise Bolt::ValidationError, "Destination must be an absolute path, received relative path #{destination}"
    end

    # Prevent path traversal so downloads can't be saved outside of the project downloads directory
    if (destination.each_filename.to_a & ['.', '..']).any?
      raise Bolt::ValidationError, "Destination must not include path traversal, received #{destination}"
    end

    # Ensure that that given targets are all Target instances
    targets = inventory.get_targets(targets)
    if targets.empty?
      call_function('debug', "Simulating file download of '#{source}' - no targets given - no action taken")
      Bolt::ResultSet.new([])
    else
      file_line = Puppet::Pops::PuppetStack.top_of_stack
      download_results = if executor.in_parallel?
                           executor.run_in_thread do
                             executor.download_file(targets, source, destination, options, file_line)
                           end
                         else
                           executor.download_file(targets, source, destination, options, file_line)
                         end

      if !download_results.ok && !options[:catch_errors]
        raise Bolt::RunFailure.new(download_results, 'download_file', source)
      end

      if omit_src_dir
        download_results.each do |result|
          target_dest_dir = result.value['path']
          target_dir = File.dirname(target_dest_dir)
          parent_dir = File.dirname(target_dir)
          files_to_move = Dir.children(target_dest_dir).map do |f|
            File.join(target_dest_dir, f)
          end
          FileUtils.mv(files_to_move, parent_dir)
          FileUtils.remove_dir(target_dir)
        end
      end
      download_results
    end
  end
end