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
@@ -20,6 +20,7 @@ require 'bolt/logger'
20
20
  require 'bolt/outputter'
21
21
  require 'bolt/puppetdb'
22
22
  require 'bolt/plugin'
23
+ require 'bolt/project_migrate'
23
24
  require 'bolt/pal'
24
25
  require 'bolt/target'
25
26
  require 'bolt/version'
@@ -38,13 +39,14 @@ module Bolt
38
39
  'inventory' => %w[show],
39
40
  'group' => %w[show],
40
41
  'project' => %w[init migrate],
41
- 'apply' => %w[] }.freeze
42
+ 'apply' => %w[],
43
+ 'guide' => %w[] }.freeze
42
44
 
43
45
  attr_reader :config, :options
44
46
 
45
47
  def initialize(argv)
46
48
  Bolt::Logger.initialize_logging
47
- @logger = Logging.logger[self]
49
+ @logger = Bolt::Logger.logger(self)
48
50
  @argv = argv
49
51
  @options = {}
50
52
  end
@@ -77,7 +79,7 @@ module Bolt
77
79
 
78
80
  # Wrapper method that is called by the Bolt executable. Parses the command and
79
81
  # then loads the project and config. Once config is loaded, it completes the
80
- # setup process by configuring Bolt and issuing warnings.
82
+ # setup process by configuring Bolt and logging messages.
81
83
  #
82
84
  # This separation is needed since the Bolt::Outputter class that normally handles
83
85
  # printing errors relies on config being loaded. All setup that happens before
@@ -165,20 +167,17 @@ module Bolt
165
167
  raise e
166
168
  end
167
169
 
168
- # Completes the setup process by configuring Bolt and issuing warnings
170
+ # Completes the setup process by configuring Bolt and log messages
169
171
  def finalize_setup
170
172
  Bolt::Logger.configure(config.log, config.color)
171
173
  Bolt::Logger.analytics = analytics
172
174
 
173
- # Logger must be configured before checking path case and project file, otherwise warnings will not display
175
+ # Logger must be configured before checking path case and project file, otherwise logs will not display
174
176
  config.check_path_case('modulepath', config.modulepath)
175
177
  config.project.check_deprecated_file
176
178
 
177
- # Log the file paths for loaded config files
178
- config_loaded
179
-
180
- # Display warnings created during parser and config initialization
181
- config.warnings.each { |warning| @logger.warn(warning[:msg]) }
179
+ # Log messages created during parser and config initialization
180
+ config.logs.each { |log| @logger.send(log.keys[0], log.values[0]) }
182
181
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
183
182
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
184
183
 
@@ -354,7 +353,7 @@ module Bolt
354
353
  # Initialize inventory and targets. Errors here are better to catch early.
355
354
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
356
355
  # options[:targets] will contain a resolved set of Target objects
357
- unless %w[project puppetfile secret].include?(options[:subcommand]) ||
356
+ unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
358
357
  %w[convert new show].include?(options[:action])
359
358
  update_targets(options)
360
359
  end
@@ -411,7 +410,7 @@ module Bolt
411
410
  list_modules
412
411
  return 0
413
412
  when 'convert'
414
- convert_plan(options[:object])
413
+ pal.convert_plan(options[:object])
415
414
  return 0
416
415
  end
417
416
 
@@ -422,12 +421,20 @@ module Bolt
422
421
  end
423
422
 
424
423
  case options[:subcommand]
424
+ when 'guide'
425
+ code = if options[:object]
426
+ show_guide(options[:object])
427
+ else
428
+ list_topics
429
+ end
425
430
  when 'project'
426
431
  case options[:action]
427
432
  when 'init'
428
433
  code = initialize_project
429
434
  when 'migrate'
430
- code = migrate_project
435
+ inv = config.inventoryfile
436
+ path = config.project.path
437
+ code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
431
438
  end
432
439
  when 'plan'
433
440
  case options[:action]
@@ -526,7 +533,7 @@ module Bolt
526
533
  tasks = pal.list_tasks
527
534
  tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
528
535
  tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
529
- outputter.print_tasks(tasks, pal.list_modulepath)
536
+ outputter.print_tasks(tasks, pal.user_modulepath)
530
537
  end
531
538
 
