bolt 2.32.0 → 2.36.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 +4 -4
- data/Puppetfile +6 -6
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
- data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
- data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
- data/guides/logging.txt +18 -0
- data/lib/bolt/analytics.rb +27 -8
- data/lib/bolt/apply_result.rb +3 -3
- data/lib/bolt/bolt_option_parser.rb +43 -15
- data/lib/bolt/cli.rb +79 -227
- data/lib/bolt/config.rb +131 -52
- data/lib/bolt/config/options.rb +46 -8
- data/lib/bolt/config/transport/base.rb +10 -19
- data/lib/bolt/config/transport/local.rb +0 -7
- data/lib/bolt/config/transport/options.rb +1 -1
- data/lib/bolt/config/transport/ssh.rb +8 -14
- data/lib/bolt/config/validator.rb +231 -0
- data/lib/bolt/error.rb +37 -3
- data/lib/bolt/executor.rb +103 -17
- data/lib/bolt/inventory/group.rb +2 -1
- data/lib/bolt/module_installer.rb +2 -1
- data/lib/bolt/module_installer/specs/forge_spec.rb +5 -4
- data/lib/bolt/module_installer/specs/git_spec.rb +4 -3
- data/lib/bolt/outputter/human.rb +21 -9
- data/lib/bolt/outputter/rainbow.rb +1 -1
- data/lib/bolt/pal.rb +48 -30
- data/lib/bolt/pal/yaml_plan.rb +11 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
- data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
- data/lib/bolt/plan_creator.rb +160 -0
- data/lib/bolt/plugin.rb +1 -8
- data/lib/bolt/project.rb +30 -36
- data/lib/bolt/project_manager.rb +199 -0
- data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +43 -5
- data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +5 -5
- data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
- data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
- data/lib/bolt/puppetdb/client.rb +3 -2
- data/lib/bolt/puppetdb/config.rb +9 -8
- data/lib/bolt/result.rb +23 -11
- data/lib/bolt/shell/bash.rb +12 -7
- data/lib/bolt/shell/powershell.rb +12 -7
- data/lib/bolt/task/run.rb +1 -1
- data/lib/bolt/transport/base.rb +18 -18
- data/lib/bolt/transport/docker.rb +23 -6
- data/lib/bolt/transport/orch.rb +23 -19
- data/lib/bolt/transport/orch/connection.rb +10 -3
- data/lib/bolt/transport/remote.rb +3 -3
- data/lib/bolt/transport/simple.rb +6 -6
- data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
- data/lib/bolt/util.rb +19 -7
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt/yarn.rb +23 -0
- data/lib/bolt_server/base_config.rb +3 -1
- data/lib/bolt_server/config.rb +3 -1
- data/lib/bolt_server/file_cache.rb +2 -0
- data/lib/bolt_server/schemas/partials/task.json +2 -2
- data/lib/bolt_server/transport_app.rb +42 -11
- data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
- data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +9 -6
- data/libexec/apply_catalog.rb +1 -1
- data/libexec/custom_facts.rb +1 -1
- data/libexec/query_resources.rb +1 -1
- metadata +12 -14
- data/lib/bolt/project_migrator.rb +0 -80
- data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'bolt/error'
|
4
4
|
require 'bolt/util'
|
5
|
+
require 'bolt/config/validator'
|
5
6
|
require 'bolt/config/transport/options'
|
6
7
|
|
7
8
|
module Bolt
|
@@ -90,6 +91,14 @@ module Bolt
|
|
90
91
|
self::OPTIONS
|
91
92
|
end
|
92
93
|
|
94
|
+
def self.schema
|
95
|
+
{
|
96
|
+
type: Hash,
|
97
|
+
properties: self::TRANSPORT_OPTIONS.slice(*self::OPTIONS),
|
98
|
+
_plugin: true
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
93
102
|
private def defaults
|
94
103
|
unless defined? self.class::DEFAULTS
|
95
104
|
raise NotImplementedError,
|
@@ -116,25 +125,7 @@ module Bolt
|
|
116
125
|
|
117
126
|
# Validation defaults to just asserting the option types
|
118
127
|
private def validate
|
119
|
-
|
120
|
-
end
|
121
|
-
|
122
|
-
# Validates that each option is the correct type. Types are loaded from the TRANSPORT_OPTIONS hash.
|
123
|
-
private def assert_type
|
124
|
-
@config.each_pair do |opt, val|
|
125
|
-
types = Array(TRANSPORT_OPTIONS.dig(opt, :type)).compact
|
126
|
-
|
127
|
-
next if val.nil? || types.empty? || types.include?(val.class)
|
128
|
-
|
129
|
-
# Ruby doesn't have a Boolean class, so add it to the types list if TrueClass or FalseClass
|
130
|
-
# are present.
|
131
|
-
if types.include?(TrueClass) || types.include?(FalseClass)
|
132
|
-
types = types - [TrueClass, FalseClass] + ['Boolean']
|
133
|
-
end
|
134
|
-
|
135
|
-
raise Bolt::ValidationError,
|
136
|
-
"#{opt} must be of type #{types.join(', ')}; received #{val.class} #{val.inspect} "
|
137
|
-
end
|
128
|
+
Bolt::Config::Validator.new.validate(@config.compact, self.class.schema.fetch(:properties))
|
138
129
|
end
|
139
130
|
end
|
140
131
|
end
|
@@ -29,13 +29,6 @@ module Bolt
|
|
29
29
|
if @config['interpreters']
|
30
30
|
@config['interpreters'] = normalize_interpreters(@config['interpreters'])
|
31
31
|
end
|
32
|
-
|
33
|
-
if (run_as_cmd = @config['run-as-command'])
|
34
|
-
unless run_as_cmd.all? { |n| n.is_a?(String) }
|
35
|
-
raise Bolt::ValidationError,
|
36
|
-
"run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
|
37
|
-
end
|
38
|
-
end
|
39
32
|
end
|
40
33
|
end
|
41
34
|
end
|
@@ -73,6 +73,14 @@ module Bolt
|
|
73
73
|
%w[ssh-command native-ssh].concat(OPTIONS)
|
74
74
|
end
|
75
75
|
|
76
|
+
def self.schema
|
77
|
+
{
|
78
|
+
type: Hash,
|
79
|
+
properties: self::TRANSPORT_OPTIONS.slice(*(self::OPTIONS + self::NATIVE_OPTIONS)),
|
80
|
+
_plugin: true
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
76
84
|
private def filter(unfiltered)
|
77
85
|
# Because we filter before merging config together it's impossible to
|
78
86
|
# know whether both ssh-command *and* native-ssh will be specified
|
@@ -87,12 +95,6 @@ module Bolt
|
|
87
95
|
super
|
88
96
|
|
89
97
|
if (key_opt = @config['private-key'])
|
90
|
-
unless key_opt.instance_of?(String) || (key_opt.instance_of?(Hash) && key_opt.include?('key-data'))
|
91
|
-
raise Bolt::ValidationError,
|
92
|
-
"private-key option must be a path to a private key file or a Hash containing the 'key-data', "\
|
93
|
-
"received #{key_opt.class} #{key_opt}"
|
94
|
-
end
|
95
|
-
|
96
98
|
if key_opt.instance_of?(String)
|
97
99
|
@config['private-key'] = File.expand_path(key_opt, @project)
|
98
100
|
|
@@ -114,14 +116,6 @@ module Bolt
|
|
114
116
|
"Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
|
115
117
|
end
|
116
118
|
|
117
|
-
%w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
|
118
|
-
next unless @config.key?(opt)
|
119
|
-
unless @config[opt].all? { |n| n.is_a?(String) }
|
120
|
-
raise Bolt::ValidationError,
|
121
|
-
"#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
119
|
if @config['login-shell'] == 'powershell'
|
126
120
|
%w[tty run-as].each do |key|
|
127
121
|
if @config[key]
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/error'
|
4
|
+
|
5
|
+
# This class validates config against a schema, raising an error that includes
|
6
|
+
# details about any invalid configuration.
|
7
|
+
#
|
8
|
+
module Bolt
|
9
|
+
class Config
|
10
|
+
class Validator
|
11
|
+
attr_reader :deprecations, :warnings
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@errors = []
|
15
|
+
@deprecations = []
|
16
|
+
@warnings = []
|
17
|
+
@path = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# This is the entry method for validating data against the schema.
|
21
|
+
# It loops over each key-value pair in the data hash and validates
|
22
|
+
# the value against the relevant schema definition.
|
23
|
+
#
|
24
|
+
def validate(data, schema, location = nil)
|
25
|
+
@location = location
|
26
|
+
|
27
|
+
validate_keys(data.keys, schema.keys)
|
28
|
+
|
29
|
+
data.each_pair do |key, value|
|
30
|
+
next unless schema.key?(key)
|
31
|
+
|
32
|
+
@path.push(key)
|
33
|
+
|
34
|
+
check_deprecated(key, schema[key], location)
|
35
|
+
validate_value(value, schema[key])
|
36
|
+
ensure
|
37
|
+
@path.pop
|
38
|
+
end
|
39
|
+
|
40
|
+
raise_error
|
41
|
+
end
|
42
|
+
|
43
|
+
# Adds a warning if the given option is deprecated.
|
44
|
+
#
|
45
|
+
def check_deprecated(key, definition, location)
|
46
|
+
if definition.key?(:_deprecation)
|
47
|
+
message = "Option '#{path}' "
|
48
|
+
message += "at #{location} " if location
|
49
|
+
message += "is deprecated. #{definition[:_deprecation]}"
|
50
|
+
@deprecations << { option: key, message: message }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Raises a ValidationError if there are any errors. All error messages
|
55
|
+
# created during validation are concatenated into a single error
|
56
|
+
# message.
|
57
|
+
#
|
58
|
+
private def raise_error
|
59
|
+
return unless @errors.any?
|
60
|
+
|
61
|
+
message = "Invalid configuration"
|
62
|
+
message += " at #{@location}" if @location
|
63
|
+
message += ":\n"
|
64
|
+
message += @errors.map { |error| "\s\s#{error}" }.join("\n")
|
65
|
+
|
66
|
+
raise Bolt::ValidationError, message
|
67
|
+
end
|
68
|
+
|
69
|
+
# Validate an individual value. This performs validation that is
|
70
|
+
# common to all values, including type validation. After validating
|
71
|
+
# the value's type, the value is passed off to an individual
|
72
|
+
# validation method for the value's type.
|
73
|
+
#
|
74
|
+
private def validate_value(value, definition)
|
75
|
+
return if plugin_reference?(value, definition)
|
76
|
+
return unless valid_type?(value, definition)
|
77
|
+
|
78
|
+
case value
|
79
|
+
when Hash
|
80
|
+
validate_hash(value, definition)
|
81
|
+
when Array
|
82
|
+
validate_array(value, definition)
|
83
|
+
when String
|
84
|
+
validate_string(value, definition)
|
85
|
+
when Numeric
|
86
|
+
validate_number(value, definition)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Validates a hash value, logging errors for any validations that fail.
|
91
|
+
# This will enumerate each key-value pair in the hash and validate each
|
92
|
+
# value individually.
|
93
|
+
#
|
94
|
+
private def validate_hash(value, definition)
|
95
|
+
properties = definition[:properties] ? definition[:properties].keys : []
|
96
|
+
|
97
|
+
if definition[:properties] && definition[:additionalProperties].nil?
|
98
|
+
validate_keys(value.keys, properties)
|
99
|
+
end
|
100
|
+
|
101
|
+
if definition[:required] && (definition[:required] - value.keys).any?
|
102
|
+
missing = definition[:required] - value.keys
|
103
|
+
@errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
|
104
|
+
end
|
105
|
+
|
106
|
+
value.each_pair do |key, val|
|
107
|
+
@path.push(key)
|
108
|
+
|
109
|
+
if properties.include?(key)
|
110
|
+
validate_value(val, definition[:properties][key])
|
111
|
+
elsif definition[:additionalProperties]
|
112
|
+
validate_value(val, definition[:additionalProperties])
|
113
|
+
end
|
114
|
+
ensure
|
115
|
+
@path.pop
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Validates an array value, logging errors for any validations that fail.
|
120
|
+
# This will enumerate the items in the array and validate each item
|
121
|
+
# individually.
|
122
|
+
#
|
123
|
+
private def validate_array(value, definition)
|
124
|
+
if definition[:uniqueItems] && value.size != value.uniq.size
|
125
|
+
@errors << "Value at '#{path}' must not include duplicate elements"
|
126
|
+
return
|
127
|
+
end
|
128
|
+
|
129
|
+
return unless definition.key?(:items)
|
130
|
+
|
131
|
+
value.each_with_index do |item, index|
|
132
|
+
@path.push(index)
|
133
|
+
validate_value(item, definition[:items])
|
134
|
+
ensure
|
135
|
+
@path.pop
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validates a string value, logging errors for any validations that fail.
|
140
|
+
#
|
141
|
+
private def validate_string(value, definition)
|
142
|
+
if definition.key?(:enum) && !definition[:enum].include?(value)
|
143
|
+
message = "Value at '#{path}' must be "
|
144
|
+
message += "one of " if definition[:enum].count > 1
|
145
|
+
message += definition[:enum].join(', ')
|
146
|
+
multitype_error(message, value, definition)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Validates a numeric value, logging errors for any validations that fail.
|
151
|
+
#
|
152
|
+
private def validate_number(value, definition)
|
153
|
+
if definition.key?(:minimum) && value < definition[:minimum]
|
154
|
+
@errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Adds warnings for unknown config options.
|
159
|
+
#
|
160
|
+
private def validate_keys(keys, known_keys)
|
161
|
+
(keys - known_keys).each do |key|
|
162
|
+
message = "Unknown option '#{key}'"
|
163
|
+
message += " at '#{path}'" if @path.any?
|
164
|
+
message += " at #{@location}" if @location
|
165
|
+
message += "."
|
166
|
+
@warnings << message
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns true if a value is a plugin reference. This also validates whether
|
171
|
+
# a value can be a plugin reference in the first place. If the value is a
|
172
|
+
# plugin reference but cannot be one according to the schema, then this will
|
173
|
+
# log an error.
|
174
|
+
#
|
175
|
+
private def plugin_reference?(value, definition)
|
176
|
+
if value.is_a?(Hash) && value.key?('_plugin')
|
177
|
+
unless definition[:_plugin]
|
178
|
+
@errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
|
179
|
+
"this location"
|
180
|
+
end
|
181
|
+
|
182
|
+
true
|
183
|
+
else
|
184
|
+
false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Asserts the type for each option against the type specified in the schema
|
189
|
+
# definition. The schema definition can specify multiple valid types, so the
|
190
|
+
# value needs to only match one of the types to be valid. Returns early if
|
191
|
+
# there is no type in the definition (in practice this shouldn't happen, but
|
192
|
+
# this will safeguard against any dev mistakes).
|
193
|
+
#
|
194
|
+
private def valid_type?(value, definition)
|
195
|
+
return unless definition.key?(:type)
|
196
|
+
|
197
|
+
types = Array(definition[:type])
|
198
|
+
|
199
|
+
if types.include?(value.class)
|
200
|
+
true
|
201
|
+
else
|
202
|
+
if types.include?(TrueClass) || types.include?(FalseClass)
|
203
|
+
types = types - [TrueClass, FalseClass] + ['Boolean']
|
204
|
+
end
|
205
|
+
|
206
|
+
@errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
|
207
|
+
|
208
|
+
false
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Adds an error that includes additional helpful information for values
|
213
|
+
# that accept multiple types.
|
214
|
+
#
|
215
|
+
private def multitype_error(message, value, definition)
|
216
|
+
if Array(definition[:type]).count > 1
|
217
|
+
types = Array(definition[:type]) - [value.class]
|
218
|
+
message += " or must be of type #{types.join(' or ')}"
|
219
|
+
end
|
220
|
+
|
221
|
+
@errors << message
|
222
|
+
end
|
223
|
+
|
224
|
+
# Returns the formatted path for the key.
|
225
|
+
#
|
226
|
+
private def path
|
227
|
+
@path.join('.')
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/lib/bolt/error.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bolt/util'
|
4
|
+
|
3
5
|
module Bolt
|
4
6
|
class Error < RuntimeError
|
5
7
|
attr_reader :kind, :details, :issue_code, :error_code
|
@@ -24,6 +26,10 @@ module Bolt
|
|
24
26
|
h
|
25
27
|
end
|
26
28
|
|
29
|
+
def add_filelineno(details)
|
30
|
+
@details.merge!(details) unless @details['file']
|
31
|
+
end
|
32
|
+
|
27
33
|
def to_json(opts = nil)
|
28
34
|
to_h.to_json(opts)
|
29
35
|
end
|
@@ -33,13 +39,17 @@ module Bolt
|
|
33
39
|
end
|
34
40
|
|
35
41
|
def self.unknown_task(task)
|
36
|
-
|
37
|
-
|
42
|
+
command = Bolt::Util.powershell? ? "Get-BoltTask" : "bolt task show"
|
43
|
+
new(
|
44
|
+
"Could not find a task named '#{task}'. For a list of available tasks, run '#{command}'.",
|
45
|
+
'bolt/unknown-task'
|
46
|
+
)
|
38
47
|
end
|
39
48
|
|
40
49
|
def self.unknown_plan(plan)
|
50
|
+
command = Bolt::Util.powershell? ? "Get-BoltPlan" : "bolt plan show"
|
41
51
|
new(
|
42
|
-
"Could not find a plan named
|
52
|
+
"Could not find a plan named '#{plan}'. For a list of available plans, run '#{command}'.",
|
43
53
|
'bolt/unknown-plan'
|
44
54
|
)
|
45
55
|
end
|
@@ -80,6 +90,20 @@ module Bolt
|
|
80
90
|
end
|
81
91
|
end
|
82
92
|
|
93
|
+
class ParallelFailure < Bolt::Error
|
94
|
+
def initialize(results, failed_indices)
|
95
|
+
details = {
|
96
|
+
'action' => 'parallelize',
|
97
|
+
'failed_indices' => failed_indices,
|
98
|
+
'results' => results
|
99
|
+
}
|
100
|
+
message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
|
101
|
+
message += "s" unless failed_indices.length == 1
|
102
|
+
super(message, 'bolt/parallel-failure', details)
|
103
|
+
@error_code = 2
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
83
107
|
class PlanFailure < Error
|
84
108
|
def initialize(*args)
|
85
109
|
super(*args)
|
@@ -121,6 +145,16 @@ module Bolt
|
|
121
145
|
end
|
122
146
|
end
|
123
147
|
|
148
|
+
class InvalidParallelResult < Error
|
149
|
+
def initialize(result_str, file, line)
|
150
|
+
super("Parallel block returned an invalid result: #{result_str}",
|
151
|
+
'bolt/invalid-plan-result',
|
152
|
+
{ 'file' => file,
|
153
|
+
'line' => line,
|
154
|
+
'result_string' => result_str })
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
124
158
|
class ValidationError < Bolt::Error
|
125
159
|
def initialize(msg)
|
126
160
|
super(msg, 'bolt/validation-error')
|
data/lib/bolt/executor.rb
CHANGED
@@ -17,6 +17,7 @@ require 'bolt/transport/orch'
|
|
17
17
|
require 'bolt/transport/local'
|
18
18
|
require 'bolt/transport/docker'
|
19
19
|
require 'bolt/transport/remote'
|
20
|
+
require 'bolt/yarn'
|
20
21
|
|
21
22
|
module Bolt
|
22
23
|
TRANSPORTS = {
|
@@ -29,7 +30,7 @@ module Bolt
|
|
29
30
|
}.freeze
|
30
31
|
|
31
32
|
class Executor
|
32
|
-
attr_reader :noop, :transports
|
33
|
+
attr_reader :noop, :transports, :in_parallel
|
33
34
|
attr_accessor :run_as
|
34
35
|
|
35
36
|
def initialize(concurrency = 1,
|
@@ -60,6 +61,7 @@ module Bolt
|
|
60
61
|
|
61
62
|
@noop = noop
|
62
63
|
@run_as = nil
|
64
|
+
@in_parallel = false
|
63
65
|
@pool = if concurrency > 0
|
64
66
|
Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
|
65
67
|
else
|
@@ -84,6 +86,14 @@ module Bolt
|
|
84
86
|
self
|
85
87
|
end
|
86
88
|
|
89
|
+
def unsubscribe(subscriber, types = nil)
|
90
|
+
if types.nil? || types.sort == @subscribers[subscriber]&.sort
|
91
|
+
@subscribers.delete(subscriber)
|
92
|
+
elsif @subscribers[subscriber].is_a?(Array)
|
93
|
+
@subscribers[subscriber] = @subscribers[subscriber] - types
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
87
97
|
def publish_event(event)
|
88
98
|
@subscribers.each do |subscriber, types|
|
89
99
|
# If types isn't set or if the subscriber is subscribed to
|
@@ -246,54 +256,54 @@ module Bolt
|
|
246
256
|
@logger.trace { "Failed to submit analytics event: #{e.message}" }
|
247
257
|
end
|
248
258
|
|
249
|
-
def with_node_logging(description, batch)
|
250
|
-
@logger.
|
259
|
+
def with_node_logging(description, batch, log_level = :info)
|
260
|
+
@logger.send(log_level, "#{description} on #{batch.map(&:safe_name)}")
|
251
261
|
result = yield
|
252
|
-
@logger.
|
262
|
+
@logger.send(log_level, result.to_json)
|
253
263
|
result
|
254
264
|
end
|
255
265
|
|
256
|
-
def run_command(targets, command, options = {})
|
266
|
+
def run_command(targets, command, options = {}, position = [])
|
257
267
|
description = options.fetch(:description, "command '#{command}'")
|
258
268
|
log_action(description, targets) do
|
259
269
|
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
260
270
|
|
261
271
|
batch_execute(targets) do |transport, batch|
|
262
272
|
with_node_logging("Running command '#{command}'", batch) do
|
263
|
-
transport.batch_command(batch, command, options, &method(:publish_event))
|
273
|
+
transport.batch_command(batch, command, options, position, &method(:publish_event))
|
264
274
|
end
|
265
275
|
end
|
266
276
|
end
|
267
277
|
end
|
268
278
|
|
269
|
-
def run_script(targets, script, arguments, options = {})
|
279
|
+
def run_script(targets, script, arguments, options = {}, position = [])
|
270
280
|
description = options.fetch(:description, "script #{script}")
|
271
281
|
log_action(description, targets) do
|
272
282
|
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
273
283
|
|
274
284
|
batch_execute(targets) do |transport, batch|
|
275
285
|
with_node_logging("Running script #{script} with '#{arguments.to_json}'", batch) do
|
276
|
-
transport.batch_script(batch, script, arguments, options, &method(:publish_event))
|
286
|
+
transport.batch_script(batch, script, arguments, options, position, &method(:publish_event))
|
277
287
|
end
|
278
288
|
end
|
279
289
|
end
|
280
290
|
end
|
281
291
|
|
282
|
-
def run_task(targets, task, arguments, options = {})
|
292
|
+
def run_task(targets, task, arguments, options = {}, position = [], log_level = :info)
|
283
293
|
description = options.fetch(:description, "task #{task.name}")
|
284
294
|
log_action(description, targets) do
|
285
295
|
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
286
296
|
arguments['_task'] = task.name
|
287
297
|
|
288
298
|
batch_execute(targets) do |transport, batch|
|
289
|
-
with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
|
290
|
-
transport.batch_task(batch, task, arguments, options, &method(:publish_event))
|
299
|
+
with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch, log_level) do
|
300
|
+
transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
|
291
301
|
end
|
292
302
|
end
|
293
303
|
end
|
294
304
|
end
|
295
305
|
|
296
|
-
def run_task_with(target_mapping, task, options = {})
|
306
|
+
def run_task_with(target_mapping, task, options = {}, position = [])
|
297
307
|
targets = target_mapping.keys
|
298
308
|
description = options.fetch(:description, "task #{task.name}")
|
299
309
|
|
@@ -303,26 +313,26 @@ module Bolt
|
|
303
313
|
|
304
314
|
batch_execute(targets) do |transport, batch|
|
305
315
|
with_node_logging("Running task #{task.name}'", batch) do
|
306
|
-
transport.batch_task_with(batch, task, target_mapping, options, &method(:publish_event))
|
316
|
+
transport.batch_task_with(batch, task, target_mapping, options, position, &method(:publish_event))
|
307
317
|
end
|
308
318
|
end
|
309
319
|
end
|
310
320
|
end
|
311
321
|
|
312
|
-
def upload_file(targets, source, destination, options = {})
|
322
|
+
def upload_file(targets, source, destination, options = {}, position = [])
|
313
323
|
description = options.fetch(:description, "file upload from #{source} to #{destination}")
|
314
324
|
log_action(description, targets) do
|
315
325
|
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
316
326
|
|
317
327
|
batch_execute(targets) do |transport, batch|
|
318
328
|
with_node_logging("Uploading file #{source} to #{destination}", batch) do
|
319
|
-
transport.batch_upload(batch, source, destination, options, &method(:publish_event))
|
329
|
+
transport.batch_upload(batch, source, destination, options, position, &method(:publish_event))
|
320
330
|
end
|
321
331
|
end
|
322
332
|
end
|
323
333
|
end
|
324
334
|
|
325
|
-
def download_file(targets, source, destination, options = {})
|
335
|
+
def download_file(targets, source, destination, options = {}, position = [])
|
326
336
|
description = options.fetch(:description, "file download from #{source} to #{destination}")
|
327
337
|
|
328
338
|
begin
|
@@ -337,7 +347,7 @@ module Bolt
|
|
337
347
|
|
338
348
|
batch_execute(targets) do |transport, batch|
|
339
349
|
with_node_logging("Downloading file #{source} to #{destination}", batch) do
|
340
|
-
transport.batch_download(batch, source, destination, options, &method(:publish_event))
|
350
|
+
transport.batch_download(batch, source, destination, options, position, &method(:publish_event))
|
341
351
|
end
|
342
352
|
end
|
343
353
|
end
|
@@ -347,6 +357,82 @@ module Bolt
|
|
347
357
|
plan.call_by_name_with_scope(scope, params, true)
|
348
358
|
end
|
349
359
|
|
360
|
+
def create_yarn(scope, block, object, index)
|
361
|
+
fiber = Fiber.new do
|
362
|
+
# Create the new scope
|
363
|
+
newscope = Puppet::Parser::Scope.new(scope.compiler)
|
364
|
+
local = Puppet::Parser::Scope::LocalScope.new
|
365
|
+
|
366
|
+
# Compress the current scopes into a single vars hash to add to the new scope
|
367
|
+
current_scope = scope.effective_symtable(true)
|
368
|
+
until current_scope.nil?
|
369
|
+
current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
|
370
|
+
current_scope = current_scope.parent
|
371
|
+
end
|
372
|
+
newscope.push_ephemerals([local])
|
373
|
+
|
374
|
+
begin
|
375
|
+
result = catch(:return) do
|
376
|
+
args = { block.parameters[0][1].to_s => object }
|
377
|
+
block.closure.call_by_name_with_scope(newscope, args, true)
|
378
|
+
end
|
379
|
+
|
380
|
+
# If we got a return from the block, get it's value
|
381
|
+
# Otherwise the result is the last line from the block
|
382
|
+
result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
|
383
|
+
|
384
|
+
# Validate the result is a PlanResult
|
385
|
+
unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
|
386
|
+
raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
|
387
|
+
end
|
388
|
+
|
389
|
+
result
|
390
|
+
rescue Puppet::PreformattedError => e
|
391
|
+
if e.cause.is_a?(Bolt::Error)
|
392
|
+
e.cause
|
393
|
+
else
|
394
|
+
raise e
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
Bolt::Yarn.new(fiber, index)
|
400
|
+
end
|
401
|
+
|
402
|
+
def handle_event(event)
|
403
|
+
case event[:type]
|
404
|
+
when :node_result
|
405
|
+
@thread_completed = true
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def round_robin(skein)
|
410
|
+
subscribe(self, [:node_result])
|
411
|
+
results = Array.new(skein.length)
|
412
|
+
@in_parallel = true
|
413
|
+
|
414
|
+
until skein.empty?
|
415
|
+
@thread_completed = false
|
416
|
+
r = nil
|
417
|
+
|
418
|
+
skein.each do |yarn|
|
419
|
+
if yarn.alive?
|
420
|
+
r = yarn.resume
|
421
|
+
else
|
422
|
+
results[yarn.index] = yarn.value
|
423
|
+
skein.delete(yarn)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
next unless r == 'unfinished'
|
428
|
+
sleep(0.1) until @thread_completed || skein.empty?
|
429
|
+
end
|
430
|
+
|
431
|
+
@in_parallel = false
|
432
|
+
unsubscribe(self, [:node_result])
|
433
|
+
results
|
434
|
+
end
|
435
|
+
|
350
436
|
class TimeoutError < RuntimeError; end
|
351
437
|
|
352
438
|
def wait_until_available(targets,
|