bolt 3.22.1 → 3.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.

Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +9 -9
  3. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +32 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +20 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +23 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +28 -23
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +22 -19
  8. data/lib/bolt/application.rb +17 -10
  9. data/lib/bolt/applicator.rb +8 -2
  10. data/lib/bolt/bolt_option_parser.rb +4 -1
  11. data/lib/bolt/catalog.rb +1 -1
  12. data/lib/bolt/config/options.rb +65 -49
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/lxd.rb +9 -0
  15. data/lib/bolt/config.rb +12 -3
  16. data/lib/bolt/inventory/inventory.rb +30 -11
  17. data/lib/bolt/outputter/human.rb +10 -4
  18. data/lib/bolt/outputter/json.rb +3 -1
  19. data/lib/bolt/outputter/rainbow.rb +2 -1
  20. data/lib/bolt/pal/yaml_plan/loader.rb +1 -1
  21. data/lib/bolt/pal.rb +6 -2
  22. data/lib/bolt/plugin/puppetdb.rb +8 -5
  23. data/lib/bolt/plugin.rb +2 -1
  24. data/lib/bolt/puppetdb/client.rb +90 -129
  25. data/lib/bolt/puppetdb/config.rb +21 -8
  26. data/lib/bolt/puppetdb/instance.rb +146 -0
  27. data/lib/bolt/result.rb +1 -1
  28. data/lib/bolt/transport/orch/connection.rb +2 -1
  29. data/lib/bolt/transport/winrm/connection.rb +18 -11
  30. data/lib/bolt/version.rb +1 -1
  31. data/lib/bolt_server/file_cache.rb +10 -8
  32. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +1 -1
  33. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +1 -1
  34. data/lib/bolt_spec/plans/action_stubs/task_stub.rb +1 -1
  35. data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +1 -1
  36. data/lib/bolt_spec/plans/action_stubs.rb +2 -2
  37. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  38. data/lib/bolt_spec/plans.rb +11 -1
  39. metadata +10 -3
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'logging'
5
+ require_relative '../../bolt/puppetdb/config'
6
+
7
+ module Bolt
8
+ module PuppetDB
9
+ class Instance
10
+ attr_reader :config
11
+
12
+ def initialize(config:, project: nil, load_defaults: false)
13
+ @config = Bolt::PuppetDB::Config.new(config: config, project: project, load_defaults: load_defaults)
14
+ @bad_urls = []
15
+ @current_url = nil
16
+ @logger = Bolt::Logger.logger(self)
17
+ end
18
+
19
+ def make_query(query, path = nil)
20
+ body = JSON.generate(query: query)
21
+ url = "#{uri}/pdb/query/v4"
22
+ url += "/#{path}" if path
23
+
24
+ begin
25
+ @logger.debug("Sending PuppetDB query to #{url}")
26
+ response = http_client.post(url, body: body, header: headers)
27
+ rescue StandardError => e
28
+ raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{e}"
29
+ end
30
+
31
+ @logger.debug("Got response code #{response.code} from PuppetDB")
32
+ if response.code != 200
33
+ msg = "Failed to query PuppetDB: #{response.body}"
34
+ if response.code == 400
35
+ raise Bolt::PuppetDBError, msg
36
+ else
37
+ raise Bolt::PuppetDBFailoverError, msg
38
+ end
39
+ end
40
+
41
+ begin
42
+ JSON.parse(response.body)
43
+ rescue JSON::ParserError
44
+ raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
45
+ end
46
+ rescue Bolt::PuppetDBFailoverError => e
47
+ @logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
48
+ reject_url
49
+ make_query(query, path)
50
+ end
51
+
52
+ # Sends a command to PuppetDB using version 1 of the commands API.
53
+ # https://puppet.com/docs/puppetdb/latest/api/command/v1/commands.html
54
+ #
55
+ # @param command [String] The command to invoke.
56
+ # @param version [Integer] The version of the command to invoke.
57
+ # @param payload [Hash] The payload to send with the command.
58
+ # @return A UUID identifying the submitted command.
59
+ #
60
+ def send_command(command, version, payload)
61
+ command = command.dup.force_encoding('utf-8')
62
+ body = JSON.generate(payload)
63
+
64
+ # PDB requires the following query parameters to the POST request.
65
+ # Error early if there's no certname, as PDB does not return a
66
+ # message indicating it's required.
67
+ unless payload['certname']
68
+ raise Bolt::Error.new(
69
+ "Payload must include 'certname', unable to invoke command.",
70
+ 'bolt/pdb-command'
71
+ )
72
+ end
73
+
74
+ url = uri.tap do |u|
75
+ u.path = 'pdb/cmd/v1'
76
+ u.query_values = { 'command' => command,
77
+ 'version' => version,
78
+ 'certname' => payload['certname'] }
79
+ end
80
+
81
+ # Send the command to PDB
82
+ begin
83
+ @logger.debug("Sending PuppetDB command '#{command}' to #{url}")
84
+ response = http_client.post(url.to_s, body: body, header: headers)
85
+ rescue StandardError => e
86
+ raise Bolt::PuppetDBFailoverError, "Failed to invoke PuppetDB command: #{e}"
87
+ end
88
+
89
+ @logger.debug("Got response code #{response.code} from PuppetDB")
90
+ if response.code != 200
91
+ raise Bolt::PuppetDBError, "Failed to invoke PuppetDB command: #{response.body}"
92
+ end
93
+
94
+ # Return the UUID string from the response body
95
+ begin
96
+ JSON.parse(response.body).fetch('uuid', nil)
97
+ rescue JSON::ParserError
98
+ raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
99
+ end
100
+ rescue Bolt::PuppetDBFailoverError => e
101
+ @logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
102
+ reject_url
103
+ send_command(command, version, payload)
104
+ end
105
+
106
+ def http_client
107
+ return @http if @http
108
+ # lazy-load expensive gem code
109
+ require 'httpclient'
110
+ @logger.trace("Creating HTTP Client")
111
+ @http = HTTPClient.new
112
+ @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
113
+ @http.ssl_config.add_trust_ca(@config.cacert)
114
+ @http.connect_timeout = @config.connect_timeout if @config.connect_timeout
115
+ @http.receive_timeout = @config.read_timeout if @config.read_timeout
116
+
117
+ @http
118
+ end
119
+
120
+ def reject_url
121
+ @bad_urls << @current_url if @current_url
122
+ @current_url = nil
123
+ end
124
+
125
+ def uri
126
+ require 'addressable/uri'
127
+
128
+ @current_url ||= (@config.server_urls - @bad_urls).first
129
+ unless @current_url
130
+ msg = "Failed to connect to all PuppetDB server_urls: #{@config.server_urls.to_a.join(', ')}."
131
+ raise Bolt::PuppetDBError, msg
132
+ end
133
+
134
+ uri = Addressable::URI.parse(@current_url)
135
+ uri.port ||= 8081
136
+ uri
137
+ end
138
+
139
+ def headers
140
+ headers = { 'Content-Type' => 'application/json' }
141
+ headers['X-Authentication'] = @config.token if @config.token
142
+ headers
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/bolt/result.rb CHANGED
@@ -128,7 +128,7 @@ module Bolt
128
128
 
