bolt 2.40.2 → 3.1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +19 -17
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  5. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  6. data/lib/bolt/analytics.rb +3 -2
  7. data/lib/bolt/applicator.rb +11 -1
  8. data/lib/bolt/bolt_option_parser.rb +3 -113
  9. data/lib/bolt/catalog.rb +10 -29
  10. data/lib/bolt/cli.rb +54 -155
  11. data/lib/bolt/config.rb +62 -239
  12. data/lib/bolt/config/options.rb +58 -97
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/options.rb +8 -1
  15. data/lib/bolt/config/transport/orch.rb +1 -0
  16. data/lib/bolt/executor.rb +15 -5
  17. data/lib/bolt/inventory.rb +3 -2
  18. data/lib/bolt/inventory/group.rb +35 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/logger.rb +115 -11
  21. data/lib/bolt/module.rb +10 -2
  22. data/lib/bolt/module_installer.rb +4 -2
  23. data/lib/bolt/module_installer/resolver.rb +65 -12
  24. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  25. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  26. data/lib/bolt/outputter/human.rb +9 -5
  27. data/lib/bolt/outputter/json.rb +16 -16
  28. data/lib/bolt/outputter/rainbow.rb +3 -3
  29. data/lib/bolt/pal.rb +94 -14
  30. data/lib/bolt/pal/yaml_plan.rb +8 -2
  31. data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
  32. data/lib/bolt/pal/yaml_plan/step.rb +3 -24
  33. data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
  34. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  35. data/lib/bolt/plugin.rb +3 -3
  36. data/lib/bolt/plugin/cache.rb +7 -7
  37. data/lib/bolt/plugin/module.rb +0 -23
  38. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  39. data/lib/bolt/plugin/puppetdb.rb +1 -1
  40. data/lib/bolt/project.rb +54 -81
  41. data/lib/bolt/project_manager.rb +4 -3
  42. data/lib/bolt/project_manager/module_migrator.rb +6 -5
  43. data/lib/bolt/rerun.rb +1 -1
  44. data/lib/bolt/result.rb +6 -1
  45. data/lib/bolt/shell/bash.rb +9 -4
  46. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  47. data/lib/bolt/shell/powershell.rb +9 -5
  48. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  49. data/lib/bolt/task.rb +1 -1
  50. data/lib/bolt/transport/base.rb +0 -9
  51. data/lib/bolt/transport/docker.rb +1 -125
  52. data/lib/bolt/transport/docker/connection.rb +86 -161
  53. data/lib/bolt/transport/local.rb +1 -9
  54. data/lib/bolt/transport/orch/connection.rb +1 -1
  55. data/lib/bolt/transport/ssh.rb +1 -2
  56. data/lib/bolt/transport/ssh/connection.rb +1 -1
  57. data/lib/bolt/validator.rb +2 -2
  58. data/lib/bolt/version.rb +1 -1
  59. data/lib/bolt_server/config.rb +1 -1
  60. data/lib/bolt_server/transport_app.rb +48 -31
  61. data/lib/bolt_spec/bolt_context.rb +9 -4
  62. data/lib/bolt_spec/plans.rb +1 -109
  63. data/libexec/bolt_catalog +1 -1
  64. data/modules/aggregate/plans/count.pp +21 -0
  65. data/modules/aggregate/plans/targets.pp +21 -0
  66. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  67. data/modules/puppetdb_fact/plans/init.pp +10 -0
  68. metadata +28 -19
  69. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -30,10 +30,6 @@ module Bolt
30
30
  @module = mod
31
31
  @config = config
32
32
  @context = context
33
-
34
- if @module.name == 'pkcs7'
35
- @config = handle_deprecated_pkcs7_keys(@config)
36
- end
37
33
  end
38
34
 
39
35
  # This method interacts with the module on disk so it's separate from initialize
@@ -160,10 +156,6 @@ module Bolt
160
156
  # out now.
161
157
  meta, params = opts.partition { |key, _val| key.start_with?('_') }.map(&:to_h)
162
158
 
163
- if task.module_name == 'pkcs7'
164
- params = handle_deprecated_pkcs7_keys(params)
165
- end
166
-
167
159
  # Reject parameters from config that are not accepted by the task and
168
160
  # merge in parameter defaults
169
161
  params = if task.parameters
@@ -181,21 +173,6 @@ module Bolt
181
173
  [params, meta]
182
174
  end
183
175
 
