bolt 0.21.3 → 0.21.4

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: bfc1d47b6d45382d603a222c1aa4a5066a560e4439799afc00a15b44002497fb
4
- data.tar.gz: 9e1c7c4c69d255092849484da518a6c9c02115917f8f5f3475ee8669b322bf70
3
+ metadata.gz: 81d6e90b40ddb51ff8c5b4817ba930417ec7aec118a63e8b585b38d6a5555040
4
+ data.tar.gz: 3e99e5ba914908058503d1feb9bd8fe5331474288d863ec158a798142b0f41be
5
5
  SHA512:
6
- metadata.gz: 29dd0f53e14ef2d7787e59ddf121f3a96a27c7b0810b3971a55a377c2e0f5d0996f78359e685459edbafea7a78e2e284b454027e22906edde2790bc9452ed23b
7
- data.tar.gz: cb7268a4c9de0d2b8fd99649d112276c8542275b8f3ce7e134f7b1b2a041cb688537e13ce98a607753c597eec028ff1fb29c09ad1b18ead6aa291014e4dfb91d
6
+ metadata.gz: 9e7d9c78f78df1dffa0e4288cefa10216182091c16b70ace73b6e1a766a4ebff7fc6cedb403b9794851c844b9050c63a13f1beb137a87e5f53af2c9b2de7af54
7
+ data.tar.gz: 1edc8a0cb84632e703a9c2b0e50d6478cdbb2e5a56e0bb7bc06e9395e6dba1bcbc7232423eca87afef58f904c5922af782041079070b819f08d5228dbb3c029b
@@ -3,5 +3,11 @@
3
3
 
4
4
  require 'bolt_ext/puppetdb_inventory'
5
5
 
6
- exitcode = Bolt::PuppetDBInventory::CLI.new(ARGV).run
7
- exit exitcode
6
+ begin
7
+ Bolt::PuppetDBInventory::CLI.new(ARGV).run
8
+ exit 0
9
+ rescue StandardError => e
10
+ warn "Error: #{e}"
11
+ warn e.backtrace if @trace
12
+ exit 1
13
+ end
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'logging'
4
5
  require 'open3'
5
6
  require 'concurrent'
7
+ require 'bolt/util/puppet_log_level'
6
8
 
7
9
  module Bolt
8
10
  Task = Struct.new(:name, :implementations, :input_method)
9
11
 
10
12
  class Applicator
11
- def initialize(inventory, executor, modulepath, pdb_config, hiera_config, max_compiles)
13
+ def initialize(inventory, executor, modulepath, pdb_client, hiera_config, max_compiles)
12
14
  @inventory = inventory
13
15
  @executor = executor
14
16
  @modulepath = modulepath
15
- @pdb_config = pdb_config
17
+ @pdb_client = pdb_client
16
18
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
17
19
 
18
20
  @pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
21
+ @logger = Logging.logger[self]
19
22
  end
20
23
 
21
24
  private def libexec
