bolt 2.23.0 → 2.27.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/exe/bolt +1 -0
  7. data/guides/inventory.txt +19 -0
  8. data/guides/project.txt +22 -0
  9. data/lib/bolt/analytics.rb +11 -7
  10. data/lib/bolt/applicator.rb +11 -10
  11. data/lib/bolt/bolt_option_parser.rb +75 -13
  12. data/lib/bolt/catalog.rb +4 -2
  13. data/lib/bolt/cli.rb +156 -176
  14. data/lib/bolt/config.rb +55 -25
  15. data/lib/bolt/config/options.rb +28 -6
  16. data/lib/bolt/executor.rb +5 -3
  17. data/lib/bolt/inventory.rb +8 -1
  18. data/lib/bolt/inventory/group.rb +4 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/inventory/target.rb +1 -1
  21. data/lib/bolt/logger.rb +12 -6
  22. data/lib/bolt/outputter/human.rb +10 -0
  23. data/lib/bolt/outputter/json.rb +11 -0
  24. data/lib/bolt/outputter/logger.rb +3 -3
  25. data/lib/bolt/outputter/rainbow.rb +15 -0
  26. data/lib/bolt/pal.rb +23 -12
  27. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  28. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  29. data/lib/bolt/plugin/puppetdb.rb +1 -1
  30. data/lib/bolt/project.rb +63 -17
  31. data/lib/bolt/project_migrate.rb +138 -0
  32. data/lib/bolt/puppetdb/client.rb +1 -1
  33. data/lib/bolt/puppetdb/config.rb +1 -1
  34. data/lib/bolt/puppetfile.rb +160 -0
  35. data/lib/bolt/puppetfile/installer.rb +43 -0
  36. data/lib/bolt/puppetfile/module.rb +66 -0
  37. data/lib/bolt/r10k_log_proxy.rb +1 -1
  38. data/lib/bolt/rerun.rb +2 -2
  39. data/lib/bolt/result.rb +23 -0
  40. data/lib/bolt/shell.rb +1 -1
  41. data/lib/bolt/shell/bash.rb +7 -7
  42. data/lib/bolt/task.rb +1 -1
  43. data/lib/bolt/transport/base.rb +1 -1
  44. data/lib/bolt/transport/docker/connection.rb +10 -10
  45. data/lib/bolt/transport/local/connection.rb +3 -3
  46. data/lib/bolt/transport/orch.rb +3 -3
  47. data/lib/bolt/transport/ssh.rb +1 -1
  48. data/lib/bolt/transport/ssh/connection.rb +6 -6
  49. data/lib/bolt/transport/ssh/exec_connection.rb +5 -5
  50. data/lib/bolt/transport/winrm.rb +1 -1
  51. data/lib/bolt/transport/winrm/connection.rb +9 -9
  52. data/lib/bolt/util.rb +2 -2
  53. data/lib/bolt/util/puppet_log_level.rb +4 -3
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt_server/base_config.rb +2 -2
  56. data/lib/bolt_server/config.rb +1 -1
  57. data/lib/bolt_server/file_cache.rb +1 -1
  58. data/lib/bolt_server/transport_app.rb +189 -14
  59. data/lib/bolt_spec/plans.rb +1 -1
  60. data/lib/bolt_spec/run.rb +3 -0
  61. metadata +12 -12
@@ -13,7 +13,7 @@ module Bolt
13
13
  @config = config
14
14
  @bad_urls = []
15
15
  @current_url = nil
16
- @logger = Logging.logger[self]
16
+ @logger = Bolt::Logger.logger(self)
17
17
  end
18
18
 
19
19
  def query_certnames(query)
@@ -35,7 +35,7 @@ module Bolt
35
35
  begin
36
36
  config = JSON.parse(File.read(filepath)) if filepath
37
37
  rescue StandardError => e
38
- Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
38
+ Bolt::Logger.logger(self).error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
39
39
  end
40
40
 