129
129
  def _pcore_init_from_hash(init_hash)
130
130
  opts = init_hash.reject { |k, _v| k == 'target' }
131
- initialize(init_hash['target'], opts.transform_keys(&:to_sym))
131
+ initialize(init_hash['target'], **opts.transform_keys(&:to_sym))
132
132
  end
133
133
 
134
134
  def _pcore_init_hash
@@ -6,7 +6,7 @@ module Bolt
6
6
  class Connection
7
7
  attr_reader :logger, :key
8
8
 
9
- CONTEXT_KEYS = Set.new(%i[plan_name description params]).freeze
9
+ CONTEXT_KEYS = Set.new(%i[plan_name description params sensitive]).freeze
10
10
 
11
11
  def self.get_key(opts)
12
12
  [
@@ -46,6 +46,7 @@ module Bolt
46
46
  if plan_context
47
47
  begin
48
48
  opts = plan_context.select { |k, _| CONTEXT_KEYS.include? k }
49
+ opts[:params] = opts[:params].reject { |k, _| plan_context[:sensitive].include?(k) }
49
50
  @client.command.plan_start(opts)['name']
50
51
  rescue OrchestratorClient::ApiError => e
51
52
  if e.code == '404'
@@ -53,9 +53,11 @@ module Bolt
53
53
  @connection = ::WinRM::Connection.new(options)
54
54
  @connection.logger = @transport_logger
55
55
 
56
- @session = @connection.shell(:powershell)
57
- @session.run('$PSVersionTable.PSVersion')
58
- @logger.trace { "Opened session" }
56
+ @connection.shell(:powershell) do |session|
57
+ session.run('$PSVersionTable.PSVersion')
58
+ end
59
+
60
+ @logger.trace { "Opened connection" }
59
61
  end
60
62
  rescue Timeout::Error
61
63
  # If we're using the default port with SSL, a timeout probably means the
@@ -95,9 +97,8 @@ module Bolt
95
97
  end
96
98
 
97
99
  def disconnect
98
- @session&.close
99
100
  @client&.disconnect!
100
- @logger.trace { "Closed session" }
101
+ @logger.trace { "Closed connection" }
101
102
  end
102
103
 
103
104
  def execute(command)
@@ -116,12 +117,18 @@ module Bolt
116
117
  # propagate to the main thread via the shell, there's no chance
117
118
  # they will be unhandled, so the default stack trace is unneeded.
118
119
  Thread.current.report_on_exception = false
119
- result = @session.run(command)
120
- out_wr << result.stdout
121
- err_wr << result.stderr
122
- out_wr.close
123
- err_wr.close
124
- result.exitcode
120
+
121
+ # Open a new shell instance for each command executed. PowerShell is
122
+ # unable to unload any DLLs loaded when running a PowerShell script
123
+ # or task from the same shell instance they were loaded in, which
124
+ # prevents Bolt from cleaning up the temp directory successfully.
125
+ # Using a new PowerShell instance avoids this limitation.
126
+ @connection.shell(:powershell) do |session|
127
+ result = session.run(command)
128
+ out_wr << result.stdout
129
+ err_wr << result.stderr
130
+ result.exitcode
131
+ end
125
132
  ensure
126
133
  # Close the streams to avoid the caller deadlocking
127
134
  out_wr.close
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.22.1'
4
+ VERSION = '3.24.0'
5
5
  end
@@ -8,6 +8,7 @@ require 'digest'
8
8
  require 'fileutils'
9
9
  require 'net/http'
10
10
  require 'logging'
11
+ require 'timeout'
11
12
 
12
13
  require 'bolt/error'
13
14
 
@@ -38,8 +39,7 @@ module BoltServer
38
39
 
39
40
  if do_purge
40
41
  @purge = Concurrent::TimerTask.new(execution_interval: purge_interval,
41
- timeout_interval: purge_timeout,
42
- run_now: true) { expire(purge_ttl) }
42
+ run_now: true) { expire(purge_ttl, purge_timeout) }
43
43
  @purge.execute