@@ -36,7 +39,7 @@ module Bolt
36
39
  catalog_input = {
37
40
  code_ast: ast,
38
41
  modulepath: @modulepath,
39
- pdb_config: @pdb_config,
42
+ pdb_config: @pdb_client.config.to_hash,
40
43
  hiera_config: @hiera_config,
41
44
  target: {
42
45
  name: target.host,
@@ -53,7 +56,28 @@ module Bolt
53
56
  out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
54
57
  ENV['PATH'] = old_path
55
58
 
56
- raise ApplyError.new(target.to_s, err) unless stat.success?
59
+ # stderr may contain formatted logs from Puppet's logger or other errors.
60
+ # Print them in order, but handle them separately. Anything not a formatted log is assumed
61
+ # to be an error message.
62
+ logs = err.lines.map do |l|
63
+ begin
64
+ JSON.parse(l)
65
+ rescue StandardError
66
+ l
67
+ end
68
+ end
69
+ logs.each do |log|
70
+ if log.is_a?(String)
71
+ @logger.error(log.chomp)
72
+ else
73
+ log.map { |k, v| [k.to_sym, v] }.each do |level, msg|
74
+ bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[level]
75
+ @logger.send(bolt_level, "#{target.name}: #{msg.chomp}")
76
+ end
77
+ end
78
+ end
79
+
80
+ raise(ApplyError, target.name) unless stat.success?
57
81
  JSON.parse(out)
58
82
  end
59
83
 
@@ -61,22 +85,70 @@ module Bolt
61
85
  if File.exist?(File.path(hiera_config))
62
86
  data = File.open(File.path(hiera_config), "r:UTF-8") { |f| YAML.safe_load(f.read) }
63
87
  unless data['version'] == 5
64
- raise ApplyError.new("All Targets", "Hiera v5 is required.")
88
+ raise Bolt::ParseError, "Hiera v5 is required, found v#{data['version'] || 3} in #{hiera_config}"
65
89
  end
66
90
  hiera_config
67
91
  end
68
92
  end
69
93
 
94
+ def provide_puppet_missing_errors(result)
95
+ error_hash = result.error_hash
96
+ exit_code = error_hash['details']['exit_code'] if error_hash && error_hash['details']
97
+ # If we get exit code 126 or 127 back, it means the shebang command wasn't found; Puppet isn't present
98
+ if [126, 127].include?(exit_code)
99
+ Result.new(result.target, error:
100
+ {
101
+ 'msg' => "Puppet is not installed on the target, please install it to enable 'apply'",
102
+ 'kind' => 'bolt/apply-error'
103
+ })
104
+ elsif exit_code == 1 && error_hash['msg'] =~ /Could not find executable 'ruby.exe'/
105
+ # Windows does not have Ruby present
106
+ Result.new(result.target, error:
107
+ {
108
+ 'msg' => "Puppet is not installed on the target in $env:ProgramFiles, please install it to enable 'apply'",
109
+ 'kind' => 'bolt/apply-error'
110
+ })
111
+ elsif exit_code == 1 && error_hash['msg'] =~ /cannot load such file -- puppet \(LoadError\)/
112
+ # Windows uses a Ruby that doesn't have Puppet installed
113
+ # TODO: fix so we don't find other Rubies, or point to a known issues URL for more info
114
+ Result.new(result.target, error:
115
+ {
116
+ 'msg' => 'Found a Ruby without Puppet present, please install Puppet ' \
117
+ "or remove Ruby from $env:Path to enable 'apply'",
118
+ 'kind' => 'bolt/apply-error'
119
+ })
120
+ else
121
+ result
122
+ end
123
+ end
124
+
125
+ def identify_resource_failures(result)
126
+ if result.ok? && result.value['status'] == 'failed'
127
+ resources = result.value['resource_statuses']
128
+ failed = resources.select { |_, r| r['failed'] }.flat_map do |key, resource|
129
+ resource['events'].select { |e| e['status'] == 'failure' }.map do |event|
130
+ "\n #{key}: #{event['message']}"
131
+ end
132
+ end
133
+
134
+ result.value['_error'] = {
135
+ 'msg' => "Resources failed to apply for #{result.target.name}#{failed.join}",
136
+ 'kind' => 'bolt/resource-failure'
137
+ }
138
+ end
139
+ result
140
+ end
141
+
70
142
  def apply(args, apply_body, scope)
71
143
  raise(ArgumentError, 'apply requires a TargetSpec') if args.empty?
72
144
  type0 = Puppet.lookup(:pal_script_compiler).type('TargetSpec')
73
145
  Puppet::Pal.assert_type(type0, args[0], 'apply targets')
74
146
 
75
- params = {}
147
+ options = {}
76
148
  if args.count > 1
77
149
  type1 = Puppet.lookup(:pal_script_compiler).type('Hash[String, Data]')
78
150
  Puppet::Pal.assert_type(type1, args[1], 'apply options')
79
- params = args[1]
151
+ options = args[1]
80
152
  end
81
153
 
82
154
  # collect plan vars and merge them over target vars
@@ -87,7 +159,7 @@ module Bolt
87
159
  ast = Puppet::Pops::Serialization::ToDataConverter.convert(apply_body, rich_data: true, symbol_to_string: true)
88
160
  notify = proc { |_| nil }
89
161
 
90
- @executor.log_action('apply catalog', targets) do
162
+ r = @executor.log_action('apply catalog', targets) do
91
163
  futures = targets.map do |target|
92
164
  Concurrent::Future.execute(executor: @pool) do
93
165
  @executor.with_node_logging("Compiling manifest block", [target]) do
@@ -99,16 +171,22 @@ module Bolt
99
171
  result_promises = targets.zip(futures).flat_map do |target, future|
100
172
  @executor.queue_execute([target]) do |transport, batch|
101
173
  @executor.with_node_logging("Applying manifest block", batch) do
102
- arguments = params.clone
103
- arguments['catalog'] = future.value
174
+ arguments = { 'catalog' => future.value, '_noop' => options['_noop'] }
104
175
  raise future.reason if future.rejected?
105
- transport.batch_task(batch, catalog_apply_task, arguments, {}, &notify)
176
+ result = transport.batch_task(batch, catalog_apply_task, arguments, options, &notify)
177
+ result = provide_puppet_missing_errors(result)
178
+ identify_resource_failures(result)
106
179
  end
107
180
  end
108
181
  end
109
182
 
110
183
  @executor.await_results(result_promises)
111
184
  end
185
+
186
+ if !r.ok && !options['_catch_errors']
187
+ raise Bolt::ApplyFailure, r
188
+ end
189
+ r
112
190
  end
113
191
  end
114
192
  end
@@ -28,6 +28,7 @@ Available subcommands:
28
28
  bolt plan show Show list of available plans
29
29
  bolt plan show <plan> Show details for plan
30
30
  bolt plan run <plan> [params] Run a Puppet task plan
31
+ bolt puppetfile install Install modules from a Puppetfile into a Boltdir
31
32
 
32
33
  Run `bolt <subcommand> --help` to view specific examples.
33
34
 
@@ -89,6 +90,18 @@ Available actions are:
89
90
  upload <src> <dest> Upload local file <src> to <dest> on each node
90
91
 
91
92
  #{examples('file upload /tmp/source /etc/profile.d/login.sh', 'upload a file to')}
93
+ Available options are:
94
+ HELP
95
+
96
+ PUPPETFILE_HELP = <<-HELP
97
+ Usage: bolt puppetfile <action> [options]
98
+
99
+ Available actions are:
100
+ install Install modules from a Puppetfile into a Boltdir
101
+
102
+ Install modules into the local Boltdir
103
+ bolt puppetfile install
104
+
92
105
  Available options are:
93
106
  HELP
94
107
 
@@ -193,7 +206,7 @@ Available options are:
193
206
  end
194
207
  define('--boltdir FILEPATH',
195
208
  'Specify what Boltdir to load config from (default: autodiscovered from current working dir)') do |path|
196
- @options[:configfile] = path
209
+ @options[:boltdir] = path
197
210
  end
198
211
  define('--configfile FILEPATH',
199
212
  'Specify where to load config from (default: ~/.puppetlabs/bolt/bolt.yaml)') do |path|
@@ -253,8 +266,8 @@ Available options are:
253
266
  # show the --nodes and --query switches by default
254
267
  @nodes.hide = @query.hide = false
255
268
 
256
- # Update the banner according to the mode
257
- self.banner = case @options[:mode]
269
+ # Update the banner according to the subcommand
270
+ self.banner = case @options[:subcommand]
258
271
  when 'plan'
259
272
  # don't show the --nodes and --query switches in the plan help
260
273
  @nodes.hide = @query.hide = true
@@ -267,6 +280,8 @@ Available options are:
267
280
  TASK_HELP
268
281
  when 'file'
269
282
  FILE_HELP
283
+ when 'puppetfile'
284
+ PUPPETFILE_HELP
270
285
  else
271
286
  BANNER
272
287
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Bolt
6
+ class Boltdir
7
+ BOLTDIR_NAME = 'Boltdir'
8
+
9
+ attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config, :puppetfile
10
+
11
+ def self.default_boltdir
12
+ Boltdir.new(File.join('~', '.puppetlabs', 'bolt'))
13
+ end
14
+
15
+ def self.find_boltdir(dir)
16
+ local_boltdir = Pathname.new(dir).ascend do |path|
17
+ boltdir = path + BOLTDIR_NAME
18
+ break new(boltdir) if boltdir.directory?
19
+ end
20
+
21
+ local_boltdir || default_boltdir
22
+ end
23
+
24
+ def initialize(path)
25
+ @path = Pathname.new(path).expand_path
26
+ @config_file = @path + 'bolt.yaml'
27
+ @inventory_file = @path + 'inventory.yaml'
28
+ @modulepath = [(@path + 'modules').to_s]
29
+ @hiera_config = @path + 'hiera.yaml'
30
+ @puppetfile = @path + 'Puppetfile'
31
+ end
32
+
33
+ def to_s
34
+ @path.to_s
35
+ end
36
+
37
+ def eql?(other)
38
+ path == other.path
39
+ end
40
+ alias == eql?
41
+ end
42
+ end
@@ -2,56 +2,11 @@
2
2
 
3
3
  require 'bolt/pal'
4
4
  require 'bolt/puppetdb'
5
- require 'bolt/util/on_access'
6
5
 
7
6
  Bolt::PAL.load_puppet
8
7
 
9
- # This class exists to override evaluate_main and let us inject
10
- # AST instead of looking for the main manifest. A better option may be to set up the
11
- # node environment so our AST is in the '' hostclass instead of doing it here.
12
- module Puppet
13
- module Parser
14
- class BoltCompiler < Puppet::Parser::Compiler
15
- def internal_evaluator
16
- @internal_evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new
17
- end
18
-
19
- def dump_ast(ast)
20
- Puppet::Pops::Serialization::ToDataConverter.convert(ast, rich_data: true, symbol_to_string: true)
21
- end
22
-
23
- def load_ast(ast_data)
24
- Puppet::Pops::Serialization::FromDataConverter.convert(ast_data)
25
- end
26
-
27
- def parse_string(string, file = '')
28
- internal_evaluator.parse_string(string, file)
29
- end
30
-
31
- def evaluate_main
32
- main = Puppet.lookup(:pal_main)
33
- ast = if main.is_a?(String)
34
- parse_string(main)
35
- else
36
- load_ast(main)
37
- end
38
-
39
- bridge = Puppet::Parser::AST::PopsBridge::Program.new(ast)
40
-
41
- # This is more or less copypaste from the super but we don't use the
42
- # original host_class.
43
- krt = environment.known_resource_types
44
- @main = krt.add(Puppet::Resource::Type.new(:hostclass, '', code: bridge))
45
- @topscope.source = @main
46
- @main_resource = Puppet::Parser::Resource.new('class', :main, scope: @topscope, source: @main)
47
- @topscope.resource = @main_resource
48
- add_resource(@topscope, @main_resource)
49
-
50
- @main_resource.evaluate
51
- end
52
- end
53
- end
54
- end
8
+ require 'bolt/catalog/compiler'
9
+ require 'bolt/catalog/logging'
55
10
 
56
11
  module Bolt
57
12
  class Catalog
@@ -64,7 +19,10 @@ module Bolt
64
19
  Puppet.settings.send(:clear_everything_for_tests)
65
20
  Puppet.initialize_settings(cli)
66
21
  Puppet.settings[:hiera_config] = hiera_config
67
- # self.class.configure_logging
22
+
23
+ # Use a special logdest that serializes all log messages and their level to stderr.
24
+ Puppet::Util::Log.newdestination(:stderr)
25
+ Puppet.settings[:log_level] = 'debug'
68
26
  yield
69
27
  end
70
28
  end
@@ -97,10 +55,7 @@ module Bolt
97
55
  pal_main = request['code_ast'] || request['code_string']
98
56
  target = request['target']
99
57
 
100
- pdb_client = Bolt::Util::OnAccess.new do
101
- pdb_config = Bolt::PuppetDB::Config.new(nil, request['pdb_config'])
102
- Bolt::PuppetDB::Client.from_config(pdb_config)
103
- end
58
+ pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
104
59
 
105
60
  with_puppet_settings(request['hiera_config']) do
106
61
  Puppet[:code] = ''
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class exists to override evaluate_main and let us inject
4
+ # AST instead of looking for the main manifest. A better option may be to set up the
5
+ # node environment so our AST is in the '' hostclass instead of doing it here.
6
+ module Puppet
7
+ module Parser
8
+ class BoltCompiler < Puppet::Parser::Compiler
9
+ def internal_evaluator
10
+ @internal_evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new
11
+ end
12
+
13
+ def dump_ast(ast)
14
+ Puppet::Pops::Serialization::ToDataConverter.convert(ast, rich_data: true, symbol_to_string: true)
15
+ end
16
+
17
+ def load_ast(ast_data)
18
+ Puppet::Pops::Serialization::FromDataConverter.convert(ast_data)
19
+ end
20
+
21
+ def parse_string(string, file = '')
22
+ internal_evaluator.parse_string(string, file)
23
+ end
24
+
25
+ def evaluate_main
26
+ main = Puppet.lookup(:pal_main)
27
+ ast = if main.is_a?(String)
28
+ parse_string(main)
29
+ else
30
+ load_ast(main)
31
+ end
32
+
33
+ bridge = Puppet::Parser::AST::PopsBridge::Program.new(ast)
34
+
35
+ # This is more or less copypaste from the super but we don't use the
36
+ # original host_class.
37
+ krt = environment.known_resource_types
38
+ @main = krt.add(Puppet::Resource::Type.new(:hostclass, '', code: bridge))
39
+ @topscope.source = @main
40
+ @main_resource = Puppet::Parser::Resource.new('class', :main, scope: @topscope, source: @main)
41
+ @topscope.resource = @main_resource
42
+ add_resource(@topscope, @main_resource)
43
+
44
+ @main_resource.evaluate
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Puppet::Util::Log.newdesttype :stderr do
4
+ def initialize
5
+ # Flush output immediately.
6
+ $stderr.sync = true
7
+ end
8
+
9
+ # Emits message as a single line of JSON mapping level to message string.
10
+ def handle(msg)
11
+ str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
12
+ str = msg.source == "Puppet" ? str : "#{msg.source}: #{str}"
13
+ warn({ msg.level => str }.to_json)
14
+ end
15
+ end
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'io/console'
7
7
  require 'logging'
8
8
  require 'optparse'
9
+ require 'r10k/action/puppetfile/install'
9
10
  require 'bolt/analytics'
10
11
  require 'bolt/bolt_option_parser'
11
12
  require 'bolt/config'
@@ -16,26 +17,27 @@ require 'bolt/logger'
16
17
  require 'bolt/outputter'
17
18
  require 'bolt/puppetdb'
18
19
  require 'bolt/pal'
20
+ require 'bolt/r10k_log_proxy'
19
21
  require 'bolt/target'
20
22
  require 'bolt/version'
21
- require 'bolt/util/on_access'
22
23
 
23
24
  module Bolt
24
25
  class CLIExit < StandardError; end
25
26
  class CLI
26
- COMMANDS = { 'command' => %w[run],
27
- 'script' => %w[run],
28
- 'task' => %w[show run],
29
- 'plan' => %w[show run],
30
- 'file' => %w[upload] }.freeze
27
+ COMMANDS = { 'command' => %w[run],
28
+ 'script' => %w[run],
29
+ 'task' => %w[show run],
30
+ 'plan' => %w[show run],
31
+ 'file' => %w[upload],
32
+ 'puppetfile' => %w[install] }.freeze
31
33
 
32
34
  attr_reader :config, :options
33
35
 
34
36
  def initialize(argv)
35
37
  Bolt::Logger.initialize_logging
36
38
  @logger = Logging.logger[self]
37
- @config = Bolt::Config.new
38
39
  @argv = argv
40
+ @config = Bolt::Config.default
39
41
  @options = {
40
42
  nodes: []
41
43
  }
@@ -48,15 +50,15 @@ module Bolt
48
50
  private :inventory
49
51
 
50
52
  def help?(parser, remaining)
51
- # Set the mode
52
- options[:mode] = remaining.shift
53
+ # Set the subcommand
54
+ options[:subcommand] = remaining.shift
53
55
 
54
- if options[:mode] == 'help'
56
+ if options[:subcommand] == 'help'
55
57
  options[:help] = true
56
- options[:mode] = remaining.shift
58
+ options[:subcommand] = remaining.shift
57
59
  end
58
60
 
59
- # Update the parser for the new mode
61
+ # Update the parser for the new subcommand
60
62
  parser.update
61
63
 
62
64
  options[:help]
@@ -73,12 +75,8 @@ module Bolt
73
75
  raise Bolt::CLIExit
74
76
  end
75
77
 
76
- config.update(options)
77
- config.validate
78
- Bolt::Logger.configure(config)
79
-
80
78
  # This section handles parsing non-flag options which are
81
- # mode specific rather then part of the config
79
+ # subcommand specific rather then part of the config
82
80
  options[:action] = remaining.shift
83
81
  options[:object] = remaining.shift
84
82
 
@@ -99,15 +97,28 @@ module Bolt
99
97
 
100
98
  validate(options)
101
99
 
100
+ @config = if options[:configfile]
101
+ Bolt::Config.from_file(options[:configfile], options)
102
+ else
103
+ boltdir = if options[:boltdir]
104
+ Bolt::Boltdir.new(options[:boltdir])
105
+ else
106
+ Bolt::Boltdir.find_boltdir(Dir.pwd)
107
+ end
108
+ Bolt::Config.from_boltdir(boltdir, options)
109
+ end
110
+
111
+ Bolt::Logger.configure(config.log, config.color)
112
+
102
113
  # After validation, initialize inventory and targets. Errors here are better to catch early.
103
- unless options[:action] == 'show'
114
+ unless options[:subcommand] == 'puppetfile' || options[:action] == 'show'
104
115
  if options[:query]
105
116
  if options[:nodes].any?
106
117
  raise Bolt::CLIError, "Only one of '--nodes' or '--query' may be specified"
107
118
  end
108
119
  nodes = query_puppetdb_nodes(options[:query])
109
120
  options[:targets] = inventory.get_targets(nodes)
110
- options[:nodes] = nodes if options[:mode] == 'plan'
121
+ options[:nodes] = nodes if options[:subcommand] == 'plan'
111
122
  else
112
123
  options[:targets] = inventory.get_targets(options[:nodes])
113
124
  end
@@ -120,42 +131,42 @@ module Bolt
120
131
  end
121
132
 
122
133
  def validate(options)
123
- unless COMMANDS.include?(options[:mode])
134
+ unless COMMANDS.include?(options[:subcommand])
124
135
  raise Bolt::CLIError,
125
- "Expected subcommand '#{options[:mode]}' to be one of " \
136
+ "Expected subcommand '#{options[:subcommand]}' to be one of " \
126
137
  "#{COMMANDS.keys.join(', ')}"
127
138
  end
128
139
 
129
140
  if options[:action].nil?
130
141
  raise Bolt::CLIError,
131
- "Expected an action of the form 'bolt #{options[:mode]} <action>'"
142
+ "Expected an action of the form 'bolt #{options[:subcommand]} <action>'"
132
143
  end
133
144
 
134
- actions = COMMANDS[options[:mode]]
145
+ actions = COMMANDS[options[:subcommand]]
135
146
  unless actions.include?(options[:action])
136
147
  raise Bolt::CLIError,
137
148
  "Expected action '#{options[:action]}' to be one of " \
138
149
  "#{actions.join(', ')}"
139
150
  end
140
151
 
141
- if options[:mode] != 'file' && options[:mode] != 'script' &&
152
+ if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
142
153
  !options[:leftovers].empty?
143
154
  raise Bolt::CLIError,
144
155
  "Unknown argument(s) #{options[:leftovers].join(', ')}"
145
156
  end
146
157
 
147
- if %w[task plan].include?(options[:mode]) && options[:action] == 'run'
158
+ if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
148
159
  if options[:object].nil?
149
- raise Bolt::CLIError, "Must specify a #{options[:mode]} to run"
160
+ raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
150
161
  end
151
162
  # This may mean that we parsed a parameter as the object
152
163
  unless options[:object] =~ /\A([a-z][a-z0-9_]*)?(::[a-z][a-z0-9_]*)*\Z/
153
164
  raise Bolt::CLIError,
154
- "Invalid #{options[:mode]} '#{options[:object]}'"
165
+ "Invalid #{options[:subcommand]} '#{options[:object]}'"
155
166
  end
156
167
  end
157
168
 
158
- if options[:mode] != 'plan' && options[:action] != 'show'
169
+ if !%w[plan puppetfile].include?(options[:subcommand]) && options[:action] != 'show'
159
170
  if options[:nodes].empty? && options[:query].nil?
160
171
  raise Bolt::CLIError, "Targets must be specified with '--nodes' or '--query'"
161
172
  elsif options[:nodes].any? && options[:query]
@@ -163,7 +174,11 @@ module Bolt
163
174
  end
164
175
  end
165
176
 
166
- if options[:noop] && (options[:mode] != 'task' || options[:action] != 'run')
177
+ if options[:boltdir] && options[:configfile]
178
+ raise Bolt::CLIError, "Only one of '--boltdir' or '--configfile' may be specified"
179
+ end
180
+
181
+ if options[:noop] && (options[:subcommand] != 'task' || options[:action] != 'run')
167
182
  raise Bolt::CLIError,
168
183
  "Option '--noop' may only be specified when running a task"
169
184
  end
@@ -181,10 +196,8 @@ module Bolt
181
196
 
182
197
  def puppetdb_client
183
198
  return @puppetdb_client if @puppetdb_client
184
- @puppetdb_client = Bolt::Util::OnAccess.new do
185
- puppetdb_config = Bolt::PuppetDB::Config.new(nil, config.puppetdb)
186
- Bolt::PuppetDB::Client.from_config(puppetdb_config)
187
- end
199
+ puppetdb_config = Bolt::PuppetDB::Config.load_config(nil, config.puppetdb)
200
+ @puppetdb_client = Bolt::PuppetDB::Client.new(puppetdb_config)
188
201
  end
189
202
 
190
203
  def query_puppetdb_nodes(query)
@@ -203,38 +216,30 @@ module Bolt
203
216
 
204
217
  @analytics = Bolt::Analytics.build_client
205
218
 
206
- screen = "#{options[:mode]}_#{options[:action]}"
219
+ screen = "#{options[:subcommand]}_#{options[:action]}"
207
220
  # submit a different screen for `bolt task show` and `bolt task show foo`
208
221
  if options[:action] == 'show' && options[:object]
209
222
  screen += '_object'
210
223
  end
211
224
 
212
225
  @analytics.screen_view(screen,
213
- output_format: config[:format],
226
+ output_format: config.format,
214
227
  target_nodes: options.fetch(:targets, []).count,
215
228
  inventory_nodes: inventory.node_names.count,
216
229
  inventory_groups: inventory.group_names.count)
217
230
 
218
- if options[:mode] == 'plan' || options[:mode] == 'task'
219
- pal = Bolt::PAL.new(config)
220
- end
221
-
222
231
  if options[:action] == 'show'
223
- if options[:mode] == 'task'
232
+ if options[:subcommand] == 'task'
224
233
  if options[:object]
225
- outputter.print_task_info(pal.get_task_info(options[:object]))
234
+ show_task(options[:object])
226
235
  else
227
- outputter.print_table(pal.list_tasks)
228
- outputter.print_message("\nUse `bolt task show <task-name>` to view "\
229
- "details and parameters for a specific task.")
236
+ list_tasks
230
237
  end
231
- elsif options[:mode] == 'plan'
238
+ elsif options[:subcommand] == 'plan'
232
239
  if options[:object]
233
- outputter.print_plan_info(pal.get_plan_info(options[:object]))
240
+ show_plan(options[:object])
234
241
  else
235
- outputter.print_table(pal.list_plans)
236
- outputter.print_message("\nUse `bolt plan show <plan-name>` to view "\
237
- "details and parameters for a specific plan.")
242
+ list_plans
238
243
  end
239
244
  end
240
245
  return 0
@@ -242,36 +247,16 @@ module Bolt
242
247
 
243
248
  message = 'There may be processes left executing on some nodes.'
244
249
 
245
- if options[:task_options] && !options[:params_parsed] && pal
246
- options[:task_options] = pal.parse_params(options[:mode], options[:object], options[:task_options])
250
+ if %w[task plan].include?(options[:subcommand]) && options[:task_options] && !options[:params_parsed] && pal
251
+ options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
247
252
  end
248
253
 
249
- if options[:mode] == 'plan'
250
- unless options[:nodes].empty?
251
- if options[:task_options]['nodes']
252
- raise Bolt::CLIError,
253
- "A plan's 'nodes' parameter may be specified using the --nodes option, but in that " \
254
- "case it must not be specified as a separate nodes=<value> parameter nor included " \
255
- "in the JSON data passed in the --params option"
256
- end
257
- options[:task_options]['nodes'] = options[:nodes].join(',')
258
- end
259
-
260
- params = options[:noop] ? options[:task_options].merge("_noop" => true) : options[:task_options]
261
- plan_context = { plan_name: options[:object],
262
- params: params }
263
- plan_context[:description] = options[:description] if options[:description]
264
-
265
- executor = Bolt::Executor.new(config, @analytics, options[:noop], bundled_content: bundled_content)
266
- executor.start_plan(plan_context)
267
- result = pal.run_plan(options[:object], options[:task_options], executor, inventory, puppetdb_client)
268
-
269
- # If a non-bolt exeception bubbles up the plan won't get finished
270
- executor.finish_plan(result)
271
- outputter.print_plan_result(result)
272
- code = result.ok? ? 0 : 1
254
+ if options[:subcommand] == 'plan'
255
+ code = run_plan(options[:object], options[:task_options], options[:nodes], options)
256
+ elsif options[:subcommand] == 'puppetfile'
257
+ code = install_puppetfile(@config.puppetfile, @config.modulepath)
273
258
  else
274
- executor = Bolt::Executor.new(config, @analytics, options[:noop], bundled_content: bundled_content)
259
+ executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop], bundled_content: bundled_content)
275
260
  targets = options[:targets]
