bolt 1.12.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +4 -3
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +7 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +8 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +7 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +7 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +6 -6
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -3
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +6 -6
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +5 -4
  11. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +5 -4
  12. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +4 -3
  13. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +6 -6
  14. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +6 -5
  15. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +8 -0
  16. data/lib/bolt/config.rb +14 -26
  17. data/lib/bolt/pal.rb +1 -0
  18. data/lib/bolt/pal/issues.rb +13 -0
  19. data/lib/bolt/transport/base.rb +8 -0
  20. data/lib/bolt/transport/docker.rb +6 -6
  21. data/lib/bolt/transport/docker/connection.rb +7 -4
  22. data/lib/bolt/transport/local.rb +25 -10
  23. data/lib/bolt/transport/local/shell.rb +1 -0
  24. data/lib/bolt/transport/orch.rb +4 -0
  25. data/lib/bolt/transport/remote.rb +4 -0
  26. data/lib/bolt/transport/ssh.rb +10 -1
  27. data/lib/bolt/transport/ssh/connection.rb +4 -0
  28. data/lib/bolt/transport/winrm.rb +24 -2
  29. data/lib/bolt/transport/winrm/connection.rb +112 -2
  30. data/lib/bolt/version.rb +1 -1
  31. data/lib/bolt_server/schemas/ssh-run_task.json +4 -0
  32. data/lib/bolt_server/schemas/winrm-run_task.json +14 -0
  33. data/lib/bolt_spec/plans.rb +5 -0
  34. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05e284722c10c61a2fe08a01b920b18739b8cc31486762db9dfadba82288f6d6
4
- data.tar.gz: 59e57d3df534fcbae81f2f5f44000e1f1930aec4029c8d664696e156a715a922
3
+ metadata.gz: 66234162abaa83dd3dd590b21dd87a54d10c3da44f0d50ccd8e28d82ddf76178
4
+ data.tar.gz: b056cb53e8fda46db48b7a7a1b63f8441f80342e11e5c0a7c50d1813cf852bbc
5
5
  SHA512:
6
- metadata.gz: ea83de65334823f418383868b21cb5f23f501e11fbb525e5d9b9ebc8fac9f818322f56e4069f2697e9de48ce5631a349e5673cf16c0b5437e0f4799e654c6b39
7
- data.tar.gz: b1d95304ae24c3f792ee9fecf9fe42073efc73f7fbd309654f78112886e5e55a2541913e1a1aa705c8e123113f53ba136f18731b39344d3b9bdd0ac31a96f496
6
+ metadata.gz: '079515ce4801e6913e2b75d276289985cf9eda0d9b60c10021a5b435c77ce69d0cc07d5a1708a4f7c43db4947fef918f5d14b8ccac8fdf6a55b5ccf5fce6cf62'
7
+ data.tar.gz: 94eba8ff4a83d463f4b5516eb69cd83d31b1fdc6c3673a0b0beda9ac5dc08becd9eb2d75953932c57345ab8099900884174df329d04f43637802df73a538a365
@@ -3,6 +3,8 @@
3
3
  require 'bolt/error'
4
4
 
5
5
  # Deep merges a hash of facts with the existing facts on a target.
6
+ #
7
+ # **NOTE:** Not available in apply block
6
8
  Puppet::Functions.create_function(:add_facts) do
7
9
  # @param target A target.
8
10
  # @param facts A hash of fact names to values that may include structured facts.
@@ -17,9 +19,8 @@ Puppet::Functions.create_function(:add_facts) do
17
19
 
18
20
  def add_facts(target, facts)
19
21
  unless Puppet[:tasks]
20
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
21
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'add_facts'
22
- )
22
+ raise Puppet::ParseErrorWithIssue
23
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'add_facts')
23
24
  end
24
25
 
25
26
  inventory = Puppet.lookup(:bolt_inventory) { nil }
@@ -3,6 +3,8 @@
3
3
  require 'bolt/error'
4
4
 
5
5
  # Adds a target to specified inventory group.
6
+ #
7
+ # **NOTE:** Not available in apply block
6
8
  Puppet::Functions.create_function(:add_to_group) do
7
9
  # @param targets A pattern or array of patterns identifying a set of targets.
8
10
  # @param group The name of the group to add targets to.
@@ -20,6 +22,11 @@ Puppet::Functions.create_function(:add_to_group) do
20
22
  end
21
23
 
22
24
  def add_to_group(targets, group)
25
+ unless Puppet[:tasks]
26
+ raise Puppet::ParseErrorWithIssue
27
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'add_to_group')
28
+ end
29
+
23
30
  inventory = Puppet.lookup(:bolt_inventory) { nil }
24
31
 
25
32
  unless inventory && Puppet.features.bolt?
@@ -10,6 +10,8 @@ require 'bolt/task'
10
10
  #
11
11
  # If no agent is detected on the target using the 'puppet_agent::version' task, it's installed
12
12
  # using 'puppet_agent::install' and the puppet service is stopped/disabled using the 'service' task.
13
+ #
14
+ # **NOTE:** Not available in apply block
13
15
  Puppet::Functions.create_function(:apply_prep) do
14
16
  # @param targets A pattern or array of patterns identifying a set of targets.
15
17
  # @example Prepare targets by name.
@@ -39,9 +41,15 @@ Puppet::Functions.create_function(:apply_prep) do
39
41
  end
