bolt 2.34.0 → 2.40.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  12. data/lib/bolt/analytics.rb +27 -8
  13. data/lib/bolt/apply_result.rb +3 -3
  14. data/lib/bolt/bolt_option_parser.rb +45 -18
  15. data/lib/bolt/cli.rb +98 -116
  16. data/lib/bolt/config.rb +184 -80
  17. data/lib/bolt/config/options.rb +148 -87
  18. data/lib/bolt/config/transport/base.rb +10 -19
  19. data/lib/bolt/config/transport/local.rb +1 -7
  20. data/lib/bolt/config/transport/options.rb +12 -69
  21. data/lib/bolt/config/transport/ssh.rb +8 -19
  22. data/lib/bolt/error.rb +24 -0
  23. data/lib/bolt/executor.rb +92 -18
  24. data/lib/bolt/inventory.rb +25 -0
  25. data/lib/bolt/inventory/group.rb +0 -8
  26. data/lib/bolt/inventory/options.rb +130 -0
  27. data/lib/bolt/inventory/target.rb +10 -11
  28. data/lib/bolt/module_installer.rb +21 -13
  29. data/lib/bolt/module_installer/resolver.rb +1 -1
  30. data/lib/bolt/outputter.rb +19 -5
  31. data/lib/bolt/outputter/human.rb +22 -3
  32. data/lib/bolt/outputter/json.rb +1 -1
  33. data/lib/bolt/outputter/logger.rb +1 -1
  34. data/lib/bolt/outputter/rainbow.rb +13 -2
  35. data/lib/bolt/pal.rb +18 -6
  36. data/lib/bolt/pal/yaml_plan.rb +7 -0
  37. data/lib/bolt/plugin.rb +41 -12
  38. data/lib/bolt/plugin/cache.rb +76 -0
  39. data/lib/bolt/plugin/module.rb +4 -4
  40. data/lib/bolt/plugin/puppetdb.rb +1 -1
  41. data/lib/bolt/project.rb +59 -40
  42. data/lib/bolt/project_manager.rb +201 -0
  43. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +49 -4
  44. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  45. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  46. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +5 -3
  47. data/lib/bolt/puppetdb/client.rb +11 -2
  48. data/lib/bolt/puppetdb/config.rb +4 -3
  49. data/lib/bolt/rerun.rb +1 -5
  50. data/lib/bolt/shell/bash.rb +8 -2
  51. data/lib/bolt/shell/powershell.rb +21 -3
  52. data/lib/bolt/target.rb +4 -0
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/local.rb +13 -0
  55. data/lib/bolt/transport/orch.rb +0 -5
  56. data/lib/bolt/transport/orch/connection.rb +10 -3
  57. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  58. data/lib/bolt/util.rb +36 -7
  59. data/lib/bolt/validator.rb +227 -0
  60. data/lib/bolt/version.rb +1 -1
  61. data/lib/bolt/yarn.rb +23 -0
  62. data/lib/bolt_server/base_config.rb +3 -1
  63. data/lib/bolt_server/config.rb +3 -1
  64. data/lib/bolt_server/plugin.rb +13 -0
  65. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  66. data/lib/bolt_server/schemas/connect-data.json +22 -0
  67. data/lib/bolt_server/schemas/partials/task.json +2 -2
  68. data/lib/bolt_server/transport_app.rb +82 -23
  69. data/lib/bolt_spec/plans/mock_executor.rb +4 -1
  70. data/libexec/apply_catalog.rb +1 -1
  71. data/libexec/custom_facts.rb +1 -1
  72. data/libexec/query_resources.rb +1 -1
  73. metadata +22 -14
  74. data/lib/bolt/project_migrator.rb +0 -80
@@ -14,6 +14,22 @@ module Bolt
14
14
  extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] == '.' ? ext : '.' + ext }
15
15
  extensions += target.options['interpreters'].keys if target.options['interpreters']
16
16
  @extensions = DEFAULT_EXTENSIONS + extensions