44
44
  end
45
45
  end
@@ -171,13 +171,15 @@ module BoltServer
171
171
  serial_execute { download_file(file_path, sha, file_data['uri']) }
172
172
  end
173
173
 
174
- def expire(purge_ttl)
174
+ def expire(purge_ttl, purge_timeout)
175
175
  expired_time = Time.now - purge_ttl
176
- @cache_dir_mutex.with_write_lock do
177
- Dir.glob(File.join(@cache_dir, '*')).select { |f| File.directory?(f) }.each do |dir|
178
- if (mtime = File.mtime(dir)) < expired_time && dir != tmppath
179
- @logger.debug("Removing #{dir}, last used at #{mtime}")
180
- FileUtils.remove_dir(dir)
176
+ Timeout.timeout(purge_timeout) do
177
+ @cache_dir_mutex.with_write_lock do
178
+ Dir.glob(File.join(@cache_dir, '*')).select { |f| File.directory?(f) }.each do |dir|
179
+ if (mtime = File.mtime(dir)) < expired_time && dir != tmppath
180
+ @logger.debug("Removing #{dir}, last used at #{mtime}")
181
+ FileUtils.remove_dir(dir)
182
+ end
181
183
  end
182
184
  end
183
185
  end
@@ -41,7 +41,7 @@ module BoltSpec
41
41
  @invocation[:options]
42
42
  end
43
43
 
44
- def result_for(_target, _data)
44
+ def result_for(_target, **_data)
45
45
  raise 'Download result cannot be changed'
46
46
  end
47
47
 
@@ -30,7 +30,7 @@ module BoltSpec
30
30
  end
31
31
 
32
32
  # Allow any data.
33
- def result_for(_target, data)
33
+ def result_for(_target, **data)
34
34
  Bolt::PlanResult.new(Bolt::Util.walk_keys(data, &:to_s), 'success')
35
35
  end
36
36
 
@@ -37,7 +37,7 @@ module BoltSpec
37
37
  end
38
38
 
39
39
  # Allow any data.
40
- def result_for(target, data)
40
+ def result_for(target, **data)
41
41
  Bolt::Result.new(target, value: Bolt::Util.walk_keys(data, &:to_s))