40
42
 
41
43
  def apply_prep(target_spec)
44
+ unless Puppet[:tasks]
45
+ raise Puppet::ParseErrorWithIssue
46
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'apply_prep')
47
+ end
48
+
42
49
  applicator = Puppet.lookup(:apply_executor) { nil }
43
50
  executor = Puppet.lookup(:bolt_executor) { nil }
44
51
  inventory = Puppet.lookup(:bolt_inventory) { nil }
52
+
45
53
  unless applicator && executor && inventory && Puppet.features.bolt?
46
54
  raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
47
55
  Puppet::Pops::Issues::TASK_MISSING_BOLT, action: _('apply_prep')
@@ -6,6 +6,8 @@ require 'bolt/error'
6
6
  #
7
7
  # Plan authors should call this function when their plan is not successful. The
8
8
  # error may then be caught by another plans run_plan function or in bolt itself
9
+ #
10
+ # **NOTE:** Not available in apply block
9
11
  Puppet::Functions.create_function(:fail_plan) do
10
12
  # Fail a plan, generating an exception from the parameters.
11
13
  # @param msg An error message.
@@ -32,6 +34,11 @@ Puppet::Functions.create_function(:fail_plan) do
32
34
  end
33
35
 
34
36
  def from_args(msg, kind = nil, details = nil, issue_code = nil)
37
+ unless Puppet[:tasks]
38
+ raise Puppet::ParseErrorWithIssue
39
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'fail_plan')
40
+ end
41
+
35
42
  executor = Puppet.lookup(:bolt_executor) { nil }
36
43
  executor&.report_function_call('fail_plan')
37
44
 
@@ -7,6 +7,8 @@ require 'bolt/task'
7
7
  #
8
8
  # Requires the Puppet Agent be installed on the target, which can be accomplished with apply_prep
9
9
  # or by directly running the puppet_agent::install task.
10
+ #
11
+ # **NOTE:** Not available in apply block
10
12
  Puppet::Functions.create_function(:get_resources) do
11
13
  # @param targets A pattern or array of patterns identifying a set of targets.
12
14
  # @param resources A resource type or instance, or an array of such.
@@ -32,6 +34,11 @@ Puppet::Functions.create_function(:get_resources) do
32
34
  end
33
35
 
34
36
  def get_resources(target_spec, resources)
37
+ unless Puppet[:tasks]
38
+ raise Puppet::ParseErrorWithIssue
39
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'get_resources')
40
+ end
41
+
35
42
  applicator = Puppet.lookup(:apply_executor) { nil }
36
43
  executor = Puppet.lookup(:bolt_executor) { nil }
37
44
  inventory = Puppet.lookup(:bolt_inventory) { nil }
@@ -4,6 +4,8 @@ require 'bolt/error'
4
4
 
5
5
  # Runs a command on the given set of targets and returns the result from each command execution.
6
6
  # This function does nothing if the list of targets is empty.
7
+ #
8
+ # **NOTE:** Not available in apply block
7
9
  Puppet::Functions.create_function(:run_command) do
8
10
  # Run a command.
9
11
  # @param command A command to run on target.
@@ -40,15 +42,13 @@ Puppet::Functions.create_function(:run_command) do
40
42
  end
41
43
 
42
44
  def run_command_with_description(command, targets, description = nil, options = nil)
43
- options ||= {}
44
- options = options.merge('_description' => description) if description
45
-
46
45
  unless Puppet[:tasks]
47
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
48
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'run_command'
49
- )
46
+ raise Puppet::ParseErrorWithIssue
47
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_command')
50
48
  end
51
49
 
50
+ options ||= {}
51
+ options = options.merge('_description' => description) if description
52
52
  executor = Puppet.lookup(:bolt_executor) { nil }
53
53
  inventory = Puppet.lookup(:bolt_inventory) { nil }
54
54
  unless executor && inventory && Puppet.features.bolt?
@@ -3,6 +3,8 @@
3
3
  require 'bolt/error'
4
4
 
5
5
  # Runs the `plan` referenced by its name. A plan is autoloaded from `<moduleroot>/plans`.
6
+ #
7
+ # **NOTE:** Not available in apply block
6
8
  Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction) do
7
9
  # @param plan_name The plan to run.
8
10
  # @param named_args Arguments to the plan. Can also include additional options: '_catch_errors', '_run_as'.
@@ -18,9 +20,8 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
18
20
 
19
21
  def run_plan(scope, plan_name, named_args = {})
20
22
  unless Puppet[:tasks]
21
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
22
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'run_plan'
23
- )
23
+ raise Puppet::ParseErrorWithIssue
24
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_plan')
24
25
  end
25
26
 
26
27
  executor = Puppet.lookup(:bolt_executor) { nil }
@@ -2,6 +2,8 @@
2
2
 
3
3
  # Uploads the given script to the given set of targets and returns the result of having each target execute the script.
4
4
  # This function does nothing if the list of targets is empty.
5
+ #
6
+ # **NOTE:** Not available in apply block
5
7
  Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFunction) do
6
8
  # Run a script.
7
9
  # @param script Path to a script to run on target. May be an absolute path or a modulename/filename selector for a
@@ -46,15 +48,13 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
46
48
  end
47
49
 
48
50
  def run_script_with_description(scope, script, targets, description = nil, options = nil)
