bolt 2.13.0 → 2.18.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  6. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +1 -0
  8. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +1 -0
  12. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +1 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +1 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +1 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +3 -0
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +7 -2
  19. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +7 -4
  20. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -1
  21. data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +1 -0
  22. data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +1 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +1 -0
  24. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +1 -0
  25. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -0
  26. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +1 -0
  27. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +1 -0
  28. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +1 -0
  29. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  30. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  31. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +2 -0
  32. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +2 -1
  33. data/bolt-modules/file/lib/puppet/functions/file/join.rb +2 -0
  34. data/bolt-modules/file/lib/puppet/functions/file/read.rb +3 -1
  35. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  36. data/bolt-modules/file/lib/puppet/functions/file/write.rb +2 -0
  37. data/bolt-modules/out/lib/puppet/functions/out/message.rb +2 -0
  38. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +1 -0
  39. data/bolt-modules/system/lib/puppet/functions/system/env.rb +2 -0
  40. data/lib/bolt/applicator.rb +36 -21
  41. data/lib/bolt/apply_inventory.rb +4 -0
  42. data/lib/bolt/apply_result.rb +1 -1
  43. data/lib/bolt/apply_target.rb +4 -0
  44. data/lib/bolt/bolt_option_parser.rb +27 -16
  45. data/lib/bolt/catalog.rb +79 -69
  46. data/lib/bolt/cli.rb +78 -58
  47. data/lib/bolt/config.rb +163 -136
  48. data/lib/bolt/config/options.rb +474 -0
  49. data/lib/bolt/config/transport/base.rb +16 -16
  50. data/lib/bolt/config/transport/docker.rb +9 -23
  51. data/lib/bolt/config/transport/local.rb +6 -44
  52. data/lib/bolt/config/transport/options.rb +460 -0
  53. data/lib/bolt/config/transport/orch.rb +9 -18
  54. data/lib/bolt/config/transport/remote.rb +3 -6
  55. data/lib/bolt/config/transport/ssh.rb +78 -119
  56. data/lib/bolt/config/transport/winrm.rb +18 -47
  57. data/lib/bolt/executor.rb +14 -1
  58. data/lib/bolt/inventory/group.rb +1 -1
  59. data/lib/bolt/inventory/inventory.rb +4 -14
  60. data/lib/bolt/inventory/target.rb +22 -5
  61. data/lib/bolt/logger.rb +15 -1
  62. data/lib/bolt/outputter.rb +3 -0
  63. data/lib/bolt/outputter/rainbow.rb +84 -0
  64. data/lib/bolt/pal.rb +23 -9
  65. data/lib/bolt/project.rb +75 -55
  66. data/lib/bolt/resource_instance.rb +5 -1
  67. data/lib/bolt/shell/bash.rb +30 -42
  68. data/lib/bolt/shell/powershell.rb +13 -8
  69. data/lib/bolt/shell/powershell/snippets.rb +23 -6
  70. data/lib/bolt/transport/docker.rb +9 -5
  71. data/lib/bolt/transport/local/connection.rb +2 -1
  72. data/lib/bolt/transport/orch.rb +8 -0
  73. data/lib/bolt/transport/ssh.rb +7 -1
  74. data/lib/bolt/transport/ssh/connection.rb +35 -0
  75. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  76. data/lib/bolt/version.rb +1 -1
  77. data/lib/bolt_spec/bolt_context.rb +1 -1
  78. data/lib/bolt_spec/run.rb +1 -1
  79. metadata +22 -18
@@ -15,6 +15,7 @@ module Bolt
15
15
  return if Logging.initialized?
16
16
 
17
17
  Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
18
+ @mutex = Mutex.new
18
19
 
