bolt 2.19.0 → 2.20.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  3. data/lib/bolt/bolt_option_parser.rb +25 -2
  4. data/lib/bolt/catalog.rb +3 -2
  5. data/lib/bolt/cli.rb +139 -92
  6. data/lib/bolt/config.rb +1 -1
  7. data/lib/bolt/config/options.rb +14 -0
  8. data/lib/bolt/executor.rb +15 -0
  9. data/lib/bolt/inventory/group.rb +3 -2
  10. data/lib/bolt/inventory/inventory.rb +4 -3
  11. data/lib/bolt/outputter/rainbow.rb +3 -2
  12. data/lib/bolt/pal.rb +8 -2
  13. data/lib/bolt/pal/yaml_plan/evaluator.rb +18 -1
  14. data/lib/bolt/pal/yaml_plan/step.rb +11 -2
  15. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  16. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  17. data/lib/bolt/plugin/puppetdb.rb +3 -2
  18. data/lib/bolt/project.rb +2 -1
  19. data/lib/bolt/puppetdb/client.rb +2 -0
  20. data/lib/bolt/puppetdb/config.rb +16 -0
  21. data/lib/bolt/result.rb +7 -0
  22. data/lib/bolt/shell/bash.rb +24 -4
  23. data/lib/bolt/shell/powershell.rb +10 -4
  24. data/lib/bolt/transport/base.rb +24 -0
  25. data/lib/bolt/transport/docker.rb +8 -0
  26. data/lib/bolt/transport/docker/connection.rb +20 -2
  27. data/lib/bolt/transport/local/connection.rb +14 -1
  28. data/lib/bolt/transport/orch.rb +12 -0
  29. data/lib/bolt/transport/simple.rb +6 -0
  30. data/lib/bolt/transport/ssh/connection.rb +9 -1
  31. data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
  32. data/lib/bolt/transport/winrm/connection.rb +109 -8
  33. data/lib/bolt/util.rb +26 -11
  34. data/lib/bolt/version.rb +1 -1
  35. data/lib/bolt_server/transport_app.rb +3 -2
  36. data/lib/bolt_spec/bolt_context.rb +7 -2
  37. data/lib/bolt_spec/plans.rb +15 -2
  38. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  39. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  40. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  41. data/lib/bolt_spec/run.rb +22 -0
  42. data/libexec/bolt_catalog +3 -2
  43. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84c1deb7ddf2b30daf415a36f7949544d3113db8ae05cf927516cf0ecfb21ca8
4
- data.tar.gz: 233d6da93d218e63e50287cbcb5ea863a16f15aa8ae6b02c62f26e87d1d43197
3
+ metadata.gz: a72c179da8e6e3fd3d8f883366e88f98026de00eee08ae2410515b2103a3810d
4
+ data.tar.gz: 968edc1c0c30a370ed06b1c9bd5498ccaaaa5e15f3c47307cad1e68f2af5dafb
5
5
  SHA512:
