bolt 2.14.0 → 2.19.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 (72) 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 +1 -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 +2 -1
  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 +34 -20
  41. data/lib/bolt/apply_result.rb +1 -1
  42. data/lib/bolt/bolt_option_parser.rb +30 -18
  43. data/lib/bolt/cli.rb +78 -56
  44. data/lib/bolt/config.rb +158 -128
  45. data/lib/bolt/config/options.rb +474 -0
  46. data/lib/bolt/config/transport/base.rb +16 -16
  47. data/lib/bolt/config/transport/docker.rb +9 -23
  48. data/lib/bolt/config/transport/local.rb +6 -44
  49. data/lib/bolt/config/transport/options.rb +460 -0
  50. data/lib/bolt/config/transport/orch.rb +9 -18
  51. data/lib/bolt/config/transport/remote.rb +3 -6
  52. data/lib/bolt/config/transport/ssh.rb +74 -154
  53. data/lib/bolt/config/transport/winrm.rb +18 -47
  54. data/lib/bolt/inventory/group.rb +1 -1
  55. data/lib/bolt/inventory/inventory.rb +0 -14
  56. data/lib/bolt/inventory/target.rb +18 -5
  57. data/lib/bolt/logger.rb +24 -1
  58. data/lib/bolt/outputter.rb +3 -0
  59. data/lib/bolt/outputter/rainbow.rb +90 -0
  60. data/lib/bolt/pal.rb +23 -9
  61. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  62. data/lib/bolt/plugin/module.rb +2 -4
  63. data/lib/bolt/project.rb +41 -52
  64. data/lib/bolt/shell/bash.rb +30 -42
  65. data/lib/bolt/shell/powershell.rb +13 -8
  66. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  67. data/lib/bolt/transport/docker.rb +9 -5
  68. data/lib/bolt/transport/orch.rb +8 -0
  69. data/lib/bolt/transport/ssh.rb +7 -1
  70. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  71. data/lib/bolt/version.rb +1 -1
  72. metadata +18 -15
@@ -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',
@@ -66,6 +67,10 @@ module Bolt
66
67
  end
67
68
  end
68
69
 
70
+ def self.analytics=(analytics)
71
+ @analytics = analytics
72
+ end
73
+
69
74
  def self.console_layout(color)
70
75
  color_scheme = :bolt if color
