bolt 2.22.0 → 2.26.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 (67) 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/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  8. data/exe/bolt +1 -0
  9. data/guides/inventory.txt +19 -0
  10. data/guides/project.txt +22 -0
  11. data/lib/bolt/analytics.rb +8 -8
  12. data/lib/bolt/applicator.rb +6 -6
  13. data/lib/bolt/bolt_option_parser.rb +47 -24
  14. data/lib/bolt/catalog.rb +4 -2
  15. data/lib/bolt/cli.rb +97 -78
  16. data/lib/bolt/config.rb +46 -24
  17. data/lib/bolt/config/options.rb +9 -6
  18. data/lib/bolt/executor.rb +10 -8
  19. data/lib/bolt/inventory.rb +8 -1
  20. data/lib/bolt/inventory/group.rb +4 -4
  21. data/lib/bolt/inventory/inventory.rb +1 -1
  22. data/lib/bolt/inventory/target.rb +1 -1
  23. data/lib/bolt/logger.rb +12 -6
  24. data/lib/bolt/outputter.rb +56 -0
  25. data/lib/bolt/outputter/human.rb +10 -9
  26. data/lib/bolt/outputter/json.rb +11 -4
  27. data/lib/bolt/outputter/logger.rb +3 -3
  28. data/lib/bolt/outputter/rainbow.rb +15 -0
  29. data/lib/bolt/pal.rb +9 -19
  30. data/lib/bolt/pal/yaml_plan/evaluator.rb +2 -2
  31. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  32. data/lib/bolt/plugin/prompt.rb +3 -3
  33. data/lib/bolt/plugin/puppetdb.rb +1 -1
  34. data/lib/bolt/project.rb +32 -19
  35. data/lib/bolt/project_migrate.rb +138 -0
  36. data/lib/bolt/puppetdb/client.rb +1 -1
  37. data/lib/bolt/puppetdb/config.rb +1 -1
  38. data/lib/bolt/r10k_log_proxy.rb +1 -1
  39. data/lib/bolt/rerun.rb +1 -1
  40. data/lib/bolt/result.rb +8 -0
  41. data/lib/bolt/shell.rb +1 -1
  42. data/lib/bolt/shell/bash.rb +7 -7
  43. data/lib/bolt/task.rb +1 -1
  44. data/lib/bolt/transport/base.rb +1 -1
  45. data/lib/bolt/transport/docker/connection.rb +10 -10
  46. data/lib/bolt/transport/local/connection.rb +3 -3
  47. data/lib/bolt/transport/orch.rb +3 -3
  48. data/lib/bolt/transport/ssh.rb +1 -1
  49. data/lib/bolt/transport/ssh/connection.rb +6 -6
  50. data/lib/bolt/transport/ssh/exec_connection.rb +5 -5
  51. data/lib/bolt/transport/winrm.rb +1 -1
  52. data/lib/bolt/transport/winrm/connection.rb +9 -9
  53. data/lib/bolt/util.rb +2 -2
  54. data/lib/bolt/util/puppet_log_level.rb +4 -3
  55. data/lib/bolt/version.rb +1 -1
  56. data/lib/bolt_server/base_config.rb +1 -1
  57. data/lib/bolt_server/file_cache.rb +1 -1
  58. data/lib/bolt_server/pe/pal.rb +1 -1
  59. data/lib/bolt_server/transport_app.rb +76 -0
  60. data/lib/bolt_spec/plans.rb +1 -1
  61. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  62. data/lib/bolt_spec/run.rb +3 -0
  63. data/libexec/apply_catalog.rb +2 -2
  64. data/libexec/bolt_catalog +1 -1
  65. data/libexec/custom_facts.rb +1 -1
  66. data/libexec/query_resources.rb +1 -1
  67. metadata +9 -12
@@ -7,7 +7,7 @@ module Bolt
7
7
  class YamlPlan
8
8
  class Evaluator
9
9
  def initialize(analytics = Bolt::Analytics::NoopClient.new)
10
- @logger = Logging.logger[self]
10
+ @logger = Bolt::Logger.logger(self)
11
11
  @analytics = analytics
12
12
  @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
13
  end
@@ -109,7 +109,7 @@ module Bolt
109
109
  end
110
110
 
111
111
  def message_step(scope, step)
112
- scope.call_function('out::message', step['message'])
112
+ scope.call_function('out::message', [step['message']])
113
113
  end
114
114
 
115
115
  def generate_manifest(resources)
