Skip to content

Commit

Permalink
(puppetlabsGH-2125) Read command from a file or stdin
Browse files Browse the repository at this point in the history
This adds support for reading a command from a file or `stdin` using
the `bolt command run` command. To read a command from a file,
pass the path to the file with the `@` symbol:

```shell
$ bolt command run @script.sh --targets targets
```

The filepath is expanded relative to the current working directory.

To read a command from `stdin`, pass a single dash `-` as the command
and then pipe the command to Bolt:

```shell
$ echo whoami | bolt command run - --targets targets
```

!feature

* **Read command from a file or `stdin` using `bolt command run`**
  ([puppetlabs#2125](puppetlabs#2125))

  The `bolt command run` command can now read a command from a file or
  `stdin`.
  • Loading branch information
beechtom committed Sep 16, 2020
1 parent adeb4d2 commit 142adf4
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 22 deletions.
35 changes: 32 additions & 3 deletions documentation/running_bolt_commands.md
Expand Up @@ -14,11 +14,13 @@ across those systems.
```shell script
bolt command run <COMMAND> --targets <TARGET NAME>,<TARGET NAME>,<TARGET NAME>
```

- To run a command on WinRM targets, indicate the WinRM protocol in the
targets string:
```shell script
bolt command run <COMMAND> --targets winrm://<WINDOWS.TARGET> --user <USERNAME> --password <PASSWORD>
```

- To run a command that contains spaces or shell special characters, wrap the
command in single quotation marks:
```shell script
Expand All @@ -27,14 +29,41 @@ across those systems.
```shell script
bolt command run "netstat -an | grep 'tcp.*LISTEN'" --targets web5.mydomain.edu,web6.mydomain.edu
```

- To run a cross-platform command:
```shell script
bolt command run "echo 'hello world'"
```

**Note:** When connecting to Bolt hosts over WinRM that have not configured
SSL for port 5986, passing the `--no-ssl` switch is required to connect to
the default WinRM port 5985.
- To read a command from a file, pass the command as an `@` symbol followed by
the path to the file:

```
bolt command run @command.txt --targets servers
```

For Windows PowerShell, add single quotation marks to define the file:

```
bolt command run '@command.txt' --targets servers
```

- To read a command from `stdin`, pass the command as a single dash `-` and
pipe the command to Bolt:

```
<COMMAND> | bolt command run - --targets servers
```

For example, if you have a command in a text file:

```
cat command.txt | bolt command run - --targets servers
```

> **Note:** When connecting to Bolt hosts over WinRM that have not configured
> SSL for port 5986, passing the `--no-ssl` switch is required to connect to
> the default WinRM port 5985.

## Running commands with redirection or pipes
Expand Down
21 changes: 2 additions & 19 deletions lib/bolt/bolt_option_parser.rb
Expand Up @@ -746,7 +746,7 @@ def initialize(options)
'For SSH, port defaults to `22`',
'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
@options[:targets] ||= []
@options[:targets] << get_arg_input(targets)
@options[:targets] << Bolt::Util.get_arg_input(targets)
end
define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query|
@options[:query] = query
Expand Down Expand Up @@ -985,27 +985,10 @@ def update
end

def parse_params(params)
json = get_arg_input(params)
json = Bolt::Util.get_arg_input(params)
JSON.parse(json)
rescue JSON::ParserError => e
raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
end

def get_arg_input(value)
if value.start_with?('@')
file = value.sub(/^@/, '')
read_arg_file(file)
elsif value == '-'
$stdin.read
else
value
end
end

def read_arg_file(file)
File.read(File.expand_path(file))
rescue StandardError => e
raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
end
end
end
5 changes: 5 additions & 0 deletions lib/bolt/cli.rb
Expand Up @@ -107,6 +107,11 @@ def parse_command

options[:object] = remaining.shift

# Handle reading a command from a file
if options[:subcommand] == 'command'
options[:object] = Bolt::Util.get_arg_input(options[:object])
end

# Only parse task_options for task or plan
if %w[task plan].include?(options[:subcommand])
task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
Expand Down
25 changes: 25 additions & 0 deletions lib/bolt/util.rb
Expand Up @@ -3,6 +3,31 @@
module Bolt
module Util
class << self
# Gets input for an argument.
def get_arg_input(value)
if value.start_with?('@')
file = value.sub(/^@/, '')
read_arg_file(file)
elsif value == '-'
$stdin.read
else
value
end
end

# Reads a file passed as an argument to a command.
def read_arg_file(file)
file = File.expand_path(file)

unless File.exist?(file)
raise Bolt::FileError.new("File #{file} does not exist", file)
end

File.read(File.expand_path(file))
rescue StandardError => e
raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
end

def read_yaml_hash(path, file_name)
require 'yaml'

Expand Down
19 changes: 19 additions & 0 deletions spec/bolt/cli_spec.rb
Expand Up @@ -1043,6 +1043,25 @@ def stub_config(file_content = {})
cli = Bolt::CLI.new(%w[command run --targets foo whoami --env-var POP=TARTS])
expect(cli.parse[:env_vars]).to eq({ 'POP' => 'TARTS' })
end

it "reads from a file when command starts with @" do
command = 'whoami'

with_tempfile_containing('command', command) do |file|
cli = Bolt::CLI.new(%W[command run @#{file.path}])
options = cli.parse
expect(options[:object]).to eq(command)
end
end

it "reads from stdin when command is '-'" do
command = 'whoami'

cli = Bolt::CLI.new(%w[command run - --targets localhost])
allow($stdin).to receive(:read).and_return(command)
options = cli.parse
expect(options[:object]).to eq(command)
end
end

it "distinguishes subcommands" do
Expand Down

0 comments on commit 142adf4

Please sign in to comment.