49
- options ||= {}
50
- options = options.merge('_description' => description) if description
51
-
52
51
  unless Puppet[:tasks]
53
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
54
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'run_script'
55
- )
52
+ raise Puppet::ParseErrorWithIssue
53
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_script')
56
54
  end
57
55
 
56
+ options ||= {}
57
+ options = options.merge('_description' => description) if description
58
58
  executor = Puppet.lookup(:bolt_executor) { nil }
59
59
  inventory = Puppet.lookup(:bolt_inventory) { nil }
60
60
  unless executor && inventory && Puppet.features.bolt?
@@ -6,6 +6,8 @@ require 'bolt/task'
6
6
 
7
7
  # Runs a given instance of a `Task` on the given set of targets and returns the result from each.
8
8
  # This function does nothing if the list of targets is empty.
9
+ #
10
+ # **NOTE:** Not available in apply block
9
11
  Puppet::Functions.create_function(:run_task) do
10
12
  # Run a task.
11
13
  # @param task_name The task to run.
@@ -67,13 +69,12 @@ Puppet::Functions.create_function(:run_task) do
67
69
  end
68
70
 
69
71
  def run_task_raw(task_name, targets, description = nil, task_args = nil, &block)
70
- task_args ||= {}
71
72
  unless Puppet[:tasks]
72
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
73
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'run_task'
74
- )
73
+ raise Puppet::ParseErrorWithIssue
74
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_task')
75
75
  end
76
76
 
77
+ task_args ||= {}
77
78
  executor = Puppet.lookup(:bolt_executor) { nil }
78
79
  inventory = Puppet.lookup(:bolt_inventory) { nil }
79
80
  unless executor && inventory && Puppet.features.bolt?
@@ -9,11 +9,13 @@ require 'bolt/error'
9
9
  # - powershell
10
10
  # - shell
11
11
  # - puppet-agent
12
+ #
13
+ # **NOTE:** Not available in apply block
12
14
  Puppet::Functions.create_function(:set_feature) do
13
15
  # @param target The Target object to add features to. See {get_targets}.
14
16
  # @param feature The string identifying the feature.
15
17
  # @param value Whether the feature is supported.
16
- # @return [Undef]
18
+ # @return The target with the updated feature
17
19
  # @example Add the puppet-agent feature to a target
18
20
  # set_feature($target, 'puppet-agent', true)
19
21
  dispatch :set_feature do
@@ -24,9 +26,8 @@ Puppet::Functions.create_function(:set_feature) do
24
26
 
25
27
  def set_feature(target, feature, value = true)
26
28
  unless Puppet[:tasks]
27
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
28
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'set_feature'
29
- )
29
+ raise Puppet::ParseErrorWithIssue
30
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'set_feature')
30
31
  end
31
32
 
32
33
  inventory = Puppet.lookup(:bolt_inventory) { nil }
@@ -3,6 +3,8 @@
3
3
  require 'bolt/error'
4
4
 
5
5
  # Sets a variable { key => value } for a target.
6
+ #
7
+ # **NOTE:** Not available in apply block
6
8
  Puppet::Functions.create_function(:set_var) do
7
9
  # @param target The Target object to set the variable for. See {get_targets}.
8
10
  # @param key The key for the variable.
@@ -18,9 +20,8 @@ Puppet::Functions.create_function(:set_var) do
18
20
 
19
21
  def set_var(target, key, value)
20
22
  unless Puppet[:tasks]
21
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
22
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'set_var'
23
- )
23
+ raise Puppet::ParseErrorWithIssue
24
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'set_var')
24
25
  end
25
26
 
26
27
  inventory = Puppet.lookup(:bolt_inventory) { nil }
@@ -4,6 +4,8 @@ require 'bolt/error'
4
4
 
5
5
  # Uploads the given file or directory to the given set of targets and returns the result from each upload.
6
6
  # This function does nothing if the list of targets is empty.
7
+ #
8
+ # **NOTE:** Not available in apply block
7
9
  Puppet::Functions.create_function(:upload_file, Puppet::Functions::InternalFunction) do
8
10
  # Upload a file.
9
11
  # @param source A source path, either an absolute path or a modulename/filename selector for a file in
@@ -50,15 +52,13 @@ Puppet::Functions.create_function(:upload_file, Puppet::Functions::InternalFunct
50
52
  end
51
53
 
52
54
  def upload_file_with_description(scope, source, destination, targets, description = nil, options = nil)
53
- options ||= {}
54
- options = options.merge('_description' => description) if description
55
-
56
55
  unless Puppet[:tasks]
57
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
58
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'upload_file'
59
- )
56
+ raise Puppet::ParseErrorWithIssue
57
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'upload_file')
60
58
  end
61
59
 
60
+ options ||= {}
61
+ options = options.merge('_description' => description) if description
62
62
  executor = Puppet.lookup(:bolt_executor) { nil }
63
63
  inventory = Puppet.lookup(:bolt_inventory) { nil }
64
64
  unless executor && inventory && Puppet.features.bolt?
@@ -3,6 +3,8 @@
3
3
  require 'bolt/util'
4
4
 
5
5
  # Wait until all targets accept connections.
6
+ #
7
+ # **NOTE:** Not available in apply block
6
8
  Puppet::Functions.create_function(:wait_until_available) do
7
9
  # Wait until targets are available.
8
10
  # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