@@ -21,10 +21,18 @@ module Bolt
21
21
  validate_path
22
22
 
23
23
  plan_object = parse_plan
24
+ param_descriptions = plan_object.parameters.map do |param|
25
+ str = String.new("# @param #{param.name}")
26
+ str << " #{param.description}" if param.description
27
+ str
28
+ end.join("\n")
24
29
 
25
- plan_string = String.new("# WARNING: This is an autogenerated plan. " \
26
- "It may not behave as expected.\n" \
27
- "plan #{plan_object.name}(")
30
+ plan_string = String.new('')
31
+ plan_string << "# #{plan_object.description}\n" if plan_object.description
32
+ plan_string << "# WARNING: This is an autogenerated plan. It may not behave as expected.\n"
33
+ plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
34
+
35
+ plan_string << "plan #{plan_object.name}("
28
36
  # Parameters are Bolt::PAL::YamlPlan::Parameter
29
37
  plan_object.parameters&.each_with_index do |param, i|
30
38
  plan_string << param.transpile
@@ -18,9 +18,9 @@ module Bolt
18
18
  end
19
19
 
20
20
  def resolve_reference(opts)
21
- STDERR.print("#{opts['message']}: ")
22
- value = STDIN.noecho(&:gets).to_s.chomp
23
- STDERR.puts
21
+ $stderr.print("#{opts['message']}: ")
22
+ value = $stdin.noecho(&:gets).to_s.chomp
23
+ $stderr.puts
24
24
 
25
25
  value
26
26
  end
@@ -19,7 +19,7 @@ module Bolt
19
19
  def initialize(config:, context:)
20
20
  pdb_config = Bolt::PuppetDB::Config.load_config(config, context.boltdir)
21
21
  @puppetdb_client = Bolt::PuppetDB::Client.new(pdb_config)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  end
24
24
 
25
25
  def name
@@ -17,35 +17,37 @@ module Bolt
17
17
  }.freeze
18
18
 
19
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
20
- :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
+ :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
21
21
  :deprecations, :downloads, :plans_path
22
22
 
23
- def self.default_project
24
- create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
23
+ def self.default_project(logs = [])
24
+ create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
25
25
  # If homedir isn't defined use the system config path
26
26
  rescue ArgumentError
27
- create_project(Bolt::Config.system_path, 'system')
27
+ create_project(Bolt::Config.system_path, 'system', logs)
28
28
  end
29
29
 
30
30
  # Search recursively up the directory hierarchy for the Project. Look for a
31
31
  # directory called Boltdir or a file called bolt.yaml (for a control repo
32
32
  # type Project). Otherwise, repeat the check on each directory up the
33
33
  # hierarchy, falling back to the default if we reach the root.
34
- def self.find_boltdir(dir)
34
+ def self.find_boltdir(dir, logs = [])
35
35
  dir = Pathname.new(dir)
36
36
 
37
37
  if (dir + BOLTDIR_NAME).directory?
38
- create_project(dir + BOLTDIR_NAME, 'embedded')
38
+ create_project(dir + BOLTDIR_NAME, 'embedded', logs)
39
39
  elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
40
- create_project(dir, 'local')
40
+ create_project(dir, 'local', logs)
41
41
  elsif dir.root?
42
- default_project
42
+ default_project(logs)
43
43
  else
44
- find_boltdir(dir.parent)
44
+ logs << { debug: "Did not detect Boltdir, bolt.yaml, or bolt-project.yaml at '#{dir}'. "\
45
+ "This directory won't be loaded as a project." }
46
+ find_boltdir(dir.parent, logs)
45
47
  end
46
48
  end
47
49
 
48
- def self.create_project(path, type = 'option')
50
+ def self.create_project(path, type = 'option', logs = [])
49
51
  fullpath = Pathname.new(path).expand_path
50
52
 
51
53
  if !Bolt::Util.windows? && type != 'environment' && fullpath.world_writable?
@@ -58,15 +60,18 @@ module Bolt
58
60
 
59
61
  project_file = File.join(fullpath, 'bolt-project.yaml')
60
62
  data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
61
- new(data, path, type)
63
+ default = type =~ /user|system/ ? 'default ' : ''
64
+ exist = File.exist?(File.expand_path(project_file))
65
+ logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
66
+ new(data, path, type, logs)
62
67
  end
63
68
 