6
- metadata.gz: a9432c3f9d79ae864971ed4e37cd258fe884b5f96addb7dac19688b9b0a8e9b8044a686321b8f6dbaecf0fdfd41ba962ae9b67d0dbc88744357acd8fafa07e15
7
- data.tar.gz: 0a62d1499e578c06900855c5cca20e834a73a7a572f81feb602e60fe72775c02cfba97c39a224477b69098948bf2d40c4dc4229ff4e43d3cb784d4d787dc2d85
6
+ metadata.gz: a906282180c5df824978979d8e12da5cbe2f835ccef2ae65f2e2fc892bcfa1514a4f8f76418b866fe9cbfe8ce8f8f28b66306055b7acc67de6a7a2e13bb7edbf
7
+ data.tar.gz: ee4c33740e8e29ad4b3026e5b75b3f0d29556c92e9277bc34101c5f8acfe80db599389c8544aab5946c6c444502a49ddb515da0a854b5796b0aae54fc0875ffc
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'bolt/error'
5
+
6
+ # Downloads the given file or directory from the given set of targets and saves it to a directory
7
+ # matching the target's name under the given destination directory. Returns the result from each
8
+ # download. This does nothing if the list of targets is empty.
9
+ #
10
+ # > **Note:** Existing content in the destination directory is deleted before downloading from
11
+ # > the targets.
12
+ #
13
+ # > **Note:** Not available in apply block
14
+ Puppet::Functions.create_function(:download_file, Puppet::Functions::InternalFunction) do
15
+ # Download a file or directory.
16
+ # @param source The absolute path to the file or directory on the target(s).
17
+ # @param destination The relative path to the destination directory on the local system. Expands
18
+ # relative to `<project>/downloads/`.
19
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
20
+ # @param options A hash of additional options.
21
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
22
+ # @option options [String] _run_as User to run as using privilege escalation.
23
+ # @return A list of results, one entry per target, with the path to the downloaded file under the
24
+ # `path` key.
25
+ # @example Download a file from multiple Linux targets to a destination directory
26
+ # download_file('/etc/ssh/ssh_config', '~/Downloads', $targets)
27
+ # @example Download a directory from multiple Linux targets to a project downloads directory
28
+ # download_file('/etc/ssh', 'ssh', $targets)
29
+ # @example Download a file from multiple Linux targets and compare its contents to a local file
30
+ # $results = download_file($source, $destination, $targets)
31
+ #
32
+ # $local_content = file::read($source)
33
+ #
34
+ # $mismatched_files = $results.filter |$result| {
35
+ # $remote_content = file::read($result['path'])
36
+ # $remote_content == $local_content
37
+ # }
38
+ dispatch :download_file do
39
+ param 'String[1]', :source
40
+ param 'String[1]', :destination
41
+ param 'Boltlib::TargetSpec', :targets
42
+ optional_param 'Hash[String[1], Any]', :options
43
+ return_type 'ResultSet'
44
+ end
45
+
46
+ # Download a file or directory, logging the provided description.
47
+ # @param source The absolute path to the file or directory on the target(s).
48
+ # @param destination The relative path to the destination directory on the local system. Expands
49
+ # relative to `<project>/downloads/`.
50
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
51
+ # @param description A description to be output when calling this function.
52
+ # @param options A hash of additional options.
53
+ # @option options [Boolean] _catch_errors Whether to catch raised errors.
54
+ # @option options [String] _run_as User to run as using privilege escalation.
55
+ # @return A list of results, one entry per target, with the path to the downloaded file under the
56
+ # `path` key.
57
+ # @example Download a file from multiple Linux targets to a destination directory
58
+ # download_file('/etc/ssh/ssh_config', '~/Downloads', $targets, 'Downloading remote SSH config')
59
+ dispatch :download_file_with_description do
60
+ param 'String[1]', :source
61
+ param 'String[1]', :destination
62
+ param 'Boltlib::TargetSpec', :targets
63
+ param 'String', :description
64
+ optional_param 'Hash[String[1], Any]', :options
65
+ return_type 'ResultSet'
66
+ end
67
+
68
+ def download_file(source, destination, targets, options = {})
69
+ download_file_with_description(source, destination, targets, nil, options)
70
+ end
71
+
72
+ def download_file_with_description(source, destination, targets, description = nil, options = {})
73
+ unless Puppet[:tasks]
74
+ raise Puppet::ParseErrorWithIssue
75
+ .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'download_file')
76
+ end
77
+
78
+ options = options.select { |opt| opt.start_with?('_') }.transform_keys { |k| k.sub(/^_/, '').to_sym }
79
+ options[:description] = description if description
80
+
81
+ executor = Puppet.lookup(:bolt_executor)
82
+ inventory = Puppet.lookup(:bolt_inventory)
83
+
84
+ if (destination = destination.strip).empty?
85
+ raise Bolt::ValidationError, "Destination cannot be an empty string"
86
+ end
87
+
88
+ if (destination = Pathname.new(destination)).absolute?
89
+ raise Bolt::ValidationError, "Destination must be a relative path, received absolute path #{destination}"
90
+ end
91
+
92
+ # Prevent path traversal so downloads can't be saved outside of the project downloads directory
93
+ if (destination.each_filename.to_a & %w[. ..]).any?
94
+ raise Bolt::ValidationError, "Destination must not include path traversal, received #{destination}"
95
+ end
96
+
97
+ # Paths expand relative to the default downloads directory for the project
98
+ # e.g. ~/.puppetlabs/bolt/downloads/
99
+ destination = Puppet.lookup(:bolt_project_data).downloads + destination
100
+
101
+ # If the destination directory already exists, delete any existing contents
102
+ if Dir.exist?(destination)
103
+ FileUtils.rm_r(Dir.glob(destination + '*'), secure: true)
104
+ end
105
+
106
+ # Send Analytics Report
107
+ executor.report_function_call(self.class.name)
108
+
109
+ # Ensure that that given targets are all Target instances
110
+ targets = inventory.get_targets(targets)
111
+ if targets.empty?
112
+ call_function('debug', "Simulating file download of '#{source}' - no targets given - no action taken")
113
+ r = Bolt::ResultSet.new([])
114
+ else
115
+ r = executor.download_file(targets, source, destination, options)
116
+ end
117
+
118
+ if !r.ok && !options[:catch_errors]
119
+ raise Bolt::RunFailure.new(r, 'download_file', source)
120
+ end
121
+ r
122
+ end
123
+ end
@@ -36,6 +36,9 @@ module Bolt
36
36
  when 'upload'