19
20
  Logging.color_scheme(
20
21
  'bolt',
@@ -89,8 +90,10 @@ module Bolt
89
90
  :notice
90
91
  end
91
92
 
93
+ # Explicitly check the log level names instead of the log level number, as levels
94
+ # that are stringified integers (e.g. "level" => "42") will return a truthy value
92
95
  def self.valid_level?(level)
93
- !Logging.level_num(level).nil?
96
+ Logging::LEVELS.include?(Logging.levelify(level))
94
97
  end
95
98
 
96
99
  def self.levels
@@ -100,5 +103,16 @@ module Bolt
100
103
  def self.reset_logging
101
104
  Logging.reset
102
105
  end
106
+
107
+ def self.warn_once(type, msg)
108
+ @mutex.synchronize {
109
+ @warnings ||= []
110
+ @logger ||= Logging.logger[self]
111
+ unless @warnings.include?(type)
112
+ @logger.warn(msg)
113
+ @warnings << type
114
+ end
115
+ }
116
+ end
103
117
  end
104
118
  end
@@ -8,6 +8,8 @@ module Bolt
8
8
  Bolt::Outputter::Human.new(color, verbose, trace)
9
9
  when 'json'
10
10
  Bolt::Outputter::JSON.new(color, verbose, trace)
11
+ when 'rainbow'
12
+ Bolt::Outputter::Rainbow.new(color, verbose, trace)
11
13
  when nil
12
14
  raise "Cannot use outputter before parsing."
13
15
  end
@@ -25,3 +27,4 @@ end
25
27
  require 'bolt/outputter/human'
26
28
  require 'bolt/outputter/json'
27
29
  require 'bolt/outputter/logger'
30
+ require 'bolt/outputter/rainbow'
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/pal'
4
+
5
+ module Bolt
6
+ class Outputter
7
+ class Rainbow < Bolt::Outputter::Human
8
+ def initialize(color, verbose, trace, stream = $stdout)
9
+ begin
10
+ require 'paint'
11
+ rescue LoadError
12
+ raise "The 'paint' gem is required to use the rainbow outputter."
13
+ end
14
+ super
15
+ @line_color = 0
16
+ @color = 0
17
+ @state = :normal
18
+ end
19
+
20
+ # The algorithm is from lolcat (https://github.com/busyloop/lolcat)
21
+ # lolcat is released with WTFPL
22
+ def rainbow
23
+ red = Math.sin(0.3 * @color + 0) * 127 + 128
24
+ green = Math.sin(0.3 * @color + 2 * Math::PI / 3) * 127 + 128
25
+ blue = Math.sin(0.3 * @color + 4 * Math::PI / 3) * 127 + 128
26
+ @color += 1 / 8.0
27
+ format("%<red>02X%<green>02X%<blue>02X", red: red, green: green, blue: blue)
28
+ end
29
+
30
+ def colorize(color, string)
31
+ if @color && @stream.isatty
32
+ if %i[green rainbow].include?(color)
33
+ a = string.chars.map do |c|
34
+ case @state
35
+ when :normal
36
+ if c == "\e"
37
+ @state = :ansi
38
+ elsif c == "\n"
39
+ @line_color += 1
40
+ @color = @line_color
41
+ c
42
+ else
43
+ Paint[c, rainbow]
44
+ end
45
+ when :ansi
46
+ @state = :normal if c == 'm'
47
+ end
48
+ end
49
+ a.join('')
50
+ else
51
+ "\033[#{COLORS[color]}m#{string}\033[0m"
52
+ end
53
+ else
54
+ string
55
+ end
56
+ end
57
+
58
+ def print_summary(results, elapsed_time = nil)
59
+ ok_set = results.ok_set
60
+ unless ok_set.empty?
61
+ @stream.puts colorize(:rainbow, format('Successful on %<size>d target%<plural>s: %<names>s',
62
+ size: ok_set.size,
63
+ plural: ok_set.size == 1 ? '' : 's',
64
+ names: ok_set.targets.map(&:safe_name).join(',')))
65
+ end
66
+
67
+ error_set = results.error_set
68
+ unless error_set.empty?
69
+ @stream.puts colorize(:red,
70
+ format('Failed on %<size>d target%<plural>s: %<names>s',
71
+ size: error_set.size,
72
+ plural: error_set.size == 1 ? '' : 's',
73
+ names: error_set.targets.map(&:safe_name).join(',')))
74
+ end
75
+
76
+ total_msg = format('Ran on %<size>d target%<plural>s',
77
+ size: results.size,
78
+ plural: results.size == 1 ? '' : 's')
79
+ total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
80
+ @stream.puts colorize(:rainbow, total_msg)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -15,25 +15,36 @@ module Bolt
15
15
  # PALError is used to convert errors from executing puppet code into
16
16
  # Bolt::Errors
17
17
  class PALError < Bolt::Error
18
- # Puppet sometimes rescues exceptions notes the location and reraises.
19
- # Return the original error.
20
18
  def self.from_preformatted_error(err)
21
19
  if err.cause&.is_a? Bolt::Error
22
20
  err.cause
23
21
  else
24
- from_error(err.cause || err)
22
+ from_error(err)
25
23
  end
26
24
  end
27
25
 
28
26
  # Generate a Bolt::Pal::PALError for non-bolt errors
29
27
  def self.from_error(err)
30
- e = new(err.message)
28
+ # Use the original error message if available
29
+ message = err.cause ? err.cause.message : err.message
30
+
31
+ # Provide the location of an error if it came from a plan
32
+ details = if defined?(err.file) && err.file
33
+ { file: err.file,
34
+ line: err.line,
35
+ column: err.pos }.compact
36
+ else
37
+ {}
38
+ end
39
+
40
+ e = new(message, details)
41
+
31
42
  e.set_backtrace(err.backtrace)
32
43
  e
33
44
  end
34
45
 
35
- def initialize(msg)
36
- super(msg, 'bolt/pal-error')
46
+ def initialize(msg, details = {})
47
+ super(msg, 'bolt/pal-error', details)
37
48
  end
38
49
  end
39
50
 
@@ -137,7 +148,9 @@ module Bolt
137
148
  # TODO: If we always call this inside a bolt_executor we can remove this here
138
149
  setup
139
150
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
140
- Puppet.override(bolt_project: @project,
151
+ # Only load the project if it a) exists, b) has a name it can be loaded with
152
+ bolt_project = @project if @project&.name
153
+ Puppet.override(bolt_project: bolt_project,
141
154
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
142
155
  pal.with_script_compiler do |compiler|
143
156
  alias_types(compiler)
@@ -157,8 +170,9 @@ module Bolt
157
170
  if e.issue_code == :UNKNOWN_VARIABLE &&
158
171
  %w[facts trusted server_facts settings].include?(e.arguments[:name])
159
172
  message = "Evaluation Error: Variable '#{e.arguments[:name]}' is not available in the current scope "\
160
- "unless explicitly defined. (file: #{e.file}, line: #{e.line}, column: #{e.pos})"
161
- PALError.new(message)
173
+ "unless explicitly defined."
174
+ details = { file: e.file, line: e.line, column: e.pos }
175
+ PALError.new(message, details)
162
176
  else
163
177
  PALError.from_preformatted_error(e)
164
178
  end
@@ -2,32 +2,27 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'bolt/pal'
5
+ require 'bolt/config'
5
6
 
6
7
  module Bolt
7
8
  class Project
8
9
  BOLTDIR_NAME = 'Boltdir'
9
10
  PROJECT_SETTINGS = {
10
11
  "name" => "The name of the project",
11
- "plans" => "An array of plan names to whitelist. Whitelisted plans are included in `bolt plan show` output",
12
- "tasks" => "An array of task names to whitelist. Whitelisted plans are included in `bolt task show` output"
12
+ "plans" => "An array of plan names to show, if they exist in the project."\
13
+ "These plans are included in `bolt plan show` output",
14
+ "tasks" => "An array of task names to show, if they exist in the project."\
15
+ "These tasks are included in `bolt task show` output"
13
16
  }.freeze
14
17
 
15
- attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
16
- :puppetfile, :rerunfile, :type, :resource_types
18
+ attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
17
20
 
18
21
  def self.default_project
19
- Project.new(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
22
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
20
23
  # If homedir isn't defined use the system config path
21
24
  rescue ArgumentError
22
- Project.new(system_path, 'system')
23
- end
24
-
25
- def self.system_path
26
- if Bolt::Util.windows?
27
- File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc')
28
- else
29
- File.join('/etc', 'puppetlabs', 'bolt')
30
- end
25
+ create_project(Bolt::Config.system_path, 'system')
31
26
  end
32
27
 
33
28
  # Search recursively up the directory hierarchy for the Project. Look for a
@@ -36,10 +31,11 @@ module Bolt
36
31
  # hierarchy, falling back to the default if we reach the root.
37
32
  def self.find_boltdir(dir)
38
33
  dir = Pathname.new(dir)
34
+
39
35
  if (dir + BOLTDIR_NAME).directory?
40
- new(dir + BOLTDIR_NAME, 'embedded')
36
+ create_project(dir + BOLTDIR_NAME, 'embedded')
41
37
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
42
- new(dir, 'local')
38
+ create_project(dir, 'local')
43
39
  elsif dir.root?
44
40
  default_project
45
41
  else
@@ -47,9 +43,35 @@ module Bolt
47
43
  end
48
44
  end
49
45
 
50
- def initialize(path, type = 'option')
46
+ def self.create_project(path, type = 'option')
47
+ fullpath = Pathname.new(path).expand_path
48
+
49
+ if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
50
+ raise Bolt::Error.new(
51
+ "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
52
+ "BOLT_PROJECT='#{fullpath}' to force the use of this project directory.",
53
+ "bolt/world-writable-error"
54
+ )
55
+ end
56
+
57
+ project_file = File.join(fullpath, 'bolt-project.yaml')
58
+ data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
59
+ new(data, path, type)
60
+ end
61
+
62
+ def initialize(raw_data, path, type = 'option')
51
63
  @path = Pathname.new(path).expand_path
52
- @config_file = @path + 'bolt.yaml'
64
+
65
+ @project_file = @path + 'bolt-project.yaml'
66
+
67
+ @warnings = []
68
+ if (@path + 'bolt.yaml').file? && project_file?
69
+ msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
70
+ "Transport config should be set in inventory.yaml, all other config should be set in "\
71
+ "bolt-project.yaml."
72
+ @warnings << { msg: msg }
73
+ end
74
+
53
75
  @inventory_file = @path + 'inventory.yaml'
54
76
  @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
55
77
  @hiera_config = @path + 'hiera.yaml'
@@ -58,9 +80,26 @@ module Bolt
58
80
  @resource_types = @path + '.resource_types'
59
81
  @type = type
60
82
 
61
- @project_file = @path + 'bolt-project.yaml'
62
- @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
63
- validate if load_as_module?
83
+ tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
84
+ if tc.any?
85
+ msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
86
+ @warnings << { msg: msg }
87
+ end
88
+
89
+ @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
90
+
91
+ # Once bolt.yaml deprecation is removed, this attribute should be removed
92
+ # and replaced with .project_file in lib/bolt/config.rb
93
+ @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
94
+ if (@path + 'bolt.yaml').file?
95
+ msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
96
+ @warnings << { msg: msg }
97
+ end
98
+ @project_file
99
+ else
100
+ @path + 'bolt.yaml'
101
+ end
102
+ validate if project_file?
64
103
  end
65
104
 
66
105
  def to_s
@@ -78,15 +117,12 @@ module Bolt
78
117
  end
79
118
  alias == eql?
80
119
 
81
- def load_as_module?
120
+ def project_file?
82
121
  @project_file.file?
83
122
  end
84
123
 
85
124
  def name
86
- # If the project is in mymod/Boltdir/bolt-project.yaml, use mymod as the project name
87
- dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
88
- pname = @data['name'] || dirname
89
- pname.include?('-') ? pname.split('-', 2)[1] : pname
125
+ @data['name']
90
126
  end
91
127
 
92
128
  def tasks
@@ -97,36 +133,20 @@ module Bolt
97
133
  @data['plans']
98
134
  end
99
135
 
100
- def project_directory_name?(name)
101
- # it must match an installed project name according to forge validator
102
- name =~ /^[a-z][a-z0-9_]*$/
103
- end
104
-
105
- def project_namespaced_name?(name)
106
- # it must match the full project name according to forge validator
107
- name =~ /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
108
- end
109
-
110
136
  def validate
111
- n = @data['name']
112
- if n && !project_directory_name?(n) && !project_namespaced_name?(n)
113
- raise Bolt::ValidationError, <<~ERROR_STRING
114
- Invalid project name '#{n}' in bolt-project.yaml; project names must match either:
115
- An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
116
- A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
117
- ERROR_STRING
118
- elsif !project_directory_name?(name) && !project_namespaced_name?(name)
119
- raise Bolt::ValidationError, <<~ERROR_STRING
120
- Invalid project name '#{name}'; project names must match either:
121
- A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
122
- A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
123
-
124
- Configure project name in <project_dir>/bolt-project.yaml
125
- ERROR_STRING
126
- # If the project name is the same as one of the built-in modules raise a warning
127
- elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
128
- raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
129
- "with a built-in Bolt module of the same name."
137
+ if name
138
+ name_regex = /^[a-z][a-z0-9_]*$/
139
+ if name !~ name_regex
140
+ raise Bolt::ValidationError, <<~ERROR_STRING
141
+ Invalid project name '#{name}' in bolt-project.yaml; project name must match #{name_regex.inspect}
142
+ ERROR_STRING
143
+ elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
144
+ raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
145
+ "with a built-in Bolt module of the same name."
146
+ end
147
+ else
148
+ message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
149
+ @warnings << { msg: message }
130
150
  end
131
151
 
132
152
  %w[tasks plans].each do |conf|
@@ -83,8 +83,12 @@ module Bolt
83
83
  to_hash.to_json(opts)
84
84
  end
85
85
 
86
+ def self.format_reference(type, title)
87
+ "#{type.capitalize}[#{title}]"
88
+ end
89
+
86
90
  def reference
87
- "#{type}[#{title}]"
91
+ self.class.format_reference(@type, @title)
88
92
  end
89
93
  alias to_s reference
90
94
 
@@ -23,7 +23,7 @@ module Bolt
23
23
 
24
24
  def run_command(command, options = {})
25
25
  running_as(options[:run_as]) do
26
- output = execute(command, sudoable: true)
26
+ output = execute(command, environment: options[:env_vars], sudoable: true)
27
27
  Bolt::Result.for_command(target,
28
28
  output.stdout.string,
29
29
  output.stderr.string,
@@ -35,7 +35,7 @@ module Bolt
35
35
  def upload(source, destination, options = {})
36
36
  running_as(options[:run_as]) do
37
37
  with_tmpdir do |dir|
38
- basename = File.basename(destination)
38
+ basename = File.basename(source)
39
39
  tmpfile = File.join(dir.to_s, basename)
40
40
  conn.copy_file(source, tmpfile)
41
41
  # pass over file ownership if we're using run-as to be a different user
@@ -58,7 +58,7 @@ module Bolt
58
58
  with_tmpdir do |dir|
59
59
  path = write_executable(dir.to_s, script)
60
60
  dir.chown(run_as)
61
- output = execute([path, *arguments], sudoable: true)
61
+ output = execute([path, *arguments], environment: options[:env_vars], sudoable: true)
62
62
  Bolt::Result.for_command(target,
63
63
  output.stdout.string,
64
64
  output.stderr.string,
@@ -170,7 +170,7 @@ module Bolt
170
170
  def check_sudo(out, inp, stdin)
171
171
  buffer = out.readpartial(CHUNK_SIZE)
172
172
  # Split on newlines, including the newline
173
- lines = buffer.split(/(?<=[\n])/)
173
+ lines = buffer.split(/(?<=\n)/)
174
174
  # handle_sudo will return the line if it is not a sudo prompt or error
175
175
  lines.map! { |line| handle_sudo(inp, line, stdin) }
176
176
  lines.join("")
@@ -277,31 +277,8 @@ module Bolt
277
277
  end
278
278
  end
279
279
 
280
- # In the case where a task is run with elevated privilege and needs stdin
281
- # a random string is echoed to stderr indicating that the stdin is available
282
- # for task input data because the sudo password has already either been
283
- # provided on stdin or was not needed.
284
- def prepend_sudo_success(sudo_id, command_str)
285
- command_str = "cd; #{command_str}" if conn.reset_cwd?
286
- "sh -c #{Shellwords.shellescape("echo #{sudo_id} 1>&2; #{command_str}")}"
287
- end
288
-
289
- def prepend_chdir(command_str)
290
- "sh -c #{Shellwords.shellescape("cd; #{command_str}")}"
291
- end
292
-
293
- # A helper to build up a single string that contains all of the options for
294
- # privilege escalation. A wrapper script is used to direct task input to stdin
295
- # when a tty is allocated and thus we do not need to prepend_sudo_success when
296
- # using the wrapper or when the task does not require stdin data.
297
- def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
298
- if options[:stdin] && !options[:wrapper]
299
- "#{sudo_str} #{prepend_sudo_success(sudo_id, command_str)}"
300
- elsif conn.reset_cwd?
301
- "#{sudo_str} #{prepend_chdir(command_str)}"
302
- else
303
- "#{sudo_str} #{command_str}"
304
- end
280
+ def sudo_success(sudo_id)
281
+ "echo #{sudo_id} 1>&2"
305
282
  end
306
283
 
307
284
  # Returns string with the interpreter conditionally prepended
@@ -322,27 +299,38 @@ module Bolt
322
299
  escalate = sudoable && run_as && conn.user != run_as
323
300
  use_sudo = escalate && @target.options['run-as-command'].nil?
324
301
 
325
- command_str = inject_interpreter(options[:interpreter], command)
302
+ # Depending on the transport, whether we're using sudo and whether
303
+ # there are environment variables to set, we may need to stitch
304
+ # together multiple commands into a single sh invocation
305
+ commands = [inject_interpreter(options[:interpreter], command)]
326
306
 
327
307
  if options[:environment]
328
- env_decls = options[:environment].map do |env, val|
308
+ env_decl = options[:environment].map do |env, val|
329
309
  "#{env}=#{Shellwords.shellescape(val)}"
330
- end
331
- command_str = "#{env_decls.join(' ')} #{command_str}"
310
+ end.join(' ')
332
311
  end
333
312
 
334
313
  if escalate
335
- if use_sudo
336
- sudo_exec = target.options['sudo-executable'] || "sudo"
337
- sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
338
- sudo_flags += ["-E"] if options[:environment]
339
- sudo_str = Shellwords.shelljoin(sudo_flags)
340
- else
341
- sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
342
- end
343
- command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
314
+ sudo_str = if use_sudo
315
+ sudo_exec = target.options['sudo-executable'] || "sudo"
316
+ sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
317
+ sudo_flags += ["-E"] if options[:environment]
318
+ Shellwords.shelljoin(sudo_flags)
319
+ else
320
+ Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
321
+ end
322
+ commands.unshift('cd') if conn.reset_cwd?
323
+ commands.unshift(sudo_success(@sudo_id)) if options[:stdin] && !options[:wrapper]
344
324
  end
345
325
 
326
+ command_str = if sudo_str || env_decl
327
+ "sh -c #{Shellwords.shellescape(commands.join('; '))}"
328
+ else
329
+ commands.last
330
+ end
331
+
332
+ command_str = [sudo_str, env_decl, command_str].compact.join(' ')
333
+
346
334
  @logger.debug { "Executing: #{command_str}" }
347
335
 
348
336
  in_buffer = if !use_sudo && options[:stdin]