184
- # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
- # modifies the keys so they are the correct format
186
- def handle_deprecated_pkcs7_keys(params)
187
- if params.key?('private-key') || params.key?('public-key')
188
- message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
189
- "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
190
- Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
191
- end
192
-
193
- params['private_key'] = params.delete('private-key') if params.key?('private-key')
194
- params['public_key'] = params.delete('public-key') if params.key?('public-key')
195
-
196
- params
197
- end
198
-
199
176
  def extract_task_parameter_schema
200
177
  # Get the intersection of expected types (using Set)
201
178
  type_set = @hook_map.each_with_object({}) do |(_hook, task), acc|
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Plugin
5
+ class PuppetConnectData
6
+ INPUT_DATA_VAR = 'PUPPET_CONNECT_INPUT_DATA'
7
+
8
+ def initialize(context:, **_opts)
9
+ if ENV.key?(INPUT_DATA_VAR)
10
+ # The user provided input data that they will copy-paste into the Puppet Connect UI
11
+ # for inventory syncing. This environment variable will likely be set when invoking a
12
+ # general "test Puppet Connect input data" command. That command tests that parsing
13
+ # the inventory with the given input data results in connectable targets. Part of
14
+ # that requires validating that the input data contains all of the referenced keys,
15
+ # which is what this plugin will do in validate_resolve_reference.
16
+ @input_data_path = ENV[INPUT_DATA_VAR]
17
+ data_path = @input_data_path
18
+ else
19
+ # The user is using this plugin during a regular Bolt invocation, so fetch the (minimal)
20
+ # required data from the default location. This data should typically be non-autoloadable
21
+ # secrets like WinRM passwords.
22
+ #
23
+ # Note that any unspecified keys will be resolved to nil.
24
+ data_path = File.join(context.boltdir, 'puppet_connect_data.yaml')
25
+ end
26
+
27
+ @data = Bolt::Util.read_optional_yaml_hash(
28
+ data_path,
29
+ File.basename(data_path)
30
+ )
31
+
32
+ if @input_data_path
33
+ # Validate that the data does not contain any plugin-reference
34
+ # values
35
+ @data.each do |key, toplevel_value|
36
+ # Use walk_vals to check for nested plugin references
37
+ Bolt::Util.walk_vals(toplevel_value) do |current_value|
38
+ if current_value.is_a?(Hash) && current_value.key?('_plugin')
39
+ raise invalid_input_data_err("the #{key} key's value contains a plugin reference")
40
+ end
41
+ current_value
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def name
48
+ 'puppet_connect_data'
49
+ end
50
+
51
+ def hooks
52
+ %i[resolve_reference validate_resolve_reference]
53
+ end
54
+
55
+ def resolve_reference(opts)
56
+ key = opts['key']
57
+ @data[key]
58
+ end
59
+
60
+ def validate_resolve_reference(opts)
61
+ unless opts['key']
62
+ raise Bolt::ValidationError,
63
+ "puppet_connect_data plugin requires that 'key' be specified"
64
+ end
65
+ if @input_data_path && !@data.key?(opts['key'])
66
+ # Input data for Puppet Connect was provided and opts['key'] does not have a
67
+ # value specified. Raise an error for this case.
68
+ raise invalid_input_data_err("a value for the #{opts['key']} key is not specified")
69
+ end
70
+ end
71
+
72
+ def invalid_input_data_err(msg)
73
+ Bolt::ValidationError.new("invalid input data #{@input_data_path}: #{msg}")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -31,7 +31,7 @@ module Bolt
31
31
  end
32
32
 
33
33
  def warn_missing_fact(certname, fact)
34
- @logger.warn("Could not find fact #{fact} for node #{certname}")
34
+ Bolt::Logger.warn("puppetdb_missing_fact", "Could not find fact #{fact} for node #{certname}")
35
35
  end
36
36
 
37
37
  def fact_path(raw_fact)
data/lib/bolt/project.rb CHANGED
@@ -11,47 +11,40 @@ module Bolt
11
11
  BOLTDIR_NAME = 'Boltdir'
12
12
  CONFIG_NAME = 'bolt-project.yaml'
13
13
 
14
- attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
15
- :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
16
- :deprecations, :downloads, :plans_path, :modulepath, :managed_moduledir,
17
- :backup_dir, :cache_file
14
+ attr_reader :path, :data, :inventory_file, :hiera_config,
15
+ :puppetfile, :rerunfile, :type, :resource_types, :project_file,
16
+ :downloads, :plans_path, :modulepath, :managed_moduledir,
17
+ :backup_dir, :plugin_cache_file, :plan_cache_file
18
18
 
