bolt 2.8.0 → 2.9.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: bdb245ba4347b6309414b645bea7e8829651886d3f85152217801ce7463fdfb6
4
- data.tar.gz: 4e93d16cbb9d5cd0314f1c12031cd1e7d1ca9eb0c40186119f15a902aa0732d9
3
+ metadata.gz: ed1409405ce8a0e0367aea1cb1b7e2d194546c594b96932288c9ae81cef73209
4
+ data.tar.gz: 0cfb52ce0dbeffb427988da5e3e338649d2f65defbeef133f01c745802661ac6
5
5
  SHA512:
6
- metadata.gz: 5264e15f6e29f8d2e8cd9b02f67161e110215c20161c9e5ef22cf85294597eb262c40f0e13491dcb83c683b5c11930bc26fa1f8e5b80bee5ce485c60567fa8f5
7
- data.tar.gz: 3b6053135ba279dc70b41cfa13dcd7c5b00a70fa8dd2ed3370bcdd8b1e9c33b33ec2ea817bc6fc5c7afb6114a5abe1c93ea328e432c5b1748c22af04daad5a6f
6
+ metadata.gz: 986a57cd4c6b101ce69f11dec9b6f95212129c2930aded3d0b0c0d8243bcf98ef66b981af0398ecb79cea291603483057c3df7f82e56fa6ab40f7981f4f7ae0d
7
+ data.tar.gz: 9ff02c10dd26709f6027c2d9ab45b051d67c0de62c87abe974faee9db28cf2ddde9e086f8cf83e2765bf43e1ff64690d849ad7e032fdc3cbce9990680f5f55f9
@@ -2,4 +2,14 @@
2
2
  # should be used as the return type of functions that run plans and return the
3
3
  # results.
4
4
 
5
- type Boltlib::PlanResult = Variant[Boolean, Numeric, String, Undef, Error, Result, ResultSet, Target, Array[Boltlib::PlanResult], Hash[String, Boltlib::PlanResult]]
5
+ type Boltlib::PlanResult = Variant[Boolean,
6
+ Numeric,
7
+ String,
8
+ Undef,
9
+ Error,
10
+ Result,
11
+ ApplyResult,
12
+ ResultSet,
13
+ Target,
14
+ Array[Boltlib::PlanResult],
15
+ Hash[String, Boltlib::PlanResult]]
@@ -804,6 +804,20 @@ module Bolt
804
804
  end
805
805
 
806
806
  def bundled_content
807
+ # If the bundled content directory is empty, Bolt is likely installed as a gem.
808
+ if ENV['BOLT_GEM'].nil? && incomplete_install?
809
+ msg = <<~MSG.chomp
810
+ Bolt may be installed as a gem. To use Bolt reliably and with all of its
811
+ dependencies, uninstall the 'bolt' gem and install Bolt as a package:
812
+ https://puppet.com/docs/bolt/latest/bolt_installing.html
813
+
814
+ If you meant to install Bolt as a gem and want to disable this warning,
815
+ set the BOLT_GEM environment variable.
816
+ MSG
817
+
818
+ @logger.warn(msg)
819
+ end
820
+
807
821
  # We only need to enumerate bundled content when running a task or plan