42
42
  end
43
43
 
@@ -40,7 +40,7 @@ module BoltSpec
40
40
  @invocation[:options]
41
41
  end
42
42
 
43
- def result_for(_target, _data)
43
+ def result_for(_target, **_data)
44
44
  raise 'Upload result cannot be changed'
45
45
  end
46
46
 
@@ -93,7 +93,7 @@ module BoltSpec
93
93
  when Bolt::Error
94
94
  Bolt::Result.from_exception(target, @data[:default])
95
95
  when Hash
96
- result_for(target, Bolt::Util.walk_keys(@data[:default], &:to_sym))
96
+ result_for(target, **Bolt::Util.walk_keys(@data[:default], &:to_sym))
97
97
  else
98
98
  raise 'Default result must be a Hash'
99
99
  end
@@ -156,7 +156,7 @@ module BoltSpec
156
156
  # set the inventory from the BoltSpec::Plans, otherwise if we try to convert
157
157
  # this target to a string, it will fail to string conversion because the
158
158
  # inventory is nil
159
- hsh[target] = result_for(Bolt::Target.new(target, @inventory), Bolt::Util.walk_keys(result, &:to_sym))
159
+ hsh[target] = result_for(Bolt::Target.new(target, @inventory), **Bolt::Util.walk_keys(result, &:to_sym))
160
160
  end
161
161
  raise "Cannot set return values and return block." if @return_block
162
162
  @data_set = true
@@ -210,7 +210,7 @@ module BoltSpec
210
210
  @allow_apply = true
211
211
  end
212
212
 
213
- def wait_until_available(targets, _options)
213
+ def wait_until_available(targets, **_options)
214
214
  Bolt::ResultSet.new(targets.map { |target| Bolt::Result.new(target) })
215
215
  end
216
216
 
@@ -103,6 +103,16 @@ module BoltSpec
103
103
 
104
104
  # Provided as a class so expectations can be placed on it.
105
105
  class MockPuppetDBClient
106
+ def initialize(config)
107
+ @instance = MockPuppetDBInstance.new(config)
108
+ end
109
+
110
+ def instance(_instance)
111
+ @instance
112
+ end
113
+ end
114
+
115
+ class MockPuppetDBInstance
106
116
  attr_reader :config
107
117
 
108
118
  def initialize(config)
@@ -111,7 +121,7 @@ module BoltSpec
111
121
  end
112
122
 
113
123
  def puppetdb_client
114
- @puppetdb_client ||= MockPuppetDBClient.new(Bolt::PuppetDB::Config.new({}))
124
+ @puppetdb_client ||= MockPuppetDBClient.new({})
115
125
  end
116
126
 
117
127
  def run_plan(name, params)
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: 3.22.1
4
+ version: 3.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: 2022-03-30 00:00:00.000000000 Z
11
+ date: 2022-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -163,6 +163,9 @@ dependencies:
163
163
  - - ">="
164
164
  - !ruby/object:Gem::Version
165
165
  version: '4.0'
166
+ - - "<"
167
+ - !ruby/object:Gem::Version
168
+ version: '7.0'
166
169
  type: :runtime
167
170
  prerelease: false
168
171
  version_requirements: !ruby/object:Gem::Requirement
@@ -170,6 +173,9 @@ dependencies:
170
173
  - - ">="
171
174
  - !ruby/object:Gem::Version
172
175
  version: '4.0'
176
+ - - "<"
177
+ - !ruby/object:Gem::Version
178
+ version: '7.0'
173
179
  - !ruby/object:Gem::Dependency
174
180
  name: net-ssh-krb
175
181
  requirement: !ruby/object:Gem::Requirement
@@ -566,6 +572,7 @@ files:
566
572
  - lib/bolt/puppetdb.rb
567
573
  - lib/bolt/puppetdb/client.rb
568
574
  - lib/bolt/puppetdb/config.rb
575
+ - lib/bolt/puppetdb/instance.rb
569
576
  - lib/bolt/r10k_log_proxy.rb
570
577
  - lib/bolt/rerun.rb
571
578
  - lib/bolt/resource_instance.rb
@@ -664,7 +671,7 @@ require_paths:
664
671
  - lib
665
672
  required_ruby_version: !ruby/object:Gem::Requirement
666
673
  requirements:
667
- - - "~>"
674
+ - - ">="
668
675
  - !ruby/object:Gem::Version
669
676
  version: '2.5'
670
677
  required_rubygems_version: !ruby/object:Gem::Requirement