bolt 3.1.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
 - data/Puppetfile +11 -11
 - data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +24 -0
 - data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -1
 - data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +20 -2
 - data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
 - data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
 - data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +44 -5
 - data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
 - data/bolt-modules/boltlib/types/planresult.pp +1 -0
 - data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -2
 - data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +20 -2
 - data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
 - data/lib/bolt/analytics.rb +4 -8
 - data/lib/bolt/apply_result.rb +1 -1
 - data/lib/bolt/bolt_option_parser.rb +6 -3
 - data/lib/bolt/cli.rb +121 -36
 - data/lib/bolt/config.rb +15 -7
 - data/lib/bolt/config/options.rb +62 -12
 - data/lib/bolt/config/transport/lxd.rb +23 -0
 - data/lib/bolt/config/transport/options.rb +8 -1
 - data/lib/bolt/config/transport/podman.rb +33 -0
 - data/lib/bolt/container_result.rb +105 -0
 - data/lib/bolt/error.rb +15 -0
 - data/lib/bolt/executor.rb +37 -18
 - data/lib/bolt/inventory/options.rb +9 -0
 - data/lib/bolt/inventory/target.rb +16 -0
 - data/lib/bolt/logger.rb +8 -0
 - data/lib/bolt/module_installer.rb +2 -2
 - data/lib/bolt/module_installer/puppetfile.rb +2 -2
 - data/lib/bolt/module_installer/specs/forge_spec.rb +2 -2
 - data/lib/bolt/module_installer/specs/git_spec.rb +2 -2
 - data/lib/bolt/node/output.rb +14 -4
 - data/lib/bolt/outputter/human.rb +259 -90
 - data/lib/bolt/outputter/json.rb +3 -1
 - data/lib/bolt/outputter/logger.rb +17 -0
 - data/lib/bolt/pal.rb +24 -4
 - data/lib/bolt/pal/yaml_plan.rb +1 -2
 - data/lib/bolt/pal/yaml_plan/evaluator.rb +5 -141
 - data/lib/bolt/pal/yaml_plan/step.rb +91 -31
 - data/lib/bolt/pal/yaml_plan/step/command.rb +21 -13
 - data/lib/bolt/pal/yaml_plan/step/download.rb +15 -16
 - data/lib/bolt/pal/yaml_plan/step/eval.rb +11 -11
 - data/lib/bolt/pal/yaml_plan/step/message.rb +13 -4
 - data/lib/bolt/pal/yaml_plan/step/plan.rb +19 -15
 - data/lib/bolt/pal/yaml_plan/step/resources.rb +82 -21
 - data/lib/bolt/pal/yaml_plan/step/script.rb +36 -17
 - data/lib/bolt/pal/yaml_plan/step/task.rb +19 -16
 - data/lib/bolt/pal/yaml_plan/step/upload.rb +16 -17
 - data/lib/bolt/pal/yaml_plan/transpiler.rb +3 -3
 - data/lib/bolt/plan_creator.rb +1 -1
 - data/lib/bolt/plugin.rb +13 -11
 - data/lib/bolt/project_manager.rb +1 -1
 - data/lib/bolt/project_manager/module_migrator.rb +1 -1
 - data/lib/bolt/result.rb +5 -14
 - data/lib/bolt/shell.rb +16 -0
 - data/lib/bolt/shell/bash.rb +68 -30
 - data/lib/bolt/shell/bash/tmpdir.rb +2 -2
 - data/lib/bolt/shell/powershell.rb +28 -11
 - data/lib/bolt/task.rb +1 -1
 - data/lib/bolt/transport/docker.rb +1 -1
 - data/lib/bolt/transport/docker/connection.rb +21 -32
 - data/lib/bolt/transport/lxd.rb +26 -0
 - data/lib/bolt/transport/lxd/connection.rb +99 -0
 - data/lib/bolt/transport/orch.rb +13 -5
 - data/lib/bolt/transport/podman.rb +19 -0
 - data/lib/bolt/transport/podman/connection.rb +98 -0
 - data/lib/bolt/transport/ssh/connection.rb +1 -1
 - data/lib/bolt/transport/winrm/connection.rb +1 -1
 - data/lib/bolt/util.rb +42 -0
 - data/lib/bolt/version.rb +1 -1
 - data/lib/bolt_server/transport_app.rb +16 -1
 - data/lib/bolt_spec/plans/action_stubs.rb +1 -1
 - data/lib/bolt_spec/plans/action_stubs/command_stub.rb +8 -1
 - data/lib/bolt_spec/plans/action_stubs/script_stub.rb +8 -1
 - data/lib/bolt_spec/plans/mock_executor.rb +91 -7
 - data/modules/puppet_connect/plans/test_input_data.pp +22 -0
 - metadata +12 -2
 
    
        data/lib/bolt/task.rb
    CHANGED
    
    | 
         @@ -148,7 +148,7 @@ module Bolt 
     | 
|
| 
       148 