17
+ validate_ps_version
18
+ end
19
+
20
+ def validate_ps_version
21
+ version = execute("$PSVersionTable.PSVersion.Major").stdout.string.chomp
22
+ if !version.empty? && version.to_i < 3
23
+ # This lets us know how many targets have Powershell 2, and lets the
24
+ # user know how many targets they have with PS2
25
+ msg = "Detected PowerShell 2 on one or more targets.\nPowerShell 2 "\
26
+ "is deprecated, and support will be removed in Bolt 3.0. See "\
27
+ "bolt-debug.log or run with '--log-level debug' to see the full "\
28
+ "list of targets with PowerShell 2."
29
+
30
+ Bolt::Logger.deprecation_warning("PowerShell 2", msg)
31
+ @logger.debug("Detected PowerShell 2 on #{target}.")
32
+ end
17
33
  end
18
34
 
19
35
  def provided_features
@@ -177,7 +193,8 @@ module Bolt
177
193
  def run_command(command, options = {}, position = [])
178
194
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
179
195
 
180
- output = execute(command)
196
+ wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
197
+ output = execute(command, wrap_command)
181
198
  Bolt::Result.for_command(target,
182
199
  output.stdout.string,
183
200
  output.stderr.string,
@@ -268,8 +285,9 @@ module Bolt
268
285
  end
269
286
  end
270
287
 
271
- def execute(command)
272
- if conn.max_command_length && command.length > conn.max_command_length
288
+ def execute(command, wrap_command = false)
289
+ if (conn.max_command_length && command.length > conn.max_command_length) ||
290
+ wrap_command
273
291
  return with_tmpdir do |dir|
274
292
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
275
293
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
@@ -80,6 +80,10 @@ module Bolt
80
80
  inventory_target.resources
81
81
  end
82
82
 
83
+ def set_local_defaults
84
+ inventory_target.set_local_defaults
85
+ end
86
+
83
87
  # rubocop:disable Naming/AccessorMethodName
84
88
  def set_resource(resource)
85
89
  inventory_target.set_resource(resource)
@@ -42,7 +42,7 @@ module Bolt
42
42
  if targets.empty?
43
43
  Bolt::ResultSet.new([])
44
44
  else
45
- result = executor.run_task_with_minimal_logging(targets, task, params, options)
45
+ result = executor.run_task(targets, task, params, options, [], :trace)
46
46
 
47
47
  if !result.ok && !options[:catch_errors]
48
48
  raise Bolt::RunFailure.new(result, 'run_task', task.name)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/logger'
3
4
  require 'bolt/transport/simple'
4
5
 
5
6
  module Bolt
@@ -10,6 +11,18 @@ module Bolt
10
11
  end
11
12
 
12
13
  def with_connection(target)
14
+ if target.transport_config['bundled-ruby'] || target.name == 'localhost'
15
+ target.set_local_defaults
16
+ end
17
+
18
+ if target.name != 'localhost' &&
19
+ !target.transport_config.key?('bundled-ruby')
20
+ msg = "The local transport will default to using Bolt's Ruby interpreter and "\
21
+ "setting the 'puppet-agent' feature in Bolt 3.0. Enable or disable these "\
22
+ "defaults by setting 'bundled-ruby' in the local transport config."
23
+ Bolt::Logger.warn_once('local default config', msg)
24
+ end
25
+
13
26
  yield Connection.new(target)
14
27
  end
15
28
  end
@@ -10,11 +10,6 @@ require 'bolt/transport/orch/connection'
10
10
  module Bolt
11
11
  module Transport
12
12
  class Orch < Base
13
- CONF_FILE = if ENV['HOME'].nil?
14
- '/etc/puppetlabs/client-tools/orchestrator.conf'
15
- else
16
- File.expand_path('~/.puppetlabs/client-tools/orchestrator.conf')
17
- end
18
13
  BOLT_COMMAND_TASK = Struct.new(:name).new('bolt_shim::command').freeze
19
14
  BOLT_SCRIPT_TASK = Struct.new(:name).new('bolt_shim::script').freeze
20
15
  BOLT_UPLOAD_TASK = Struct.new(:name).new('bolt_shim::upload').freeze
@@ -17,13 +17,20 @@ module Bolt
17
17
  end
18
18
 
19
19
  def initialize(opts, plan_context, logger)
20
+ require 'addressable/uri'
21
+
20
22
  @logger = logger
21
23
  @key = self.class.get_key(opts)
22
- client_keys = %w[service-url token-file cacert job-poll-interval job-poll-timeout]
23
- client_opts = client_keys.each_with_object({}) do |k, acc|
24
- acc[k] = opts[k] if opts.include?(k)
24
+ client_opts = opts.slice('token-file', 'cacert', 'job-poll-interval', 'job-poll-timeout')
25
+
26
+ if opts['service-url']
27
+ uri = Addressable::URI.parse(opts['service-url'])
28
+ uri&.port ||= 8143
29
+ client_opts['service-url'] = uri.to_s
25
30
  end
31
+
26
32
  client_opts['User-Agent'] = "Bolt/#{VERSION}"
33
+
27
34
  %w[token-file cacert].each do |f|
28
35
  client_opts[f] = File.expand_path(client_opts[f]) if client_opts[f]
29
36
  end
@@ -12,8 +12,12 @@ module Bolt
12
12
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
13
13
 
14
14
  @target = target
15
- ssh_config = Net::SSH::Config.for(target.host)
16
- @user = @target.user || ssh_config[:user] || Etc.getlogin
15
+ begin
16
+ ssh_config = Net::SSH::Config.for(target.host)
17
+ @user = @target.user || ssh_config[:user] || Etc.getlogin
18
+ rescue StandardError
19
+ @user = @target.user || Etc.getlogin
20
+ end
17
21
  @logger = Bolt::Logger.logger(self)
18
22
  end
19
23
 
@@ -22,6 +22,28 @@ module Bolt
22
22
  raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
23
23
  end
24
24
 
25
+ def read_json_file(path, filename)
26
+ require 'json'
27
+
28
+ logger = Bolt::Logger.logger(self)
29
+ path = File.expand_path(path)
30
+ content = JSON.parse(File.read(path))
31
+ logger.trace("Loaded #{filename} from #{path}")
32
+ content
33
+ rescue Errno::ENOENT
34
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}", path)
35
+ rescue JSON::ParserError => e
36
+ msg = "Unable to parse #{filename} file at #{path} as JSON: #{e.message}"
37
+ raise Bolt::FileError.new(msg, path)
38
+ rescue IOError, SystemCallError => e
39
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}\n#{e.message}",
40
+ path)
41
+ end
42
+
43
+ def read_optional_json_file(path, file_name)
44
+ File.exist?(path) && !File.zero?(path) ? read_yaml_hash(path, file_name) : {}
45
+ end
46
+
25
47
  def read_yaml_hash(path, file_name)