71
76
  Logging.layouts.pattern(
@@ -89,8 +94,10 @@ module Bolt
89
94
  :notice
90
95
  end
91
96
 
97
+ # Explicitly check the log level names instead of the log level number, as levels
98
+ # that are stringified integers (e.g. "level" => "42") will return a truthy value
92
99
  def self.valid_level?(level)
93
- !Logging.level_num(level).nil?
100
+ Logging::LEVELS.include?(Logging.levelify(level))
94
101
  end
95
102
 
96
103
  def self.levels
@@ -100,5 +107,21 @@ module Bolt
100
107
  def self.reset_logging
101
108
  Logging.reset
102
109
  end
110
+
111
+ def self.warn_once(type, msg)
112
+ @mutex.synchronize {
113
+ @warnings ||= []
114
+ @logger ||= Logging.logger[self]
115
+ unless @warnings.include?(type)
116
+ @logger.warn(msg)
117
+ @warnings << type
118
+ end
119
+ }
120
+ end
121
+
122
+ def self.deprecation_warning(type, msg)
123
+ @analytics&.event('Warn', 'deprecation', label: type)
124
+ warn_once(type, msg)
125
+ end
103
126
  end
104
127
  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,90 @@
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
+ if Bolt::Util.windows?
12
+ # the Paint gem thinks that windows does not support ansi colors
13
+ # but windows 10 or later does
14
+ # we can display colors if we force mode to TRUE_COLOR
15
+ Paint.mode = 0xFFFFFF
16
+ end
17
+ rescue LoadError
18
+ raise "The 'paint' gem is required to use the rainbow outputter."
19
+ end
20
+ super
21
+ @line_color = 0
22
+ @color = 0
23
+ @state = :normal
24
+ end
25
+
26
+ # The algorithm is from lolcat (https://github.com/busyloop/lolcat)
27
+ # lolcat is released with WTFPL
28
+ def rainbow
29
+ red = Math.sin(0.3 * @color + 0) * 127 + 128
30
+ green = Math.sin(0.3 * @color + 2 * Math::PI / 3) * 127 + 128
31
+ blue = Math.sin(0.3 * @color + 4 * Math::PI / 3) * 127 + 128
32
+ @color += 1 / 8.0
33
+ format("%<red>02X%<green>02X%<blue>02X", red: red, green: green, blue: blue)
34
+ end
35
+
36
+ def colorize(color, string)
37
+ if @color && @stream.isatty
38
+ if %i[green rainbow].include?(color)
39
+ a = string.chars.map do |c|
40
+ case @state
41
+ when :normal
42
+ if c == "\e"
43
+ @state = :ansi
44
+ elsif c == "\n"
45
+ @line_color += 1
46
+ @color = @line_color
47
+ c
48
+ else
49
+ Paint[c, rainbow]
50
+ end
51
+ when :ansi
52
+ @state = :normal if c == 'm'
53
+ end
54
+ end
55
+ a.join('')
56
+ else
57
+ "\033[#{COLORS[color]}m#{string}\033[0m"
58
+ end
59
+ else
60
+ string
61
+ end
62
+ end
63
+
64
+ def print_summary(results, elapsed_time = nil)
65
+ ok_set = results.ok_set
66
+ unless ok_set.empty?
67
+ @stream.puts colorize(:rainbow, format('Successful on %<size>d target%<plural>s: %<names>s',
68
+ size: ok_set.size,
69
+ plural: ok_set.size == 1 ? '' : 's',
70
+ names: ok_set.targets.map(&:safe_name).join(',')))
71
+ end
72
+
73
+ error_set = results.error_set
74
+ unless error_set.empty?
75
+ @stream.puts colorize(:red,
76
+ format('Failed on %<size>d target%<plural>s: %<names>s',
77
+ size: error_set.size,
78
+ plural: error_set.size == 1 ? '' : 's',
79
+ names: error_set.targets.map(&:safe_name).join(',')))
80
+ end
81
+
82
+ total_msg = format('Ran on %<size>d target%<plural>s',
83
+ size: results.size,
84
+ plural: results.size == 1 ? '' : 's')
85
+ total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
86
+ @stream.puts colorize(:rainbow, total_msg)
87
+ end
88
+ end
89
+ end
90
+ 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
@@ -142,7 +142,7 @@ module Bolt
142
142
  if plan.steps.any? { |step| step.body.key?('target') }
143
143
  msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
144
144
  "in a future version of Bolt. Use the 'targets' parameter instead."
145
- @logger.warn(msg)
145
+ Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
146
146
  end
147
147
 
148
148
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
@@ -184,12 +184,10 @@ module Bolt
184
184
  # Raises a deprecation warning if the pkcs7 plugin is using deprecated keys and
185
185
  # modifies the keys so they are the correct format
186
186
  def handle_deprecated_pkcs7_keys(params)
187
- if (params.key?('private-key') || params.key?('public-key')) && !@deprecation_warning_issued
188
- @deprecation_warning_issued = true
189
-
187
+ if params.key?('private-key') || params.key?('public-key')
190
188
  message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
191
189
  "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
192
- Logging.logger[self].warn(message)
190
+ Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
193
191
  end
194
192
 
195
193
  params['private_key'] = params.delete('private-key') if params.key?('private-key')
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
+ require 'bolt/config'
4
5
  require 'bolt/pal'
5
6
 
6
7
  module Bolt
@@ -8,26 +9,21 @@ module Bolt
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
18
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
16
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
19
+ :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
+ :deprecations
17
21
 
18
22
  def self.default_project
19
23
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
20
24
  # If homedir isn't defined use the system config path
21
25
  rescue ArgumentError
22
- create_project(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
26
+ create_project(Bolt::Config.system_path, 'system')
31
27
  end
32
28
 
33
29
  # Search recursively up the directory hierarchy for the Project. Look for a
@@ -36,6 +32,7 @@ module Bolt
36
32
  # hierarchy, falling back to the default if we reach the root.
37
33
  def self.find_boltdir(dir)
38
34
  dir = Pathname.new(dir)
35
+
39
36
  if (dir + BOLTDIR_NAME).directory?
40
37
  create_project(dir + BOLTDIR_NAME, 'embedded')
41
38
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
@@ -49,6 +46,15 @@ module Bolt
49
46
 
50
47
  def self.create_project(path, type = 'option')
51
48
  fullpath = Pathname.new(path).expand_path
49
+
50
+ if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
51
+ raise Bolt::Error.new(
52
+ "Project directory '#{fullpath}' is world-writable which poses a security risk. Set "\
53
+ "BOLT_PROJECT='#{fullpath}' to force the use of this project directory.",
54
+ "bolt/world-writable-error"
55
+ )
56
+ end
57
+
52
58
  project_file = File.join(fullpath, 'bolt-project.yaml')
53
59
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
54
60
  new(data, path, type)
@@ -56,14 +62,16 @@ module Bolt
56
62
 
57
63
  def initialize(raw_data, path, type = 'option')
58
64
  @path = Pathname.new(path).expand_path
65
+
59
66
  @project_file = @path + 'bolt-project.yaml'
60
67
 
61
68
  @warnings = []
69
+ @deprecations = []
62
70
  if (@path + 'bolt.yaml').file? && project_file?
63
71
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
64
72
  "Transport config should be set in inventory.yaml, all other config should be set in "\
65
73
  "bolt-project.yaml."
66
- @warnings << { msg: msg }
74
+ @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
67
75
  end
68
76
 
69
77
  @inventory_file = @path + 'inventory.yaml'
@@ -74,17 +82,17 @@ module Bolt
74
82
  @resource_types = @path + '.resource_types'
75
83
  @type = type
76
84
 
77
- tc = Bolt::Config::CONFIG_IN_INVENTORY.keys & raw_data.keys
85
+ tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
78
86
  if tc.any?
79
87
  msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
80
88
  @warnings << { msg: msg }
81
89
  end
82
90
 
83
- @data = raw_data.reject { |k, _| Bolt::Config::CONFIG_IN_INVENTORY.keys.include?(k) }
91
+ @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
84
92
 
85
93
  # Once bolt.yaml deprecation is removed, this attribute should be removed
86
94
  # and replaced with .project_file in lib/bolt/config.rb
87
- @config_file = if (Bolt::Config::OPTIONS.keys & @data.keys).any?
95
+ @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
88
96
  if (@path + 'bolt.yaml').file?
89
97
  msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
90
98
  @warnings << { msg: msg }
@@ -103,7 +111,7 @@ module Bolt
103
111
  # This API is used to prepend the project as a module to Puppet's internal
104
112
  # module_references list. CHANGE AT YOUR OWN RISK
105
113
  def to_h
106
- { path: @path, name: name }
114
+ { path: @path.to_s, name: name }
107
115
  end
108
116
 
109
117
  def eql?(other)
@@ -116,10 +124,7 @@ module Bolt
116
124
  end
117
125
 
118
126
  def name
119
- # If the project is in mymod/Boltdir/bolt-project.yaml, use mymod as the project name
120
- dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
121
- pname = @data['name'] || dirname
122
- pname.include?('-') ? pname.split('-', 2)[1] : pname
127
+ @data['name']
123
128
  end
124
129
 
125
130
  def tasks
@@ -130,36 +135,20 @@ module Bolt
130
135
  @data['plans']
131
136
  end
132
137
 
133
- def project_directory_name?(name)
134
- # it must match an installed project name according to forge validator
135
- name =~ /^[a-z][a-z0-9_]*$/
136
- end
137
-
138
- def project_namespaced_name?(name)
139
- # it must match the full project name according to forge validator
140
- name =~ /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
141
- end
142
-
143
138
  def validate
144
- n = @data['name']
145
- if n && !project_directory_name?(n) && !project_namespaced_name?(n)
146
- raise Bolt::ValidationError, <<~ERROR_STRING
147
- Invalid project name '#{n}' in bolt-project.yaml; project names must match either:
148
- An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
149
- A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
150
- ERROR_STRING
151
- elsif !project_directory_name?(name) && !project_namespaced_name?(name)
152
- raise Bolt::ValidationError, <<~ERROR_STRING
153
- Invalid project name '#{name}'; project names must match either:
154
- A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
155
- A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
156
-
157
- Configure project name in <project_dir>/bolt-project.yaml
158
- ERROR_STRING
159
- # If the project name is the same as one of the built-in modules raise a warning
160
- elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
161
- raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
162
- "with a built-in Bolt module of the same name."
139
+ if name
140
+ name_regex = /^[a-z][a-z0-9_]*$/
141
+ if name !~ name_regex
142
+ raise Bolt::ValidationError, <<~ERROR_STRING
143
+ Invalid project name '#{name}' in bolt-project.yaml; project name must match #{name_regex.inspect}
144
+ ERROR_STRING
145
+ elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
146
+ raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
147
+ "with a built-in Bolt module of the same name."
148
+ end
149
+ else
150
+ message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
151
+ @warnings << { msg: message }
163
152
  end
164
153
 
165
154
  %w[tasks plans].each do |conf|
@@ -171,8 +160,8 @@ module Bolt
171
160
 
172
161
  def check_deprecated_file
173
162
  if (@path + 'project.yaml').file?
174
- logger = Logging.logger[self]
175
- logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
163
+ msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
164
+ Bolt::Logger.deprecation_warning('Using project.yaml instead of bolt-project.yaml', msg)
176
165
  end
177
166
  end
178
167
  end
@@ -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]