37
37
  { flags: ACTION_OPTS + %w[tmpdir],
38
38
  banner: FILE_UPLOAD_HELP }
39
+ when 'download'
40
+ { flags: ACTION_OPTS,
41
+ banner: FILE_DOWNLOAD_HELP }
39
42
  else
40
43
  { flags: OPTIONS[:global],
41
44
  banner: FILE_HELP }
@@ -218,10 +221,30 @@ module Bolt
218
221
  bolt file <action> [options]
219
222
 
220
223
  DESCRIPTION
221
- Upload a local file or directory
224
+ Copy files and directories between the controller and targets
222
225
 
223
226
  ACTIONS
224
- upload Upload a local file or directory
227
+ download Download a file or directory to the controller
228
+ upload Upload a local file or directory from the controller
229
+ HELP
230
+
231
+ FILE_DOWNLOAD_HELP = <<~HELP
232
+ NAME
233
+ download
234
+
235
+ USAGE
236
+ bolt file download <src> <dest> [options]
237
+
238
+ DESCRIPTION
239
+ Download a file or directory from one or more targets.
240
+
241
+ Downloaded files and directories are saved to the a subdirectory
242
+ matching the target's name under the destination directory. The
243
+ destination directory is expanded relative to the downloads
244
+ subdirectory of the project directory.
245
+
246
+ EXAMPLES
247
+ bolt file download /etc/ssh_config ssh_config -t all
225
248
  HELP
226
249
 
227
250
  FILE_UPLOAD_HELP = <<~HELP
@@ -138,9 +138,10 @@ module Bolt
138
138
  # That means the apply body either a) consists of just a
139
139
  # NodeDefinition, b) consists of a BlockExpression which may
140
140
  # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
141
- definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
141
+ definitions = case ast
142
+ when Puppet::Pops::Model::BlockExpression
142
143
  ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
143
- elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
144
+ when Puppet::Pops::Model::NodeDefinition
144
145
  [ast]
145
146
  else
146
147
  []
@@ -32,7 +32,7 @@ module Bolt
32
32
  'script' => %w[run],
33
33
  'task' => %w[show run],
34
34
  'plan' => %w[show run convert],
35
- 'file' => %w[upload],
35
+ 'file' => %w[download upload],
36
36
  'puppetfile' => %w[install show-modules generate-types],
37
37
  'secret' => %w[encrypt decrypt createkeys],
38
38
  'inventory' => %w[show],
@@ -75,72 +75,100 @@ module Bolt
75
75
  end
76
76
  private :help?
77
77
 
78
+ # Wrapper method that is called by the Bolt executable. Parses the command and
79
+ # then loads the project and config. Once config is loaded, it completes the
80
+ # setup process by configuring Bolt and issuing warnings.
81
+ #
82
+ # This separation is needed since the Bolt::Outputter class that normally handles
83
+ # printing errors relies on config being loaded. All setup that happens before
84
+ # config is loaded will have errors printed directly to stdout, while all errors
85
+ # raised after config is loaded are handled by the outputter.
78
86
  def parse
79
- begin
80
- parser = BoltOptionParser.new(options)
81
- # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
82
- remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
83
- if @argv.empty? || help?(remaining)
84
- # Update the parser for the subcommand (or lack thereof)
85
- parser.update
86
- puts parser.help
87
- raise Bolt::CLIExit
87
+ parse_command
88
+ load_config
89
+ finalize_setup
90
+ end
91
+
92
+ # Parses the command and validates options. All errors that are raised here
93
+ # are not handled by the outputter, as it relies on config being loaded.
94
+ def parse_command
95
+ parser = BoltOptionParser.new(options)
96
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
97
+ remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
98
+ if @argv.empty? || help?(remaining)
99
+ # Update the parser for the subcommand (or lack thereof)
100
+ parser.update
101
+ puts parser.help
102
+ raise Bolt::CLIExit
103
+ end
104
+
105
+ options[:object] = remaining.shift
106
+
107
+ # Only parse task_options for task or plan
108
+ if %w[task plan].include?(options[:subcommand])
109
+ task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
110
+ if options[:task_options]
111
+ unless task_options.empty?
112
+ raise Bolt::CLIError,
113
+ "Parameters must be specified through either the --params " \
114
+ "option or param=value pairs, not both"
115
+ end
116
+ options[:params_parsed] = true
117
+ elsif task_options.any?
118
+ options[:params_parsed] = false
119
+ options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
120
+ else
121
+ options[:params_parsed] = true
122
+ options[:task_options] = {}
88
123
  end