26
48
  require 'yaml'
27
49
 
@@ -29,19 +51,26 @@ module Bolt
29
51
  path = File.expand_path(path)
30
52
  content = File.open(path, "r:UTF-8") { |f| YAML.safe_load(f.read) } || {}
31
53
  unless content.is_a?(Hash)
32
- msg = "Invalid content for #{file_name} file: #{path} should be a Hash or empty, not #{content.class}"
33
- raise Bolt::FileError.new(msg, path)
54
+ raise Bolt::FileError.new(
55
+ "Invalid content for #{file_name} file at #{path}\nContent should be a Hash or empty, "\
56
+ "not #{content.class}",
57
+ path
58
+ )
34
59
  end
35
60
  logger.trace("Loaded #{file_name} from #{path}")
36
61
  content
37
62
  rescue Errno::ENOENT
38
- raise Bolt::FileError.new("Could not read #{file_name} file: #{path}", path)
63
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}", path)
64
+ rescue Psych::SyntaxError => e
65
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}, line #{e.line}, "\
66
+ "column #{e.column}\n#{e.problem}",
67
+ path)
39
68
  rescue Psych::Exception => e
40
- raise Bolt::FileError.new("Could not parse #{file_name} file: #{path}\n"\
41
- "Error at line #{e.line} column #{e.column}", path)
69
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}\n#{e.message}",
70
+ path)
42
71
  rescue IOError, SystemCallError => e