@@ -17,14 +19,13 @@ Puppet::Functions.create_function(:wait_until_available) do
17
19
  end
18
20
 
19
21
  def wait_until_available(targets, options = nil)
20
- options ||= {}
21
-
22
22
  unless Puppet[:tasks]
23
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
24
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'wait_until_available'
25
- )
23
+ raise Puppet::ParseErrorWithIssue
24
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
25
+ action: 'wait_until_available')
26
26
  end
27
27
 
28
+ options ||= {}
28
29
  executor = Puppet.lookup(:bolt_executor) { nil }
29
30
  inventory = Puppet.lookup(:bolt_inventory) { nil }
30
31
  unless executor && inventory && Puppet.features.bolt?
@@ -5,6 +5,8 @@
5
5
  # Messages for actions within this block will be logged at `info` level instead
6
6
  # of `notice`, so they will not be seen normally but # will still be present
7
7
  # when `verbose` logging is requested.
8
+ #
9
+ # **NOTE:** Not available in apply block
8
10
  Puppet::Functions.create_function(:without_default_logging) do
9
11
  # @param block The block where action logging is suppressed.
10
12
  # @return [Undef]
@@ -20,6 +22,12 @@ Puppet::Functions.create_function(:without_default_logging) do
20
22
  end
21
23
 
22
24
  def without_default_logging
25
+ unless Puppet[:tasks]
26
+ raise Puppet::ParseErrorWithIssue
27
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING,
28
+ action: 'without_default_logging')
29
+ end
30
+
23
31
  executor = Puppet.lookup(:bolt_executor) { nil }
24
32
  executor.report_function_call('without_default_logging')
25
33
 
@@ -36,29 +36,7 @@ module Bolt
36
36
 
37
37
  TRANSPORT_OPTIONS = %i[password run-as sudo-password extensions
38
38
  private-key tty tmpdir user connect-timeout
39
- cacert token-file service-url].freeze
40
-
41
- # TODO: move these to the transport themselves
42
- TRANSPORT_SPECIFIC_DEFAULTS = {
43
- ssh: {
44
- 'connect-timeout' => 10,
45
- 'host-key-check' => true,
46
- 'tty' => false
47
- },
48
- winrm: {
49
- 'connect-timeout' => 10,
50
- 'ssl' => true,
51
- 'ssl-verify' => true
52
- },
53
- pcp: {
54
- 'task-environment' => 'production'
55
- },
56
- local: {},
57
- docker: {},
58
- remote: {
59
- 'run-on' => 'localhost'
60
- }
61
- }.freeze
39
+ cacert token-file service-url interpreters file-protocol smb-port].freeze
62
40
 
63
41
  def self.default
64
42
  new(Bolt::Boltdir.new('.'), {})
@@ -91,8 +69,9 @@ module Bolt
91
69
  @log = { 'console' => {} }
92
70
 
93
71
  @transports = {}
94
- TRANSPORTS.each_key do |transport|
95
- @transports[transport] = TRANSPORT_SPECIFIC_DEFAULTS[transport].dup
72
+
73
+ TRANSPORTS.each do |key, transport|
74
+ @transports[key] = transport.default_options
96
75
  end
97
76
 
98
77
  update_from_file(config_data)
@@ -114,6 +93,12 @@ module Bolt
114
93
  Bolt::Util.deep_clone(self)
115
94
  end
116
95
 
96
+ def normalize_interpreters(interpreters)
97
+ Bolt::Util.walk_keys(interpreters) do |key|
98
+ key.chars[0] == '.' ? key : '.' + key
99
+ end
100
+ end
101
+
117
102
  def normalize_log(target)
118
103
  return target if target == 'console'
119
104
  target = target[5..-1] if target.start_with?('file:')
@@ -168,7 +153,10 @@ module Bolt
168
153
  TRANSPORTS.each do |key, impl|
169
154
  if data[key.to_s]
170
155
  selected = impl.filter_options(data[key.to_s])
171
- @transports[key].merge!(selected)
156
+ @transports[key] = Bolt::Util.deep_merge(@transports[key], selected)
157
+ end
158
+ if @transports[key]['interpreters']
159
+ @transports[key]['interpreters'] = normalize_interpreters(@transports[key]['interpreters'])
172
160
  end
173
161
  end
174
162
  end
@@ -80,6 +80,7 @@ module Bolt
80
80
  end
81
81
 
82
82
  require 'bolt/pal/logging'
83
+ require 'bolt/pal/issues'
83
84
 
84
85
  # Now that puppet is loaded we can include puppet mixins in data types
85
86
  Bolt::ResultSet.include_iterable
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ module Issues
6
+ # Create issue using Issues api
7
+ PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING =
8
+ Puppet::Pops::Issues.issue :PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, :action do
9
+ "Plan language function '#{action}' cannot be used from declarative manifest code or apply blocks"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -48,6 +48,10 @@ module Bolt
48
48
  "self.options() or self.filter_options(unfiltered) must be implemented by the transport class"
49
49
  end
50
50
 
51
+ def self.default_options
52
+ {}
53
+ end
54
+
51
55
  def self.filter_options(unfiltered)
52
56
  unfiltered.select { |k| options.include?(k) }
53
57
  end
@@ -87,6 +91,10 @@ module Bolt
87
91
  impl
88
92
  end
89
93
 