124
+ end
125
+ options[:leftovers] = remaining
89
126
 
90
- options[:object] = remaining.shift
127
+ # Default to verbose for everything except plans
128
+ unless options.key?(:verbose)
129
+ options[:verbose] = options[:subcommand] != 'plan'
130
+ end
91
131
 
92
- # Only parse task_options for task or plan
93
- if %w[task plan].include?(options[:subcommand])
94
- task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
95
- if options[:task_options]
96
- unless task_options.empty?
97
- raise Bolt::CLIError,
98
- "Parameters must be specified through either the --params " \
99
- "option or param=value pairs, not both"
100
- end
101
- options[:params_parsed] = true
102
- elsif task_options.any?
103
- options[:params_parsed] = false
104
- options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
105
- else
106
- options[:params_parsed] = true
107
- options[:task_options] = {}
108
- end
109
- end
110
- options[:leftovers] = remaining
111
-
112
- validate(options)
113
-
114
- @config = if ENV['BOLT_PROJECT']
115
- project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
116
- Bolt::Config.from_project(project, options)
117
- elsif options[:configfile]
118
- Bolt::Config.from_file(options[:configfile], options)
119
- else
120
- project = if options[:boltdir]
121
- dir = Pathname.new(options[:boltdir])
122
- if (dir + Bolt::Project::BOLTDIR_NAME).directory?
123
- Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
124
- else
125
- Bolt::Project.create_project(dir)
126
- end
132
+ validate(options)
133
+
134
+ # Deprecation warnings can't be issued until after config is loaded, so
135
+ # store them for later.
136
+ @parser_deprecations = parser.deprecations
137
+ rescue Bolt::Error => e
138
+ fatal_error(e)
139
+ raise e
140
+ end
141
+
142
+ # Loads the project and configuration. All errors that are raised here are not
143
+ # handled by the outputter, as it relies on config being loaded.
144
+ def load_config
145
+ @config = if ENV['BOLT_PROJECT']
146
+ project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
147
+ Bolt::Config.from_project(project, options)
148
+ elsif options[:configfile]
149
+ Bolt::Config.from_file(options[:configfile], options)
150
+ else
151
+ project = if options[:boltdir]
152
+ dir = Pathname.new(options[:boltdir])
153
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
154
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
127
155
  else
128
- Bolt::Project.find_boltdir(Dir.pwd)
156
+ Bolt::Project.create_project(dir)
129
157
  end
130
- Bolt::Config.from_project(project, options)
131
- end
132
-
133
- Bolt::Logger.configure(config.log, config.color)
134
- Bolt::Logger.analytics = analytics
135
- rescue Bolt::Error => e
136
- if $stdout.isatty
137
- # Print the error message in red, mimicking outputter.fatal_error
138
- $stdout.puts("\033[31m#{e.message}\033[0m")
139
- else
140
- $stdout.puts(e.message)
141
- end
142
- raise e
143
- end
158
+ else
159
+ Bolt::Project.find_boltdir(Dir.pwd)
160
+ end
161
+ Bolt::Config.from_project(project, options)
162
+ end
163
+ rescue Bolt::Error => e
164
+ fatal_error(e)
165
+ raise e
166
+ end
167
+
168
+ # Completes the setup process by configuring Bolt and issuing warnings
169
+ def finalize_setup
170
+ Bolt::Logger.configure(config.log, config.color)
171
+ Bolt::Logger.analytics = analytics
144
172
 
145
173
  # Logger must be configured before checking path case and project file, otherwise warnings will not display
146
174
  config.check_path_case('modulepath', config.modulepath)
@@ -151,28 +179,11 @@ module Bolt
151
179
 
152
180
  # Display warnings created during parser and config initialization
153
181
  config.warnings.each { |warning| @logger.warn(warning[:msg]) }
154
- parser.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
182
+ @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
155
183
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
156
184
 