532
539
  def show_plan(plan_name)
@@ -537,7 +544,7 @@ module Bolt
537
544
  plans = pal.list_plans
538
545
  plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
539
546
  plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
540
- outputter.print_plans(plans, pal.list_modulepath)
547
+ outputter.print_plans(plans, pal.user_modulepath)
541
548
  end
542
549
 
543
550
  def list_targets
@@ -770,8 +777,26 @@ module Bolt
770
777
  # Initializes a specified directory as a Bolt project and installs any modules
771
778
  # specified by the user, along with their dependencies
772
779
  def initialize_project
773
- project = Pathname.new(File.expand_path(options[:object] || Dir.pwd))
774
- config = project + 'bolt.yaml'
780
+ # Dir.pwd will return backslashes on Windows, but Pathname always uses
781
+ # forward slashes to concatenate paths. This results in paths like
782
+ # C:\User\Administrator/modules, which fail module install. This ensure
783
+ # forward slashes in the cwd path.
784
+ dir = File.expand_path(Dir.pwd)
785
+ name = options[:object] || File.basename(dir)
786
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
787
+ if options[:object]
788
+ raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
789
+ "project name must begin with a lowercase letter and can include lowercase "\
790
+ "letters, numbers, and underscores."
791
+ else
792
+ raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
793
+ "project name. Please specify a name using 'bolt project init <name>'."
794
+ end
795
+ end
796
+
797
+ project = Pathname.new(dir)
798
+ old_config = project + 'bolt.yaml'
799
+ config = project + 'bolt-project.yaml'
775
800
  puppetfile = project + 'Puppetfile'
776
801
  modulepath = [project + 'modules']
777
802
 
@@ -792,18 +817,24 @@ module Bolt
792
817
 
793
818
  # Warn the user if the project directory already exists. We don't error here since users
794
819
  # might not have installed any modules yet.
820
+ # If both bolt.yaml and bolt-project.yaml exist, this will just warn
821
+ # about bolt-project.yaml and subsequent Bolt actions will warn about
822
+ # both files existing
795
823
  if config.exist?
796
- @logger.warn "Found existing project directory at #{project}"
797
- end
798
-
799
- # Create the project directory
800
- FileUtils.mkdir_p(project)
801
-
824
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
825
+ # This won't get called if bolt-project.yaml exists
826
+ elsif old_config.exist?
827
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
828
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
802
829
  # Bless the project directory as a...wait for it...project
803
- if FileUtils.touch(config)
804
- outputter.print_message "Successfully created Bolt project at #{project}"
805
830
  else
806
- raise Bolt::FileError.new("Could not create Bolt project directory at #{project}", nil)
831
+ begin
832
+ content = { 'name' => name }
833
+ File.write(config.to_path, content.to_yaml)
834
+ outputter.print_message "Successfully created Bolt project at #{project}"
835
+ rescue StandardError => e
836
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
837
+ end
807
838
  end
808
839
 
809
840
  # Write the generated Puppetfile to the fancy new project
@@ -879,49 +910,6 @@ module Bolt
879
910
  end
880
911
  end
881
912
 
882
- def migrate_project
883
- inventory_file = config.inventoryfile || config.default_inventoryfile
884
- data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
885
-
886
- data.delete('version') if data['version'] != 2
887
-
888
- migrated = migrate_group(data)
889
-
890
- ok = File.write(inventory_file, data.to_yaml) if migrated
891
-
892
- result = if migrated && ok
893
- "Successfully migrated Bolt project to latest version."
894
- elsif !migrated
895
- "Bolt project already on latest version. Nothing to do."
896
- else
897
- "Could not migrate Bolt project to latest version."
898
- end
899
- outputter.print_message result
900
-
901
- ok ? 0 : 1
902
- end
903
-
904
- # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
905
- # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
906
- # modified in place.
907
- def migrate_group(group)
908
- migrated = false
909
- if group.key?('nodes')
910
- migrated = true
911
- targets = group['nodes'].map do |target|
912
- target['uri'] = target.delete('name') if target.is_a?(Hash)
913
- target
914
- end
915
- group.delete('nodes')
916
- group['targets'] = targets
917
- end
918
- (group['groups'] || []).each do |subgroup|
919
- migrated_group = migrate_group(subgroup)
920
- migrated ||= migrated_group
921
- end
922
- migrated
923
- end
924
-
925
913
  def install_puppetfile(config, puppetfile, modulepath)