94
+ def select_interpreter(executable, interpreters)
95
+ interpreters[Pathname(executable).extname] if interpreters
96
+ end
97
+
90
98
  def reject_transport_options(target, options)
91
99
  if target.options['run-as']
92
100
  options.reject { |k, _v| k == '_run_as' }
@@ -8,7 +8,7 @@ module Bolt
8
8
  module Transport
9
9
  class Docker < Base
10
10
  def self.options
11
- %w[service-url service-options tmpdir]
11
+ %w[service-url service-options tmpdir interpreters]
12
12
  end
13
13
 
14
14
  def provided_features
@@ -48,7 +48,7 @@ module Bolt
48
48
 
49
49
  _, stderr, exitcode = conn.execute('mv', tmpfile, destination, {})
50
50
  if exitcode != 0
51
- message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{stderr.join}"
51
+ message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{stderr}"
52
52
  raise Bolt::Node::FileError.new(message, 'MV_ERROR')
53
53
  end
54
54
  end
@@ -59,7 +59,7 @@ module Bolt
59
59
  def run_command(target, command, _options = {})
60
60
  with_connection(target) do |conn|
61
61
  stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), {})
62
- Bolt::Result.for_command(target, stdout.join, stderr.join, exitcode)
62
+ Bolt::Result.for_command(target, stdout, stderr, exitcode)
63
63
  end
64
64
  end
65
65
 
@@ -71,7 +71,7 @@ module Bolt
71
71
  conn.with_remote_tempdir do |dir|
72
72
  remote_path = conn.write_remote_executable(dir, script)
73
73
  stdout, stderr, exitcode = conn.execute(remote_path, *arguments, {})
74
- Bolt::Result.for_command(target, stdout.join, stderr.join, exitcode)
74
+ Bolt::Result.for_command(target, stdout, stderr, exitcode)
75
75
  end
76
76
  end
77
77
  end
@@ -87,7 +87,7 @@ module Bolt
87
87
  arguments = unwrap_sensitive_args(arguments)
88
88
  with_connection(target) do |conn|
89
89
  execute_options = {}
90
-
90
+ execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
91
91
  conn.with_remote_tempdir do |dir|
92
92
  if extra_files.empty?
93
93
  task_dir = dir
@@ -112,7 +112,7 @@ module Bolt
112
112
  end
113
113
 
114
114
  stdout, stderr, exitcode = conn.execute(remote_task_path, execute_options)
115
- Bolt::Result.for_task(target, stdout.join, stderr.join, exitcode)
115
+ Bolt::Result.for_task(target, stdout, stderr, exitcode)
116
116
  end
117
117
  end
118
118
  end
@@ -27,6 +27,7 @@ module Bolt
27
27
  end
28
28
 
29
29
  def execute(*command, options)
30
+ command.unshift(options[:interpreter]) if options[:interpreter]
30
31
  if options[:environment]
31
32
  envs = options[:environment].map { |env, val| "#{env}=#{val}" }
32
33
  command = ['env'] + envs + command
@@ -39,6 +40,8 @@ module Bolt
39
40
  else
40
41
  @logger.info { "Command failed with exit code #{result[2]}" }
41
42
  end
43
+ result[0] = result[0].join.force_encoding('UTF-8')
44
+ result[1] = result[1].join.force_encoding('UTF-8')
42
45
  result
43
46
  rescue StandardError
44
47
  @logger.debug { "Command aborted" }
@@ -62,7 +65,7 @@ module Bolt
62
65
  def mkdirs(dirs)
63
66
  _, stderr, exitcode = execute('mkdir', '-p', *dirs, {})
64
67
  if exitcode != 0
65
- message = "Could not create directories: #{stderr.join}"
68
+ message = "Could not create directories: #{stderr}"
66
69
  raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
67
70
  end
68
71
  end
@@ -73,7 +76,7 @@ module Bolt
73
76
 
74
77
  stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
75
78
  if exitcode != 0
76
- raise Bolt::Node::FileError.new("Could not make tempdir: #{stderr.join}", 'TEMPDIR_ERROR')
79
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{stderr}", 'TEMPDIR_ERROR')
77
80
  end
78
81
  tmppath || stdout.first
79
82
  end
@@ -85,7 +88,7 @@ module Bolt
85
88
  if dir
86
89
  _, stderr, exitcode = execute('rm', '-rf', dir, {})
87
90
  if exitcode != 0
88
- @logger.warn("Failed to clean up tempdir '#{dir}': #{stderr.join}")
91
+ @logger.warn("Failed to clean up tempdir '#{dir}': #{stderr}")
89
92
  end
90
93
  end
91
94
  end
@@ -101,7 +104,7 @@ module Bolt
101
104
  def make_executable(path)
102
105
  _, stderr, exitcode = execute('chmod', 'u+x', path, {})
103
106
  if exitcode != 0
104
- message = "Could not make file '#{path}' executable: #{stderr.join}"
107
+ message = "Could not make file '#{path}' executable: #{stderr}"
105
108
  raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
106
109
  end
107
110
  end
@@ -11,7 +11,13 @@ module Bolt
11
11
  module Transport
12
12
  class Local < Base
13
13
  def self.options
14
- %w[tmpdir]
14
+ %w[tmpdir interpreters]
15
+ end
16
+
17
+ def self.default_options
18
+ {
19
+ 'interpreters' => { '.rb' => RbConfig.ruby }
20
+ }
15
21
  end