64
- def initialize(raw_data, path, type = 'option')
69
+ def initialize(raw_data, path, type = 'option', logs = [])
65
70
  @path = Pathname.new(path).expand_path
66
71
 
67
72
  @project_file = @path + 'bolt-project.yaml'
68
73
 
69
- @warnings = []
74
+ @logs = logs
70
75
  @deprecations = []
71
76
  if (@path + 'bolt.yaml').file? && project_file?
72
77
  msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
@@ -88,7 +93,7 @@ module Bolt
88
93
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
89
94
  if tc.any?
90
95
  msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
91
- @warnings << { msg: msg }
96
+ @logs << { warn: msg }
92
97
  end
93
98
 
94
99
  @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
@@ -98,7 +103,7 @@ module Bolt
98
103
  @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
99
104
  if (@path + 'bolt.yaml').file?
100
105
  msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
101
- @warnings << { msg: msg }
106
+ @logs << { warn: msg }
102
107
  end
103
108
  @project_file
104
109
  else
@@ -114,7 +119,9 @@ module Bolt
114
119
  # This API is used to prepend the project as a module to Puppet's internal
115
120
  # module_references list. CHANGE AT YOUR OWN RISK
116
121
  def to_h
117
- { path: @path.to_s, name: name }
122
+ { path: @path.to_s,
123
+ name: name,
124
+ load_as_module?: load_as_module? }
118
125
  end
119
126
 
120
127
  def eql?(other)
@@ -126,6 +133,10 @@ module Bolt
126
133
  @project_file.file?
127
134
  end
128
135
 
136
+ def load_as_module?
137
+ !name.nil?
138
+ end
139
+
129
140
  def name
130
141
  @data['name']
131
142
  end
@@ -141,15 +152,17 @@ module Bolt
141
152
  def validate
142
153
  if name
143
154
  if name !~ Bolt::Module::MODULE_NAME_REGEX
144
- raise Bolt::ValidationError, "Invalid project name #{name.inspect.tr('"', "'")} in bolt-project.yaml; "\
145
- "project name must match #{Bolt::Module::MODULE_NAME_REGEX.inspect}"
155
+ raise Bolt::ValidationError, <<~ERROR_STRING
156
+ Invalid project name '#{name}' in bolt-project.yaml; project name must begin with a lowercase letter
157
+ and can include lowercase letters, numbers, and underscores.
158
+ ERROR_STRING
146
159
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
147
160
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
148
161
  "with a built-in Bolt module of the same name."
149
162
  end
150
163
  else
151
164
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
152
- @warnings << { msg: message }
165
+ @logs << { warn: message }
153
166
  end
154
167
 