41
41
  config = config.fetch('puppetdb', {})
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/puppetfile/module'
5
+
6
+ # This class manages the logical contents of a Puppetfile. It includes methods
7
+ # for parsing a Puppetfile and its modules, resolving module dependencies,
8
+ # and writing a Puppetfile.
9
+ #
10
+ module Bolt
11
+ class Puppetfile
12
+ attr_reader :modules
13
+
14
+ def initialize(modules = [])
15
+ @modules = Set.new
16
+ add_modules(modules)
17
+ end
18
+
19
+ # Loads a Puppetfile and parses its module specifications, returning a
20
+ # Bolt::Puppetfile object with the modules set.
21
+ #
22
+ def self.parse(path)
23
+ require 'puppetfile-resolver'
24
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval'
25
+
26
+ begin
27
+ parsed = ::PuppetfileResolver::Puppetfile::Parser::R10KEval.parse(File.read(path))
28
+ rescue StandardError => e
29
+ raise Bolt::Error.new(
30
+ "Unable to parse Puppetfile #{path}: #{e.message}",
31
+ 'bolt/puppetfile-parsing'
32
+ )
33
+ end
34
+
35
+ unless parsed.valid?
36
+ raise Bolt::ValidationError,
37
+ "Unable to parse Puppetfile #{path}"
38
+ end
39
+
40
+ modules = parsed.modules.map do |mod|
41
+ Bolt::Puppetfile::Module.new(mod.owner, mod.name, mod.version)
42
+ end
43
+
44
+ new(modules)
45
+ end
46
+
47
+ # Writes a Puppetfile that includes specifications for each of the
48
+ # modules.
49
+ #
50
+ def write(path, force: false)
51
+ if File.exist?(path) && !force
52
+ raise Bolt::FileError.new(
53
+ "Cannot overwrite existing Puppetfile at #{path}. To forcibly overwrite, "\
54
+ "run with the '--force' option.",
55
+ path
56
+ )
57
+ end
58
+
59
+ File.open(path, 'w') do |file|
60
+ file.puts '# This Puppetfile is managed by Bolt. Do not edit.'
61
+ modules.each { |mod| file.puts mod.to_spec }
62
+ file.puts
63
+ end
64
+ rescue SystemCallError => e
65
+ raise Bolt::FileError.new(
66
+ "#{e.message}: unable to write Puppetfile.",
67
+ path
68
+ )
69
+ end
70
+
71
+ # Resolves module dependencies using the puppetfile-resolver library. The
72
+ # resolver will return a document model including all module dependencies
73
+ # and the latest version that can be installed for each. The document model
74
+ # is parsed and turned into a Set of Bolt::Puppetfile::Module objects.
75
+ #
76
+ def resolve
77
+ require 'puppetfile-resolver'
78
+
79
+ # Build the document model from the modules.
80
+ model = PuppetfileResolver::Puppetfile::Document.new('')
81
+
82
+ @modules.each do |mod|
83
+ model.add_module(
84
+ PuppetfileResolver::Puppetfile::ForgeModule.new(mod.title).tap do |tap|
85
+ tap.version = :latest
86
+ end
87
+ )
88
+ end
89
+
90
+ # Make sure the Puppetfile model is valid.
91
+ unless model.valid?
92
+ raise Bolt::ValidationError,
93
+ "Unable to resolve dependencies for modules: #{@modules.map(&:title).join(', ')}"
94
+ end
95
+
96
+ # Create the resolver using the Puppetfile model. nil disables Puppet
97
+ # version restrictions.
98
+ resolver = PuppetfileResolver::Resolver.new(model, nil)
99
+
100
+ # Configure and resolve the dependency graph, catching any errors
101
+ # raised by puppetfile-resolver and re-raising them as Bolt errors.
102
+ begin
103
+ result = resolver.resolve(
104
+ cache: nil,
105
+ ui: nil,
106
+ module_paths: [],
107
+ allow_missing_modules: true
108
+ )
109
+ rescue StandardError => e
110
+ raise Bolt::Error.new(e.message, 'bolt/puppetfile-resolver-error')
111
+ end
112
+
113
+ # Validate that the modules exist.
114
+ missing_graph = result.specifications.select do |_name, spec|
115
+ spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
116
+ end
117
+
118
+ if missing_graph.any?
119
+ titles = model.modules.each_with_object({}) do |mod, acc|
120
+ acc[mod.name] = mod.title
121
+ end
122
+
123
+ names = titles.values_at(*missing_graph.keys)
124
+ plural = names.count == 1 ? '' : 's'
125
+
126
+ raise Bolt::Error.new(
127
+ "Unknown module name#{plural} #{names.join(', ')}",
128
+ 'bolt/unknown-modules'
129
+ )
130
+ end
131
+
132
+ # Filter the dependency graph to only include module specifications. This
133
+ # will only remove the Puppet version specification, which is not needed.
134
+ specs = result.specifications.select do |_name, spec|
135
+ spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
136
+ end
137
+
138
+ @modules = specs.map do |_name, spec|
139
+ Bolt::Puppetfile::Module.new(spec.owner, spec.name, spec.version.to_s)
140
+ end.to_set
141
+ end
142
+
143
+ # Adds to the set of modules.
144
+ #
145
+ def add_modules(modules)
146
+ modules.each do |mod|
147
+ case mod
148
+ when Bolt::Puppetfile::Module
149
+ @modules << mod
150
+ when Hash
151
+ @modules << Bolt::Puppetfile::Module.from_hash(mod)
152
+ else
153
+ raise Bolt::ValidationError, "Module must be a Bolt::Puppetfile::Module or Hash."
154
+ end
155
+ end
156
+
157
+ @modules
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'r10k/cli'
4
+ require 'bolt/r10k_log_proxy'
5
+ require 'bolt/error'
6
+
7
+ # This class is used to install modules from a Puppetfile to a module directory.
8
+ #
9
+ module Bolt
10
+ class Puppetfile
11
+ class Installer
12
+ def initialize(config = {})
13
+ @config = config
14
+ end
15
+
16
+ def install(path, moduledir)
17
+ unless File.exist?(path)
18
+ raise Bolt::FileError.new(
19
+ "Could not find a Puppetfile at #{path}",
20
+ path
21
+ )
22
+ end
23
+
24
+ r10k_opts = {
25
+ root: File.dirname(path),
26
+ puppetfile: path.to_s,
27
+ moduledir: moduledir.to_s
28
+ }
29
+
30
+ settings = R10K::Settings.global_settings.evaluate(@config)
31
+ R10K::Initializers::GlobalInitializer.new(settings).call
32
+ install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
33
+
34
+ # Override the r10k logger with a proxy to our own logger
35
+ R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
36
+
37
+ install_action.call
38
+ rescue R10K::Error => e
39
+ raise PuppetfileError, e
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class represents a module specification. It used by the Bolt::Puppetfile
6
+ # class to have a consistent API for accessing a module's attributes.
7
+ #
8
+ module Bolt
9
+ class Puppetfile
10
+ class Module
11
+ attr_reader :owner, :name, :version
12
+
13
+ def initialize(owner, name, version = nil)
14
+ @owner = owner
15
+ @name = name
16
+ @version = version
17
+ end
18
+
19
+ # Creates a new module from a hash.
20
+ #
21
+ def self.from_hash(mod)
22
+ unless mod['name'].is_a?(String)
23
+ raise Bolt::ValidationError,
24
+ "Module name must be a String, not #{mod['name'].inspect}"
25
+ end
26
+
27
+ owner, name = mod['name'].tr('/', '-').split('-', 2)
28
+
29
+ unless owner && name
30
+ raise Bolt::ValidationError, "Module name #{mod['name']} must include both the owner and module name."
31
+ end
32
+
33
+ new(owner, name)
34
+ end
35
+
36
+ # Returns the module's title.
37
+ #
38
+ def title
39
+ "#{@owner}-#{@name}"
40
+ end
41
+
42
+ # Checks two modules for equality.
43
+ #
44
+ def eql?(other)
45
+ self.class == other.class && @owner == other.owner && @name == other.name
46
+ end
47
+ alias == eql?
48
+
49
+ # Hashes the module.
50
+ #
51
+ def hash
52
+ [@owner, @name].hash
53
+ end
54
+
55
+ # Returns the Puppetfile specification for the module.
56
+ #
57
+ def to_spec
58
+ if @version
59
+ "mod #{title.inspect}, #{@version.inspect}"
60
+ else
61
+ "mod #{title.inspect}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -7,7 +7,7 @@ module Bolt
7
7
  def initialize