148 
     | 
    
         | 
| 
       149 
149 
     | 
    
         
             
                  if unknown_keys.any?
         
     | 
| 
       150 
150 
     | 
    
         
             
                    msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
         
     | 
| 
       151 
     | 
    
         
            -
                    msg += " This could be a typo in the task metadata or  
     | 
| 
      
 151 
     | 
    
         
            +
                    msg += " This could be a typo in the task metadata or might result in incorrect behavior."
         
     | 
| 
       152 
152 
     | 
    
         
             
                    Bolt::Logger.warn("unknown_task_metadata_keys", msg)
         
     | 
| 
       153 
153 
     | 
    
         
             
                  end
         
     | 
| 
       154 
154 
     | 
    
         
             
                end
         
     | 
| 
         @@ -34,6 +34,15 @@ module Bolt 
     | 
|
| 
       34 
34 
     | 
    
         
             
                      @container_info["Id"]
         
     | 
| 
       35 
35 
     | 
    
         
             
                    end
         
     | 
| 
       36 
36 
     | 
    
         | 
| 
      
 37 
     | 
    
         
            +
                    def run_cmd(cmd, env_vars)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      Bolt::Util.exec_docker(cmd, env_vars)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                    private def env_hash
         
     | 
| 
      
 42 
     | 
    
         
            +
                      # Set the DOCKER_HOST if we are using a non-default service-url
         
     | 
| 
      
 43 
     | 
    
         
            +
                      @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
         
     | 
| 
      
 44 
     | 
    
         
            +
                    end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
       37 
46 
     | 
    
         
             
                    def connect
         
     | 
| 
       38 
47 
     | 
    
         
             
                      # We don't actually have a connection, but we do need to
         
     | 
| 
       39 
48 
     | 
    
         
             
                      # check that the container exists and is running.
         
     | 
| 
         @@ -54,10 +63,7 @@ module Bolt 
     | 
|
| 
       54 
63 
     | 
    
         
             
                    end
         
     | 
| 
       55 
64 
     | 
    
         | 
| 
       56 
65 
     | 
    
         
             
                    def add_env_vars(env_vars)
         
     | 
| 
       57 
     | 
    
         
            -
                      @env_vars =  
     | 
| 
       58 
     | 
    
         
            -
                        acc << "--env"
         
     | 
| 
       59 
     | 
    
         
            -
                        acc << "#{env_var[0]}=#{env_var[1]}"
         
     | 
| 
       60 
     | 
    
         
            -
                      end
         
     | 
| 
      
 66 
     | 
    
         
            +
                      @env_vars = Bolt::Util.format_env_vars_for_cli(env_vars)
         
     | 
| 
       61 
67 
     | 
    
         
             
                    end
         
     | 
| 
       62 
68 
     | 
    
         | 
| 
       63 
69 
     | 
    
         
             
                    # Executes a command inside the target container. This is called from the shell class.
         
     | 
| 
         @@ -88,9 +94,9 @@ module Bolt 
     | 
|
| 
       88 
94 
     | 
    
         | 
| 
       89 
95 
     | 
    
         
             
                    def upload_file(source, destination)
         
     | 
| 
       90 
96 
     | 
    
         
             
                      @logger.trace { "Uploading #{source} to #{destination}" }
         
     | 
| 
       91 
     | 
    
         
            -
                       
     | 
| 
       92 
     | 
    
         
            -
                      unless  
     | 
| 
       93 
     | 
    
         
            -
                        raise "Error writing to container #{container_id}: #{ 
     | 
| 
      
 97 
     | 
    
         
            +
                      _out, err, stat = run_cmd(['cp', source, "#{container_id}:#{destination}"], env_hash)
         
     | 
| 
      
 98 
     | 
    
         
            +
                      unless stat.exitstatus.zero?
         
     | 
| 
      
 99 
     | 
    
         
            +
                        raise "Error writing to container #{container_id}: #{err}"
         
     | 
| 
       94 
100 
     | 
    
         
             
                      end
         
     | 
| 
       95 
101 
     | 
    
         
             
                    rescue StandardError => e
         
     | 
| 
       96 
102 
     | 
    
         
             
                      raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
         
     | 
| 
         @@ -102,31 +108,14 @@ module Bolt 
     | 
|
| 
       102 
108 
     | 
    
         
             
                      # copy the *contents* of the directory.
         
     | 
| 
       103 
109 
     | 
    
         
             
                      # https://docs.docker.com/engine/reference/commandline/cp/
         
     | 
| 
       104 
110 
     | 
    
         
             
                      FileUtils.mkdir_p(destination)
         
     | 
| 
       105 
     | 
    
         
            -
                       
     | 
| 
       106 
     | 
    
         
            -
                      unless  
     | 
| 
       107 
     | 
    
         
            -
                        raise "Error downloading content from container #{container_id}: #{ 
     | 
| 
      
 111 
     | 
    
         
            +
                      _out, err, stat = run_cmd(['cp', "#{container_id}:#{source}", destination], env_hash)
         
     | 