276
261
 
277
262
  results = nil
@@ -281,7 +266,7 @@ module Bolt
281
266
  executor_opts = {}
282
267
  executor_opts['_description'] = options[:description] if options.key?(:description)
283
268
  results =
284
- case options[:mode]
269
+ case options[:subcommand]
285
270
  when 'command'
286
271
  executor.run_command(targets, options[:object], executor_opts) do |event|
287
272
  outputter.print_event(event)
@@ -330,6 +315,80 @@ module Bolt
330
315
  @analytics&.finish
331
316
  end
332
317
 
318
+ def show_task(task_name)
319
+ outputter.print_task_info(pal.get_task_info(task_name))
320
+ end
321
+
322
+ def list_tasks
323
+ outputter.print_table(pal.list_tasks)
324
+ outputter.print_message("\nUse `bolt task show <task-name>` to view "\
325
+ "details and parameters for a specific task.")
326
+ end
327
+
328
+ def show_plan(plan_name)
329
+ outputter.print_plan_info(pal.get_plan_info(plan_name))
330
+ end
331
+
332
+ def list_plans
333
+ outputter.print_table(pal.list_plans)
334
+ outputter.print_message("\nUse `bolt plan show <plan-name>` to view "\
335
+ "details and parameters for a specific plan.")
336
+ end
337
+
338
+ def run_plan(plan_name, plan_arguments, nodes, options)
339
+ unless nodes.empty?
340
+ if plan_arguments['nodes']
341
+ raise Bolt::CLIError,
342
+ "A plan's 'nodes' parameter may be specified using the --nodes option, but in that " \
343
+ "case it must not be specified as a separate nodes=<value> parameter nor included " \
344
+ "in the JSON data passed in the --params option"
345
+ end
346
+ plan_arguments['nodes'] = nodes.join(',')
347
+ end
348
+
349
+ params = options[:noop] ? plan_arguments.merge('_noop' => true) : plan_arguments
350
+ plan_context = { plan_name: plan_name,
351
+ params: params }
352
+ plan_context[:description] = options[:description] if options[:description]
353
+
354
+ executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop], bundled_content: bundled_content)
355
+ executor.start_plan(plan_context)
356
+ result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
357
+
358
+ # If a non-bolt exeception bubbles up the plan won't get finished
359
+ executor.finish_plan(result)
360
+ outputter.print_plan_result(result)
361
+ result.ok? ? 0 : 1
362
+ end
363
+
364
+ def install_puppetfile(puppetfile, modulepath)
365
+ if puppetfile.exist?
366
+ moduledir = modulepath.first.to_s
367
+ r10k_config = {
368
+ root: puppetfile.dirname.to_s,
369
+ puppetfile: puppetfile.to_s,
370
+ moduledir: moduledir
371
+ }
372
+ install_action = R10K::Action::Puppetfile::Install.new(r10k_config, nil)
373
+
374
+ # Override the r10k logger with a proxy to our own logger
375
+ R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
376
+
377
+ ok = install_action.call
378
+ outputter.print_puppetfile_result(ok, puppetfile, moduledir)
379
+
380
+ ok ? 0 : 1
381
+ else
382
+ raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
383
+ end
384
+ rescue R10K::Error => e
385
+ raise PuppetfileError, e
386
+ end
387
+
388
+ def pal
389
+ @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.compile_concurrency)
390
+ end
391
+
333
392
  def validate_file(type, path)