926
914
  require 'r10k/cli'
927
915
  require 'bolt/r10k_log_proxy'
@@ -964,8 +952,46 @@ module Bolt
964
952
  config.project)
965
953
  end
966
954
 
967
- def convert_plan(plan)
968
- pal.convert_plan(plan)
955
+ # Collects the list of Bolt guides and maps them to their topics.
956
+ def guides
957
+ @guides ||= begin
958
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
959
+ files = Dir.children(root_path).sort
960
+
961
+ files.each_with_object({}) do |file, guides|
962
+ next if file !~ /\.txt\z/
963
+ topic = File.basename(file, '.txt')
964
+ guides[topic] = File.join(root_path, file)
965
+ end
966
+ rescue SystemCallError => e
967
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
968
+ end
969
+ end
970
+
971
+ # Display the list of available Bolt guides.
972
+ def list_topics
973
+ outputter.print_topics(guides.keys)
974
+ 0
975
+ end
976
+
977
+ # Display a specific Bolt guide.
978
+ def show_guide(topic)
979
+ if guides[topic]
980
+ analytics.event('Guide', 'known_topic', label: topic)
981
+
982
+ begin
983
+ guide = File.read(guides[topic])
984
+ rescue SystemCallError => e
985
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
986
+ end
987
+
988
+ outputter.print_guide(guide, topic)
989
+ else
990
+ analytics.event('Guide', 'unknown_topic', label: topic)
991
+ outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
992
+ list_topics
993
+ end
994
+ 0
969
995
  end
970
996
 
971
997
  def validate_file(type, path, allow_dir = false)
@@ -1028,13 +1054,6 @@ module Bolt
1028
1054
  content
1029
1055
  end
1030
1056
 
1031
- def config_loaded
1032
- msg = <<~MSG.chomp
1033
- Loaded configuration from: '#{config.config_files.join("', '")}'
1034
- MSG
1035
- @logger.debug(msg)
1036
- end
1037
-
1038
1057
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1039
1058
  # package installs include modules listed in the Bolt repo Puppetfile
1040
1059
  def incomplete_install?
@@ -19,7 +19,7 @@ module Bolt
19
19
  class Config
20
20
  include Bolt::Config::Options
21
21
 
22
- attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency, :deprecations
22
+ attr_reader :config_files, :logs, :data, :transports, :project, :modified_concurrency, :deprecations
23
23
 
24
24
  BOLT_CONFIG_NAME = 'bolt.yaml'
25
25
  BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
@@ -32,16 +32,19 @@ module Bolt
32
32
  end
33
33
 
34
34
  def self.from_project(project, overrides = {})
35
+ logs = []
35
36
  conf = if project.project_file == project.config_file
36
37
  project.data
37
38
  else
38
- Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
39
+ c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
40
+ logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
41
+ c
39
42
  end
40
43
 
41
44
  data = load_defaults(project).push(
42
45
  filepath: project.config_file,
43
46
  data: conf,
44
- warnings: [],
47
+ logs: logs,
45
48
  deprecations: []
46
49
  )
47
50
 
@@ -50,17 +53,20 @@ module Bolt
50
53
 
51
54
  def self.from_file(configfile, overrides = {})
52
55
  project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
+ logs = []
53
57
 
54
58
  conf = if project.project_file == project.config_file
55
59
  project.data
56
60
  else
57
- Bolt::Util.read_yaml_hash(configfile, 'config')
61
+ c = Bolt::Util.read_yaml_hash(configfile, 'config')
62
+ logs << { debug: "Loaded configuration from #{configfile}" }
63
+ c
58
64
  end
59
65
 
60
66
  data = load_defaults(project).push(
61
67
  filepath: project.config_file,
62
68
  data: conf,
63
- warnings: [],
69
+ logs: logs,
64
70
  deprecations: []
65
71
  )
66
72
 
@@ -90,13 +96,13 @@ module Bolt
90
96
  def self.load_bolt_defaults_yaml(dir)
91
97
  filepath = dir + BOLT_DEFAULTS_NAME