43
- raise Bolt::FileError.new("Could not read #{file_name} file: #{path}\n"\
44
- "error: #{e}", path)
72
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}\n#{e.message}",
73
+ path)
45
74
  end
46
75
 
47
76
  def read_optional_yaml_hash(path, file_name)
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class validates config against a schema, raising an error that includes
6
+ # details about any invalid configuration.
7
+ #
8
+ module Bolt
9
+ class Validator
10
+ attr_reader :deprecations, :warnings
11
+
12
+ def initialize
13
+ @errors = []
14
+ @deprecations = []
15
+ @warnings = []
16
+ @path = []
17
+ end
18
+
19
+ # This is the entry method for validating data against the schema.
20
+ #
21
+ def validate(data, schema, location = nil)
22
+ @schema = schema
23
+ @location = location
24
+
25
+ validate_value(data, schema)
26
+
27
+ raise_error
28
+ end
29
+
30
+ # Raises a ValidationError if there are any errors. All error messages
31
+ # created during validation are concatenated into a single error
32
+ # message.
33
+ #
34
+ private def raise_error
35
+ return unless @errors.any?
36
+
37
+ message = "Invalid configuration"
38
+ message += " at #{@location}" if @location
39
+ message += ":\n"
40
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
41
+
42
+ raise Bolt::ValidationError, message
43
+ end
44
+
45
+ # Validate an individual value. This performs validation that is
46
+ # common to all values, including type validation. After validating
47
+ # the value's type, the value is passed off to an individual
48
+ # validation method for the value's type.
49
+ #
50
+ private def validate_value(value, definition, plugin_supported = false)
51
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
52
+ plugin_supported = definition[:_plugin] if definition.key?(:_plugin)
53
+
54
+ return if plugin_reference?(value, plugin_supported)
55
+ return unless valid_type?(value, definition)
56
+
57
+ case value
58
+ when Hash
59
+ validate_hash(value, definition, plugin_supported)
60
+ when Array
61
+ validate_array(value, definition, plugin_supported)
62
+ when String
63
+ validate_string(value, definition)
64
+ when Numeric
65
+ validate_number(value, definition)
66
+ end
67
+ end
68
+
69
+ # Validates a hash value, logging errors for any validations that fail.
70
+ # This will enumerate each key-value pair in the hash and validate each
71
+ # value individually.
72
+ #
73
+ private def validate_hash(value, definition, plugin_supported)
74
+ properties = definition[:properties] ? definition[:properties].keys : []
75
+
76
+ if definition[:properties] && definition[:additionalProperties].nil?
77
+ validate_keys(value.keys, properties)
78
+ end
79
+
80
+ if definition[:required] && (definition[:required] - value.keys).any?
81
+ missing = definition[:required] - value.keys
82
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
83
+ end
84
+
85
+ value.each_pair do |key, val|
86
+ @path.push(key)
87
+
88
+ if properties.include?(key)
89
+ check_deprecated(key, definition[:properties][key])
90
+ validate_value(val, definition[:properties][key], plugin_supported)
91
+ elsif definition[:additionalProperties].is_a?(Hash)
92
+ validate_value(val, definition[:additionalProperties], plugin_supported)
93
+ end
94
+ ensure
95
+ @path.pop
96
+ end
97
+ end
98
+
99
+ # Validates an array value, logging errors for any validations that fail.
100
+ # This will enumerate the items in the array and validate each item
101
+ # individually.
102
+ #
103
+ private def validate_array(value, definition, plugin_supported)
104
+ if definition[:uniqueItems] && value.size != value.uniq.size
105
+ @errors << "Value at '#{path}' must not include duplicate elements"
106
+ return
107
+ end
108
+
109
+ return unless definition.key?(:items)
110
+
111
+ value.each_with_index do |item, index|
112
+ @path.push(index)
113
+ validate_value(item, definition[:items], plugin_supported)
114
+ ensure
115
+ @path.pop
116
+ end
117
+ end
118
+
119
+ # Validates a string value, logging errors for any validations that fail.
120
+ #
121
+ private def validate_string(value, definition)
122
+ if definition.key?(:enum) && !definition[:enum].include?(value)
123
+ message = "Value at '#{path}' must be "
124
+ message += "one of " if definition[:enum].count > 1
125
+ message += definition[:enum].join(', ')
126
+ multitype_error(message, value, definition)
127
+ end
128
+ end
129
+
130
+ # Validates a numeric value, logging errors for any validations that fail.
131
+ #
132
+ private def validate_number(value, definition)
133
+ if definition.key?(:minimum) && value < definition[:minimum]
134
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
135
+ end
136
+
137
+ if definition.key?(:maximum) && value > definition[:maximum]
138
+ @errors << "Value at '#{path}' must be a maximum of #{definition[:maximum]}"
139
+ end
140
+ end
141
+
142
+ # Adds warnings for unknown config options.
143
+ #
144
+ private def validate_keys(keys, known_keys)
145
+ (keys - known_keys).each do |key|
146
+ message = "Unknown option '#{key}'"
147
+ message += " at '#{path}'" if @path.any?
148
+ message += " at #{@location}" if @location
149
+ message += "."
150
+ @warnings << message
151
+ end
152
+ end
153
+
154
+ # Adds a warning if the given option is deprecated.
155
+ #
156
+ private def check_deprecated(key, definition)
157
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
158
+
159
+ if definition.key?(:_deprecation)
160
+ message = "Option '#{path}' "
161
+ message += "at #{@location} " if @location
162
+ message += "is deprecated. #{definition[:_deprecation]}"
163
+ @deprecations << { option: key, message: message }
164
+ end
165
+ end
166
+
167
+ # Returns true if a value is a plugin reference. This also validates whether
168
+ # a value can be a plugin reference in the first place. If the value is a
169
+ # plugin reference but cannot be one according to the schema, then this will
170
+ # log an error.
171
+ #
172
+ private def plugin_reference?(value, plugin_supported)
173
+ if value.is_a?(Hash) && value.key?('_plugin')
174
+ unless plugin_supported
175
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
176
+ "this location"
177
+ end
178
+
179
+ true
180
+ else
181
+ false
182
+ end
183
+ end
184
+
185
+ # Asserts the type for each option against the type specified in the schema
186
+ # definition. The schema definition can specify multiple valid types, so the
187
+ # value needs to only match one of the types to be valid. Returns early if
188
+ # there is no type in the definition (in practice this shouldn't happen, but
189
+ # this will safeguard against any dev mistakes).
190
+ #
191
+ private def valid_type?(value, definition)
192
+ return unless definition.key?(:type)
193
+
194
+ types = Array(definition[:type])
195
+
196
+ if types.include?(value.class)
197
+ true
198
+ else
199
+ if types.include?(TrueClass) || types.include?(FalseClass)
200
+ types = types - [TrueClass, FalseClass] + ['Boolean']
201
+ end
202
+
203
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
204
+
205
+ false
206
+ end
207
+ end
208
+
209
+ # Adds an error that includes additional helpful information for values
210
+ # that accept multiple types.
211
+ #
212
+ private def multitype_error(message, value, definition)
213
+ if Array(definition[:type]).count > 1
214
+ types = Array(definition[:type]) - [value.class]
215
+ message += " or must be of type #{types.join(' or ')}"
216
+ end
217
+
218
+ @errors << message
219
+ end
220
+
221
+ # Returns the formatted path for the key.
222
+ #
223
+ private def path
224
+ @path.join('.')
225
+ end
226
+ end
227
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.34.0'
4
+ VERSION = '2.40.1'
5
5
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module Bolt
6
+ class Yarn
7
+ attr_reader :fiber, :value, :index
8
+
9
+ def initialize(fiber, index)
10
+ @fiber = fiber
11
+ @index = index
12
+ @value = nil
13
+ end
14
+
15
+ def alive?
16
+ fiber.alive?
17
+ end
18
+
19
+ def resume
20
+ @value = fiber.resume
21
+ end
22
+ end
23
+ end