334
393
  if path.nil?
335
394
  raise Bolt::CLIError, "A #{type} must be specified"
@@ -351,18 +410,20 @@ module Bolt
351
410
  end
352
411
 
353
412
  def outputter
354
- @outputter ||= Bolt::Outputter.for_format(config[:format], config[:color], config[:trace])
413
+ @outputter ||= Bolt::Outputter.for_format(config.format, config.color, config.trace)
355
414
  end
356
415
 
357
416
  def bundled_content
358
- default_content = Bolt::PAL.new(Bolt::Config.new)
359
- plans = default_content.list_plans.each_with_object([]) do |iter, col|
360
- col << iter&.first
361
- end
362
- tasks = default_content.list_tasks.each_with_object([]) do |iter, col|
363
- col << iter&.first
417
+ if %w[plan task].include?(options[:subcommand])
418
+ default_content = Bolt::PAL.new([], nil)
419
+ plans = default_content.list_plans.each_with_object([]) do |iter, col|
420
+ col << iter&.first
421
+ end
422
+ tasks = default_content.list_tasks.each_with_object([]) do |iter, col|
423
+ col << iter&.first
424
+ end
425
+ plans.concat tasks
364
426
  end
365
- plans.concat tasks
366
427
  end
367
428
  end
368
429
  end