bolt 3.13.0 → 3.16.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
  9. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
  10. data/guides/{debugging.txt → debugging.yaml} +5 -6
  11. data/guides/{inventory.txt → inventory.yaml} +6 -7
  12. data/guides/{links.txt → links.yaml} +3 -4
  13. data/guides/{logging.txt → logging.yaml} +5 -6
  14. data/guides/{module.txt → module.yaml} +5 -6
  15. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  16. data/guides/{project.txt → project.yaml} +6 -7
  17. data/guides/{targets.txt → targets.yaml} +5 -6
  18. data/guides/{transports.txt → transports.yaml} +6 -7
  19. data/lib/bolt/analytics.rb +3 -20
  20. data/lib/bolt/application.rb +620 -0
  21. data/lib/bolt/bolt_option_parser.rb +17 -5
  22. data/lib/bolt/cli.rb +592 -772
  23. data/lib/bolt/config/transport/options.rb +12 -0
  24. data/lib/bolt/config/transport/ssh.rb +7 -0
  25. data/lib/bolt/executor.rb +12 -4
  26. data/lib/bolt/fiber_executor.rb +63 -14
  27. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  28. data/lib/bolt/outputter/human.rb +199 -43
  29. data/lib/bolt/outputter/json.rb +66 -43
  30. data/lib/bolt/outputter/logger.rb +1 -1
  31. data/lib/bolt/pal.rb +67 -14
  32. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  33. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  34. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  36. data/lib/bolt/plan_creator.rb +2 -20
  37. data/lib/bolt/plan_future.rb +23 -3
  38. data/lib/bolt/plan_result.rb +1 -1
  39. data/lib/bolt/plugin/task.rb +1 -1
  40. data/lib/bolt/project.rb +0 -7
  41. data/lib/bolt/result_set.rb +2 -1
  42. data/lib/bolt/transport/local/connection.rb +17 -1
  43. data/lib/bolt/transport/orch/connection.rb +13 -1
  44. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  45. data/lib/bolt/version.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -0
  47. data/lib/bolt_server/schemas/action-apply.json +32 -0
  48. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +180 -60
  52. data/lib/bolt_spec/plans/mock_executor.rb +16 -6
  53. metadata +23 -15
  54. data/guides/guide.txt +0 -17
  55. data/lib/bolt/secret.rb +0 -37
@@ -59,7 +59,7 @@ module Bolt
59
59
  run_opts = {}
60
60
  run_opts[:run_as] = opts['_run_as'] if opts['_run_as']
61
61
  begin
62
- task = apply_prep.get_task(opts['task'], params)
62
+ task = @context.get_validated_task(opts['task'], params)
63
63
  rescue Bolt::Error => e
64
64
  raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, name, 'puppet_library')
65
65
  end
data/lib/bolt/project.rb CHANGED
@@ -209,12 +209,5 @@ module Bolt
209
209
  Bolt::Logger.warn("missing_project_name", message)
210
210
  end
211
211
  end
212
-
213
- def check_deprecated_file
214
- if (@path + 'project.yaml').file?
215
- msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
216
- Bolt::Logger.warn("project_yaml", msg)
217
- end
218
- end
219
212
  end
220
213
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Bolt
4
4
  class ResultSet
5
- attr_reader :results
5
+ attr_accessor :elapsed_time
6
+ attr_reader :results
6
7
 
7
8
  include Enumerable
8
9
 
@@ -10,6 +10,8 @@ module Bolt
10
10
  module Transport
11
11
  class Local < Simple
12
12
  class Connection
13
+ RUBY_ENV_VARS = %w[GEM_PATH GEM_HOME RUBYLIB RUBYLIB_PREFIX RUBYOPT RUBYPATH RUBYSHELL].freeze
14
+
13
15
  attr_accessor :user, :logger, :target
14
16
 
15
17
  def initialize(target)
@@ -68,7 +70,21 @@ module Bolt
68
70
  end
69
71
  end
70
72
 