| 
      
 112 
     | 
    
         
            +
                      unless stat.exitstatus.zero?
         
     | 
| 
      
 113 
     | 
    
         
            +
                        raise "Error downloading content from container #{container_id}: #{err}"
         
     | 
| 
       108 
114 
     | 
    
         
             
                      end
         
     | 
| 
       109 
115 
     | 
    
         
             
                    rescue StandardError => e
         
     | 
| 
       110 
116 
     | 
    
         
             
                      raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
         
     | 
| 
       111 
117 
     | 
    
         
             
                    end
         
     | 
| 
       112 
118 
     | 
    
         | 
| 
       113 
     | 
    
         
            -
                    # Executes a Docker CLI command. This is useful for running commands as
         
     | 
| 
       114 
     | 
    
         
            -
                    # part of this class without having to go through the `execute`
         
     | 
| 
       115 
     | 
    
         
            -
                    # function and manage pipes.
         
     | 
| 
       116 
     | 
    
         
            -
                    #
         
     | 
| 
       117 
     | 
    
         
            -
                    # @param subcommand [String] The docker subcommand to run
         
     | 
| 
       118 
     | 
    
         
            -
                    #   e.g. 'inspect' for `docker inspect`
         
     | 
| 
       119 
     | 
    
         
            -
                    # @param arguments [Array] Arguments to pass to the docker command
         
     | 
| 
       120 
     | 
    
         
            -
                    #   e.g. 'src' and 'dest' for `docker cp <src> <dest>
         
     | 
| 
       121 
     | 
    
         
            -
                    # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
         
     | 
| 
       122 
     | 
    
         
            -
                    private def execute_local_command(subcommand, arguments = [])
         
     | 
| 
       123 
     | 
    
         
            -
                      # Set the DOCKER_HOST if we are using a non-default service-url
         
     | 
| 
       124 
     | 
    
         
            -
                      env_hash = @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
         
     | 
| 
       125 
     | 
    
         
            -
                      docker_command = [subcommand].concat(arguments)
         
     | 
| 
       126 
     | 
    
         
            -
             
     | 
| 
       127 
     | 
    
         
            -
                      Open3.capture3(env_hash, 'docker', *docker_command, { binmode: true })
         
     | 
| 
       128 
     | 
    
         
            -
                    end
         
     | 
| 
       129 
     | 
    
         
            -
             
     | 
| 
       130 
119 
     | 
    
         
             
                    # Executes a Docker CLI command and parses the output in JSON format
         
     | 
| 
       131 
120 
     | 
    
         
             
                    #
         
     | 
| 
       132 
121 
     | 
    
         
             
                    # @param subcommand [String] The docker subcommand to run
         
     | 
| 
         @@ -134,15 +123,15 @@ module Bolt 
     | 
|
| 
       134 
123 
     | 
    
         
             
                    # @param arguments [Array] Arguments to pass to the docker command
         
     | 
| 
       135 
124 
     | 
    
         
             
                    #   e.g. 'src' and 'dest' for `docker cp <src> <dest>
         
     | 
| 
       136 
125 
     | 
    
         
             
                    # @return [Object] Ruby object representation of the JSON string
         
     | 
| 
       137 
     | 
    
         
            -
                     
     | 
| 
       138 
     | 
    
         
            -
                       
     | 
| 
       139 
     | 
    
         
            -
                       
     | 
| 
       140 
     | 
    
         
            -
                      extract_json( 
     | 
| 
      
 126 
     | 
    
         
            +
                    def execute_local_json_command(subcommand, arguments = [])
         
     | 
| 
      
 127 
     | 
    
         
            +
                      cmd = [subcommand, '--format', '{{json .}}'].concat(arguments)
         
     | 
| 
      
 128 
     | 
    
         
            +
                      out, _err, _stat = run_cmd(cmd, env_hash)
         
     | 
| 
      
 129 
     | 
    
         
            +
                      extract_json(out)
         
     | 
| 
       141 
130 
     | 
    
         
             
                    end
         
     | 
| 
       142 
131 
     | 
    
         | 
| 
       143 
132 
     | 
    
         
             
                    # Converts the JSON encoded STDOUT string from the docker cli into ruby objects
         
     | 
| 
       144 
133 
     | 
    
         
             
                    #
         
     | 
| 
       145 
     | 
    
         
            -
                    # @param  
     | 
| 
      
 134 
     | 
    
         
            +
                    # @param stdout [String] The string to convert
         
     | 
| 
       146 
135 
     | 
    
         
             
                    # @return [Object] Ruby object representation of the JSON string
         
     | 
| 
       147 
136 
     | 
    
         
             
                    private def extract_json(stdout)
         
     | 
| 
       148 
137 
     | 
    
         
             
                      # The output from the docker format command is a JSON string per line.
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bolt/logger'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'bolt/node/errors'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'bolt/transport/simple'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Bolt
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Transport
         
     | 
| 
      
 9 
     | 
    
         
            +
                class LXD < Simple
         
     | 
| 
      
 10 
     | 
    
         
            +
                  def provided_features
         
     | 
| 
      
 11 
     | 
    
         
            +
                    ['shell']
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  def with_connection(target, options = {})
         
     | 
| 
      
 15 
     | 
    
         
            +
                    Bolt::Logger.warn_once("lxd_experimental",
         
     | 
| 
      
 16 
     | 
    
         
            +
                                           "The LXD transport is experimental, and might "\
         
     | 
| 
      
 17 
     | 
    
         
            +
                                           "include breaking changes between minor versions.")
         
     | 
| 
      
 18 
     | 
    
         
            +
                    conn = Connection.new(target, options)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    conn.connect
         
     | 
| 
      
 20 
     | 
    
         
            +
                    yield conn
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            require 'bolt/transport/lxd/connection'
         
     | 
| 
         @@ -0,0 +1,99 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'logging'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'bolt/node/errors'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Bolt
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Transport
         
     | 
| 
      
 8 
     | 
    
         
            +
                class LXD < Simple
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class Connection
         
     | 
| 
      
 10 
     | 
    
         
            +
                    attr_reader :user, :target
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    def initialize(target, options)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      @target = target
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @user = ENV['USER'] || Etc.getlogin
         
     | 
| 
      
 17 
     | 
    
         
            +
                      @options = options
         
     | 
| 
      
 18 
     | 
    
         
            +
                      @logger = Bolt::Logger.logger(target.safe_name)
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @logger.trace("Initializing LXD connection to #{target.safe_name}")
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    def shell
         
     | 
| 
      
 23 
     | 
    
         
            +
                      Bolt::Shell::Bash.new(target, self)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    def container_id
         
     | 
| 
      
 27 
     | 
    
         
            +
                      "#{@target.transport_config['remote']}:#{@target.host}"
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    def connect
         
     | 
| 
      
 31 
     | 
    
         
            +
                      out, err, status = execute_local_command(%W[list #{container_id} --format json])
         
     | 
| 
      
 32 
     | 
    
         
            +
                      unless status.exitstatus.zero?
         
     | 
| 
      
 33 
     | 
    
         
            +
                        raise "Error listing available containers: #{err}"
         
     | 
| 
      
 34 
     | 
    
         
            +
                      end
         
     | 
| 
      
 35 
     | 
    
         
            +
                      containers = JSON.parse(out)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      if containers.empty?
         
     | 
| 
      
 37 
     | 
    
         
            +
                        raise "Could not find a container with name or ID matching '#{container_id}'"
         
     | 
| 
      
 38 
     | 
    
         
            +
                      end
         
     | 
| 
      
 39 
     | 
    
         
            +
                      @logger.trace("Opened session")
         
     | 
| 
      
 40 
     | 
    
         
            +
                      true
         
     | 
| 
      
 41 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 42 
     | 
    
         
            +
                      raise Bolt::Node::ConnectError.new(
         
     | 
| 
      
 43 
     | 
    
         
            +
                        "Failed to connect to #{container_id}: #{e.message}",
         
     | 
| 
      
 44 
     | 
    
         
            +
                        'CONNECT_ERROR'
         
     | 
| 
      
 45 
     | 
    
         
            +
                      )
         
     | 
| 
      
 46 
     | 
    
         
            +
                    end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    def add_env_vars(env_vars)
         
     | 
| 
      
 49 
     | 
    
         
            +
                      @env_vars = env_vars.each_with_object([]) do |env_var, acc|
         
     | 
| 
      
 50 
     | 
    
         
            +
                        acc << "--env"
         
     | 
| 
      
 51 
     | 
    
         
            +
                        acc << "#{env_var[0]}=#{Shellwords.shellescape(env_var[1])}"
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end
         
     | 
| 
      
 53 
     | 
    
         
            +
                    end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    def execute(command)
         
     | 
| 
      
 56 
     | 
    
         
            +
                      lxc_command = %w[lxc exec]
         
     | 
| 
      
 57 
     | 
    
         
            +
                      lxc_command += @env_vars if @env_vars
         
     | 
| 
      
 58 
     | 
    
         
            +
                      lxc_command += %W[#{container_id} -- sh -c #{Shellwords.shellescape(command)}]
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      @logger.trace { "Executing: #{lxc_command.join(' ')}" }
         
     | 
| 
      
 61 
     | 
    
         
            +
                      Open3.popen3(lxc_command.join(' '))
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    private def execute_local_command(command)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      Open3.capture3('lxc', *command, { binmode: true })
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    def upload_file(source, destination)
         
     | 
| 
      
 69 
     | 
    
         
            +
                      @logger.trace { "Uploading #{source} to #{destination}" }
         
     | 
| 
      
 70 
     | 
    
         
            +
                      args = %w[--create-dirs]
         
     | 
| 
      
 71 
     | 
    
         
            +
                      if File.directory?(source)
         
     | 
| 
      
 72 
     | 
    
         
            +
                        args << '--recursive'
         
     | 
| 
      
 73 
     | 
    
         
            +
                        # If we don't do this, LXD will upload to
         
     | 
| 
      
 74 
     | 
    
         
            +
                        # /tmp/d2020-11/d2020-11/dir instead of /tmp/d2020-11/dir
         
     | 
| 
      
 75 
     | 
    
         
            +
                        destination = Pathname.new(destination).dirname.to_s
         
     | 
| 
      
 76 
     | 
    
         
            +
                      end
         
     | 
| 
      
 77 
     | 
    
         
            +
                      cmd = %w[file push] + args + %W[#{source} #{container_id}#{destination}]
         
     | 
| 
      
 78 
     | 
    
         
            +
                      _out, err, stat = execute_local_command(cmd)
         
     | 
| 
      
 79 
     | 
    
         
            +
                      unless stat.exitstatus.zero?
         
     | 
| 
      
 80 
     | 
    
         
            +
                        raise "Error writing to #{container_id}: #{err}"
         
     | 
| 
      
 81 
     | 
    
         
            +
                      end
         
     | 
| 
      
 82 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 83 
     | 
    
         
            +
                      raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    def download_file(source, destination, _download)
         
     | 
| 
      
 87 
     | 
    
         
            +
                      @logger.trace { "Downloading #{source} to #{destination}" }
         
     | 
| 
      
 88 
     | 
    
         
            +
                      FileUtils.mkdir_p(destination)
         
     | 
| 
      
 89 
     | 
    
         
            +
                      _out, err, stat = execute_local_command(%W[file pull --recursive #{container_id}#{source} #{destination}])
         
     | 
| 
      
 90 
     | 
    
         
            +
                      unless stat.exitstatus.zero?
         
     | 
| 
      
 91 
     | 
    
         
            +
                        raise "Error downloading content from container #{container_id}: #{err}"
         
     | 
| 
      
 92 
     | 
    
         
            +
                      end
         
     | 
| 
      
 93 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 94 
     | 
    
         
            +
                      raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
         
     | 
| 
      
 95 
     | 
    
         
            +
                    end
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/bolt/transport/orch.rb
    CHANGED
    
    | 
         @@ -59,6 +59,18 @@ module Bolt 
     | 
|
| 
       59 
59 
     | 
    
         
             
                      # the result otherwise make sure an error is generated
         
     | 
| 
       60 
60 
     | 
    
         
             
                      if state == 'finished' || (result && result['_error'])
         
     | 
| 
       61 
61 
     | 
    
         
             
                        if result['_error']
         
     | 
| 
      
 62 
     | 
    
         
            +
                          unless result['_error'].is_a?(Hash)
         
     | 
| 
      
 63 
     | 
    
         
            +
                            result['_error'] = { 'kind' => 'puppetlabs.tasks/task-error',
         
     | 
| 
      
 64 
     | 
    
         
            +
                                                 'issue_code' => 'TASK_ERROR',
         
     | 
| 
      
 65 
     | 
    
         
            +
                                                 'msg' => result['_error'],
         
     | 
| 
      
 66 
     | 
    
         
            +
                                                 'details' => {} }
         
     | 
| 
      
 67 
     | 
    
         
            +
                          end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                          result['_error']['details'] ||= {}
         
     | 
| 
      
 70 
     | 
    
         
            +
                          unless result['_error']['details'].is_a?(Hash)
         
     | 
| 
      
 71 
     | 
    
         
            +
                            deets = result['_error']['details']
         
     | 
| 
      
 72 
     | 
    
         
            +
                            result['_error']['details'] = { 'msg' => deets }
         
     | 
| 
      
 73 
     | 
    
         
            +
                          end
         
     | 
| 
       62 
74 
     | 
    
         
             
                          file_line = %w[file line].zip(position).to_h.compact
         
     | 
| 
       63 
75 
     | 
    
         
             
                          result['_error']['details'].merge!(file_line) unless result['_error']['details']['file']
         
     | 
| 
       64 
76 
     | 
    
         
             
                        end
         
     | 
| 
         @@ -252,11 +264,7 @@ module Bolt 
     | 
|
| 
       252 
264 
     | 
    
         | 
| 
       253 
265 
     | 
    
         
             
                    # If we get here, there's no error so we don't need the file or line
         
     | 
| 
       254 
266 
     | 
    
         
             
                    # number
         
     | 
| 
       255 
     | 
    
         
            -
                    Bolt::Result.for_command(target,
         
     | 
| 
       256 
     | 
    
         
            -
                                             result.value['stdout'],
         
     | 
| 
       257 
     | 
    
         
            -
                                             result.value['stderr'],
         
     | 
| 
       258 
     | 
    
         
            -
                                             result.value['exit_code'],
         
     | 
| 
       259 
     | 
    
         
            -
                                             action, obj, [])
         
     | 
| 
      
 267 
     | 
    
         
            +
                    Bolt::Result.for_command(target, result.value, action, obj, [])
         
     | 
| 
       260 
268 
     | 
    
         
             
                  end
         
     | 
| 
       261 
269 
     | 
    
         
             
                end
         
     | 
| 
       262 
270 
     | 
    
         
             
              end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'shellwords'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'bolt/transport/base'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Bolt
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Transport
         
     | 
| 
      
 9 
     | 
    
         
            +
                class Podman < Docker
         
     | 
| 
      
 10 
     | 
    
         
            +
                  def with_connection(target)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    conn = Connection.new(target)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    conn.connect
         
     | 
| 
      
 13 
     | 
    
         
            +
                    yield conn
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            require 'bolt/transport/podman/connection'
         
     | 
| 
         @@ -0,0 +1,98 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'logging'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'bolt/node/errors'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Bolt
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Transport
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Podman < Docker
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class Connection < Connection
         
     | 
| 
      
 10 
     | 
    
         
            +
                    attr_reader :user, :target
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    def initialize(target)
         
     | 
| 
      
 13 
     | 
    
         
            +
                      raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @target = target
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @user = ENV['USER'] || Etc.getlogin
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @logger = Bolt::Logger.logger(target.safe_name)
         
     | 
| 
      
 17 
     | 
    
         
            +
                      @container_info = {}
         
     | 
| 
      
 18 
     | 
    
         
            +
                      @logger.trace("Initializing podman connection to #{target.safe_name}")
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    def run_cmd(cmd, env_vars)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      Bolt::Util.exec_podman(cmd, env_vars)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                    def shell
         
     | 
| 
      
 26 
     | 
    
         
            +
                      @shell ||= if Bolt::Util.windows?
         
     | 
| 
      
 27 
     | 
    
         
            +
                                   Bolt::Shell::Powershell.new(target, self)
         
     | 
| 
      
 28 
     | 
    
         
            +
                                 else
         
     | 
| 
      
 29 
     | 
    
         
            +
                                   Bolt::Shell::Bash.new(target, self)
         
     | 
| 
      
 30 
     | 
    
         
            +
                                 end
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    def connect
         
     | 
| 
      
 34 
     | 
    
         
            +
                      # We don't actually have a connection, but we do need to
         
     | 
| 
      
 35 
     | 
    
         
            +
                      # check that the container exists and is running.
         
     | 
| 
      
 36 
     | 
    
         
            +
                      ps = execute_local_json_command('ps')
         
     | 
| 
      
 37 
     | 
    
         
            +
                      container = Array(ps).find { |item|
         
     | 
| 
      
 38 
     | 
    
         
            +
                        item["ID"].to_s.eql?(@target.host) ||
         
     | 
| 
      
 39 
     | 
    
         
            +
                          item["Id"].to_s.start_with?(@target.host) ||
         
     | 
| 
      
 40 
     | 
    
         
            +
                          Array(item["Names"]).include?(@target.host)
         
     | 
| 
      
 41 
     | 
    
         
            +
                      }
         
     | 
| 
      
 42 
     | 
    
         
            +
                      raise "Could not find a container with name or ID matching '#{@target.host}'" if container.nil?
         
     | 
| 
      
 43 
     | 
    
         
            +
                      # Now find the indepth container information
         
     | 
| 
      
 44 
     | 
    
         
            +
                      id = container["ID"] || container["Id"]
         
     | 
| 
      
 45 
     | 
    
         
            +
                      output = execute_local_json_command('inspect', [id])
         
     | 
| 
      
 46 
     | 
    
         
            +
                      # Store the container information for later
         
     | 
| 
      
 47 
     | 
    
         
            +
                      @container_info = output.first
         
     | 
| 
      
 48 
     | 
    
         
            +
                      @logger.trace { "Opened session" }
         
     | 
| 
      
 49 
     | 
    
         
            +
                      true
         
     | 
| 
      
 50 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 51 
     | 
    
         
            +
                      raise Bolt::Node::ConnectError.new(
         
     | 
| 
      
 52 
     | 
    
         
            +
                        "Failed to connect to #{target.safe_name}: #{e.message}",
         
     | 
| 
      
 53 
     | 
    
         
            +
                        'CONNECT_ERROR'
         
     | 
| 
      
 54 
     | 
    
         
            +
                      )
         
     | 
| 
      
 55 
     | 
    
         
            +
                    end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                    # Executes a command inside the target container. This is called from the shell class.
         
     | 
| 
      
 58 
     | 
    
         
            +
                    #
         
     | 
| 
      
 59 
     | 
    
         
            +
                    # @param command [string] The command to run
         
     | 
| 
      
 60 
     | 
    
         
            +
                    def execute(command)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      args = []
         
     | 
| 
      
 62 
     | 
    
         
            +
                      args += %w[--interactive]
         
     | 
| 
      
 63 
     | 
    
         
            +
                      args += %w[--tty] if target.options['tty']
         
     | 
| 
      
 64 
     | 
    
         
            +
                      args += @env_vars if @env_vars
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                      if target.options['shell-command'] && !target.options['shell-command'].empty?
         
     | 
| 
      
 67 
     | 
    
         
            +
                        # escape any double quotes in command
         
     | 
| 
      
 68 
     | 
    
         
            +
                        command = command.gsub('"', '\"')
         
     | 
| 
      
 69 
     | 
    
         
            +
                        command = "#{target.options['shell-command']} \"#{command}\""
         
     | 
| 
      
 70 
     | 
    
         
            +
                      end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      podman_command = %w[podman exec] + args + [container_id] + Shellwords.split(command)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      @logger.trace { "Executing: #{podman_command.join(' ')}" }
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                      Open3.popen3(*podman_command)
         
     | 
| 
      
 76 
     | 
    
         
            +
                    rescue StandardError
         
     | 
| 
      
 77 
     | 
    
         
            +
                      @logger.trace { "Command aborted" }
         
     | 
| 
      
 78 
     | 
    
         
            +
                      raise
         
     | 
| 
      
 79 
     | 
    
         
            +
                    end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    # Converts the JSON encoded STDOUT string from the podman cli into ruby objects
         
     | 
| 
      
 82 
     | 
    
         
            +
                    #
         
     | 
| 
      
 83 
     | 
    
         
            +
                    # @param stdout [String] The string to convert
         
     | 
| 
      
 84 
     | 
    
         
            +
                    # @return [Object] Ruby object representation of the JSON string
         
     | 
| 
      
 85 
     | 
    
         
            +
                    private def extract_json(stdout)
         
     | 
| 
      
 86 
     | 
    
         
            +
                      # Podman renders the output in pretty JSON, which results in a newline
         
     | 
| 
      
 87 
     | 
    
         
            +
                      # appearing in the output before the closing bracket.
         
     | 
| 
      
 88 
     | 
    
         
            +
                      # should we only get a single line with no newline at all, we also
         
     | 
| 
      
 89 
     | 
    
         
            +
                      # assume it is a single minified JSON object
         
     | 
| 
      
 90 
     | 
    
         
            +
                      stdout.strip!
         
     | 
| 
      
 91 
     | 
    
         
            +
                      newline = stdout.index("\n") || -1
         
     | 
| 
      
 92 
     | 
    
         
            +
                      bracket = stdout.index('}') || -1
         
     | 
| 
      
 93 
     | 
    
         
            +
                      JSON.parse(stdout) if bracket > newline
         
     | 
| 
      
 94 
     | 
    
         
            +
                    end
         
     | 
| 
      
 95 
     | 
    
         
            +
                  end
         
     | 
| 
      
 96 
     | 
    
         
            +
                end
         
     | 
| 
      
 97 
     | 
    
         
            +
              end
         
     | 
| 
      
 98 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -230,7 +230,7 @@ module Bolt 
     | 
|
| 
       230 
230 
     | 
    
         
             
                      end
         
     | 
| 
       231 
231 
     | 
    
         
             
                      [in_wr, out_rd, err_rd, th]
         
     | 
| 
       232 
232 
     | 
    
         
             
                    rescue Errno::EMFILE => e
         
     | 
| 
       233 
     | 
    
         
            -
                      msg = "#{e.message}. This  
     | 
| 
      
 233 
     | 
    
         
            +
                      msg = "#{e.message}. This might be resolved by increasing your user limit "\
         
     | 
| 
       234 
234 
     | 
    
         
             
                        "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
         
     | 
| 
       235 
235 
     | 
    
         
             
                      raise Bolt::Error.new(msg, 'bolt/too-many-files')
         
     | 
| 
       236 
236 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -130,7 +130,7 @@ module Bolt 
     | 
|
| 
       130 
130 
     | 
    
         | 
| 
       131 
131 
     | 
    
         
             
                      [inp, out_rd, err_rd, th]
         
     | 
| 
       132 
132 
     | 
    
         
             
                    rescue Errno::EMFILE => e
         
     | 
| 
       133 
     | 
    
         
            -
                      msg = "#{e.message}. This  
     | 
| 
      
 133 
     | 
    
         
            +
                      msg = "#{e.message}. This might be resolved by increasing your user limit "\
         
     | 
| 
       134 
134 
     | 
    
         
             
                        "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
         
     | 
| 
       135 
135 
     | 
    
         
             
                      raise Bolt::Error.new(msg, 'bolt/too-many-files')
         
     | 
| 
       136 
136 
     | 
    
         
             
                    rescue StandardError
         
     | 
    
        data/lib/bolt/util.rb
    CHANGED
    
    | 
         @@ -77,6 +77,14 @@ module Bolt 
     | 
|
| 
       77 
77 
     | 
    
         
             
                    File.exist?(path) ? read_yaml_hash(path, file_name) : {}
         
     | 
| 
       78 
78 
     | 
    
         
             
                  end
         
     | 
| 
       79 
79 
     | 
    
         | 
| 
      
 80 
     | 
    
         
            +
                  def first_runs_free
         
     | 
| 
      
 81 
     | 
    
         
            +
                    Bolt::Config.user_path + '.first_runs_free'
         
     | 
| 
      
 82 
     | 
    
         
            +
                  end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                  def first_run?
         
     | 
| 
      
 85 
     | 
    
         
            +
                    Bolt::Config.user_path && !File.exist?(first_runs_free)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
       80 
88 
     | 
    
         
             
                  # Accepts a path with either 'plans' or 'tasks' in it and determines
         
     | 
| 
       81 
89 
     | 
    
         
             
                  # the name of the module
         
     | 
| 
       82 
90 
     | 
    
         
             
                  def module_name(path)
         
     | 
| 
         @@ -324,6 +332,40 @@ module Bolt 
     | 
|
| 
       324 
332 
     | 
    
         
             
                    end
         
     | 
| 
       325 
333 
     | 
    
         
             
                  end
         
     | 
| 
       326 
334 
     | 
    
         | 
| 
      
 335 
     | 
    
         
            +
                  # Executes a Docker CLI command. This is useful for running commands as
         
     | 
| 
      
 336 
     | 
    
         
            +
                  # part of this class without having to go through the `execute`
         
     | 
| 
      
 337 
     | 
    
         
            +
                  # function and manage pipes.
         
     | 
| 
      
 338 
     | 
    
         
            +
                  #
         
     | 
| 
      
 339 
     | 
    
         
            +
                  # @param cmd [String] The docker command and arguments to run
         
     | 
| 
      
 340 
     | 
    
         
            +
                  #   e.g. 'cp <src> <dest>' for `docker cp <src> <dest>`
         
     | 
| 
      
 341 
     | 
    
         
            +
                  # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
         
     | 
| 
      
 342 
     | 
    
         
            +
                  def exec_docker(cmd, env = {})
         
     | 
| 
      
 343 
     | 
    
         
            +
                    Open3.capture3(env, 'docker', *cmd, { binmode: true })
         
     | 
| 
      
 344 
     | 
    
         
            +
                  end
         
     | 
| 
      
 345 
     | 
    
         
            +
             
     | 
| 
      
 346 
     | 
    
         
            +
                  # Executes a Podman CLI command. This is useful for running commands as
         
     | 
| 
      
 347 
     | 
    
         
            +
                  # part of this class without having to go through the `execute`
         
     | 
| 
      
 348 
     | 
    
         
            +
                  # function and manage pipes.
         
     | 
| 
      
 349 
     | 
    
         
            +
                  #
         
     | 
| 
      
 350 
     | 
    
         
            +
                  # @param cmd [String] The podman command and arguments to run
         
     | 
| 
      
 351 
     | 
    
         
            +
                  #   e.g. 'cp <src> <dest>' for `podman cp <src> <dest>`
         
     | 
| 
      
 352 
     | 
    
         
            +
                  # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
         
     | 
| 
      
 353 
     | 
    
         
            +
                  def exec_podman(cmd, env = {})
         
     | 
| 
      
 354 
     | 
    
         
            +
                    Open3.capture3(env, 'podman', *cmd, { binmode: true })
         
     | 
| 
      
 355 
     | 
    
         
            +
                  end
         
     | 
| 
      
 356 
     | 
    
         
            +
             
     | 
| 
      
 357 
     | 
    
         
            +
                  # Formats a map of environment variables to be passed to a command that
         
     | 
| 
      
 358 
     | 
    
         
            +
                  # accepts repeated `--env` flags
         
     | 
| 
      
 359 
     | 
    
         
            +
                  #
         
     | 
| 
      
 360 
     | 
    
         
            +
                  # @param env_vars [Hash] A map of environment variables keys and their values
         
     | 
| 
      
 361 
     | 
    
         
            +
                  # @return [String]
         
     | 
| 
      
 362 
     | 
    
         
            +
                  def format_env_vars_for_cli(env_vars)
         
     | 
| 
      
 363 
     | 
    
         
            +
                    @env_vars = env_vars.each_with_object([]) do |(key, value), acc|
         
     | 
| 
      
 364 
     | 
    
         
            +
                      acc << "--env"
         
     | 
| 
      
 365 
     | 
    
         
            +
                      acc << "#{key}=#{value}"
         
     | 
| 
      
 366 
     | 
    
         
            +
                    end
         
     | 
| 
      
 367 
     | 
    
         
            +
                  end
         
     | 
| 
      
 368 
     | 
    
         
            +
             
     | 
| 
       327 
369 
     | 
    
         
             
                  def unix_basename(path)
         
     | 
| 
       328 
370 
     | 
    
         
             
                    raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
         
     | 
| 
       329 
371 
     | 
    
         
             
                    path.split('/').last
         
     |