155
168
  %w[tasks plans].each do |conf|
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class ProjectMigrate
5
+ attr_reader :path, :project_file, :backup_dir, :outputter, :inventory_file, :config_file
6
+
7
+ # This init mostly makes testing easier
8
+ def initialize(path, outputter, configured_inventory = nil)
9
+ @path = Pathname.new(path).expand_path
10
+ @project_file = @path + 'bolt-project.yaml'
11
+ @config_file = @path + 'bolt.yaml'
12
+ @backup_dir = @path + '.bolt-bak'
13
+ @inventory_file = configured_inventory || @path + 'inventory.yaml'
14
+ @outputter = outputter
15
+ end
16
+
17
+ def migrate_project
18
+ inv_ok = inventory_1_to_2(inventory_file, outputter) if inventory_file.file?
19
+ config_ok = bolt_yaml_to_bolt_project(inventory_file, outputter)
20
+ inv_ok && config_ok ? 0 : 1
21
+ end
22
+
23
+ # This could be made public and used elsewhere if the need arises
24
+ private def backup_file(origin_path)
25
+ unless File.exist?(origin_path)
26
+ outputter.print_message "Could not find file #{origin_path}, skipping backup."
27
+ return
28
+ end
29
+
30
+ date = Time.new.strftime("%Y%m%d_%H%M%S%L")
31
+ FileUtils.mkdir_p(backup_dir)
32
+
33
+ filename = File.basename(origin_path)
34
+ backup_path = File.join(backup_dir, "#{filename}.#{date}.bak")
35
+
36
+ outputter.print_message "Backing up #{filename} from #{origin_path} to #{backup_path}"
37
+
38
+ begin
39
+ FileUtils.cp(origin_path, backup_path)
40
+ rescue StandardError => e
41
+ raise Bolt::FileError.new("#{e.message}; unable to create backup of #{filename}.", origin_path)
42
+ end
43
+ end
44
+
45
+ private def bolt_yaml_to_bolt_project(inventory_file, outputter)
46
+ # If bolt-project.yaml already exists
47
+ if project_file.file?
48
+ outputter.print_message "bolt-project.yaml already exists in Bolt "\
49
+ "project at #{path}. Skipping project file update."
50
+
51
+ # If bolt.yaml doesn't exist
52
+ elsif !config_file.file?
53
+ outputter.print_message "Could not find bolt.yaml in project at "\
54
+ "#{path}. Skipping project file update."
55
+
56
+ else
57
+ config_data = Bolt::Util.read_optional_yaml_hash(config_file, 'config')
58
+ transport_data, project_data = config_data.partition do |k, _|
59
+ Bolt::Config::INVENTORY_OPTIONS.keys.include?(k)
60
+ end.map(&:to_h)
61
+
62
+ if transport_data.any?
63
+ if File.exist?(inventory_file)
64
+ inventory_data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
65
+ merged = Bolt::Util.deep_merge(transport_data, inventory_data['config'] || {})
66
+ inventory_data['config'] = merged
67
+ backup_file(inventory_file)
68
+ else
69
+ FileUtils.touch(inventory_file)
70
+ inventory_data = { 'config' => transport_data }
71
+ end
72
+
73
+ backup_file(config_file)
74
+
75
+ begin
76
+ outputter.print_message "Moving transportation configuration options "\
77
+ "'#{transport_data.keys.join(', ')}' from bolt.yaml to inventory.yaml"
78
+ File.write(inventory_file, inventory_data.to_yaml)
79
+ File.write(config_file, project_data.to_yaml)
80
+ rescue StandardError => e
81
+ raise Bolt::FileError.new("#{e.message}; unable to write inventory.", inventory_file)
82
+ end
83
+ end
84
+
85
+ outputter.print_message "Renaming bolt.yaml to bolt-project.yaml"
86
+ FileUtils.mv(config_file, project_file)
87
+ outputter.print_message "Successfully updated project. Please add a "\
88
+ "'name' key to bolt-project.yaml to use project-level tasks and plans. "\
89
+ "Learn more about projects by running 'bolt guide project'."
90
+ # If nothing errored, this succeeded
91
+ true
92
+ end
93
+ end
94
+
95
+ private def inventory_1_to_2(inventory_file, outputter)
96
+ data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
97
+ data.delete('version') if data['version'] != 2
98
+ migrated = migrate_group(data)
99
+
100
+ ok = if migrated
101
+ backup_file(inventory_file)
102
+ File.write(inventory_file, data.to_yaml)
103
+ end
104
+
105
+ result = if migrated && ok
106
+ "Successfully migrated Bolt inventory to the latest version."
107
+ elsif !migrated
108
+ "Bolt inventory is already on the latest version. Skipping inventory update."
109
+ else
110
+ "Could not migrate Bolt inventory to the latest version. See "\
111
+ "https://puppet.com/docs/bolt/latest/inventory_file_v2.html to manually update."
112
+ end
113
+ outputter.print_message(result)
114
+ ok
115
+ end
116
+
117
+ # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
118
+ # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
119
+ # modified in place.
120
+ private def migrate_group(group)
121
+ migrated = false
122
+ if group.key?('nodes')
123
+ migrated = true
124
+ targets = group['nodes'].map do |target|
125
+ target['uri'] = target.delete('name') if target.is_a?(Hash)
126
+ target
127
+ end
128
+ group.delete('nodes')
129
+ group['targets'] = targets
130
+ end
131
+ (group['groups'] || []).each do |subgroup|
132
+ migrated_group = migrate_group(subgroup)
133
+ migrated ||= migrated_group
134
+ end
135
+ migrated
136
+ end
137
+ end
138
+ end
@@ -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', {})
@@ -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
@@ -65,6 +65,10 @@ module Bolt
65
65
  'msg' => msg,
66
66
  'details' => { 'exit_code' => exit_code } }
67
67
  end
68
+
69
+ if value.key?('_sensitive')
70
+ value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value['_sensitive'])
71
+ end
68
72
  new(target, value: value, action: 'task', object: task)
69
73
  end
70
74
 
@@ -205,5 +209,9 @@ module Bolt
205
209
 
206
210
  end
207
211
  end
212
+
213
+ def sensitive
214
+ value['_sensitive']
215
+ end
208
216
  end
209
217
  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