16
22
 
17
23
  def provided_features
@@ -129,8 +135,10 @@ module Bolt
129
135
  copy_file(executable, script)
130
136
  File.chmod(0o750, script)
131
137
 
138
+ interpreter = select_interpreter(script, target.options['interpreters'])
139
+ interpreter_debug = interpreter ? " using '#{interpreter}' interpreter" : nil
132
140
  # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
133
- logger.debug("Running '#{script}' with #{arguments}")
141
+ logger.debug("Running '#{script}' with #{arguments}#{interpreter_debug}")
134
142
  unwrapped_arguments = unwrap_sensitive_args(arguments)
135
143
 
136
144
  stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
@@ -146,25 +154,32 @@ module Bolt
146
154
  environment_params = ""
147
155
  end
148
156
 
149
- output =
150
- if Powershell.powershell_file?(script) && stdin.nil?
151
- command = Powershell.run_ps_task(arguments, script, input_method)
152
- command = environment_params + Powershell.shell_init + command
157
+ if Powershell.powershell_file?(script) && stdin.nil?
158
+ command = Powershell.run_ps_task(arguments, script, input_method)
159
+ command = environment_params + Powershell.shell_init + command
160
+ interpreter ||= 'powershell.exe'
161
+ output =
153
162
  if input_method == 'powershell'
154
- @conn.execute(command, dir: dir, env: "powershell.exe")
163
+ @conn.execute(command, dir: dir, interpreter: interpreter)
155
164
  else
156
- @conn.execute(command, dir: dir, stdin: stdin, env: "powershell.exe")
165
+ @conn.execute(command, dir: dir, stdin: stdin, interpreter: interpreter)
157
166
  end
167
+ end
168
+ unless output
169
+ if interpreter
170
+ env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
171
+ output = @conn.execute(script, stdin: stdin, env: env, dir: dir, interpreter: interpreter)
158
172
  else
159
173
  path, args = *Powershell.process_from_extension(script)
160
174
  command = args.unshift(path).join(' ')
161
175
  command = environment_params + Powershell.shell_init + command
162
- @conn.execute(command, dir: dir, stdin: stdin, env: "powershell.exe")
176
+ output = @conn.execute(command, dir: dir, stdin: stdin, interpreter: 'powershell.exe')
163
177
  end
178
+ end
164
179
  else
165
180
  # POSIX
166
181
  env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
167
- output = @conn.execute(script, stdin: stdin, env: env, dir: dir)
182
+ output = @conn.execute(script, stdin: stdin, env: env, dir: dir, interpreter: interpreter)
168
183
  end
169
184
  Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code)
170
185
  end
@@ -8,6 +8,7 @@ module Bolt
8
8
  class Local
9
9
  class Shell
10
10
  def execute(*command, options)
11
+ command.unshift(options[:interpreter]) if options[:interpreter]
11
12
  command = [options[:env]] + command if options[:env]
12
13
 
13
14
  if options[:stdin]
@@ -29,6 +29,10 @@ module Bolt
29
29
  %w[service-url cacert token-file task-environment]
30
30
  end
31
31
 
32
+ def self.default_options
33
+ { 'task-environment' => 'production' }
34
+ end
35
+
32
36
  def provided_features
33
37
  ['puppet-agent']
34
38
  end
@@ -11,6 +11,10 @@ module Bolt
11
11
  unfiltered
12
12
  end
13
13
 
14
+ def self.default_options
15
+ { 'run-on' => 'localhost' }
16
+ end
17
+
14
18
  def self.validate(options)
15
19
  # This will fail when validating global config
16
20
  # unless options['device-type']
@@ -10,7 +10,15 @@ module Bolt
10
10
  class SSH < Base
11
11
  def self.options
12
12
  %w[port user password sudo-password private-key host-key-check
13
- connect-timeout tmpdir run-as tty run-as-command proxyjump]
13
+ connect-timeout tmpdir run-as tty run-as-command proxyjump interpreters]
14
+ end
15
+
16
+ def self.default_options
17
+ {
18
+ 'connect-timeout' => 10,
19
+ 'host-key-check' => true,
20
+ 'tty' => false
21
+ }
14
22
  end
15
23
 
16
24
  def provided_features
@@ -169,6 +177,7 @@ module Bolt
169
177
  dir.chown(conn.run_as)
170
178
 
171
179
  execute_options[:sudoable] = true if conn.run_as
180
+ execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
172
181
  output = conn.execute(command, execute_options)
173
182
  end