19
- def self.default_project(logs = [])
20
- create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
19
+ def self.default_project
20
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
21
21
  # If homedir isn't defined use the system config path
22
22
  rescue ArgumentError
23
- create_project(Bolt::Config.system_path, 'system', logs)
23
+ create_project(Bolt::Config.system_path, 'system')
24
24
  end
25
25
 
26
26
  # Search recursively up the directory hierarchy for the Project. Look for a
27
- # directory called Boltdir or a file called bolt.yaml (for a control repo
28
- # type Project). Otherwise, repeat the check on each directory up the
27
+ # directory called Boltdir or a file called bolt-project.yaml (for a control
28
+ # repo type Project). Otherwise, repeat the check on each directory up the
29
29
  # hierarchy, falling back to the default if we reach the root.
30
- def self.find_boltdir(dir, logs = [], deprecations = [])
30
+ def self.find_boltdir(dir)
31
31
  dir = Pathname.new(dir)
32
32
 
33
33
  if (dir + BOLTDIR_NAME).directory?
34
- create_project(dir + BOLTDIR_NAME, 'embedded', logs)
35
- elsif (dir + 'bolt.yaml').file?
36
- command = Bolt::Util.powershell? ? 'Update-BoltProject' : 'bolt project migrate'
37
- msg = "Configuration file #{dir + 'bolt.yaml'} is deprecated and will be "\
38
- "removed in Bolt 3.0.\nUpdate your Bolt project to the latest Bolt practices "\
39
- "using #{command}"
40
- deprecations << { type: "Project level bolt.yaml",
41
- msg: msg }
42
- create_project(dir, 'local', logs, deprecations)
34
+ create_project(dir + BOLTDIR_NAME, 'embedded')
43
35
  elsif (dir + CONFIG_NAME).file?
44
- create_project(dir, 'local', logs)
36
+ create_project(dir, 'local')
45
37
  elsif dir.root?
46
- default_project(logs)
38
+ default_project
47
39
  else
48
- logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
49
- "This directory won't be loaded as a project." }
50
- find_boltdir(dir.parent, logs, deprecations)
40
+ Bolt::Logger.debug(
41
+ "Did not detect Boltdir or bolt-project.yaml at '#{dir}'. This directory won't be loaded as a project."
42
+ )
43
+ find_boltdir(dir.parent)
51
44
  end
52
45
  end
53
46
 
54
- def self.create_project(path, type = 'option', logs = [], deprecations = [])
47
+ def self.create_project(path, type = 'option')
55
48
  fullpath = Pathname.new(path).expand_path
56
49
 
57
50
  if type == 'user'
@@ -59,8 +52,11 @@ module Bolt
59
52
  # This is already expanded if the type is user
60
53
  FileUtils.mkdir_p(path)
61
54
  rescue StandardError
62
- logs << { warn: "Could not create default project at #{path}. Continuing without a writeable project. "\
63
- "Log and rerun files will not be written." }
55
+ Bolt::Logger.warn(
56
+ "non_writeable_project",
57
+ "Could not create default project at #{path}. Continuing without a writeable project. "\
58
+ "Log and rerun files will not be written."
59
+ )
64
60
  end
65
61
  end
66
62
 
@@ -79,21 +75,18 @@ module Bolt
79
75
  project_file = File.join(fullpath, CONFIG_NAME)
80
76
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
81
77
  default = type =~ /user|system/ ? 'default ' : ''
82
- exist = File.exist?(File.expand_path(project_file))
83
78
 
84
- logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
79
+ if File.exist?(File.expand_path(project_file))
80
+ Bolt::Logger.info("Loaded #{default}project from '#{fullpath}'")
81
+ end
85
82
 
86
83
  Bolt::Validator.new.tap do |validator|
87
84
  validator.validate(data, schema, project_file)
88
-
89
- validator.warnings.each { |warning| logs << { warn: warning } }
90
-
91
- validator.deprecations.each do |dep|
92
- deprecations << { type: "#{CONFIG_NAME} #{dep[:option]}", msg: dep[:message] }
93
- end
85
+ validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
86
+ validator.deprecations.each { |dep| Bolt::Logger.deprecate(dep[:id], dep[:msg]) }
94
87
  end
95
88
 
96
- new(data, path, type, logs, deprecations)
89
+ new(data, path, type)
97
90
  end
98
91
 
99
92
  # Builds the schema for bolt-project.yaml used by the validator.