71
- Open3.popen3(*command)
73
+ # Only do this if bundled-ruby is set to false, not nil
74
+ ruby_env_vars = if target.transport_config['bundled-ruby'] == false
75
+ RUBY_ENV_VARS.each_with_object({}) do |e, acc|
76
+ acc[e] = ENV["BOLT_ORIG_#{e}"] if ENV["BOLT_ORIG_#{e}"]
77
+ end
78
+ end
79
+
80
+ if target.transport_config['bundled-ruby'] == false &&
81
+ Gem.loaded_specs.keys.include?('bundler')
82
+ Bundler.with_unbundled_env do
83
+ Open3.popen3(ruby_env_vars || {}, *command)
84
+ end
85
+ else
86
+ Open3.popen3(ruby_env_vars || {}, *command)
87
+ end
72
88
  end
73
89
 
74
90
  # This is used by the Bash shell to decide whether to `cd` before
@@ -36,7 +36,8 @@ module Bolt
36
36
  end
37
37
  logger.debug("Creating orchestrator client for #{client_opts}")
38
38
  @client = OrchestratorClient.new(client_opts, true)
39
- @plan_job = start_plan(plan_context)
39
+ @plan_context = plan_context
40
+ @plan_job = start_plan(@plan_context)
40
41
  logger.debug("Started plan #{@plan_job}")
41
42
  @environment = opts["task-environment"]
42
43
  end
@@ -87,6 +88,17 @@ module Bolt
87
88
  def run_task(targets, task, arguments, options)
88
89
  body = build_request(targets, task, arguments, options[:description])
89
90
  @client.run_task(body)
91
+ rescue OrchestratorClient::ApiError => e
92
+ if e.data['kind'] == 'puppetlabs.orchestrator/plan-already-finished'
93
+ @logger.debug("Retrying the task")
94
+ # Instead of recursing, just retry once
95
+ @plan_job = start_plan(@plan_context)
96
+ # Rebuild the request with the new plan job ID
97
+ body = build_request(targets, task, arguments, options[:description])
98
+ @client.run_task(body)
99
+ else
100
+ raise e
101
+ end
90
102
  end
91
103
 
92
104
  def query_inventory(targets)
@@ -47,7 +47,9 @@ module Bolt
47
47
  cmd = []
48
48
  # BatchMode is SSH's noninteractive option: if key authentication
49
49
  # fails it will error out instead of falling back to password prompt
50
- cmd += %w[-o BatchMode=yes]
50
+ batch_mode = @target.transport_config['batch-mode'] ? 'yes' : 'no'
51
+ cmd += %W[-o BatchMode=#{batch_mode}]
52
+
51
53
  cmd += %W[-o Port=#{@target.port}] if @target.port
52
54
 
53
55
  if @target.transport_config.key?('host-key-check')
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.13.0'
4
+ VERSION = '3.16.1'
5
5
  end
@@ -182,5 +182,17 @@ module BoltServer
182
182
  end
183
183
  end
184
184
  end
185
+
186
+ def get_cached_project_file(versioned_project, file_name)
187
+ file_dir = create_cache_dir(versioned_project)
188
+ file_path = File.join(file_dir, file_name)
189
+ serial_execute { File.read(file_path) if File.exist?(file_path) }
190
+ end
191
+
192
+ def cache_project_file(versioned_project, file_name, data)
193
+ file_dir = create_cache_dir(versioned_project)
194
+ file_path = File.join(file_dir, file_name)
195
+ serial_execute { File.open(file_path, 'w') { |f| f.write(data) } }
196
+ end
185
197
  end
186
198
  end
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "apply request",
4
+ "description": "POST <transport>/apply request schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "versioned_project": {
8
+ "type": "string",
9
+ "description": "Project from which to load code"
10
+ },
11
+ "parameters": {
12
+ "type": "object",
13
+ "properties": {
14
+ "catalog": {
15
+ "type": "object",
16
+ "description": "Compiled catalog to apply"
17
+ },
18
+ "apply_options": {
19
+ "type": "object",
20
+ "description": "Options for application of a catalog"
21
+ }
22
+ }
23
+ },
24
+ "job_id": {
25
+ "type": "integer",
26
+ "description": "job-id associated with request"
27
+ },
28
+ "target": { "$ref": "partial:target-any" }
29
+ },
30
+ "required": ["target", "versioned_project", "parameters", "job_id"],
31
+ "additionalProperties": false
32
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "apply_prep request",
4
+ "description": "POST <transport>/apply_prep request schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "versioned_project": {
8
+ "type": "String",
9
+ "description": "Project from which to load code"
10
+ },
11
+ "target": { "$ref": "partial:target-any" },
12
+ "job_id": {
13
+ "type": "integer",
14
+ "description": "job-id associated with request"
15
+ }
16
+ },
17
+ "required": ["target", "versioned_project", "job_id"],
18
+ "additionalProperties": false
19
+ }
@@ -62,6 +62,10 @@
62
62
  "interpreters": {
63
63
  "type": "object",
64
64
  "description": "Map of file extensions to remote executable"
65
+ },
66
+ "plugin_hooks": {
67
+ "type": "object",
68
+ "description": "Configuration for plugins to use"
65
69
  }
66
70
  },
67
71
  "oneOf": [
@@ -56,6 +56,10 @@
56
56
  "smb-port": {
57
57
  "type": "integer",
58
58
  "description": "Port for SMB protocol"
59
+ },
60
+ "plugin_hooks": {
61
+ "type": "object",
62
+ "description": "Configuration for plugins to use"
59
63
  }
60
64
  },
61
65
  "required": ["hostname", "user", "password"],
@@ -43,6 +43,8 @@ module BoltServer
43
43
  transport-ssh
44
44
  transport-winrm
45
45
  connect-data
46
+ action-apply_prep
47
+ action-apply
46
48
  ].freeze
47
49
 
48
50
  # PE_BOLTLIB_PATH is intended to function exactly like the BOLTLIB_PATH used
@@ -75,6 +77,12 @@ module BoltServer
75
77
  # This is needed until the PAL is threadsafe.
76
78
  @pal_mutex = Mutex.new
77
79
 
80
+ # Avoid redundant plugin tarbal construction
81
+ @plugin_mutex = Mutex.new
82
+
83
+ # Avoid redundant project_task metadata construction
84
+ @task_metadata_mutex = Mutex.new
85
+
78
86
  @logger = Bolt::Logger.logger(self)
79
87
 
80
88
  super(nil)
@@ -118,12 +126,7 @@ module BoltServer
118
126
  end
119
127
  end
120
128
 
121
- def run_task(target, body)
122
- validate_schema(@schemas["action-run_task"], body)
123
-
124
- task_data = body['task']
125
- task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
126
- parameters = body['parameters'] || {}
129
+ def task_helper(target, task, parameters)
127
130
  # Wrap parameters marked with '"sensitive": true' in the task metadata with a
128
131
  # Sensitive wrapper type. This way it's not shown in logs.
129
132
  if (param_spec = task.parameters)
@@ -142,6 +145,101 @@ module BoltServer
142
145
  end
143
146
  end
144
147
 