174
183
  Bolt::Result.for_task(target, output.stdout.string,
@@ -227,6 +227,10 @@ module Bolt
227
227
  escalate = sudoable && run_as && @user != run_as
228
228
  use_sudo = escalate && @target.options['run-as-command'].nil?
229
229
 
230
+ if options[:interpreter]
231
+ command.is_a?(Array) ? command.unshift(options[:interpreter]) : [options[:interpreter], command]
232
+ end
233
+
230
234
  command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
231
235
  if escalate
232
236
  if use_sudo
@@ -8,7 +8,19 @@ module Bolt
8
8
  module Transport
9
9
  class WinRM < Base
10
10
  def self.options
11
- %w[port user password connect-timeout ssl ssl-verify tmpdir cacert extensions]
11
+ %w[
12
+ port user password connect-timeout ssl ssl-verify tmpdir cacert
13
+ extensions interpreters file-protocol smb-port
14
+ ]
15
+ end
16
+
17
+ def self.default_options
18
+ {
19
+ 'connect-timeout' => 10,
20
+ 'ssl' => true,
21
+ 'ssl-verify' => true,
22
+ 'file-protocol' => 'winrm'
23
+ }
12
24
  end
13
25
 
14
26
  def provided_features
@@ -26,6 +38,10 @@ module Bolt
26
38
  raise Bolt::ValidationError, 'ssl option must be a Boolean true or false'
27
39
  end
28
40
 
41
+ if ssl_flag && (options['file-protocol'] == 'smb')
42
+ raise Bolt::ValidationError, 'SMB file transfers are not allowed with SSL enabled'
43
+ end
44
+
29
45
  ssl_verify_flag = options['ssl-verify']
30
46
  unless !!ssl_verify_flag == ssl_verify_flag
31
47
  raise Bolt::ValidationError, 'ssl-verify option must be a Boolean true or false'
@@ -135,7 +151,13 @@ module Bolt
135
151
  if Powershell.powershell_file?(remote_task_path) && stdin.nil?
136
152
  conn.execute(Powershell.run_ps_task(arguments, remote_task_path, input_method))
137
153
  else
138
- path, args = *Powershell.process_from_extension(remote_task_path)
154
+ interpreter = select_interpreter(remote_task_path, target.options['interpreters'])
155
+ if interpreter
156
+ path = interpreter
157
+ args = [remote_task_path]
158
+ else
159
+ path, args = *Powershell.process_from_extension(remote_task_path)
160
+ end
139
161
  conn.execute_process(path, args, stdin)
140
162
  end
141
163
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bolt/node/errors'
4
4
  require 'bolt/node/output'
5
+ require 'ruby_smb'
5
6
 
6
7
  module Bolt
7
8
  module Transport
@@ -17,9 +18,9 @@ module Bolt
17
18
  default_port = target.options['ssl'] ? HTTPS_PORT : HTTP_PORT
18
19
  @port = @target.port || default_port
19
20
  @user = @target.user
20
-
21
- # Accept a single entry or a list, ensure each is prefixed with '.'
21
+ # Build set of extensions from extensions config as well as interpreters
22
22
  extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] != '.' ? '.' + ext : ext }
23
+ extensions += target.options['interpreters'].keys if target.options['interpreters']
23
24
  @extensions = DEFAULT_EXTENSIONS.to_set.merge(extensions)
24
25
 
25
26
  @logger = Logging.logger[@target.host]
@@ -93,6 +94,7 @@ module Bolt
93
94
 
94
95
  def disconnect
95
96
  @session&.close
97
+ @client&.disconnect!
96
98
  @logger.debug { "Closed session" }
97
99
  end
98
100
 
@@ -141,12 +143,47 @@ module Bolt
141
143
  end
142
144
 
143
145
  def write_remote_file(source, destination)
146
+ if target.options['file-protocol'] == 'smb'
147
+ write_remote_file_smb(source, destination)
148
+ else
149
+ write_remote_file_winrm(source, destination)
150
+ end
151
+ end
152
+
153
+ def write_remote_file_winrm(source, destination)
144
154
  fs = ::WinRM::FS::FileManager.new(@connection)
145
155
  fs.upload(source, destination)
146
156
  rescue StandardError => e
147
157
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
148
158
  end
149
159
 
160
+ def write_remote_file_smb(source, destination)
161
+ win_dest = destination.tr('/', '\\')
162
+ if (md = win_dest.match(/^([a-z]):\\(.*)/i))
163
+ # if drive, use admin share for that drive, so path is '\\host\C$'
164
+ path = "\\\\#{@target.host}\\#{md[1]}$"
165
+ dest = md[2]
166
+ elsif (md = win_dest.match(/^(\\\\[^\\]+\\[^\\]+)\\(.*)/))
167
+ # if unc, path is '\\host\share'
168
+ path = md[1]
169
+ dest = md[2]
170
+ else
171
+ raise ArgumentError, "Unknown destination '#{destination}'"
172
+ end
173
+
174
+ client = smb_client_login
175
+ tree = client.tree_connect(path)
176
+ begin
177
+ write_remote_file_smb_recursive(tree, source, dest)
178
+ ensure
179
+ tree.disconnect!
180
+ end
181
+ rescue ::RubySMB::Error::UnexpectedStatusCode => e
182
+ raise Bolt::Node::FileError.new("SMB Error: #{e.message}", 'WRITE_ERROR')
183
+ rescue StandardError => e
184
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
185
+ end
186
+
150
187
  def make_tempdir
151
188
  find_parent = target.options['tmpdir'] ? "\"#{target.options['tmpdir']}\"" : '[System.IO.Path]::GetTempPath()'
152
189
  result = execute(Powershell.make_tempdir(find_parent))
@@ -184,6 +221,79 @@ module Bolt
184
221
  write_remote_file(content, remote_path)
185
222
  remote_path
186
223
  end