@@ -101,63 +94,37 @@ module Bolt
101
94
  def self.schema
102
95
  {
103
96
  type: Hash,
104
- properties: Bolt::Config::BOLT_PROJECT_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
97
+ properties: Bolt::Config::PROJECT_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
105
98
  definitions: Bolt::Config::OPTIONS
106
99
  }
107
100
  end
108
101
 
109
- def initialize(raw_data, path, type = 'option', logs = [], deprecations = [])
110
- @path = Pathname.new(path).expand_path
111
- @project_file = @path + CONFIG_NAME
112
- @logs = logs
113
- @deprecations = deprecations
114
-
115
- if (@path + 'bolt.yaml').file? && project_file?
116
- msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
117
- "Transport config should be set in inventory.yaml, all other config should be set in "\
118
- "bolt-project.yaml."
119
- @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
120
- end
121
-
102
+ def initialize(data, path, type = 'option')
103
+ @type = type
104
+ @path = Pathname.new(path).expand_path
105
+ @project_file = @path + CONFIG_NAME
122
106
  @inventory_file = @path + 'inventory.yaml'
123
107
  @hiera_config = @path + 'hiera.yaml'
124
108
  @puppetfile = @path + 'Puppetfile'
125
109
  @rerunfile = @path + '.rerun.json'
126
110
  @resource_types = @path + '.resource_types'
127
- @type = type
128
111
  @downloads = @path + 'downloads'
129
112
  @plans_path = @path + 'plans'
130
113
  @managed_moduledir = @path + '.modules'
131
114
  @backup_dir = @path + '.bolt-bak'
132
- @cache_file = @path + '.plugin_cache.json'
133
-
134
- tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
135
- if tc.any?
136
- msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
137
- @logs << { warn: msg }
115
+ @plugin_cache_file = @path + '.plugin_cache.json'
116
+ @plan_cache_file = @path + '.plan_cache.json'
117
+ @modulepath = [(@path + 'modules').to_s]
118
+
119
+ if (tc = Bolt::Config::INVENTORY_OPTIONS.keys & data.keys).any?
120
+ Bolt::Logger.warn(
121
+ "project_transport_config",
122
+ "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}."
123
+ )
138
124
  end
139
125
 
140
- @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
141
-
142
- # If the 'modules' key is present in the project configuration file,
143
- # use the new, shorter modulepath.
144
- @modulepath = if @data.key?('modules')
145
- [(@path + 'modules').to_s]
146
- else
147
- [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
148
- end
149
-
150
- # Once bolt.yaml deprecation is removed, this attribute should be removed
151
- # and replaced with .project_file in lib/bolt/config.rb
152
- @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
153
- if (@path + 'bolt.yaml').file?
154
- msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
155
- @logs << { warn: msg }
156
- end
157
- @project_file
158
- else
159
- @path + 'bolt.yaml'
160
- end
126
+ @data = data.slice(*Bolt::Config::PROJECT_OPTIONS)
127
+
161
128
  validate if project_file?
162
129
  end
163
130
 
@@ -206,8 +173,13 @@ module Bolt
206
173
  @data['module-install']
207
174
  end
208
175
 
176
+ def disable_warnings
177
+ @data['disable-warnings'] || []
178
+ end
179
+
209
180
  def modules
210
- @modules ||= @data['modules']&.map do |mod|
181
+ mod_data = @data['modules'] || []
182
+ @modules ||= mod_data.map do |mod|
211
183
  if mod.is_a?(String)
212
184
  { 'name' => mod }
213
185
  else
@@ -232,14 +204,15 @@ module Bolt
232
204
  File.directory?(@path + 'tasks') ||
233
205
  File.directory?(@path + 'files'))
234
206
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
235
- @logs << { warn: message }
207
+
208
+ Bolt::Logger.warn("missing_project_name", message)
236
209
  end
237
210
  end
238
211
 
239
212
  def check_deprecated_file
240
213
  if (@path + 'project.yaml').file?
241
214
  msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
242
- Bolt::Logger.deprecation_warning('Using project.yaml instead of bolt-project.yaml', msg)
215
+ Bolt::Logger.warn("project_yaml", msg)
243
216
  end
244
217
  end
245
218
  end
@@ -166,10 +166,11 @@ module Bolt
166
166
  # Migrates the project-level configuration file to the latest version.
167
167
  #
168
168
  private def migrate_config
169
- migrator = ConfigMigrator.new(@outputter)
169
+ migrator = ConfigMigrator.new(@outputter)
170
+ configfile = @config.project.path + 'bolt.yaml'
170
171
 