148
+ def run_task(target, body)
149
+ validate_schema(@schemas["action-run_task"], body)
150
+
151
+ task_data = body['task']
152
+ task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
153
+ task_helper(target, task, body['parameters'] || {})
154
+ end
155
+
156
+ def extract_install_task(target)
157
+ unless target.plugin_hooks['puppet_library']['task']
158
+ raise BoltServer::RequestError,
159
+ "Target must have 'task' plugin hook"
160
+ end
161
+ install_task = target.plugin_hooks['puppet_library']['task'].split('::', 2)
162
+ install_task << 'init' if install_task.count == 1
163
+ install_task
164
+ end
165
+
166
+ # This helper is responsible for computing or retrieving from the cache a plugin tarball. There are
167
+ # two supported plugin types 'fact_plugins', and 'all_plugins'. Note that this is cached based on
168
+ # versioned_project as there are no plugins in the "builtin content" directory
169
+ def plugin_tarball(versioned_project, tarball_type)
170
+ tarball_types = %w[fact_plugins all_plugins]
171
+ unless tarball_types.include?(tarball_type)
172
+ raise ArgumentError,
173
+ "tarball_type must be one of: #{tarball_types.join(', ')}"
174
+ end
175
+ # lock this so that in the case an apply/apply_prep with multiple targets hits this endpoint
176
+ # the tarball computation only happens once (all the other targets will just need to read the cached data)
177
+ @plugin_mutex.synchronize do
178
+ if (tarball = @file_cache.get_cached_project_file(versioned_project, tarball_type))
179
+ tarball
180
+ else
181
+ new_tarball = build_project_plugins_tarball(versioned_project) do |mod|
182
+ search_dirs = []
183
+ search_dirs << mod.plugins if mod.plugins?
184
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
185
+ if tarball_type == 'all_plugins'
186
+ search_dirs << mod.files if mod.files?
187
+ type_files = "#{mod.path}/types"
188
+ search_dirs << type_files if File.exist?(type_files)
189
+ end
190
+ search_dirs
191
+ end
192
+ @file_cache.cache_project_file(versioned_project, tarball_type, new_tarball)
193
+ new_tarball
194
+ end
195
+ end
196
+ end
197
+
198
+ # This helper is responsible for computing or retrieving task metadata for a project.
199
+ # It expects task name in segments and uses the combination of task name and versioned_project
200
+ # as a unique identifier for caching in addition to the job_id. The job id is added to protect against
201
+ # a case where the buildtin content is update (where the apply_helpers would be managed)
202
+ def project_task_metadata(versioned_project, task_name_segments, job_id)
203
+ cached_file_name = "#{task_name_segments.join('_')}_#{job_id}"
204
+ # lock this so that in the case an apply/apply_prep with multiple targets hits this endpoint the
205
+ # metadata computation will only be computed once, then the cache will be read.
206
+ @task_metadata_mutex.synchronize do
207
+ if (metadata = @file_cache.get_cached_project_file(versioned_project, cached_file_name))
208
+ JSON.parse(metadata)
209
+ else
210
+ new_metadata = in_bolt_project(versioned_project) do |context|
211
+ ps_parameters = {
212
+ 'versioned_project' => versioned_project
213
+ }
214
+ pe_task_info(context[:pal], *task_name_segments, ps_parameters)
215
+ end
216
+ @file_cache.cache_project_file(versioned_project, cached_file_name, new_metadata.to_json)
217
+ new_metadata
218
+ end
219
+ end
220
+ end
221
+
222
+ def apply_prep(target, body)
223
+ validate_schema(@schemas["action-apply_prep"], body)
224
+ plugins_tarball = plugin_tarball(body['versioned_project'], 'fact_plugins')
225
+ install_task_segments = extract_install_task(target.first)
226
+ task_data = project_task_metadata(body['versioned_project'], install_task_segments, body["job_id"])
227
+ task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
228
+ install_task_result = task_helper(target, task, target.first.plugin_hooks['puppet_library']['parameters'] || {})
229
+ return install_task_result unless install_task_result.ok
230
+ task_data = project_task_metadata(body['versioned_project'], %w[apply_helpers custom_facts], body["job_id"])
231
+ task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
232
+ task_helper(target, task, { 'plugins' => plugins_tarball })
233
+ end
234
+
235
+ def apply(target, body)
236
+ validate_schema(@schemas["action-apply"], body)
237
+ plugins_tarball = plugin_tarball(body['versioned_project'], 'all_plugins')
238
+ task_data = project_task_metadata(body['versioned_project'], %w[apply_helpers apply_catalog], body["job_id"])
239
+ task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
240
+ task_helper(target, task, body['parameters'].merge({ 'plugins' => plugins_tarball }))
241
+ end
242
+
145
243
  def run_command(target, body)
146
244
  validate_schema(@schemas["action-run_command"], body)
147
245
  command = body['command']
@@ -389,6 +487,63 @@ module BoltServer
389
487
  end
390
488
  end
391
489
 
490
+ # The provided block takes a module object and returns the list
491
+ # of directories to search through. This is similar to
492
+ # Bolt::Applicator.build_plugin_tarball.
493
+ def build_project_plugins_tarball(versioned_project, &block)
494
+ start_time = Time.now
495
+
496
+ # Fetch the plugin files
497
+ plugin_files = in_bolt_project(versioned_project) do |context|
498
+ files = {}
499
+
500
+ # Bolt also sets plugin_modulepath to user modulepath so do it here too for
501
+ # consistency
502
+ plugin_modulepath = context[:pal].user_modulepath
503
+ Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
504
+ search_dirs = block.call(mod)
505
+
506
+ files[mod] ||= []
507
+ Find.find(*search_dirs).each do |file|
508
+ files[mod] << file if File.file?(file)
509
+ end
510
+ end
511
+
512
+ files
513
+ end
514
+
515
+ # Pack the plugin files
516
+ sio = StringIO.new
517
+ begin
518
+ output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
519
+
520
+ plugin_files.each do |mod, files|
521
+ tar_dir = Pathname.new(mod.name)
522
+ mod_dir = Pathname.new(mod.path)
523
+
524
+ files.each do |file|
525
+ tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
526
+ stat = File.stat(file)
527
+ content = File.binread(file)
528
+ output.tar.add_file_simple(
529
+ tar_path.to_s,
530
+ data: content,
531
+ size: content.size,
532
+ mode: stat.mode & 0o777,
533
+ mtime: stat.mtime
534
+ )
535
+ end
536
+ end
537
+
538
+ duration = Time.now - start_time
539
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
540
+ ensure
541
+ output.close
542
+ end
543
+
544
+ Base64.encode64(sio.string)
545
+ end
546
+
392
547
  get '/' do