8
8
  super('bolt')
9
9
 
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
13
13
  def canonical_log(event)
@@ -8,7 +8,7 @@ module Bolt
8
8
  def initialize(path, save_failures)
9
9
  @path = path
10
10
  @save_failures = save_failures
11
- @logger = Logging.logger[self]
11
+ @logger = Bolt::Logger.logger(self)
12
12
  end
13
13
 
14
14
  def data
@@ -53,7 +53,7 @@ module Bolt
53
53
  end
54
54
  end
55
55
  rescue StandardError => e
56
- @logger.warn("Failed to save result to #{@path}: #{e.message}")
56
+ Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
57
57
  end
58
58
  end
59
59
  end
@@ -65,6 +65,25 @@ module Bolt
65
65
  'msg' => msg,
66
66
  'details' => { 'exit_code' => exit_code } }
67
67
  end
68
+
69
+ if value.key?('_error')
70
+ unless value['_error'].is_a?(Hash) && value['_error'].key?('msg')
71
+ value['_error'] = {
72
+ 'msg' => "Invalid error returned from task #{task}: #{value['_error'].inspect}. Error "\
73
+ "must be an object with a msg key.",
74
+ 'kind' => 'bolt/invalid-task-error',
75
+ 'details' => { 'original_error' => value['_error'] }
76
+ }
77
+ end
78
+
79
+ value['_error']['kind'] ||= 'bolt/error'
80
+ value['_error']['details'] ||= {}
81
+ end
82
+
83
+ if value.key?('_sensitive')
84
+ value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value['_sensitive'])
85
+ end
86
+
68
87
  new(target, value: value, action: 'task', object: task)