92
98
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
93
- warnings = []
99
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
94
100
 
95
101
  # Warn if 'bolt.yaml' detected in same directory.
96
102
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
97
- warnings.push(
98
- msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
99
- "will be ignored."
103
+ logs.push(
104
+ warn: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
105
+ "will be ignored."
100
106
  )
101
107
  end
102
108
 
@@ -105,9 +111,9 @@ module Bolt
105
111
 
106
112
  if project_config.any?
107
113
  data.reject! { |key, _| project_config.include?(key) }
108
- warnings.push(
109
- msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
110
- "Project configuration should be set in 'bolt-project.yaml'."
114
+ logs.push(
115
+ warn: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
116
+ "Project configuration should be set in 'bolt-project.yaml'."
111
117
  )
112
118
  end
113
119
 
@@ -116,10 +122,10 @@ module Bolt
116
122
 
117
123
  if transport_config.any?
118
124
  data.reject! { |key, _| transport_config.include?(key) }
119
- warnings.push(
120
- msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
121
- "Transport configuration should be set under the 'inventory-config' option or "\
122
- "in 'inventory.yaml'."
125
+ logs.push(
126
+ warn: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
127
+ "Transport configuration should be set under the 'inventory-config' option or "\
128
+ "in 'inventory.yaml'."
123
129
  )
124
130
  end
125
131
 
@@ -142,7 +148,7 @@ module Bolt
142
148
  data = data.merge(data.delete('inventory-config'))
143
149
  end
144
150
 
145
- { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
151
+ { filepath: filepath, data: data, logs: logs, deprecations: [] }
146
152
  end
147
153
 
148
154
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -150,11 +156,12 @@ module Bolt
150
156
  def self.load_bolt_yaml(dir)
151
157
  filepath = dir + BOLT_CONFIG_NAME
152
158
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
159
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
153
160
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
154
161
  msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
155
162
  "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
156
163
 
157
- { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
164
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
158
165
  end
159
166
 
160
167
  def self.load_defaults(project)
@@ -187,13 +194,13 @@ module Bolt
187
194
  unless config_data.is_a?(Array)
188
195
  config_data = [{ filepath: project.config_file,
189
196
  data: config_data,
190
- warnings: [],
197
+ logs: [],
191
198
  deprecations: [] }]
192
199
  end
193
200
 
194
- @logger = Logging.logger[self]
201
+ @logger = Bolt::Logger.logger(self)
195
202
  @project = project
196
- @warnings = @project.warnings.dup
203
+ @logs = @project.logs.dup
197
204
  @deprecations = @project.deprecations.dup
198
205
  @transports = {}
199
206
  @config_files = []
@@ -213,8 +220,15 @@ module Bolt
213
220
  'transport' => 'ssh'
214
221
  }
215
222
 
223
+ if project.path.directory?
224
+ default_data['log']['bolt-debug.log'] = {
225
+ 'level' => 'debug',
226
+ 'append' => false
227
+ }
228
+ end
229
+
216
230
  loaded_data = config_data.each_with_object([]) do |data, acc|
217
- @warnings.concat(data[:warnings]) if data[:warnings].any?
231
+ @logs.concat(data[:logs]) if data[:logs].any?
218
232
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
219
233
 
220
234
  if data[:data].any?
@@ -331,9 +345,17 @@ module Bolt
331
345
 
332
346
  private def update_logs(logs)
333
347
  logs.each_with_object({}) do |(key, val), acc|
334
- next unless val.is_a?(Hash)
348
+ # Remove any disabled logs
349
+ next if val == 'disable'
335
350
 
336
351
  name = normalize_log(key)
352
+
353
+ # But otherwise it has to be a Hash
354
+ unless val.is_a?(Hash)
355
+ raise Bolt::ValidationError,
356
+ "config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
357
+ end
358
+
337
359
  acc[name] = val.slice('append', 'level')
338
360
  .transform_keys(&:to_sym)
339
361
 
@@ -358,7 +380,7 @@ module Bolt
358
380
  def validate
359
381
  if @data['future']
360
382
  msg = "Configuration option 'future' no longer exposes future behavior."
361
- @warnings << { option: 'future', msg: msg }
383
+ @logs << { warn: msg }
362
384
  end
363
385
 
364
386
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]