224
+
225
+ private
226
+
227
+ def smb_client_login
228
+ return @client if @client
229
+
230
+ dispatcher = RubySMB::Dispatcher::Socket.new(smb_socket_connect)
231
+ @client = RubySMB::Client.new(dispatcher, smb1: false, smb2: true, username: @user, password: target.password)
232
+ status = @client.login
233
+ case status
234
+ when WindowsError::NTStatus::STATUS_SUCCESS
235
+ @logger.debug { "Connected to #{@client.dns_host_name}" }
236
+ when WindowsError::NTStatus::STATUS_LOGON_FAILURE
237
+ raise Bolt::Node::ConnectError.new(
238
+ "SMB authentication failed for #{target.host}",
239
+ 'AUTH_ERROR'
240
+ )
241
+ else
242
+ raise Bolt::Node::ConnectError.new(
243
+ "Failed to connect to #{target.host} using SMB: #{status.description}",
244
+ 'CONNECT_ERROR'
245
+ )
246
+ end
247
+
248
+ @client
249
+ end
250
+
251
+ SMB_PORT = 445
252
+
253
+ def smb_socket_connect
254
+ # It's lame that TCPSocket doesn't take a connect timeout
255
+ # Using Timeout.timeout is bad, but is done elsewhere...
256
+ Timeout.timeout(target.options['connect-timeout']) do
257
+ TCPSocket.new(target.host, target.options['smb-port'] || SMB_PORT)
258
+ end
259
+ rescue Errno::ECONNREFUSED => e
260
+ # handle this to prevent obscuring error message as SMB problem
261
+ raise Bolt::Node::ConnectError.new(
262
+ "Failed to connect to #{target.host} using SMB: #{e.message}",
263
+ 'CONNECT_ERROR'
264
+ )
265
+ rescue Timeout::Error
266
+ raise Bolt::Node::ConnectError.new(
267
+ "Timeout after #{target.options['connect-timeout']} seconds connecting to #{target.host}",
268
+ 'CONNECT_ERROR'
269
+ )
270
+ end
271
+
272
+ def write_remote_file_smb_recursive(tree, source, dest)
273
+ if Dir.exist?(source)
274
+ tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
275
+
276
+ (Dir.entries(source) - ['.', '..']).each do |child|
277
+ child_dest = dest + '\\' + child
278
+ write_remote_file_smb_recursive(tree, File.join(source, child), child_dest)
279
+ end
280
+ return
281
+ end
282
+
283
+ file = tree.open_file(filename: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OVERWRITE_IF)
284
+ begin
285
+ # `file` doesn't derive from IO, so can't use IO.copy_stream
286
+ File.open(source, 'rb') do |f|
287
+ pos = 0
288
+ while (buf = f.read(8 * 1024 * 1024))
289
+ file.write(data: buf, offset: pos)
290
+ pos += buf.length
291
+ end
292
+ end
293
+ ensure
294
+ file.close
295
+ end
296
+ end
187
297
  end
188
298
  end
189
299
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.12.0'
4
+ VERSION = '1.13.0'
5
5
  end
@@ -56,6 +56,10 @@
56
56
  "sudo-password": {
57
57
  "type": "string",
58
58
  "description": "Password to use when changing users via run-as"
59
+ },
60
+ "interpreters": {
61
+ "type": "object",
62
+ "description": "Map of file extensions to remote executable"
59
63
  }
60
64
  },
61
65
  "oneOf": [
@@ -47,8 +47,22 @@
47
47
  "extensions": {
48
48
  "type": "array",
49
49
  "description": "List of file extensions that are accepted for scripts or tasks"
50
+ },
51
+ "interpreters": {
52
+ "type": "object",
53
+ "description": "Map of file extensions to remote executable"
54
+ },
55
+ "file-protocol": {
56
+ "type": "string",
57
+ "enum": ["winrm", "smb"],
58
+ "description": "Protocol for file transfer, WinRM or SMB"
59
+ },
60
+ "smb-port": {
61
+ "type": "integer",
62
+ "description": "Port for SMB protocol"
50
63
  }
51
64
  },
65
+
52
66
  "required": ["hostname", "user", "password"],
53
67
  "additionalProperties": false
54
68
  },
@@ -32,6 +32,11 @@ require 'bolt/pal'
32
32
  #
33
33
  # Configuration
34
34
  #
35
+ # To configure Puppet and Bolt at the beginning of tests, add the following
36
+ # line to your spec_helper.rb:
37
+ #
38
+ # BoltSpec::Plans.init
39
+ #
35
40
  # By default the plan helpers use the modulepath set up for rspec-puppet and
36
41
  # an otherwise empty bolt config and inventory. To create your own values for
37
42
  # these override the modulepath, config, or inventory methods.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-02-21 00:00:00.000000000 Z
11
+ date: 2019-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -184,6 +184,20 @@ dependencies:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
186
  version: '2.6'
187
+ - !ruby/object:Gem::Dependency
188
+ name: ruby_smb
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '1.0'
194
+ type: :runtime
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: '1.0'
187
201
  - !ruby/object:Gem::Dependency
188
202
  name: terminal-table
189
203
  requirement: !ruby/object:Gem::Requirement
@@ -345,6 +359,7 @@ files:
345
359
  - lib/bolt/outputter/human.rb
346
360
  - lib/bolt/outputter/json.rb
347
361
  - lib/bolt/pal.rb
362
+ - lib/bolt/pal/issues.rb
348
363
  - lib/bolt/pal/logging.rb
349
364
  - lib/bolt/plan_result.rb
350
365
  - lib/bolt/puppetdb.rb