393
548
  200
394
549
  end
@@ -419,6 +574,8 @@ module BoltServer
419
574
  run_task
420
575
  run_script
421
576
  upload_file
577
+ apply
578
+ apply_prep
422
579
  ].freeze
423
580
 
424
581
  def make_ssh_target(target_hash)
@@ -441,8 +598,9 @@ module BoltServer
441
598
  'uri' => target_hash['hostname'],
442
599
  'config' => {
443
600
  'transport' => 'ssh',
444
- 'ssh' => opts
445
- }
601
+ 'ssh' => opts.slice(*Bolt::Config::Transport::SSH.options)
602
+ },
603
+ 'plugin_hooks' => target_hash['plugin_hooks']
446
604
  }
447
605
 
448
606
  inventory = Bolt::Inventory.empty
@@ -479,8 +637,9 @@ module BoltServer
479
637
  'uri' => target_hash['hostname'],
480
638
  'config' => {
481
639
  'transport' => 'winrm',
482
- 'winrm' => opts
483
- }
640
+ 'winrm' => opts.slice(*Bolt::Config::Transport::WinRM.options)
641
+ },
642
+ 'plugin_hooks' => target_hash['plugin_hooks']
484
643
  }
485
644
 
486
645
  inventory = Bolt::Inventory.empty
@@ -699,60 +858,21 @@ module BoltServer
699
858
  raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
700
859
  content_type :json
701
860
 
702
- # Inspired by Bolt::Applicator.build_plugin_tarball
703
- start_time = Time.now
704
-
705
- # Fetch the plugin files
706
- plugin_files = in_bolt_project(params['versioned_project']) do |context|
707
- files = {}
708
-
709
- # Bolt also sets plugin_modulepath to user modulepath so do it here too for
710
- # consistency
711
- plugin_modulepath = context[:pal].user_modulepath
712
- Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
713
- search_dirs = []
714
- search_dirs << mod.plugins if mod.plugins?
715
- search_dirs << mod.pluginfacts if mod.pluginfacts?
716
-
717
- files[mod] ||= []
718
- Find.find(*search_dirs).each do |file|
719
- files[mod] << file if File.file?(file)
720
- end
721
- end
722
-
723
- files
724
- end
861
+ plugins_tarball = plugin_tarball(params['versioned_project'], 'fact_plugins')
725
862
 
726
- # Pack the plugin files
727
- sio = StringIO.new
728
- begin
729
- output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
730
-
731
- plugin_files.each do |mod, files|
732
- tar_dir = Pathname.new(mod.name)
733
- mod_dir = Pathname.new(mod.path)
863
+ [200, plugins_tarball.to_json]
864
+ end
734
865
 
735
- files.each do |file|
736
- tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
737
- stat = File.stat(file)
738
- content = File.binread(file)
739
- output.tar.add_file_simple(
740
- tar_path.to_s,
741
- data: content,
742
- size: content.size,
743
- mode: stat.mode & 0o777,
744
- mtime: stat.mtime
745
- )
746
- end
747
- end
866
+ # Returns the base64 encoded tar archive of _all_ plugin code for a project
867
+ #
868
+ # @param versioned_project [String] the versioned_project to build the plugin tarball from
869
+ get '/project_plugin_tarball' do
870
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
871
+ content_type :json
748
872
 
749
- duration = Time.now - start_time
750
- @logger.trace("Packed plugins in #{duration * 1000} ms")
751
- ensure
752
- output.close
753
- end
873
+ plugins_tarball = plugin_tarball(params['versioned_project'], 'all_plugins')
754
874
 
755
- [200, Base64.encode64(sio.string).to_json]
875
+ [200, plugins_tarball.to_json]
756
876
  end
757
877
 
758
878
  error 404 do