157
- # After validation, initialize inventory and targets. Errors here are better to catch early.
158
- # After this step
159
- # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
160
- # options[:targets] will contain a resolved set of Target objects
161
- unless options[:subcommand] == 'puppetfile' ||
162
- options[:subcommand] == 'secret' ||
163
- options[:subcommand] == 'project' ||
164
- options[:action] == 'show' ||
165
- options[:action] == 'convert'
166
-
167
- update_targets(options)
168
- end
169
-
170
- unless options.key?(:verbose)
171
- # Default to verbose for everything except plans
172
- options[:verbose] = options[:subcommand] != 'plan'
173
- end
174
-
175
185
  warn_inventory_overrides_cli(options)
186
+
176
187
  options
177
188
  rescue Bolt::Error => e
178
189
  outputter.fatal_error(e)
@@ -329,6 +340,17 @@ module Bolt
329
340
  exit!
330
341
  end
331
342
 
343
+ # Initialize inventory and targets. Errors here are better to catch early.
344
+ # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
345
+ # options[:targets] will contain a resolved set of Target objects
346
+ unless options[:subcommand] == 'puppetfile' ||
347
+ options[:subcommand] == 'secret' ||
348
+ options[:subcommand] == 'project' ||
349
+ options[:action] == 'show' ||
350
+ options[:action] == 'convert'
351
+ update_targets(options)
352
+ end
353
+
332
354
  if options[:action] == 'convert'
333
355
  convert_plan(options[:object])
334
356
  return 0
@@ -357,30 +379,32 @@ module Bolt
357
379
 
358
380
  analytics.screen_view(screen, screen_view_fields)
359
381
 
360
- if options[:action] == 'show'
361
- if options[:subcommand] == 'task'
382
+ case options[:action]
383
+ when 'show'
384
+ case options[:subcommand]
385
+ when 'task'
362
386
  if options[:object]
363
387
  show_task(options[:object])
364
388
  else
365
389
  list_tasks
366
390
  end
367
- elsif options[:subcommand] == 'plan'
391
+ when 'plan'
368
392
  if options[:object]
369
393
  show_plan(options[:object])
370
394
  else
371
395
  list_plans
372
396
  end
373
- elsif options[:subcommand] == 'inventory'
397
+ when 'inventory'
374
398
  if options[:detail]
375
399
  show_targets
376
400
  else
377
401
  list_targets
378
402
  end
379
- elsif options[:subcommand] == 'group'
403
+ when 'group'
380
404
  list_groups
381
405
  end
382
406
  return 0
383
- elsif options[:action] == 'show-modules'
407
+ when 'show-modules'
384
408
  list_modules
385
409
  return 0
386
410
  end
@@ -393,17 +417,19 @@ module Bolt
393
417
 
394
418
  case options[:subcommand]
395
419
  when 'project'
396
- if options[:action] == 'init'
420
+ case options[:action]
421
+ when 'init'
397
422
  code = initialize_project
398
- elsif options[:action] == 'migrate'
423
+ when 'migrate'
399
424
  code = migrate_project
400
425
  end
401
426
  when 'plan'
402
427
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
403
428
  when 'puppetfile'
404
- if options[:action] == 'generate-types'
429
+ case options[:action]
430
+ when 'generate-types'
405
431
  code = generate_types
406
- elsif options[:action] == 'install'
432
+ when 'install'
407
433
  code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
408
434
  end
409
435
  when 'secret'
@@ -445,11 +471,22 @@ module Bolt
445
471
  src = options[:object]
446
472
  dest = options[:leftovers].first
447
473
 
474
+ if src.nil?
475
+ raise Bolt::CLIError, "A source path must be specified"
476
+ end
477
+
448
478
  if dest.nil?
449
479
  raise Bolt::CLIError, "A destination path must be specified"
450
480
  end
451
- validate_file('source file', src, true)
452
- executor.upload_file(targets, src, dest, executor_opts)
481
+
482
+ case options[:action]
483
+ when 'download'
484
+ dest = File.expand_path(dest, Dir.pwd)
485
+ executor.download_file(targets, src, dest, executor_opts)
486
+ when 'upload'
487
+ validate_file('source file', src, true)
488
+ executor.upload_file(targets, src, dest, executor_opts)
489
+ end
453
490
  end
454
491
  end
455
492
 
@@ -879,5 +916,15 @@ module Bolt
879
916
  def incomplete_install?
880
917
  (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
881
918
  end
919
+
920
+ # Mimicks the output from Outputter::Human#fatal_error. This should be used to print
921
+ # errors prior to config being loaded, as the outputter relies on config being loaded.
922
+ def fatal_error(error)
923
+ if $stdout.isatty
924
+ $stdout.puts("\033[31m#{error.message}\033[0m")
925
+ else
926
+ $stdout.puts(error.message)
927
+ end
928
+ end
882
929
  end
883
930
  end