69
88
  end
70
89
 
@@ -205,5 +224,9 @@ module Bolt
205
224
 
206
225
  end
207
226
  end
227
+
228
+ def sensitive
229
+ value['_sensitive']
230
+ end
208
231
  end
209
232
  end
@@ -7,7 +7,7 @@ module Bolt
7
7
  def initialize(target, conn)
8
8
  @target = target
9
9
  @conn = conn
10
- @logger = Logging.logger[@target.safe_name]
10
+ @logger = Bolt::Logger.logger(@target.safe_name)
11
11
  end
12
12
 
13
13
  def run_command(*_args)
@@ -103,7 +103,7 @@ module Bolt
103
103
  " using '#{execute_options[:interpreter]}' interpreter"
104
104
  end
105
105
  # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
106
- logger.debug("Running '#{executable}' with #{arguments.to_json}#{interpreter_debug}")
106
+ logger.trace("Running '#{executable}' with #{arguments.to_json}#{interpreter_debug}")
107
107
  # unpack any Sensitive data
108
108
  arguments = unwrap_sensitive_args(arguments)
109
109
 
@@ -203,13 +203,13 @@ module Bolt
203
203
 
204
204
  def handle_sudo_errors(err)
205
205
  if err =~ /^#{conn.user} is not in the sudoers file\./
206
- @logger.debug { err }
206
+ @logger.trace { err }
207
207
  raise Bolt::Node::EscalateError.new(
208
208
  "User #{conn.user} does not have sudo permission on #{target}",
209
209
  'SUDO_DENIED'
210
210
  )
211
211
  elsif err =~ /^Sorry, try again\./