808
822
  content = { 'Plan' => [],
809
823
  'Task' => [],
@@ -827,5 +841,11 @@ module Bolt
827
841
  MSG
828
842
  @logger.debug(msg)
829
843
  end
844
+
845
+ # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
846
+ # package installs include modules listed in the Bolt repo Puppetfile
847
+ def incomplete_install?
848
+ (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
849
+ end
830
850
  end
831
851
  end
@@ -34,6 +34,8 @@ module Bolt
34
34
  'remote' => Bolt::Config::Transport::Remote
35
35
  }.freeze
36
36
 
37
+ # NOTE: All configuration options should have a corresponding schema property
38
+ # in schemas/bolt-config.schema.json
37
39
  OPTIONS = {
38
40
  "apply_settings" => "A map of Puppet settings to use when applying Puppet code",
39
41
  "color" => "Whether to use colored output when printing messages to the console.",
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Docker < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cleanup" => { type: TrueClass,
12
14
  desc: "Whether to clean up temporary files created on targets." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Local < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cleanup" => { type: TrueClass,
12
14
  desc: "Whether to clean up temporary files created on targets." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Orch < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cacert" => { type: String,
12
14
  desc: "The path to the CA certificate." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Remote < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "run-on" => { type: String,
12
14
  desc: "The proxy target that the task executes on." }
@@ -8,6 +8,9 @@ module Bolt
8
8
  module Transport
9
9
  class SSH < Base
10
10
  LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
11
+
12
+ # NOTE: All transport configuration options should have a corresponding schema definition
13
+ # in schemas/bolt-transport-definitions.json
11
14
  OPTIONS = {
12
15
  "cleanup" => { type: TrueClass,
13
16
  desc: "Whether to clean up temporary files created on targets." },
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class WinRM < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "basic-auth-only" => { type: TrueClass,
12
14
  desc: "Force basic authentication. This option is only available when using SSL." },
@@ -12,6 +12,7 @@ module Bolt
12
12
  # Regex used to validate group names and target aliases.
13
13
  NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
14
14
 
15
+ # NOTE: All keys should have a corresponding schema property in schemas/bolt-inventory.schema.json
15
16
  DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
16
17
  TARGET_KEYS = DATA_KEYS + %w[name alias uri]
17
18
  GROUP_KEYS = DATA_KEYS + %w[name groups targets]
@@ -107,13 +107,14 @@ module Bolt
107
107
 
108
108
  # Use special handling if the result looks like a command or script result
109
109
  if result.generic_value.keys == %w[stdout stderr exit_code]
110
- unless result['stdout'].strip.empty?
110
+ safe_value = result.safe_value
111
+ unless safe_value['stdout'].strip.empty?
111
112
  @stream.puts(indent(2, "STDOUT:"))
112
- @stream.puts(indent(4, result['stdout']))
113
+ @stream.puts(indent(4, safe_value['stdout']))
113
114
  end
114
- unless result['stderr'].strip.empty?
115
+ unless safe_value['stderr'].strip.empty?
115
116
  @stream.puts(indent(2, "STDERR:"))
116
- @stream.puts(indent(4, result['stderr']))
117
+ @stream.puts(indent(4, safe_value['stderr']))
117
118
  end
118
119
  elsif result.generic_value.any?
119
120
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
@@ -28,7 +28,7 @@ module Bolt
28
28
 
29
29
  def print_result(result)
30
30
  @stream.puts ',' if @preceding_item
31
- @stream.puts result.status_hash.to_json
31
+ @stream.puts result.to_json
32
32
  @preceding_item = true
33
33
  end
34
34
 
@@ -329,7 +329,8 @@ module Bolt
329
329
  raise Bolt::Error.unknown_plan(plan_name)
330
330
  end
331
331
 
332
- mod = plan_sig.instance_variable_get(:@plan_func).loader.parent.path
332
+ # path may be a Pathname object, so make sure to stringify it
333
+ mod = plan_sig.instance_variable_get(:@plan_func).loader.parent.path.to_s
333
334
 
334
335
  # If it's a Puppet language plan, use strings to extract data. The only
335
336
  # way to tell is to check which filename exists in the module.
@@ -45,7 +45,7 @@ module Bolt
45
45
  end
46
46
 
47
47
  if result_set.is_a?(Bolt::ResultSet)
48
- data = result_set.map { |res| res.status_hash.select { |k, _| %i[target status].include? k } }
48
+ data = result_set.map { |res| { target: res.target.name, status: res.status } }
49
49
  FileUtils.mkdir_p(File.dirname(@path))
50
50
  File.write(@path, data.to_json)
51
51
  elsif File.exist?(@path)
@@ -40,18 +40,23 @@ module Bolt
40
40
  end
41
41
 
42
42
  def self.for_task(target, stdout, stderr, exit_code, task)
43
- begin
44
- value = JSON.parse(stdout)
45
- unless value.is_a? Hash
46
- value = nil
47
- end
48
- rescue JSON::ParserError
49
- value = nil
50
- end
51
- value ||= { '_output' => stdout }
43
+ stdout.force_encoding('utf-8') unless stdout.encoding == Encoding::UTF_8
44
+ value = if stdout.valid_encoding?
45
+ parse_hash(stdout) || { '_output' => stdout }
46
+ else
47
+ { '_error' => { 'kind' => 'puppetlabs.tasks/task-error',
48
+ 'issue_code' => 'TASK_ERROR',
49
+ 'msg' => 'The task result contained invalid UTF-8 on stdout',
50
+ 'details' => {} } }
51
+ end
52
+
52
53
  if exit_code != 0 && value['_error'].nil?
53
54
  msg = if stdout.empty?
54
- "The task failed with exit code #{exit_code}:\n#{stderr}"
55
+ if stderr.empty?
56
+ "The task failed with exit code #{exit_code} and no output"
57
+ else
58
+ "The task failed with exit code #{exit_code} and no stdout, but stderr contained:\n#{stderr}"
59
+ end
55
60
  else
56
61
  "The task failed with exit code #{exit_code}"
57
62
  end
@@ -63,6 +68,13 @@ module Bolt
63
68
  new(target, value: value, action: 'task', object: task)
64
69
  end
65
70
 
71
+ def self.parse_hash(string)
72
+ value = JSON.parse(string)
73
+ value if value.is_a? Hash
74
+ rescue JSON::ParserError
75
+ nil
76
+ end
77
+
66
78
  def self.for_upload(target, source, destination)
67
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
68
80
  end
@@ -110,18 +122,8 @@ module Bolt
110
122
  message && !message.strip.empty?
111
123
  end
112
124
 
113
- def status_hash
114
- {
115
- target: @target.name,
116
- action: action,
117
- object: object,
118
- status: status,
119
- value: @value
120
- }
121
- end
122
-
123
125
  def generic_value
124
- value.reject { |k, _| %w[_error _output].include? k }
126
+ safe_value.reject { |k, _| %w[_error _output].include? k }
125
127
  end
126
128
 
127
129
  def eql?(other)
@@ -139,15 +141,36 @@ module Bolt
139
141
  end
140
142
 
141
143
  def to_json(opts = nil)
142
- status_hash.to_json(opts)
144
+ to_data.to_json(opts)
143
145
  end
144
146
 
145
147
  def to_s
146
148
  to_json
147
149
  end
148
150
 
151
+ # This is the value with all non-UTF-8 characters removed, suitable for
152
+ # printing or converting to JSON. It *should* only be possible to have
153
+ # non-UTF-8 characters in stdout/stderr keys as they are not allowed from
154
+ # tasks but we scrub the whole thing just in case.
155
+ def safe_value
156
+ Bolt::Util.walk_vals(value) do |val|
157
+ if val.is_a?(String)
158
+ # Replace invalid bytes with hex codes, ie. \xDE\xAD\xBE\xEF
159
+ val.scrub { |c| c.bytes.map { |b| "\\x" + b.to_s(16).upcase }.join }
160
+ else
161
+ val
162
+ end
163
+ end
164
+ end
165
+
149
166
  def to_data
150
- Bolt::Util.walk_keys(status_hash, &:to_s)
167
+ {
168
+ "target" => @target.name,
169
+ "action" => action,
170
+ "object" => object,
171
+ "status" => status,
172
+ "value" => safe_value
173
+ }
151
174
  end
152
175
 
153
176
  def status
@@ -99,17 +99,14 @@ module Bolt
99
99
  self.class == other.class && @results == other.results
100
100
  end
101
101
 
102
- def to_a
103
- @results.map(&:status_hash)
104
- end
105
-
106
102
  def to_json(opts = nil)
107
- @results.map(&:status_hash).to_json(opts)
103
+ to_data.to_json(opts)
108
104
  end
109
105
 
110
106
  def to_data
111
107
  @results.map(&:to_data)
112
108
  end
109
+ alias to_a to_data
113
110
 
114
111
  def to_s
115
112
  to_json
@@ -267,10 +267,18 @@ module Bolt
267
267
 
268
268
  result = Bolt::Node::Output.new
269
269
  inp.close
270
- out.binmode
271
- err.binmode
272
- stdout = Thread.new { result.stdout << out.read }
273
- stderr = Thread.new { result.stderr << err.read }
270
+ stdout = Thread.new do
271
+ # Set to binmode to preserve \r\n line endings, but save and restore
272
+ # the proper encoding so the string isn't later misinterpreted
273
+ encoding = out.external_encoding
274
+ out.binmode
275
+ result.stdout << out.read.force_encoding(encoding)
276
+ end
277
+ stderr = Thread.new do
278
+ encoding = err.external_encoding
279
+ err.binmode
280
+ result.stderr << err.read.force_encoding(encoding)
281
+ end
274
282
 
275
283
  stdout.join
276
284
  stderr.join
@@ -108,8 +108,8 @@ module Bolt
108
108
  # it will fail if the shell attempts to provide stdin
109
109
  inp.close
110
110
 
111
- out_rd, out_wr = IO.pipe
112
- err_rd, err_wr = IO.pipe
111
+ out_rd, out_wr = IO.pipe('UTF-8')
112
+ err_rd, err_wr = IO.pipe('UTF-8')
113
113
  th = Thread.new do
114
114
  result = @session.run(command)
115
115
  out_wr << result.stdout
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.8.0'
4
+ VERSION = '2.9.0'
5
5
  end
@@ -57,8 +57,8 @@ module BoltServer
57
57
  end
58
58
 
59
59
  def scrub_stack_trace(result)
60
- if result.dig(:value, '_error', 'details', 'stack_trace')
61
- result[:value]['_error']['details'].reject! { |k| k == 'stack_trace' }
60
+ if result.dig('value', '_error', 'details', 'stack_trace')
61
+ result['value']['_error']['details'].reject! { |k| k == 'stack_trace' }
62
62
  end
63
63
  result
64
64
  end
@@ -87,14 +87,14 @@ module BoltServer
87
87
  # If the `result_set` contains only one item, it will be returned
88
88
  # as a single result object. Set `aggregate` to treat it as a set
89
89
  # of results with length 1 instead.
90
- def result_set_to_status_hash(result_set, aggregate: false)
90
+ def result_set_to_data(result_set, aggregate: false)
91
91
  scrubbed_results = result_set.map do |result|
92
- scrub_stack_trace(result.status_hash)
92
+ scrub_stack_trace(result.to_data)
93
93
  end
94
94
 
95
95
  if aggregate || scrubbed_results.length > 1
96
96
  # For actions that act on multiple targets, construct a status hash for the aggregate result
97
- all_succeeded = scrubbed_results.all? { |r| r[:status] == 'success' }
97
+ all_succeeded = scrubbed_results.all? { |r| r['status'] == 'success' }
98
98
  {
99
99
  status: all_succeeded ? 'success' : 'failure',
100
100
  result: scrubbed_results
@@ -297,7 +297,7 @@ module BoltServer
297
297
  return [400, error.to_json] unless error.nil?
298
298
 
299
299
  aggregate = body['target'].nil?
300
- [200, result_set_to_status_hash(result_set, aggregate: aggregate).to_json]
300
+ [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
301
301
  end
302
302
 
303
303
  def make_winrm_target(target_hash)
@@ -337,7 +337,7 @@ module BoltServer
337
337
  return [400, error.to_json] if error
338
338
 
339
339
  aggregate = body['target'].nil?
340
- [200, result_set_to_status_hash(result_set, aggregate: aggregate).to_json]
340
+ [200, result_set_to_data(result_set, aggregate: aggregate).to_json]
341
341
  end
342
342
 
343
343
  # Fetches the metadata for a single plan
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.8.0
4
+ version: 2.9.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-05-05 00:00:00.000000000 Z
11
+ date: 2020-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable