bolt 2.23.0 → 2.24.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f58ca99a18fca1c109d531c61b55bcee45d73c14e55a074575ffaaf1d9f40aac
4
- data.tar.gz: ea55e4c9b9ba4ccd8afed70a6c62c603a119a46f406b573f429bd2cd89983672
3
+ metadata.gz: 1e572a2c5aec9f127f48764b6a2fe903c91566c8aa0f3a1db33e40347bd1630b
4
+ data.tar.gz: 1445543fb941e0b8c12ec60900975a45e368a08e593ebe75c87ee946317f57b7
5
5
  SHA512:
6
- metadata.gz: dc8845ebe2a171ce79b704c474594a9679e7078feaca324eb7f26b88e9e903de43361a4dbaa357de3cf47be4ae72b4db4690a76e0910c9595f52aab384768fbc
7
- data.tar.gz: 60b30a31debf0d031902d8df21372b2524246ee98749eeccfd588c00b3458bcb9c0ed2a1fbdeab266cce86332537bde1005aead37fece867d440d88989c2486d
6
+ metadata.gz: 4cdee0a056e3c248a72ea3a4cb1872453611ca241d19ad3c98f5e0e89c550368ab19e34999e121f0e98c5e5cac353fed32eb174fb54fd149bd4558bd84e5706e
7
+ data.tar.gz: d505e8baf7c6ce5b21c20e27d7979c39c871e5bc69ea7bcf5d898ac94261850afea03028569ed60e192196d215837a2b339fef24231ea9491bc4901e93cba3dd
data/exe/bolt CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'bolt'
5
5
  require 'bolt/cli'
6
6
 
7
+ Thread.current[:name] ||= 'main'
7
8
  cli = Bolt::CLI.new(ARGV)
8
9
  begin
9
10
  opts = cli.parse
@@ -0,0 +1,19 @@
1
+ TOPIC
2
+ inventory
3
+
4
+ DESCRIPTION
5
+ The inventory describes the targets that you run Bolt commands on, along
6
+ with any data and configuration for the targets. Targets in an inventory can
7
+ belong to one or more groups, allowing you to share data and configuration
8
+ across multiple targets and to specify multiple targets for your Bolt
9
+ commands without the need to list each target individually.
10
+
11
+ In most cases, Bolt loads the inventory from an inventory file in your Bolt
12
+ project. The inventory file is a YAML file named 'inventory.yaml'. Because
13
+ Bolt loads the inventory file from a Bolt project, you must have an existing
14
+ project configuration file named 'bolt-project.yaml' alongside the inventory
15
+ file.
16
+
17
+ DOCUMENTATION
18
+ https://pup.pt/bolt-inventory
19
+ https://pup.pt/bolt-inventory-reference
@@ -0,0 +1,22 @@
1
+ TOPIC
2
+ project
3
+
4
+ DESCRIPTION
5
+ A Bolt project is a directory that serves as the launching point for Bolt
6
+ and allows you to create a shareable orchestration application. Projects
7
+ typically include a project configuration file, an inventory file, and any
8
+ content you use in your project workflow, such as tasks and plans.
9
+
10
+ When you run Bolt, it runs in the context of a project. If the directory you
11
+ run Bolt from is not a project, Bolt attempts to find a project by
12
+ traversing the parent directories. If Bolt is unable to find a project, it
13
+ runs from the default project, located at '~/.puppetlabs/bolt'.
14
+
15
+ A directory is only considered a Bolt project when it has a project
16
+ configuration file named 'bolt-project.yaml'. Bolt doesn't load project data
17
+ and content, including inventory files, unless the data and content are part
18
+ of a project.
19
+
20
+ DOCUMENTATION
21
+ https://pup.pt/bolt-projects
22
+ https://pup.pt/bolt-project-reference
@@ -161,9 +161,9 @@ module Bolt
161
161
  # Handle analytics submission in the background to avoid blocking the
162
162
  # app or polluting the log with errors
163
163
  Concurrent::Future.execute(executor: @executor) do
164
- @logger.debug "Submitting analytics: #{JSON.pretty_generate(params)}"
164
+ @logger.trace "Submitting analytics: #{JSON.pretty_generate(params)}"
165
165
  @http.post(TRACKING_URL, params)
166
- @logger.debug "Completed analytics submission"
166
+ @logger.trace "Completed analytics submission"
167
167
  end
168
168
  end
169
169
 
@@ -215,13 +215,13 @@ module Bolt
215
215
  end
216
216
 
217
217
  def screen_view(screen, **_kwargs)
218
- @logger.debug "Skipping submission of '#{screen}' screenview because analytics is disabled"
218
+ @logger.trace "Skipping submission of '#{screen}' screenview because analytics is disabled"
219
219
  end
220
220
 
221
221
  def report_bundled_content(mode, name); end
222
222
 
223
223
  def event(category, action, **_kwargs)
224
- @logger.debug "Skipping submission of '#{category} #{action}' event because analytics is disabled"
224
+ @logger.trace "Skipping submission of '#{category} #{action}' event because analytics is disabled"
225
225
  end
226
226
 
227
227
  def finish; end
@@ -27,7 +27,7 @@ module Bolt
27
27
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
28
28
  @apply_settings = apply_settings || {}
29
29
 
30
- @pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
30
+ @pool = Concurrent::ThreadPoolExecutor.new(name: 'apply', max_threads: max_compiles)
31
31
  @logger = Logging.logger[self]
32
32
  end
33
33
 
@@ -217,6 +217,7 @@ module Bolt
217
217
  r = @executor.log_action(description, targets) do
218
218
  futures = targets.map do |target|
219
219
  Concurrent::Future.execute(executor: @pool) do
220
+ Thread.current[:name] ||= Thread.current.name
220
221
  @executor.with_node_logging("Compiling manifest block", [target]) do
221
222
  compile(target, scope)
222
223
  end
@@ -300,7 +301,7 @@ module Bolt
300
301
 
301
302
  files.each do |file|
302
303
  tar_path = Pathname.new(file).relative_path_from(parent)
303
- @logger.debug("Packing plugin #{file} to #{tar_path}")
304
+ @logger.trace("Packing plugin #{file} to #{tar_path}")
304
305
  stat = File.stat(file)
305
306
  content = File.binread(file)
306
307
  output.tar.add_file_simple(
@@ -314,7 +315,7 @@ module Bolt
314
315
  end
315
316
 
316
317
  duration = Time.now - start_time
317
- @logger.debug("Packed plugins in #{duration * 1000} ms")
318
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
318
319
 
319
320
  output.close
320
321
  Base64.encode64(sio.string)
@@ -61,6 +61,9 @@ module Bolt
61
61
  { flags: OPTIONS[:global],
62
62
  banner: GROUP_HELP }
63
63
  end
64
+ when 'guide'
65
+ { flags: OPTIONS[:global] + %w[format],
66
+ banner: GUIDE_HELP }
64
67
  when 'plan'
65
68
  case action
66
69
  when 'convert'
@@ -85,7 +88,7 @@ module Bolt
85
88
  { flags: OPTIONS[:global] + %w[modules],
86
89
  banner: PROJECT_INIT_HELP }
87
90
  when 'migrate'
88
- { flags: OPTIONS[:global] + %w[inventoryfile boltdir configfile],
91
+ { flags: OPTIONS[:global] + %w[inventoryfile project configfile],
89
92
  banner: PROJECT_MIGRATE_HELP }
90
93
  else
91
94
  { flags: OPTIONS[:global],
@@ -164,6 +167,7 @@ module Bolt
164
167
  command Run a command remotely
165
168
  file Copy files between the controller and targets
166
169
  group Show the list of groups in the inventory
170
+ guide View guides for Bolt concepts and features
167
171
  inventory Show the list of targets an action would run on
168
172
  plan Convert, create, show, and run Bolt plans
169
173
  project Create and migrate Bolt projects
@@ -171,6 +175,9 @@ module Bolt
171
175
  script Upload a local script and run it remotely
172
176
  secret Create encryption keys and encrypt and decrypt values
173
177
  task Show and run Bolt tasks
178
+
179
+ GUIDES
180
+ For a list of guides on Bolt's concepts and features, run 'bolt guide'.
174
181
  HELP
175
182
 
176
183
  APPLY_HELP = <<~HELP
@@ -289,6 +296,26 @@ module Bolt
289
296
  Show the list of groups in the inventory.
290
297
  HELP
291
298
 
299
+ GUIDE_HELP = <<~HELP
300
+ NAME
301
+ guide
302
+
303
+ USAGE
304
+ bolt guide [topic] [options]
305
+
306
+ DESCRIPTION
307
+ View guides for Bolt's concepts and features.
308
+
309
+ Omitting a topic will display a list of available guides,
310
+ while providing a topic will display the relevant guide.
311
+
312
+ EXAMPLES
313
+ View a list of available guides
314
+ bolt guide
315
+ View the 'project' guide page
316
+ bolt guide project
317
+ HELP
318
+
292
319
  INVENTORY_HELP = <<~HELP
293
320
  NAME
294
321
  inventory
@@ -444,10 +471,7 @@ module Bolt
444
471
  bolt project migrate [options]
445
472
 
446
473
  DESCRIPTION
447
- Migrate a Bolt project to the latest version.
448
-
449
- Loads a Bolt project's inventory file and migrates it to the latest version. The
450
- inventory file is modified in place and will not preserve comments or formatting.
474
+ Migrate a Bolt project to use current best practices and the latest version of configuration files.
451
475
  HELP
452
476
 
453
477
  PUPPETFILE_HELP = <<~HELP
@@ -863,7 +887,7 @@ module Bolt
863
887
  end
864
888
  define('--log-level LEVEL',
865
889
  "Set the log level for the console. Available options are",
866
- "debug, info, notice, warn, error, fatal, any.") do |level|
890
+ "trace, debug, info, warn, error, fatal, any.") do |level|
867
891
  @options[:log] = { 'console' => { 'level' => level } }
868
892
  end
869
893
  define('--plugin PLUGIN', 'Select the plugin to use') do |plug|
@@ -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,7 +39,8 @@ 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
 
@@ -354,7 +356,7 @@ module Bolt
354
356
  # Initialize inventory and targets. Errors here are better to catch early.
355
357
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
356
358
  # options[:targets] will contain a resolved set of Target objects
357
- unless %w[project puppetfile secret].include?(options[:subcommand]) ||
359
+ unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
358
360
  %w[convert new show].include?(options[:action])
359
361
  update_targets(options)
360
362
  end
@@ -411,7 +413,7 @@ module Bolt
411
413
  list_modules
412
414
  return 0
413
415
  when 'convert'
414
- convert_plan(options[:object])
416
+ pal.convert_plan(options[:object])
415
417
  return 0
416
418
  end
417
419
 
@@ -422,12 +424,20 @@ module Bolt
422
424
  end
423
425
 
424
426
  case options[:subcommand]
427
+ when 'guide'
428
+ code = if options[:object]
429
+ show_guide(options[:object])
430
+ else
431
+ list_topics
432
+ end
425
433
  when 'project'
426
434
  case options[:action]
427
435
  when 'init'
428
436
  code = initialize_project
429
437
  when 'migrate'
430
- code = migrate_project
438
+ inv = config.inventoryfile
439
+ path = config.project.path
440
+ code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
431
441
  end
432
442
  when 'plan'
433
443
  case options[:action]
@@ -903,49 +913,6 @@ module Bolt
903
913
  end
904
914
  end
905
915
 
906
- def migrate_project
907
- inventory_file = config.inventoryfile || config.default_inventoryfile
908
- data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
909
-
910
- data.delete('version') if data['version'] != 2
911
-
912
- migrated = migrate_group(data)
913
-
914
- ok = File.write(inventory_file, data.to_yaml) if migrated
915
-
916
- result = if migrated && ok
917
- "Successfully migrated Bolt project to latest version."
918
- elsif !migrated
919
- "Bolt project already on latest version. Nothing to do."
920
- else
921
- "Could not migrate Bolt project to latest version."
922
- end
923
- outputter.print_message result
924
-
925
- ok ? 0 : 1
926
- end
927
-
928
- # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
929
- # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
930
- # modified in place.
931
- def migrate_group(group)
932
- migrated = false
933
- if group.key?('nodes')
934
- migrated = true
935
- targets = group['nodes'].map do |target|
936
- target['uri'] = target.delete('name') if target.is_a?(Hash)
937
- target
938
- end
939
- group.delete('nodes')
940
- group['targets'] = targets
941
- end
942
- (group['groups'] || []).each do |subgroup|
943
- migrated_group = migrate_group(subgroup)
944
- migrated ||= migrated_group
945
- end
946
- migrated
947
- end
948
-
949
916
  def install_puppetfile(config, puppetfile, modulepath)
950
917
  require 'r10k/cli'
951
918
  require 'bolt/r10k_log_proxy'
@@ -988,8 +955,46 @@ module Bolt
988
955
  config.project)
989
956
  end
990
957
 
991
- def convert_plan(plan)
992
- pal.convert_plan(plan)
958
+ # Collects the list of Bolt guides and maps them to their topics.
959
+ def guides
960
+ @guides ||= begin
961
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
962
+ files = Dir.children(root_path).sort
963
+
964
+ files.each_with_object({}) do |file, guides|
965
+ next if file !~ /\.txt\z/
966
+ topic = File.basename(file, '.txt')
967
+ guides[topic] = File.join(root_path, file)
968
+ end
969
+ rescue SystemCallError => e
970
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
971
+ end
972
+ end
973
+
974
+ # Display the list of available Bolt guides.
975
+ def list_topics
976
+ outputter.print_topics(guides.keys)
977
+ 0
978
+ end
979
+
980
+ # Display a specific Bolt guide.
981
+ def show_guide(topic)
982
+ if guides[topic]
983
+ analytics.event('Guide', 'known_topic', label: topic)
984
+
985
+ begin
986
+ guide = File.read(guides[topic])
987
+ rescue SystemCallError => e
988
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
989
+ end
990
+
991
+ outputter.print_guide(guide, topic)
992
+ else
993
+ analytics.event('Guide', 'unknown_topic', label: topic)
994
+ outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
995
+ list_topics
996
+ end
997
+ 0
993
998
  end
994
999
 
995
1000
  def validate_file(type, path, allow_dir = false)
@@ -1056,7 +1061,7 @@ module Bolt
1056
1061
  msg = <<~MSG.chomp
1057
1062
  Loaded configuration from: '#{config.config_files.join("', '")}'
1058
1063
  MSG
1059
- @logger.debug(msg)
1064
+ @logger.info(msg)
1060
1065
  end
1061
1066
 
1062
1067
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
@@ -204,7 +204,7 @@ module Bolt
204
204
  'compile-concurrency' => Etc.nprocessors,
205
205
  'concurrency' => default_concurrency,
206
206
  'format' => 'human',
207
- 'log' => { 'console' => {} },
207
+ 'log' => { 'console' => {}, 'bolt-debug.log' => { 'level' => 'debug', 'append' => false } },
208
208
  'plugin_hooks' => {},
209
209
  'plugins' => {},
210
210
  'puppetdb' => {},
@@ -186,8 +186,8 @@ module Bolt
186
186
  "level" => {
187
187
  description: "The type of information to log.",
188
188
  type: String,
189
- enum: %w[debug error info notice warn fatal any],
190
- _default: "warn for console, notice for file"
189
+ enum: %w[trace debug error info warn fatal any],
190
+ _default: "warn"
191
191
  }
192
192
  }
193
193
  }
@@ -204,8 +204,8 @@ module Bolt
204
204
  "level" => {
205
205
  description: "The type of information to log.",
206
206
  type: String,
207
- enum: %w[debug error info notice warn fatal any],
208
- _default: "warn for console, notice for file"
207
+ enum: %w[trace debug error info warn fatal any],
208
+ _default: "warn"
209
209
  }
210
210
  }
211
211
  },
@@ -56,11 +56,12 @@ module Bolt
56
56
  @reported_transports = Set.new
57
57
  @subscribers = {}
58
58
  @publisher = Concurrent::SingleThreadExecutor.new
59
+ @publisher.post { Thread.current[:name] = 'event-publisher' }
59
60
 
60
61
  @noop = noop
61
62
  @run_as = nil
62
63
  @pool = if concurrency > 0
63
- Concurrent::ThreadPoolExecutor.new(max_threads: concurrency)
64
+ Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
64
65
  else
65
66
  Concurrent.global_immediate_executor
66
67
  end
@@ -125,6 +126,7 @@ module Bolt
125
126
  # Pass this argument through to avoid retaining a reference to a
126
127
  # local variable that will change on the next iteration of the loop.
127
128
  @pool.post(batch_promises) do |result_promises|
129
+ Thread.current[:name] ||= Thread.current.name
128
130
  results = yield transport, batch
129
131
  Array(results).each do |result|
130
132
  result_promises[result.target].set(result)
@@ -241,7 +243,7 @@ module Bolt
241
243
 
242
244
  @analytics&.event('Plan', 'yaml', plan_steps: steps, return_type: return_type)
243
245
  rescue StandardError => e
244
- @logger.debug { "Failed to submit analytics event: #{e.message}" }
246
+ @logger.trace { "Failed to submit analytics event: #{e.message}" }
245
247
  end
246
248
 
247
249
  def with_node_logging(description, batch)
@@ -119,7 +119,7 @@ module Bolt
119
119
  end
120
120
 
121
121
  if contains_target?(t_name)
122
- @logger.warn("Ignoring duplicate target in #{@name}: #{target}")
122
+ @logger.debug("Ignoring duplicate target in #{@name}: #{target}")
123
123
  return
124
124
  end
125
125
 
@@ -200,14 +200,14 @@ module Bolt
200
200
  # If this is an alias for an existing target, then add it to this group
201
201
  elsif (canonical_name = aliases[string_target])
202
202
  if contains_target?(canonical_name)
203
- @logger.warn("Ignoring duplicate target in #{@name}: #{canonical_name}")
203
+ @logger.debug("Ignoring duplicate target in #{@name}: #{canonical_name}")
204
204
  else
205
205
  @unresolved_targets[canonical_name] = { 'name' => canonical_name }
206
206
  end
207
207
  # If it's not the name or alias of an existing target, then make a
208
208
  # new target using the string as the URI
209
209
  elsif contains_target?(string_target)
210
- @logger.warn("Ignoring duplicate target in #{@name}: #{string_target}")
210
+ @logger.debug("Ignoring duplicate target in #{@name}: #{string_target}")
211
211
  else
212
212
  @unresolved_targets[string_target] = { 'uri' => string_target }
213
213
  end
@@ -14,13 +14,12 @@ module Bolt
14
14
  # redefs, so skip it if it's already been initialized
15
15
  return if Logging.initialized?
16
16
 
17
- Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
17
+ Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
18
18
  @mutex = Mutex.new
19
19
 
20
20
  Logging.color_scheme(
21
21
  'bolt',
22
22
  lines: {
23
- notice: :green,
24
23
  warn: :yellow,
25
24
  error: :red,
26
25
  fatal: %i[white on_red]
@@ -81,7 +80,7 @@ module Bolt
81
80
 
82
81
  def self.default_layout
83
82
  Logging.layouts.pattern(
84
- pattern: '%d %-6l %c: %m\n',
83
+ pattern: '%d %-6l [%T] [%c] %m\n',
85
84
  date_pattern: '%Y-%m-%dT%H:%M:%S.%6N'
86
85
  )
87
86
  end
@@ -91,7 +90,7 @@ module Bolt
91
90
  end
92
91
 
93
92
  def self.default_file_level
94
- :notice
93
+ :warn
95
94
  end
96
95
 
97
96
  # Explicitly check the log level names instead of the log level number, as levels
@@ -286,6 +286,16 @@ module Bolt
286
286
  "details and parameters for a specific plan.")
287
287
  end
288
288
 
289
+ def print_topics(topics)
290
+ print_message("Available topics are:")
291
+ print_message(topics.join("\n"))
292
+ print_message("\nUse `bolt guide <topic>` to view a specific guide.")
293
+ end
294
+
295
+ def print_guide(guide, _topic)
296
+ @stream.puts(guide)
297
+ end
298
+
289
299
  def print_module_list(module_list)
290
300
  module_list.each do |path, modules|
291
301
  if (mod = modules.find { |m| m[:internal_module_group] })
@@ -83,6 +83,17 @@ module Bolt
83
83
  @stream.puts result.to_json
84
84
  end
85
85
 
86
+ def print_topics(topics)
87
+ print_table('topics' => topics)
88
+ end
89
+
90
+ def print_guide(guide, topic)
91
+ @stream.puts({
92
+ 'topic' => topic,
93
+ 'guide' => guide
94
+ }.to_json)
95
+ end
96
+
86
97
  def print_puppetfile_result(success, puppetfile, moduledir)
87
98
  @stream.puts({ "success": success,
88
99
  "puppetfile": puppetfile,
@@ -40,13 +40,13 @@ module Bolt
40
40
 
41
41
  def log_plan_start(event)
42
42
  plan = event[:plan]
43
- @logger.notice("Starting: plan #{plan}")
43
+ @logger.info("Starting: plan #{plan}")
44
44
  end
45
45
 
46
46
  def log_plan_finish(event)
47
47
  plan = event[:plan]
48
48
  duration = event[:duration]
49
- @logger.notice("Finished: plan #{plan} in #{duration.round(2)} sec")
49
+ @logger.info("Finished: plan #{plan} in #{duration.round(2)} sec")
50
50
  end
51
51
  end
52
52
  end
@@ -86,6 +86,21 @@ module Bolt
86
86
  total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
87
87
  @stream.puts colorize(:rainbow, total_msg)
88
88
  end
89
+
90
+ def print_guide(guide, _topic)
91
+ @stream.puts colorize(:rainbow, guide)
92
+ end
93
+
94
+ def print_topics(topics)
95
+ content = String.new("Available topics are:\n")
96
+ content += topics.join("\n")
97
+ content += "\n\nUse `bolt guide <topic>` to view a specific guide."
98
+ @stream.puts colorize(:rainbow, content)
99
+ end
100
+
101
+ def print_message(message)
102
+ @stream.puts colorize(:rainbow, message)
103
+ end
89
104
  end
90
105
  end
91
106
  end
@@ -67,7 +67,7 @@ module Bolt
67
67
 
68
68
  @logger = Logging.logger[self]
69
69
  if modulepath && !modulepath.empty?
70
- @logger.info("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
70
+ @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
71
71
  end
72
72
 
73
73
  @loaded = false
@@ -409,7 +409,7 @@ module Bolt
409
409
  end
410
410
  params[name] = { 'type' => type_str }
411
411
  params[name]['sensitive'] = param.type_expr.instance_of?(Puppet::Pops::Types::PSensitiveType)
412
- params[name]['default_value'] = param.value
412
+ params[name]['default_value'] = param.value unless param.value.nil?
413
413
  params[name]['description'] = param.description if param.description
414
414
  end
415
415
  {
@@ -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
@@ -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
@@ -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
 
@@ -12,7 +12,7 @@ module Bolt
12
12
  @target = target
13
13
  @logger = Logging.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/
@@ -28,7 +28,7 @@ module Bolt
28
28
  end
29
29
 
30
30
  def upload_file(source, dest)
31
- @logger.debug { "Uploading #{source}, to #{dest}" }
31
+ @logger.trace { "Uploading #{source} to #{dest}" }
32
32
  if source.is_a?(StringIO)
33
33
  Tempfile.create(File.basename(dest)) do |f|
34
34
  f.write(source.read)
@@ -46,7 +46,7 @@ module Bolt
46
46
  end
47
47
 
48
48
  def download_file(source, dest, _download)
49
- @logger.debug { "Downloading #{source} to #{dest}" }
49
+ @logger.trace { "Downloading #{source} to #{dest}" }
50
50
  # Create the destination directory for the target, or the
51
51
  # copied file will have the target's name
52
52
  FileUtils.mkdir_p(dest)
@@ -38,7 +38,7 @@ module Bolt
38
38
  @connections.each_value do |conn|
39
39
  conn.finish_plan(result)
40
40
  rescue StandardError => e
41
- @logger.debug("Failed to finish plan on #{conn.key}: #{e.message}")
41
+ @logger.trace("Failed to finish plan on #{conn.key}: #{e.message}")
42
42
  end
43
43
  end
44
44
  end
@@ -133,7 +133,7 @@ module Bolt
133
133
  next unless File.file?(file)
134
134
 
135
135
  tar_path = Pathname.new(file).relative_path_from(Pathname.new(directory))
136
- @logger.debug("Packing #{file} to #{tar_path}")
136
+ @logger.trace("Packing #{file} to #{tar_path}")
137
137
  stat = File.stat(file)
138
138
  content = File.binread(file)
139
139
  output.tar.add_file_simple(
@@ -146,7 +146,7 @@ module Bolt
146
146
  end
147
147
 
148
148
  duration = Time.now - start_time
149
- @logger.debug("Packed upload in #{duration * 1000} ms")
149
+ @logger.trace("Packed upload in #{duration * 1000} ms")
150
150
 
151
151
  output.close
152
152
  io.string
@@ -28,7 +28,7 @@ module Bolt
28
28
 
29
29
  @logger = Logging.logger[@target.safe_name]
30
30
  @transport_logger = transport_logger
31
- @logger.debug("Initializing ssh connection to #{@target.safe_name}")
31
+ @logger.trace("Initializing ssh connection to #{@target.safe_name}")
32
32
 
33
33
  if target.options['private-key']&.instance_of?(String)
34
34
  begin
@@ -131,7 +131,7 @@ module Bolt
131
131
 
132
132
  @session = Net::SSH.start(target.host, @user, options)
133
133
  validate_ssh_version
134
- @logger.debug { "Opened session" }
134
+ @logger.trace { "Opened session" }
135
135
  rescue Net::SSH::AuthenticationFailed => e
136
136
  raise Bolt::Node::ConnectError.new(
137
137
  e.message,
@@ -161,7 +161,7 @@ module Bolt
161
161
  rescue Timeout::Error
162
162
  @session.shutdown!
163
163
  end
164
- @logger.debug { "Closed session" }
164
+ @logger.trace { "Closed session" }
165
165
  end
166
166
  end
167
167
 
@@ -237,7 +237,7 @@ module Bolt
237
237
 
238
238
  def upload_file(source, destination)
239
239
  # Do not log wrapper script content
240
- @logger.debug { "Uploading #{source}, to #{destination}" } unless source.is_a?(StringIO)
240
+ @logger.trace { "Uploading #{source} to #{destination}" } unless source.is_a?(StringIO)
241
241
  @session.scp.upload!(source, destination, recursive: true)
242
242
  rescue StandardError => e
243
243
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
@@ -245,7 +245,7 @@ module Bolt
245
245
 
246
246
  def download_file(source, destination, _download)
247
247
  # Do not log wrapper script content
248
- @logger.debug { "Downloading #{source} to #{destination}" }
248
+ @logger.trace { "Downloading #{source} to #{destination}" }
249
249
  @session.scp.download!(source, destination, recursive: true)
250
250
  rescue StandardError => e
251
251
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
@@ -66,7 +66,7 @@ module Bolt
66
66
  end
67
67
 
68
68
  def upload_file(source, dest)
69
- @logger.debug { "Uploading #{source}, to #{userhost}:#{dest}" } unless source.is_a?(StringIO)
69
+ @logger.trace { "Uploading #{source} to #{dest}" } unless source.is_a?(StringIO)
70
70
 
71
71
  cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
72
72
  cp_cmd = Array(cp_conf)
@@ -87,7 +87,7 @@ module Bolt
87
87
  end
88
88
 
89
89
  if stat.success?
90
- @logger.debug "Successfully uploaded #{source} to #{dest}"
90
+ @logger.trace "Successfully uploaded #{source} to #{dest}"
91
91
  else
92
92
  message = "Could not copy file to #{dest}: #{err}"
93
93
  raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
@@ -95,7 +95,7 @@ module Bolt
95
95
  end
96
96
 
97
97
  def download_file(source, dest, _download)
98
- @logger.debug { "Downloading #{userhost}:#{source} to #{dest}" }
98
+ @logger.trace { "Downloading #{userhost}:#{source} to #{dest}" }
99
99
 
100
100
  FileUtils.mkdir_p(dest)
101
101
 
@@ -108,7 +108,7 @@ module Bolt
108
108
  _, err, stat = Open3.capture3(*cp_cmd)
109
109
 
110
110
  if stat.success?
111
- @logger.debug "Successfully downloaded #{userhost}:#{source} to #{dest}"
111
+ @logger.trace "Successfully downloaded #{userhost}:#{source} to #{dest}"
112
112
  else
113
113
  message = "Could not copy file to #{dest}: #{err}"
114
114
  raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
@@ -19,7 +19,7 @@ module Bolt
19
19
  # Build set of extensions from extensions config as well as interpreters
20
20
 
21
21
  @logger = Logging.logger[@target.safe_name]
22
- logger.debug("Initializing winrm connection to #{@target.safe_name}")
22
+ logger.trace("Initializing winrm connection to #{@target.safe_name}")
23
23
  @transport_logger = transport_logger
24
24
  end
25
25
 
@@ -55,7 +55,7 @@ module Bolt
55
55
 
56
56
  @session = @connection.shell(:powershell)
57
57
  @session.run('$PSVersionTable.PSVersion')
58
- @logger.debug { "Opened session" }
58
+ @logger.trace { "Opened session" }
59
59
  end
60
60
  rescue Timeout::Error
61
61
  # If we're using the default port with SSL, a timeout probably means the
@@ -97,11 +97,11 @@ module Bolt
97
97
  def disconnect
98
98
  @session&.close
99
99
  @client&.disconnect!
100
- @logger.debug { "Closed session" }
100
+ @logger.trace { "Closed session" }
101
101
  end
102
102
 
103
103
  def execute(command)
104
- @logger.debug { "Executing command: #{command}" }
104
+ @logger.trace { "Executing command: #{command}" }
105
105
 
106
106
  inp = StringIO.new
107
107
  # This transport doesn't accept stdin, so close the stream to ensure
@@ -134,12 +134,12 @@ module Bolt
134
134
  "with 'ulimit -n 1024'. See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details."
135
135
  raise Bolt::Error.new(msg, 'bolt/too-many-files')
136
136
  rescue StandardError
137
- @logger.debug { "Command aborted" }
137
+ @logger.trace { "Command aborted" }
138
138
  raise
139
139
  end
140
140
 
141
141
  def upload_file(source, destination)
142
- @logger.debug { "Uploading #{source}, to #{destination}" }
142
+ @logger.trace { "Uploading #{source} to #{destination}" }
143
143
  if target.options['file-protocol'] == 'smb'
144
144
  upload_file_smb(source, destination)
145
145
  else
@@ -185,7 +185,7 @@ module Bolt
185
185
  end
186
186
 
187
187
  def download_file(source, destination, download)
188
- @logger.debug { "Downloading #{source} to #{destination}" }
188
+ @logger.trace { "Downloading #{source} to #{destination}" }
189
189
  if target.options['file-protocol'] == 'smb'
190
190
  download_file_smb(source, destination)
191
191
  else
@@ -257,7 +257,7 @@ module Bolt
257
257
  status = @client.login
258
258
  case status
259
259
  when WindowsError::NTStatus::STATUS_SUCCESS
260
- @logger.debug { "Connected to #{@client.dns_host_name}" }
260
+ @logger.trace { "Connected to #{@client.dns_host_name}" }
261
261
  when WindowsError::NTStatus::STATUS_LOGON_FAILURE
262
262
  raise Bolt::Node::ConnectError.new(
263
263
  "SMB authentication failed for #{target.safe_name}",
@@ -13,7 +13,7 @@ module Bolt
13
13
  msg = "Invalid content for #{file_name} file: #{path} should be a Hash or empty, not #{content.class}"
14
14
  raise Bolt::FileError.new(msg, path)
15
15
  end
16
- logger.debug("Loaded #{file_name} from #{path}")
16
+ logger.trace("Loaded #{file_name} from #{path}")
17
17
  content
18
18
  rescue Errno::ENOENT
19
19
  raise Bolt::FileError.new("Could not read #{file_name} file: #{path}", path)
@@ -4,9 +4,10 @@ module Bolt
4
4
  module Util
5
5
  module PuppetLogLevel
6
6
  MAPPING = {
7
- debug: :debug,
8
- info: :info,
9
- notice: :notice,
7
+ # Demote Puppet's logs by one level, since Puppet is an implementation detail of Bolt
8
+ debug: :trace,
9
+ info: :debug,
10
+ notice: :info,
10
11
  warning: :warn,
11
12
  err: :error,
12
13
  # The following are used by Puppet functions of the same name, and are all treated as
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.23.0'
4
+ VERSION = '2.24.0'
5
5
  end
@@ -16,7 +16,7 @@ module BoltServer
16
16
 
17
17
  def defaults
18
18
  { 'host' => '127.0.0.1',
19
- 'loglevel' => 'notice',
19
+ 'loglevel' => 'warn',
20
20
  'ssl-cipher-suites' => %w[ECDHE-ECDSA-AES256-GCM-SHA384
21
21
  ECDHE-RSA-AES256-GCM-SHA384
22
22
  ECDHE-ECDSA-CHACHA20-POLY1305
@@ -221,6 +221,56 @@ module BoltServer
221
221
  plan_info
222
222
  end
223
223
 
224
+ def build_puppetserver_uri(file_identifier, module_name, environment)
225
+ segments = file_identifier.split('/', 3)
226
+ if segments.size == 1
227
+ {
228
+ 'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
229
+ 'params' => {
230
+ 'environment' => environment
231
+ }
232
+ }
233
+ else
234
+ module_segment, mount_segment, name_segment = *segments
235
+ {
236
+ 'path' => case mount_segment
237
+ when 'files'
238
+ "/puppet/v3/file_content/modules/#{module_segment}/#{name_segment}"
239
+ when 'tasks'
240
+ "/puppet/v3/file_content/tasks/#{module_segment}/#{name_segment}"
241
+ when 'lib'
242
+ "/puppet/v3/file_content/plugins/#{name_segment}"
243
+ end,
244
+ 'params' => {
245
+ 'environment' => environment
246
+ }
247
+ }
248
+ end
249
+ end
250
+
251
+ def pe_task_info(pal, module_name, task_name, environment)
252
+ # Handle case where task name is simply module name with special `init` task
253
+ task_name = if task_name == 'init' || task_name.nil?
254
+ module_name
255
+ else
256
+ "#{module_name}::#{task_name}"
257
+ end
258
+ task = pal.get_task(task_name)
259
+ files = task.files.map do |file_hash|
260
+ {
261
+ 'filename' => file_hash['name'],
262
+ 'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
263
+ 'size_bytes' => File.size(file_hash['path']),
264
+ 'uri' => build_puppetserver_uri(file_hash['name'], module_name, environment)
265
+ }
266
+ end
267
+ {
268
+ 'metadata' => task.metadata,
269
+ 'name' => task.name,
270
+ 'files' => files
271
+ }
272
+ end
273
+
224
274
  get '/' do
225
275
  200
226
276
  end
@@ -351,6 +401,16 @@ module BoltServer
351
401
  end
352
402
  end
353
403
 
404
+ # Fetches the metadata for a single task
405
+ #
406
+ # @param environment [String] the environment to fetch the task from
407
+ get '/tasks/:module_name/:task_name' do
408
+ in_pe_pal_env(params['environment']) do |pal|
409
+ task_info = pe_task_info(pal, params[:module_name], params[:task_name], params['environment'])
410
+ [200, task_info.to_json]
411
+ end
412
+ end
413
+
354
414
  # Fetches the list of plans for an environment, optionally fetching all metadata for each plan
355
415
  #
356
416
  # @param environment [String] the environment to fetch the list of plans from
@@ -375,6 +435,22 @@ module BoltServer
375
435
  end
376
436
  end
377
437
 
438
+ # Fetches the list of tasks for an environment
439
+ #
440
+ # @param environment [String] the environment to fetch the list of tasks from
441
+ get '/tasks' do
442
+ in_pe_pal_env(params['environment']) do |pal|
443
+ tasks = pal.list_tasks
444
+ tasks_response = tasks.map { |task_name, _description| { 'name' => task_name } }.to_json
445
+
446
+ # We structure this array of tasks to be an array of hashes so that it matches the structure
447
+ # returned by the puppetserver API that serves data like this. Structuring the output this way
448
+ # makes switching between puppetserver and bolt-server easier, which makes changes to switch
449
+ # to bolt-server smaller/simpler.
450
+ [200, tasks_response]
451
+ end
452
+ end
453
+
378
454
  error 404 do
379
455
  err = Bolt::Error.new("Could not find route #{request.path}",
380
456
  'boltserver/not-found')
@@ -206,7 +206,7 @@ module BoltSpec
206
206
  Puppet[:tasks] = true
207
207
 
208
208
  # Ensure logger is initialized with Puppet levels so 'notice' works when running plan specs.
209
- Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
209
+ Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
210
210
  end
211
211
 
212
212
  # Provided as a class so expectations can be placed on it.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.23.0
4
+ version: 2.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-17 00:00:00.000000000 Z
11
+ date: 2020-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -437,6 +437,8 @@ files:
437
437
  - bolt-modules/prompt/lib/puppet/functions/prompt.rb
438
438
  - bolt-modules/system/lib/puppet/functions/system/env.rb
439
439
  - exe/bolt
440
+ - guides/inventory.txt
441
+ - guides/project.txt
440
442
  - lib/bolt.rb
441
443
  - lib/bolt/analytics.rb
442
444
  - lib/bolt/applicator.rb
@@ -498,6 +500,7 @@ files:
498
500
  - lib/bolt/plugin/puppetdb.rb
499
501
  - lib/bolt/plugin/task.rb
500
502
  - lib/bolt/project.rb
503
+ - lib/bolt/project_migrate.rb
501
504
  - lib/bolt/puppetdb.rb
502
505
  - lib/bolt/puppetdb/client.rb
503
506
  - lib/bolt/puppetdb/config.rb