bolt 2.16.0 → 2.21.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 +3 -1
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +6 -4
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
- data/lib/bolt/applicator.rb +19 -14
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +60 -16
- data/lib/bolt/catalog.rb +3 -2
- data/lib/bolt/cli.rb +121 -43
- data/lib/bolt/config.rb +37 -34
- data/lib/bolt/config/options.rb +340 -173
- data/lib/bolt/config/transport/options.rb +315 -160
- data/lib/bolt/config/transport/ssh.rb +24 -10
- data/lib/bolt/executor.rb +21 -0
- data/lib/bolt/inventory/group.rb +3 -2
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/logger.rb +24 -1
- data/lib/bolt/outputter.rb +1 -1
- data/lib/bolt/outputter/rainbow.rb +14 -3
- data/lib/bolt/pal.rb +28 -10
- data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
- data/lib/bolt/pal/yaml_plan/step.rb +24 -2
- data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
- data/lib/bolt/plugin/module.rb +2 -4
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +20 -6
- data/lib/bolt/puppetdb/client.rb +2 -0
- data/lib/bolt/puppetdb/config.rb +16 -0
- data/lib/bolt/result.rb +7 -0
- data/lib/bolt/shell/bash.rb +45 -37
- data/lib/bolt/shell/powershell.rb +21 -11
- data/lib/bolt/shell/powershell/snippets.rb +15 -6
- data/lib/bolt/transport/base.rb +24 -0
- data/lib/bolt/transport/docker.rb +16 -4
- data/lib/bolt/transport/docker/connection.rb +20 -2
- data/lib/bolt/transport/local/connection.rb +14 -1
- data/lib/bolt/transport/orch.rb +20 -0
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh.rb +7 -1
- data/lib/bolt/transport/ssh/connection.rb +9 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
- data/lib/bolt/transport/winrm/connection.rb +118 -8
- data/lib/bolt/util.rb +26 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +3 -2
- data/lib/bolt_spec/bolt_context.rb +7 -2
- data/lib/bolt_spec/plans.rb +15 -2
- data/lib/bolt_spec/plans/action_stubs.rb +2 -1
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/mock_executor.rb +14 -1
- data/lib/bolt_spec/run.rb +22 -0
- data/libexec/bolt_catalog +3 -2
- data/modules/secure_env_vars/plans/init.pp +20 -0
- metadata +21 -29
@@ -32,13 +32,14 @@ module Bolt
|
|
32
32
|
user
|
33
33
|
].concat(RUN_AS_OPTIONS).sort.freeze
|
34
34
|
|
35
|
-
# Options available when using the
|
36
|
-
|
35
|
+
# Options available when using the native ssh transport
|
36
|
+
NATIVE_OPTIONS = %w[
|
37
37
|
cleanup
|
38
38
|
copy-command
|
39
39
|
host
|
40
40
|
host-key-check
|
41
41
|
interpreters
|
42
|
+
native-ssh
|
42
43
|
port
|
43
44
|
private-key
|
44
45
|
script-dir
|
@@ -56,17 +57,30 @@ module Bolt
|
|
56
57
|
"tty" => false
|
57
58
|
}.freeze
|
58
59
|
|
59
|
-
# The set of options available for the ssh and
|
60
|
+
# The set of options available for the ssh and native ssh transports overlap, so we
|
60
61
|
# need to check which transport is used before fully initializing, otherwise options
|
61
62
|
# may not be filtered correctly.
|
62
63
|
def initialize(data = {}, project = nil)
|
63
64
|
assert_hash_or_config(data)
|
64
|
-
@
|
65
|
+
@native = true if data['native-ssh']
|
65
66
|
super(data, project)
|
66
67
|
end
|
67
68
|
|
69
|
+
# This method is used to filter CLI options in the Config class. This
|
70
|
+
# should include `ssh-command` so that we can later warn if the option
|
71
|
+
# is present without `native-ssh`
|
72
|
+
def self.options
|
73
|
+
%w[ssh-command native-ssh].concat(OPTIONS)
|
74
|
+
end
|
75
|
+
|
68
76
|
private def filter(unfiltered)
|
69
|
-
|
77
|
+
# Because we filter before merging config together it's impossible to
|
78
|
+
# know whether both ssh-command *and* native-ssh will be specified
|
79
|
+
# unless they are both in the filter. However, we can't add
|
80
|
+
# ssh-command to OPTIONS since that's used for documenting available
|
81
|
+
# options. This makes it so that ssh-command is preserved so we can
|
82
|
+
# warn once all config is resolved if native-ssh isn't set.
|
83
|
+
@native ? unfiltered.slice(*NATIVE_OPTIONS) : unfiltered.slice(*self.class.options)
|
70
84
|
end
|
71
85
|
|
72
86
|
private def validate
|
@@ -83,11 +97,11 @@ module Bolt
|
|
83
97
|
@config['private-key'] = File.expand_path(key_opt, @project)
|
84
98
|
|
85
99
|
# We have an explicit test for this to only warn if using net-ssh transport
|
86
|
-
Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['ssh
|
100
|
+
Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['native-ssh']
|
87
101
|
end
|
88
102
|
|
89
|
-
if key_opt.instance_of?(Hash) && @config['ssh
|
90
|
-
raise Bolt::ValidationError, 'private-key must be a filepath when using ssh
|
103
|
+
if key_opt.instance_of?(Hash) && @config['native-ssh']
|
104
|
+
raise Bolt::ValidationError, 'private-key must be a filepath when using native-ssh'
|
91
105
|
end
|
92
106
|
end
|
93
107
|
|
@@ -117,8 +131,8 @@ module Bolt
|
|
117
131
|
end
|
118
132
|
end
|
119
133
|
|
120
|
-
if @config['ssh
|
121
|
-
msg = 'Cannot use
|
134
|
+
if @config['native-ssh'] && !@config['load-config']
|
135
|
+
msg = 'Cannot use native SSH transport with load-config set to false'
|
122
136
|
raise Bolt::ValidationError, msg
|
123
137
|
end
|
124
138
|
end
|
data/lib/bolt/executor.rb
CHANGED
@@ -320,6 +320,27 @@ module Bolt
|
|
320
320
|
end
|
321
321
|
end
|
322
322
|
|
323
|
+
def download_file(targets, source, destination, options = {})
|
324
|
+
description = options.fetch(:description, "file download from #{source} to #{destination}")
|
325
|
+
|
326
|
+
begin
|
327
|
+
FileUtils.mkdir_p(destination)
|
328
|
+
rescue Errno::EEXIST => e
|
329
|
+
message = "#{e.message}; unable to create destination directory #{destination}"
|
330
|
+
raise Bolt::Error.new(message, 'bolt/file-exist-error')
|
331
|
+
end
|
332
|
+
|
333
|
+
log_action(description, targets) do
|
334
|
+
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
335
|
+
|
336
|
+
batch_execute(targets) do |transport, batch|
|
337
|
+
with_node_logging("Downloading file #{source} to #{destination}", batch) do
|
338
|
+
transport.batch_download(batch, source, destination, options, &method(:publish_event))
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
323
344
|
def run_plan(scope, plan, params)
|
324
345
|
plan.call_by_name_with_scope(scope, params, true)
|
325
346
|
end
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -50,10 +50,11 @@ module Bolt
|
|
50
50
|
# or it could be a name/alias of a target defined in another group.
|
51
51
|
# We can't tell the difference until all groups have been resolved,
|
52
52
|
# so we store the string on its own here and process it later.
|
53
|
-
|
53
|
+
case target
|
54
|
+
when String
|
54
55
|
@string_targets << target
|
55
56
|
# Handle plugins at this level so that lookups cannot trigger recursive lookups
|
56
|
-
|
57
|
+
when Hash
|
57
58
|
add_target_definition(target)
|
58
59
|
else
|
59
60
|
raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
|
@@ -109,11 +109,12 @@ module Bolt
|
|
109
109
|
private :resolve_name
|
110
110
|
|
111
111
|
def expand_targets(targets)
|
112
|
-
|
112
|
+
case targets
|
113
|
+
when Bolt::Target
|
113
114
|
targets
|
114
|
-
|
115
|
+
when Array
|
115
116
|
targets.map { |tish| expand_targets(tish) }
|
116
|
-
|
117
|
+
when String
|
117
118
|
# Expand a comma-separated list
|
118
119
|
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
119
120
|
ts = resolve_name(name)
|
data/lib/bolt/logger.rb
CHANGED
@@ -15,6 +15,7 @@ module Bolt
|
|
15
15
|
return if Logging.initialized?
|
16
16
|
|
17
17
|
Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
|
18
|
+
@mutex = Mutex.new
|
18
19
|
|
19
20
|
Logging.color_scheme(
|
20
21
|
'bolt',
|
@@ -66,6 +67,10 @@ module Bolt
|
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
70
|
+
def self.analytics=(analytics)
|
71
|
+
@analytics = analytics
|
72
|
+
end
|
73
|
+
|
69
74
|
def self.console_layout(color)
|
70
75
|
color_scheme = :bolt if color
|
71
76
|
Logging.layouts.pattern(
|
@@ -89,8 +94,10 @@ module Bolt
|
|
89
94
|
:notice
|
90
95
|
end
|
91
96
|
|
97
|
+
# Explicitly check the log level names instead of the log level number, as levels
|
98
|
+
# that are stringified integers (e.g. "level" => "42") will return a truthy value
|
92
99
|
def self.valid_level?(level)
|
93
|
-
|
100
|
+
Logging::LEVELS.include?(Logging.levelify(level))
|
94
101
|
end
|
95
102
|
|
96
103
|
def self.levels
|
@@ -100,5 +107,21 @@ module Bolt
|
|
100
107
|
def self.reset_logging
|
101
108
|
Logging.reset
|
102
109
|
end
|
110
|
+
|
111
|
+
def self.warn_once(type, msg)
|
112
|
+
@mutex.synchronize {
|
113
|
+
@warnings ||= []
|
114
|
+
@logger ||= Logging.logger[self]
|
115
|
+
unless @warnings.include?(type)
|
116
|
+
@logger.warn(msg)
|
117
|
+
@warnings << type
|
118
|
+
end
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.deprecation_warning(type, msg)
|
123
|
+
@analytics&.event('Warn', 'deprecation', label: type)
|
124
|
+
warn_once(type, msg)
|
125
|
+
end
|
103
126
|
end
|
104
127
|
end
|
data/lib/bolt/outputter.rb
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bolt/pal'
|
4
|
-
require 'paint'
|
5
4
|
|
6
5
|
module Bolt
|
7
6
|
class Outputter
|
8
7
|
class Rainbow < Bolt::Outputter::Human
|
9
8
|
def initialize(color, verbose, trace, stream = $stdout)
|
9
|
+
begin
|
10
|
+
require 'paint'
|
11
|
+
if Bolt::Util.windows?
|
12
|
+
# the Paint gem thinks that windows does not support ansi colors
|
13
|
+
# but windows 10 or later does
|
14
|
+
# we can display colors if we force mode to TRUE_COLOR
|
15
|
+
Paint.mode = 0xFFFFFF
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
raise "The 'paint' gem is required to use the rainbow outputter."
|
19
|
+
end
|
10
20
|
super
|
11
21
|
@line_color = 0
|
12
22
|
@color = 0
|
@@ -29,9 +39,10 @@ module Bolt
|
|
29
39
|
a = string.chars.map do |c|
|
30
40
|
case @state
|
31
41
|
when :normal
|
32
|
-
|
42
|
+
case c
|
43
|
+
when "\e"
|
33
44
|
@state = :ansi
|
34
|
-
|
45
|
+
when "\n"
|
35
46
|
@line_color += 1
|
36
47
|
@color = @line_color
|
37
48
|
c
|
data/lib/bolt/pal.rb
CHANGED
@@ -15,25 +15,36 @@ module Bolt
|
|
15
15
|
# PALError is used to convert errors from executing puppet code into
|
16
16
|
# Bolt::Errors
|
17
17
|
class PALError < Bolt::Error
|
18
|
-
# Puppet sometimes rescues exceptions notes the location and reraises.
|
19
|
-
# Return the original error.
|
20
18
|
def self.from_preformatted_error(err)
|
21
19
|
if err.cause&.is_a? Bolt::Error
|
22
20
|
err.cause
|
23
21
|
else
|
24
|
-
from_error(err
|
22
|
+
from_error(err)
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
28
26
|
# Generate a Bolt::Pal::PALError for non-bolt errors
|
29
27
|
def self.from_error(err)
|
30
|
-
|
28
|
+
# Use the original error message if available
|
29
|
+
message = err.cause ? err.cause.message : err.message
|
30
|
+
|
31
|
+
# Provide the location of an error if it came from a plan
|
32
|
+
details = if defined?(err.file) && err.file
|
33
|
+
{ file: err.file,
|
34
|
+
line: err.line,
|
35
|
+
column: err.pos }.compact
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
|
40
|
+
e = new(message, details)
|
41
|
+
|
31
42
|
e.set_backtrace(err.backtrace)
|
32
43
|
e
|
33
44
|
end
|
34
45
|
|
35
|
-
def initialize(msg)
|
36
|
-
super(msg, 'bolt/pal-error')
|
46
|
+
def initialize(msg, details = {})
|
47
|
+
super(msg, 'bolt/pal-error', details)
|
37
48
|
end
|
38
49
|
end
|
39
50
|
|
@@ -139,7 +150,12 @@ module Bolt
|
|
139
150
|
r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
|
140
151
|
# Only load the project if it a) exists, b) has a name it can be loaded with
|
141
152
|
bolt_project = @project if @project&.name
|
153
|
+
# Puppet currently won't receive the project unless it is a named project. Since
|
154
|
+
# the download_file plan function needs access to the project path, add it to the
|
155
|
+
# context.
|
156
|
+
bolt_project_data = @project
|
142
157
|
Puppet.override(bolt_project: bolt_project,
|
158
|
+
bolt_project_data: bolt_project_data,
|
143
159
|
yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
|
144
160
|
pal.with_script_compiler do |compiler|
|
145
161
|
alias_types(compiler)
|
@@ -159,8 +175,9 @@ module Bolt
|
|
159
175
|
if e.issue_code == :UNKNOWN_VARIABLE &&
|
160
176
|
%w[facts trusted server_facts settings].include?(e.arguments[:name])
|
161
177
|
message = "Evaluation Error: Variable '#{e.arguments[:name]}' is not available in the current scope "\
|
162
|
-
|
163
|
-
|
178
|
+
"unless explicitly defined."
|
179
|
+
details = { file: e.file, line: e.line, column: e.pos }
|
180
|
+
PALError.new(message, details)
|
164
181
|
else
|
165
182
|
PALError.from_preformatted_error(e)
|
166
183
|
end
|
@@ -267,9 +284,10 @@ module Bolt
|
|
267
284
|
|
268
285
|
def parse_params(type, object_name, params)
|
269
286
|
in_bolt_compiler do |compiler|
|
270
|
-
|
287
|
+
case type
|
288
|
+
when 'task'
|
271
289
|
param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
|
272
|
-
|
290
|
+
when 'plan'
|
273
291
|
plan = compiler.plan_signature(object_name)
|
274
292
|
param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
|
275
293
|
end
|
@@ -73,7 +73,7 @@ module Bolt
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def upload_step(scope, step)
|
76
|
-
source = step['source']
|
76
|
+
source = step['upload'] || step['source']
|
77
77
|
destination = step['destination']
|
78
78
|
targets = step['targets'] || step['target']
|
79
79
|
description = step['description']
|
@@ -83,6 +83,17 @@ module Bolt
|
|
83
83
|
scope.call_function('upload_file', args)
|
84
84
|
end
|
85
85
|
|
86
|
+
def download_step(scope, step)
|
87
|
+
source = step['download']
|
88
|
+
destination = step['destination']
|
89
|
+
targets = step['targets'] || step['target']
|
90
|
+
description = step['description']
|
91
|
+
|
92
|
+
args = [source, destination, targets]
|
93
|
+
args << description if description
|
94
|
+
scope.call_function('download_file', args)
|
95
|
+
end
|
96
|
+
|
86
97
|
def eval_step(_scope, step)
|
87
98
|
step['eval']
|
88
99
|
end
|
@@ -97,6 +108,10 @@ module Bolt
|
|
97
108
|
apply_manifest(scope, targets, manifest)
|
98
109
|
end
|
99
110
|
|
111
|
+
def message_step(scope, step)
|
112
|
+
scope.call_function('out::message', step['message'])
|
113
|
+
end
|
114
|
+
|
100
115
|
def generate_manifest(resources)
|
101
116
|
# inspect returns the Ruby representation of the resource hashes,
|
102
117
|
# which happens to be the same as the Puppet representation
|
@@ -142,7 +157,13 @@ module Bolt
|
|
142
157
|
if plan.steps.any? { |step| step.body.key?('target') }
|
143
158
|
msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
|
144
159
|
"in a future version of Bolt. Use the 'targets' parameter instead."
|
145
|
-
|
160
|
+
Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
|
161
|
+
end
|
162
|
+
|
163
|
+
if plan.steps.any? { |step| step.body.key?('source') }
|
164
|
+
msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
|
165
|
+
"in a future version of Bolt. Use the 'upload' parameter instead."
|
166
|
+
Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
|
146
167
|
end
|
147
168
|
|
148
169
|
plan_result = closure_scope.with_local_scope(args_hash) do |scope|
|
@@ -12,7 +12,19 @@ module Bolt
|
|
12
12
|
Set['name', 'description', 'target', 'targets']
|
13
13
|
end
|
14
14
|
|
15
|
-
STEP_KEYS = %w[
|
15
|
+
STEP_KEYS = %w[
|
16
|
+
command
|
17
|
+
destination
|
18
|
+
download
|
19
|
+
eval
|
20
|
+
message
|
21
|
+
plan
|
22
|
+
resources
|
23
|
+
script
|
24
|
+
source
|
25
|
+
task
|
26
|
+
upload
|
27
|
+
].freeze
|
16
28
|
|
17
29
|
def self.create(step_body, step_number)
|
18
30
|
type_keys = (STEP_KEYS & step_body.keys)
|
@@ -22,8 +34,10 @@ module Bolt
|
|
22
34
|
when 1
|
23
35
|
type = type_keys.first
|
24
36
|
else
|
25
|
-
if
|
37
|
+
if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
|
26
38
|
type = 'upload'
|
39
|
+
elsif type_keys.to_set == Set['download', 'destination']
|
40
|
+
type = 'download'
|
27
41
|
else
|
28
42
|
raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
|
29
43
|
end
|
@@ -89,6 +103,12 @@ module Bolt
|
|
89
103
|
missing_keys -= ['targets']
|
90
104
|
end
|
91
105
|
|
106
|
+
# Handle cases where upload step uses deprecated 'source' key instead of 'upload'
|
107
|
+
# TODO: Remove when 'source' is removed
|
108
|
+
if body.include?('source')
|
109
|
+
missing_keys -= ['upload']
|
110
|
+
end
|
111
|
+
|
92
112
|
if missing_keys.any?
|
93
113
|
error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
|
94
114
|
err = step_error(error_message, body['name'], step_number)
|
@@ -156,3 +176,5 @@ require 'bolt/pal/yaml_plan/step/resources'
|
|
156
176
|
require 'bolt/pal/yaml_plan/step/script'
|
157
177
|
require 'bolt/pal/yaml_plan/step/task'
|
158
178
|
require 'bolt/pal/yaml_plan/step/upload'
|
179
|
+
require 'bolt/pal/yaml_plan/step/download'
|
180
|
+
require 'bolt/pal/yaml_plan/step/message'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bolt
|
4
|
+
class PAL
|
5
|
+
class YamlPlan
|
6
|
+
class Step
|
7
|
+
class Download < Step
|
8
|
+
def self.allowed_keys
|
9
|
+
super + Set['download', 'destination']
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.required_keys
|
13
|
+
Set['download', 'destination', 'targets']
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(step_body)
|
17
|
+
super
|
18
|
+
@source = step_body['download']
|
19
|
+
@destination = step_body['destination']
|
20
|
+
end
|
21
|
+
|
22
|
+
def transpile
|
23
|
+
code = String.new(" ")
|
24
|
+
code << "$#{@name} = " if @name
|
25
|
+
|
26
|
+
fn = 'download_file'
|
27
|
+
args = [@source, @destination, @targets]
|
28
|
+
args << @description if @description
|
29
|
+
|
30
|
+
code << function_call(fn, args)
|
31
|
+
|
32
|
+
code << "\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|