212
- @logger.debug { err }
212
+ @logger.trace { err }
213
213
  raise Bolt::Node::EscalateError.new(
214
214
  "Sudo password for user #{conn.user} not recognized on #{target}",
215
215
  'BAD_PASSWORD'
@@ -351,7 +351,7 @@ module Bolt
351
351
 
352
352
  command_str = [sudo_str, env_decl, command_str].compact.join(' ')
353
353
 
354
- @logger.debug { "Executing: #{command_str}" }
354
+ @logger.trace { "Executing `#{command_str}`" }
355
355
 
356
356
  in_buffer = if !use_sudo && options[:stdin]
357
357
  String.new(options[:stdin], encoding: 'binary')
@@ -431,16 +431,16 @@ module Bolt
431
431
  result_output.exit_code = t.value.respond_to?(:exitstatus) ? t.value.exitstatus : t.value
432
432
 
433
433
  if result_output.exit_code == 0
434
- @logger.debug { "Command returned successfully" }
434
+ @logger.trace { "Command `#{command_str}` returned successfully" }
435
435
  else
436
- @logger.info { "Command failed with exit code #{result_output.exit_code}" }
436
+ @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
437
437
  end
438
438
  result_output
439
439
  rescue StandardError
440
440
  # Ensure we close stdin and kill the child process
441
441
  inp&.close
442
442
  t&.terminate if t&.alive?
443
- @logger.debug { "Command aborted" }
443
+ @logger.trace { "Command aborted" }
444
444
  raise
445
445
  end
446
446
 
@@ -26,7 +26,7 @@ module Bolt
26
26
  @metadata = metadata
27
27
  @files = files
28
28
  @remote = remote
29
- @logger = Logging.logger[self]
29
+ @logger = Bolt::Logger.logger(self)
30
30
 
31
31
  validate_metadata
32
32
  end
@@ -40,7 +40,7 @@ module Bolt
40
40
  attr_reader :logger
41
41
 
42
42
  def initialize
43
- @logger = Logging.logger[self]
43
+ @logger = Bolt::Logger.logger(self)
44
44
  end
45
45
 
46
46
  def with_events(target, callback, action)
@@ -10,9 +10,9 @@ module Bolt
10
10
  def initialize(target)
11
11
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
12
12
  @target = target
13
- @logger = Logging.logger[target.safe_name]
13
+ @logger = Bolt::Logger.logger(target.safe_name)
14
14
  @docker_host = @target.options['service-url']
15
- @logger.debug("Initializing docker connection to #{@target.safe_name}")
15
+ @logger.trace("Initializing docker connection to #{@target.safe_name}")
16
16
  end
17
17
 
18
18
  def connect
@@ -25,7 +25,7 @@ module Bolt
25
25
  output = execute_local_docker_json_command('inspect', [output[index]["ID"]])
26
26
  # Store the container information for later
27
27
  @container_info = output[0]
28
- @logger.debug { "Opened session" }
28
+ @logger.trace { "Opened session" }
29
29
  true
30
30
  rescue StandardError => e
31
31
  raise Bolt::Node::ConnectError.new(
@@ -57,16 +57,16 @@ module Bolt
57
57
  command_options << container_id
58
58
  command_options.concat(command)
59
59
 
60
- @logger.debug { "Executing: exec #{command_options}" }
60
+ @logger.trace { "Executing: exec #{command_options}" }
61
61
 
62
62
  stdout_str, stderr_str, status = execute_local_docker_command('exec', command_options, options[:stdin])
63
63
 
64
64
  # The actual result is the exitstatus not the process object
65
65
  status = status.nil? ? -32768 : status.exitstatus
66
66
  if status == 0
67
- @logger.debug { "Command returned successfully" }
67
+ @logger.trace { "Command returned successfully" }
68
68
  else
69
- @logger.info { "Command failed with exit code #{status}" }
69
+ @logger.trace { "Command failed with exit code #{status}" }
70
70
  end
71
71
  stdout_str.force_encoding(Encoding::UTF_8)
72
72
  stderr_str.force_encoding(Encoding::UTF_8)
@@ -75,12 +75,12 @@ module Bolt
75
75
  stderr_str.gsub!("\r\n", "\n")
76
76
  [stdout_str, stderr_str, status]
77
77
  rescue StandardError
78
- @logger.debug { "Command aborted" }
78
+ @logger.trace { "Command aborted" }
79
79
  raise
80
80
  end
81
81
 
82
82
  def write_remote_file(source, destination)
83
- @logger.debug { "Uploading #{source}, to #{destination}" }
83
+ @logger.trace { "Uploading #{source} to #{destination}" }
84
84
  _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
85
85
  unless status.exitstatus.zero?
86
86
  raise "Error writing file to container #{@container_id}: #{stdout_str}"
@@ -90,7 +90,7 @@ module Bolt
90
90
  end
91
91
 
92
92
  def write_remote_directory(source, destination)
93
- @logger.debug { "Uploading #{source}, to #{destination}" }
93
+ @logger.trace { "Uploading #{source} to #{destination}" }
94
94
  _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
95
95
  unless status.exitstatus.zero?
96
96
  raise "Error writing directory to container #{@container_id}: #{stdout_str}"
@@ -100,7 +100,7 @@ module Bolt
100
100
  end
101
101
 
102
102
  def download_remote_content(source, destination)
103
- @logger.debug { "Downloading #{source} to #{destination}" }
103
+ @logger.trace { "Downloading #{source} to #{destination}" }
104
104
  # Create the destination directory, otherwise copying a source directory with Docker will
105
105
  # copy the *contents* of the directory.
106
106
  # https://docs.docker.com/engine/reference/commandline/cp/