Skip to content

Commit

Permalink
(puppetlabsGH-2651) Add pwsh_params option to run_script function
Browse files Browse the repository at this point in the history
This adds a `pwsh_params` option to the `run_script` plan function.
The option accepts a hash of named parameters to pass to a PowerShell
script, where each key is the name of the parameter. If the `run_script`
function is called with both the `arguments` and `pwsh_params` options,
Bolt will error and provide a helpful message that both cannot be
specified.

This also adds a `pwsh_params` key to the YAML plan script step.

!feature

* **Add `pwsh_params` option to `run_script` plan function**
  ([puppetlabs#2651](puppetlabs#2651))

  The `run_script` plan function now accepts a `pwsh_params` option
  which can be used to pass named parameters to a PowerShell script.
  • Loading branch information
beechtom committed Mar 8, 2021
1 parent 66968f9 commit 2a2cd2b
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 31 deletions.
26 changes: 24 additions & 2 deletions bolt-modules/boltlib/lib/puppet/functions/run_script.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@
# @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
# @param options A hash of additional options.
# @option options [Array[String]] arguments An array of arguments to be passed to the script.
# Cannot be used with `pwsh_params`.
# @option options [Hash] pwsh_params Map of named parameters to pass to a PowerShell script.
# Cannot be used with `arguments`.
# @option options [Boolean] _catch_errors Whether to catch raised errors.
# @option options [String] _run_as User to run as using privilege escalation.
# @option options [Hash] _env_vars Map of environment variables to set
# @option options [Hash] _env_vars Map of environment variables to set.
# @return A list of results, one entry per target.
# @example Run a local script on Linux targets as 'root'
# run_script('/var/tmp/myscript', $targets, '_run_as' => 'root')
# @example Run a module-provided script with arguments
# run_script('iis/setup.ps1', $target, 'arguments' => ['/u', 'Administrator'])
# @example Pass named parameters to a PowerShell script
# run_script('iis/setup.ps1', $target, 'pwsh_params' => { 'User' => 'Administrator' })
dispatch :run_script do
scope_param
param 'String[1]', :script
Expand All @@ -34,9 +39,12 @@
# @param description A description to be output when calling this function.
# @param options A hash of additional options.
# @option options [Array[String]] arguments An array of arguments to be passed to the script.
# Cannot be used with `pwsh_params`.
# @option options [Hash] pwsh_params Map of named parameters to pass to a PowerShell script.
# Cannot be used with `arguments`.
# @option options [Boolean] _catch_errors Whether to catch raised errors.
# @option options [String] _run_as User to run as using privilege escalation.
# @option options [Hash] _env_vars Map of environment variables to set
# @option options [Hash] _env_vars Map of environment variables to set.
# @return A list of results, one entry per target.
# @example Run a script
# run_script('/var/tmp/myscript', $targets, 'Downloading my application')
Expand All @@ -59,9 +67,23 @@ def run_script_with_description(scope, script, targets, description = nil, optio
.from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_script')
end

if options.key?('arguments') && options.key?('pwsh_params')
raise Bolt::ValidationError, "Cannot specify both 'arguments' and 'pwsh_params'"
end

if options.key?('pwsh_params') && !options['pwsh_params'].is_a?(Hash)
raise Bolt::ValidationError, "Option 'pwsh_params' must be a hash"
end

if options.key?('arguments') && !options['arguments'].is_a?(Array)
raise Bolt::ValidationError, "Option 'arguments' must be an array"
end

arguments = options['arguments'] || []
pwsh_params = options['pwsh_params']
options = options.select { |opt| opt.start_with?('_') }.transform_keys { |k| k.sub(/^_/, '').to_sym }
options[:description] = description if description
options[:pwsh_params] = pwsh_params if pwsh_params

executor = Puppet.lookup(:bolt_executor)
inventory = Puppet.lookup(:bolt_inventory)
Expand Down
33 changes: 33 additions & 0 deletions bolt-modules/boltlib/spec/functions/run_script_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@
.and_return(result_set)
end

it 'with pwsh_params' do
executor.expects(:run_script)
.with([target], full_path, [], { pwsh_params: { 'Name' => 'BoltyMcBoltface' } }, [])
.returns(result_set)
inventory.expects(:get_targets).with(hostname).returns([target])

is_expected.to run
.with_params('test/uploads/hostname.sh',
hostname,
{ 'pwsh_params' => { 'Name' => 'BoltyMcBoltface' } })
.and_return(result_set)
end

it 'with _run_as' do
executor.expects(:run_script)
.with([target], full_path, [], { run_as: 'root' }, [])
Expand Down Expand Up @@ -246,4 +259,24 @@
.and_raise_error(/Plan language function 'run_script' cannot be used/)
end
end

context 'with arguments and pwsh_params' do
it 'fails' do
is_expected.to run
.with_params('test/uploads/script.sh', [], 'arguments' => [], 'pwsh_params' => {})
.and_raise_error(/Cannot specify both 'arguments' and 'pwsh_params'/)
end
end

it 'fails if arguments is not an array' do
is_expected.to run
.with_params('test/uploads/script.sh', [], 'arguments' => { 'foo' => 'bar' })
.and_raise_error(/Option 'arguments' must be an array/)
end

it 'fails if pwsh_params is not a hash' do
is_expected.to run
.with_params('test/uploads/script.sh', [], 'pwsh_params' => %w[foo bar])
.and_raise_error(/Option 'pwsh_params' must be a hash/)
end
end
3 changes: 1 addition & 2 deletions documentation/templates/plan_functions.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ functions](https://puppet.com/docs/puppet/latest/writing_custom_functions.html).
<%= sig['desc'].lines.drop(1).join %>
<% end -%>


```
<%= sig['signature'] %>
```
Expand All @@ -41,7 +40,7 @@ This function<%= func['signatures'].count == 1 ? '' : ' signature' %> accepts th
| --- | --- | --- |
<% sig['options'].each do |name, data| -%>
| `<%= name %>` | `<%= data['type'] %>` | <%= data['desc'] %> |
<% end -%>
<% end %>
<% end -%>
<% end -%>
Expand Down
3 changes: 2 additions & 1 deletion documentation/writing_yaml_plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,12 @@ Script steps support the following keys:

| Key | Type | Description | Required |
| --- | --- | --- | --- |
| `arguments` | `Array` | An array of command-line arguments to pass to the script. | |
| `arguments` | `Array` | An array of command-line arguments to pass to the script. Cannot be used with `pwsh_params`. | |
| `catch_errors` | `Boolean` | Whether to catch raised errors. If set to true, the plan continues execution if the step fails. | |
| `description` | `String` | The step's description. Logged by Bolt when the step is run. | |
| `env_vars` | `Hash` | A map of environment variables to set on the target when running the script. | |
| `name` | `String` | The name of the variable to save the step result to. | |
| `pwsh_params` | `Hash` | A map of named parameters to pass to a PowerShell script. Cannot be used with `arguments`. | |
| `run_as` | `String` | The user to run as when running the script on the target. Only applies to targets using a transport that supports `run-as` configuration. | |
| `script` | `String` | The script to run. | ✓ |
| `targets` | `Array`, `String` | A target or list of targets to run the script on. | ✓ |
Expand Down
20 changes: 18 additions & 2 deletions lib/bolt/pal/yaml_plan/step/script.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class YamlPlan
class Step
class Script < Step
def self.allowed_keys
super + Set['arguments']
super + Set['arguments', 'pwsh_params']
end

def self.option_keys
Expand All @@ -17,11 +17,27 @@ def self.required_keys
Set['script', 'targets']
end

def self.validate_step_keys(body, number)
super

unless body.fetch('arguments', []).is_a?(Array)
raise StepError.new('arguments key must be an array', body['name'], number)
end

unless body.fetch('pwsh_params', {}).is_a?(Hash)
raise StepError.new('pwsh_params key must be a hash', body['name'], number)
end
end

# Returns an array of arguments to pass to the step's function call
#
private def format_args(body)
args = body['arguments'] || []
pwsh_params = body['pwsh_params'] || {}

opts = format_options(body)
opts = opts.merge('arguments' => body['arguments'] || []) if body.key?('arguments')
opts = opts.merge('arguments' => args) if args.any?
opts = opts.merge('pwsh_params' => pwsh_params) if pwsh_params.any?

args = [body['script'], body['targets']]
args << body['description'] if body['description']
Expand Down
11 changes: 8 additions & 3 deletions lib/bolt/shell/powershell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,21 @@ def run_script(script, arguments, options = {}, position = [])
arguments = unwrap_sensitive_args(arguments)
with_tmpdir do |dir|
script_path = write_executable(dir, script)
command = if powershell_file?(script_path)
command = if powershell_file?(script_path) && options[:pwsh_params]
# Scripts run with pwsh_params can be run like tasks
Snippets.ps_task(script_path, options[:pwsh_params])
elsif powershell_file?(script_path)
Snippets.run_script(arguments, script_path)
else
path, args = *process_from_extension(script_path)
args += escape_arguments(arguments)
execute_process(path, args)
end
command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
env_assignments = options[:env_vars] ? env_declarations(options[:env_vars]) : []
shell_init = options[:pwsh_params] ? Snippets.shell_init : ''

output = execute([shell_init, *env_assignments, command].join("\r\n"))

output = execute(command)
Bolt::Result.for_command(target,
output.stdout.string,
output.stderr.string,
Expand Down
99 changes: 78 additions & 21 deletions spec/bolt/pal/yaml_plan/evaluator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -281,38 +281,95 @@ def call_plan(plan, params = {})
'arguments' => %w[a b c] }
end

it 'passes arguments to the script' do
args = ['mymodule/myscript.sh', 'foo.example.com', 'arguments' => %w[a b c]]
expect(scope).to receive(:call_function).with('run_script', args)
context 'with arguments' do
it 'passes arguments to the script' do
args = ['mymodule/myscript.sh', 'foo.example.com', 'arguments' => %w[a b c]]
expect(scope).to receive(:call_function).with('run_script', args)

step.evaluate(scope, subject)
end
step.evaluate(scope, subject)
end

it 'succeeds if no arguments are specified' do
step_body.delete('arguments')
it 'succeeds if no arguments are specified' do
step_body.delete('arguments')

args = ['mymodule/myscript.sh', 'foo.example.com']
expect(scope).to receive(:call_function).with('run_script', args)
args = ['mymodule/myscript.sh', 'foo.example.com']
expect(scope).to receive(:call_function).with('run_script', args)

step.evaluate(scope, subject)
end
step.evaluate(scope, subject)
end

it 'succeeds if empty arguments are specified' do
step_body['arguments'] = []
it 'succeeds if empty arguments are specified' do
step_body['arguments'] = []

args = ['mymodule/myscript.sh', 'foo.example.com', 'arguments' => []]
expect(scope).to receive(:call_function).with('run_script', args)
args = ['mymodule/myscript.sh', 'foo.example.com']
expect(scope).to receive(:call_function).with('run_script', args)

step.evaluate(scope, subject)
step.evaluate(scope, subject)
end

it 'succeeds if nil arguments are specified' do
step_body['arguments'] = nil

args = ['mymodule/myscript.sh', 'foo.example.com']
expect(scope).to receive(:call_function).with('run_script', args)

step.evaluate(scope, subject)
end

it 'errors if arguments is not an array' do
step_body['arguments'] = { 'foo' => 'bar' }

expect { step }.to raise_error(
Bolt::PAL::YamlPlan::Step::StepError,
/arguments key must be an array/
)
end
end

it 'succeeds if nil arguments are specified' do
step_body['arguments'] = nil
context 'with pwsh_params' do
let(:params) { { 'Name' => 'BoltyMcBoltface' } }
let(:script) { 'mymodule/myscript.sh' }
let(:target) { 'foo.example.com' }

args = ['mymodule/myscript.sh', 'foo.example.com', 'arguments' => []]
expect(scope).to receive(:call_function).with('run_script', args)
let(:step_body) do
{
'script' => script,
'targets' => target,
'pwsh_params' => params
}
end

step.evaluate(scope, subject)
it 'passes pwsh_params to the script' do
args = [script, target, { 'pwsh_params' => params }]

expect(scope).to receive(:call_function).with('run_script', args)
step.evaluate(scope, subject)
end

it 'succeeds if empty pwsh_params are specified' do
step_body['pwsh_params'] = {}
args = [script, target]

expect(scope).to receive(:call_function).with('run_script', args)
step.evaluate(scope, subject)
end

it 'succeeds if nil pwsh_params are specified' do
step_body['pwsh_params'] = nil
args = [script, target]

expect(scope).to receive(:call_function).with('run_script', args)
step.evaluate(scope, subject)
end

it 'errors if pwsh_params is not a hash' do
step_body['pwsh_params'] = ['-Name', 'foo']

expect { step }.to raise_error(
Bolt::PAL::YamlPlan::Step::StepError,
/pwsh_params key must be a hash/
)
end
end

it 'supports a description' do
Expand Down
18 changes: 18 additions & 0 deletions spec/integration/transport/winrm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,24 @@ def stub_winrm_to_raise(klass, message)
end
end

it "can run a PowerShell script with named parameters", winrm: true do
contents = <<~PS
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[String]
$Name
)
Write-Output "Hello $Name"
PS

with_tempfile_containing('script-test-winrm', contents, '.ps1') do |file|
result = winrm.run_script(target, file.path, [], pwsh_params: { 'Name' => 'BoltyMcBoltface' })
expect(result['stdout']).to eq("Hello BoltyMcBoltface\r\n")
end
end

it "ignores run_as", winrm: true do
contents = "Write-Output \"hellote\""
with_tempfile_containing('script-test-winrm', contents, '.ps1') do |file|
Expand Down

0 comments on commit 2a2cd2b

Please sign in to comment.