171
172
  migrator.migrate(
172
- @config.project.config_file,
173
+ configfile,
173
174
  @config.project.project_file,
174
175
  @config.inventoryfile || @config.project.inventory_file,
175
176
  @config.project.backup_dir
@@ -194,7 +195,7 @@ module Bolt
194
195
 
195
196
  migrator.migrate(
196
197
  @config.project,
197
- @config.modulepath
198
+ @config.modulepath[0...-1]
198
199
  )
199
200
  end
200
201
  end
@@ -6,19 +6,20 @@ module Bolt
6
6
  class ProjectManager
7
7
  class ModuleMigrator < Migrator
8
8
  def migrate(project, configured_modulepath)
9
- return true unless project.modules.nil?
9
+ return true if project.managed_moduledir.exist?
10
10
 
11
11
  @outputter.print_message "Migrating project modules\n\n"
12
12
 
13
13
  config = project.project_file
14
14
  puppetfile = project.puppetfile
15
15
  managed_moduledir = project.managed_moduledir
16
- modulepath = [(project.path + 'modules').to_s,
16
+ new_modulepath = [(project.path + 'modules').to_s]
17
+ old_modulepath = [(project.path + 'modules').to_s,
17
18
  (project.path + 'site-modules').to_s,
18
19
  (project.path + 'site').to_s]
19
20
 
20
21
  # Notify user to manually migrate modules if using non-default modulepath
21
- if configured_modulepath != modulepath
22
+ if configured_modulepath != new_modulepath && configured_modulepath != old_modulepath
22
23
  @outputter.print_action_step(
23
24
  "Project has a non-default configured modulepath, unable to automatically "\
24
25
  "migrate project modules. To migrate project modules manually, see "\
@@ -27,10 +28,10 @@ module Bolt
27
28
  true
28
29
  # Migrate modules from Puppetfile
29
30
  elsif File.exist?(puppetfile)
30
- migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, modulepath)
31
+ migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, old_modulepath)
31
32
  # Migrate modules to updated modulepath
32
33
  else
33
- consolidate_modules(modulepath)
34
+ consolidate_modules(old_modulepath)
34
35
  update_project_config([], config)
35
36
  end
36
37
  end
data/lib/bolt/rerun.rb CHANGED
@@ -49,7 +49,7 @@ module Bolt
49
49
  end
50
50
  end
51
51
  rescue StandardError => e
52
- Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
52
+ Bolt::Logger.warn_once("unwriteable_file", "Failed to save result to #{@path}: #{e.message}")
53
53
  end
54
54
  end
55
55
  end
data/lib/bolt/result.rb CHANGED
@@ -203,12 +203,17 @@ module Bolt
203
203
  end
204
204
 
205
205
  def to_data
206
+ serialized_value = safe_value
207
+ if serialized_value.key?('_sensitive') &&
208
+ serialized_value['_sensitive'].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
209
+ serialized_value['_sensitive'] = serialized_value['_sensitive'].to_s
210
+ end
206
211
  {
207
212
  "target" => @target.name,
208
213
  "action" => action,
209
214
  "object" => object,
210
215
  "status" => status,
211
- "value" => safe_value
216
+ "value" => serialized_value
212
217
  }
213
218
  end
214
219
 
@@ -299,7 +299,7 @@ module Bolt
299
299
  if target.options['cleanup']
300
300
  dir.delete
301
301
  else
302
- @logger.warn("Skipping cleanup of tmpdir #{dir}")
302
+ Bolt::Logger.warn("skip_cleanup", "Skipping cleanup of tmpdir #{dir}")
303
303
  end
304
304
  end
305
305
  end
@@ -331,10 +331,15 @@ module Bolt
331
331
  # together multiple commands into a single sh invocation
332
332
  commands = [inject_interpreter(options[:interpreter], command)]
333
333
 
334
+ # Let the transport handle adding environment variables if it's custom.
334
335
  if options[:environment]
335
- env_decl = options[:environment].map do |env, val|
336
- "#{env}=#{Shellwords.shellescape(val)}"
337
- end.join(' ')
336
+ if defined? conn.add_env_vars
337
+ conn.add_env_vars(options[:environment])
338
+ else
339
+ env_decl = options[:environment].map do |env, val|
340
+ "#{env}=#{Shellwords.shellescape(val)}"
341
+ end.join(' ')
342
+ end
338
343
  end
339
344
 
340
345
  if escalate