bolt 1.14.0 → 1.15.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: a29df5e9c84d323a9d9b739bff71705573816f077ef68848cf7deb0fba373879
4
- data.tar.gz: add9bd7f64f5cc1a61b237421f9973f2d9afe52dcb2fa3f89db1adf94270a93f
3
+ metadata.gz: 271a7fea2ecaf024054a9ae2266bc352b19a9969300c882f9622341582585d48
4
+ data.tar.gz: d10c047f108f62efdfe1e6ac1a35bc7b3303332f0ca0bd50c55635857d41648e
5
5
  SHA512:
6
- metadata.gz: 58002395a520aa67a681c85efe894f98d7fb172eaebf11ddc15719c918d7479335d16ad181873d5ad9cd15a1151eb59551752e4d6f6ecdd5bd1e15335ab7ef83
7
- data.tar.gz: 92fbc2af2713bc301e476de777067bca1f9d391c6a538e40e0907a7d7adb821cd8a0b5ba0a9da666b4799a4392bd783d7abcb846993c7f51c1baf923e18a80b8
6
+ metadata.gz: 7d6355d7246bfa1305c02e13edf5fd7873584b70306e505f48ee1470babf6aa282b9b5f141ee5af6703b60a589f937c7997110ad21a2d6705926835a8b42b415
7
+ data.tar.gz: 4a1e5f3bf454ce08b084c0310b98c71119305f3c34fbeca552dffe3f6029cc16e7c410e2490605a985552e9603d7a9e653509b8a71679f3b2bb5e0a56e13951d
@@ -59,6 +59,11 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
59
59
  executor.run_as = run_as
60
60
  end
61
61
 
62
+ closure = func.class.dispatcher.dispatchers[0]
63
+ if closure.model.is_a?(Bolt::PAL::YamlPlan)
64
+ executor.report_yaml_plan(closure.model.body)
65
+ end
66
+
62
67
  # wrap plan execution in logging messages
63
68
  executor.log_plan(plan_name) do
64
69
  result = nil
@@ -66,7 +71,9 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
66
71
  # If the plan does not throw :return by calling the return function it's result is
67
72
  # undef/nil
68
73
  result = catch(:return) do
69
- func.class.dispatcher.dispatchers[0].call_by_name_with_scope(scope, params, true)
74
+ scope.with_global_scope do |global_scope|
75
+ closure.call_by_name_with_scope(global_scope, params, true)
76
+ end
70
77
  nil
71
78
  end&.value
72
79
  # Validate the result is a PlanResult
@@ -11,6 +11,6 @@ Puppet::Functions.create_function(:'system::env') do
11
11
  end
12
12
 
13
13
  def env(name)
14
- ENV[name].freeze
14
+ ENV[name]
15
15
  end
16
16
  end
@@ -21,7 +21,9 @@ module Bolt
21
21
  target_nodes: :cd4,
22
22
  output_format: :cd5,
23
23
  statement_count: :cd6,
24
- resource_mean: :cd7
24
+ resource_mean: :cd7,
25
+ plan_steps: :cd8,
26
+ return_type: :cd9
25
27
  }.freeze
26
28
 
27
29
  def self.build_client
@@ -62,6 +64,10 @@ module Bolt
62
64
  attr_reader :user_id
63
65
 
64
66
  def initialize(user_id)
67
+ # lazy-load expensive gem code
68
+ require 'concurrent/configuration'
69
+ require 'concurrent/future'
70
+
65
71
  @logger = Logging.logger[self]
66
72
  @http = HTTPClient.new
67
73
  @user_id = user_id
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'base64'
4
- require 'concurrent'
5
4
  require 'find'
6
5
  require 'json'
7
6
  require 'logging'
@@ -15,6 +14,9 @@ require 'bolt/util/puppet_log_level'
15
14
  module Bolt
16
15
  class Applicator
17
16
  def initialize(inventory, executor, modulepath, plugin_dirs, pdb_client, hiera_config, max_compiles)
17
+ # lazy-load expensive gem code
18
+ require 'concurrent'
19
+
18
20
  @inventory = inventory
19
21
  @executor = executor
20
22
  @modulepath = modulepath
@@ -65,6 +65,7 @@ module Bolt
65
65
  def initialize(target, error: nil, report: nil)
66
66
  @target = target
67
67
  @value = {}
68
+ @type = 'apply'
68
69
  value['report'] = report if report
69
70
  value['_error'] = error if error
70
71
  value['_output'] = metrics_message if metrics_message
data/lib/bolt/config.rb CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'logging'
5
- require 'concurrent'
5
+ # limit the loaded portion of concurrent as its expensive
6
+ require 'concurrent/utility/processor_counter'
6
7
  require 'pathname'
7
8
  require 'bolt/boltdir'
8
9
  require 'bolt/transport/ssh'
data/lib/bolt/executor.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  # Used for $ERROR_INFO. This *must* be capitalized!
4
4
  require 'English'
5
5
  require 'json'
6
- require 'concurrent'
7
6
  require 'logging'
8
7
  require 'set'
9
8
  require 'bolt/analytics'
@@ -25,10 +24,13 @@ module Bolt
25
24
  noop = nil,
26
25
  bundled_content: nil,
27
26
  load_config: true)
27
+
28
+ # lazy-load expensive gem code
29
+ require 'concurrent'
30
+
28
31
  @analytics = analytics
29
32
  @bundled_content = bundled_content
30
33
  @logger = Logging.logger[self]
31
- @plan_logging = false
32
34
  @load_config = load_config
33
35
 
34
36
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
@@ -199,6 +201,22 @@ module Bolt
199
201
  @analytics&.event('Apply', 'ast', data)
200
202
  end
201
203
 
204
+ def report_yaml_plan(plan)
205
+ steps = plan.steps.count
206
+ return_type = case plan.return
207
+ when Bolt::PAL::YamlPlan::EvaluableString
208
+ 'expression'
209
+ when nil
210
+ nil
211
+ else
212
+ 'value'
213
+ end
214
+
215
+ @analytics&.event('Plan', 'yaml', plan_steps: steps, return_type: return_type)
216
+ rescue StandardError => e
217
+ @logger.debug { "Failed to submit analytics event: #{e.message}" }
218
+ end
219
+
202
220
  def with_node_logging(description, batch)
203
221
  @logger.info("#{description} on #{batch.map(&:uri)}")
204
222
  result = yield
@@ -10,6 +10,11 @@ module Bolt
10
10
  # Regex used to validate group names and target aliases.
11
11
  NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
12
12
 
13
+ DATA_KEYS = %w[name config facts vars features].freeze
14
+ NODE_KEYS = DATA_KEYS + ['alias']
15
+ GROUP_KEYS = DATA_KEYS + %w[groups nodes]
16
+ CONFIG_KEYS = Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']
17
+
13
18
  def initialize(data)
14
19
  @logger = Logging.logger[self]
15
20
 
@@ -20,11 +25,21 @@ module Bolt
20
25
  raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
21
26
  raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
22
27
 
28
+ unless (unexpected_keys = data.keys - GROUP_KEYS).empty?
29
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
30
+ @logger.warn(msg)
31
+ end
32
+
23
33
  @vars = fetch_value(data, 'vars', Hash)
24
34
  @facts = fetch_value(data, 'facts', Hash)
25
35
  @features = fetch_value(data, 'features', Array)
26
36
  @config = fetch_value(data, 'config', Hash)
27
37
 
38
+ unless (unexpected_keys = @config.keys - CONFIG_KEYS).empty?
39
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for group #{@name}"
40
+ @logger.warn(msg)
41
+ end
42
+
28
43
  nodes = fetch_value(data, 'nodes', Array)
29
44
  groups = fetch_value(data, 'groups', Array)
30
45
 
@@ -43,6 +58,16 @@ module Bolt
43
58
  raise ValidationError.new("Node #{node} does not have a name", @name) unless node['name']
44
59
  @nodes[node['name']] = node
45
60
 
61
+ unless (unexpected_keys = node.keys - NODE_KEYS).empty?
62
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in node #{node['name']}"
63
+ @logger.warn(msg)
64
+ end
65
+ config_keys = node['config']&.keys || []
66
+ unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
67
+ msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for node #{node['name']}"
68
+ @logger.warn(msg)
69
+ end
70
+
46
71
  next unless node.include?('alias')
47
72
 
48
73
  aliases = node['alias']
@@ -67,9 +92,6 @@ module Bolt
67
92
  @name_or_alias = nodes.select { |node| node.is_a?(String) }
68
93
 
69
94
  @groups = groups.map { |g| Group.new(g) }
70
-
71
- # this allows arbitrary info for the top level
72
- @rest = data.reject { |k, _| %w[name nodes config groups vars facts features].include? k }
73
95
  end
74
96
 
75
97
  private def fetch_value(data, key, type)
data/lib/bolt/notifier.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
4
-
5
3
  module Bolt
6
4
  class Notifier
7
- def initialize(executor = Concurrent::SingleThreadExecutor.new)
8
- @executor = executor
5
+ def initialize(executor = nil)
6
+ # lazy-load expensive gem code
7
+ require 'concurrent'
8
+
9
+ @executor = executor || Concurrent::SingleThreadExecutor.new
9
10
  end
10
11
 
11
12
  def notify(callback, event)
data/lib/bolt/pal.rb CHANGED
@@ -81,6 +81,7 @@ module Bolt
81
81
 
82
82
  require 'bolt/pal/logging'
83
83
  require 'bolt/pal/issues'
84
+ require 'bolt/pal/yaml_plan/loader'
84
85
 
85
86
  # Now that puppet is loaded we can include puppet mixins in data types
86
87
  Bolt::ResultSet.include_iterable
@@ -103,7 +104,9 @@ module Bolt
103
104
  pal.with_script_compiler do |compiler|
104
105
  alias_types(compiler)
105
106
  begin
106
- yield compiler
107
+ Puppet.override(yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
108
+ yield compiler
109
+ end
107
110
  rescue Bolt::Error => err
108
111
  err
109
112
  rescue Puppet::PreformattedError => err
@@ -247,7 +250,12 @@ module Bolt
247
250
 
248
251
  def list_plans
249
252
  in_bolt_compiler do |compiler|
250
- compiler.list_plans.map { |plan| [plan.name] }.sort
253
+ errors = []
254
+ plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
255
+ errors.each do |error|
256
+ @logger.warn(error.details['original_error'])
257
+ end
258
+ plans
251
259
  end
252
260
  end
253
261
 
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ Parameter = Struct.new(:name, :value, :type_expr) do
7
+ def captures_rest
8
+ false
9
+ end
10
+ end
11
+
12
+ attr_reader :name, :parameters, :steps, :return
13
+
14
+ def initialize(name, plan)
15
+ # Top-level plan keys aren't allowed to be Puppet code, so force them
16
+ # all to strings.
17
+ plan = Bolt::Util.walk_keys(plan) { |key| stringify(key) }
18
+
19
+ @name = name.freeze
20
+
21
+ # Nothing in parameters is allowed to be code, since no variables are defined yet
22
+ params_hash = stringify(plan.fetch('parameters', {}))
23
+
24
+ # Munge parameters into an array of Parameter objects, which is what
25
+ # the Puppet API expects
26
+ @parameters = params_hash.map do |param, definition|
27
+ definition ||= {}
28
+ type = Puppet::Pops::Types::TypeParser.singleton.parse(definition['type']) if definition.key?('type')
29
+ Parameter.new(param, definition['default'], type)
30
+ end.freeze
31
+
32
+ @steps = plan['steps']&.map do |step|
33
+ # Step keys also aren't allowed to be code and neither is the value of "name"
34
+ stringified_step = Bolt::Util.walk_keys(step) { |key| stringify(key) }
35
+ stringified_step['name'] = stringify(stringified_step['name']) if stringified_step.key?('name')
36
+ stringified_step
37
+ end.freeze
38
+
39
+ @return = plan['return']
40
+
41
+ validate
42
+ end
43
+
44
+ VAR_NAME_PATTERN = /\A[a-z_][a-z0-9_]*\z/.freeze
45
+
46
+ def validate
47
+ unless @steps.is_a?(Array)
48
+ raise Bolt::Error.new("Plan must specify an array of steps", "bolt/invalid-plan")
49
+ end
50
+
51
+ used_names = Set.new
52
+
53
+ # Parameters come in a hash, so they must be unique
54
+ @parameters.each do |param|
55
+ unless param.name.is_a?(String) && param.name.match?(VAR_NAME_PATTERN)
56
+ raise Bolt::Error.new("Invalid parameter name #{param.name.inspect}", "bolt/invalid-plan")
57
+ end
58
+
59
+ used_names << param.name
60
+ end
61
+
62
+ @steps.each do |step|
63
+ next unless step.key?('name')
64
+
65
+ unless step['name'].is_a?(String) && step['name'].match?(VAR_NAME_PATTERN)
66
+ raise Bolt::Error.new("Invalid step name #{step['name'].inspect}", "bolt/invalid-plan")
67
+ end
68
+
69
+ if used_names.include?(step['name'])
70
+ msg = "Step name #{step['name'].inspect} matches an existing parameter or step name"
71
+ raise Bolt::Error.new(msg, "bolt/invalid-plan")
72
+ end
73
+
74
+ used_names << step['name']
75
+ end
76
+ end
77
+
78
+ def body
79
+ self
80
+ end
81
+
82
+ # Turn all "potential" strings in the object into actual strings.
83
+ # Because we interpret bare strings as potential Puppet code, even in
84
+ # places where Puppet code isn't allowed (like some hash keys), we need
85
+ # to be able to force them back into regular strings, as if we had
86
+ # parsed them normally.
87
+ def stringify(value)
88
+ case value
89
+ when Array
90
+ value.map { |element| stringify(element) }
91
+ when Hash
92
+ value.each_with_object({}) do |(k, v), o|
93
+ o[stringify(k)] = stringify(v)
94
+ end
95
+ when EvaluableString
96
+ value.value
97
+ else
98
+ value
99
+ end
100
+ end
101
+
102
+ def return_type
103
+ Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult')
104
+ end
105
+
106
+ # This class wraps a value parsed from YAML which may be Puppet code.
107
+ # That includes double-quoted strings and string literals, each of which
108
+ # subclasses this parent class in order to implement its own evaluation
109
+ # logic.
110
+ class EvaluableString
111
+ attr_reader :value
112
+ def initialize(value)
113
+ @value = value
114
+ end
115
+ end
116
+
117
+ # This class represents a double-quoted YAML string, which is interpreted
118
+ # as though it were a double-quoted Puppet string (with associated
119
+ # variable interpolations)
120
+ class DoubleQuotedString < EvaluableString
121
+ def evaluate(scope, evaluator)
122
+ # "inspect" allows us to get back a double-quoted string literal with
123
+ # special characters escaped. This is based on the assumption that
124
+ # YAML, Ruby and Puppet all support similar escape sequences.
125
+ parse_result = evaluator.parse_string(@value.inspect)
126
+
127
+ scope.with_local_scope({}) do
128
+ evaluator.evaluate(scope, parse_result)
129
+ end
130
+ end
131
+ end
132
+
133
+ # This represents a literal snippet of Puppet code
134
+ class CodeLiteral < EvaluableString
135
+ def evaluate(scope, evaluator)
136
+ parse_result = evaluator.parse_string(@value)
137
+
138
+ scope.with_local_scope({}) do
139
+ evaluator.evaluate(scope, parse_result)
140
+ end
141
+ end
142
+ end
143
+
144
+ # This class stores a bare YAML string, which is fuzzily interpreted as
145
+ # either Puppet code or a literal string, depending on whether it starts
146
+ # with a variable reference.
147
+ class BareString < EvaluableString
148
+ def evaluate(scope, evaluator)
149
+ if @value.start_with?('$')
150
+ # Try to parse the string as Puppet code. If it's invalid code,
151
+ # return the original string.
152
+ parse_result = evaluator.parse_string(@value)
153
+ scope.with_local_scope({}) do
154
+ evaluator.evaluate(scope, parse_result)
155
+ end
156
+ else
157
+ @value
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/pal/yaml_plan'
4
+
5
+ module Bolt
6
+ class PAL
7
+ class YamlPlan
8
+ class Evaluator
9
+ def initialize(analytics = Bolt::Analytics::NoopClient.new)
10
+ @logger = Logging.logger[self]
11
+ @analytics = analytics
12
+ @evaluator = Puppet::Pops::Parser::EvaluatingParser.new
13
+ end
14
+
15
+ STEP_KEYS = %w[task command eval script source plan].freeze
16
+
17
+ def dispatch_step(scope, step)
18
+ step = evaluate_code_blocks(scope, step)
19
+
20
+ step_type, *extra_keys = STEP_KEYS.select { |key| step.key?(key) }
21
+ if !step_type || extra_keys.any?
22
+ unsupported_step(scope, step)
23
+ end
24
+
25
+ case step_type
26
+ when 'task'
27
+ task_step(scope, step)
28
+ when 'command'
29
+ command_step(scope, step)
30
+ when 'plan'
31
+ plan_step(scope, step)
32
+ when 'script'
33
+ script_step(scope, step)
34
+ when 'source'
35
+ upload_file_step(scope, step)
36
+ when 'eval'
37
+ eval_step(scope, step)
38
+ else
39
+ # This shouldn't be able to happen since this case statement should
40
+ # match the STEP_KEYS list, but raise an error *just in case*,
41
+ # instead of silently skipping the step.
42
+ unsupported_step(scope, step)
43
+ end
44
+ end
45
+
46
+ def task_step(scope, step)
47
+ task = step['task']
48
+ target = step['target']
49
+ description = step['description']
50
+ params = step['parameters'] || {}
51
+ raise "Can't run a task without specifying a target" unless target
52
+
53
+ args = if description
54
+ [task, target, description, params]
55
+ else
56
+ [task, target, params]
57
+ end
58
+
59
+ scope.call_function('run_task', args)
60
+ end
61
+
62
+ def plan_step(scope, step)
63
+ plan = step['plan']
64
+ parameters = step['parameters'] || {}
65
+
66
+ args = [plan, parameters]
67
+
68
+ scope.call_function('run_plan', args)
69
+ end
70
+
71
+ def script_step(scope, step)
72
+ script = step['script']
73
+ target = step['target']
74
+ description = step['description']
75
+ arguments = step['arguments'] || []
76
+ raise "Can't run a script without specifying a target" unless target
77
+
78
+ options = { 'arguments' => arguments }
79
+ args = if description
80
+ [script, target, description, options]
81
+ else
82
+ [script, target, options]
83
+ end
84
+
85
+ scope.call_function('run_script', args)
86
+ end
87
+
88
+ def command_step(scope, step)
89
+ command = step['command']
90
+ target = step['target']
91
+ description = step['description']
92
+ raise "Can't run a command without specifying a target" unless target
93
+
94
+ args = [command, target]
95
+ args << description if description
96
+ scope.call_function('run_command', args)
97
+ end
98
+
99
+ def upload_file_step(scope, step)
100
+ source = step['source']
101
+ destination = step['destination']
102
+ target = step['target']
103
+ description = step['description']
104
+ raise "Can't upload a file without specifying a target" unless target
105
+ raise "Can't upload a file without specifying a destination" unless destination
106
+
107
+ args = [source, destination, target]
108
+ args << description if description
109
+ scope.call_function('upload_file', args)
110
+ end
111
+
112
+ def eval_step(_scope, step)
113
+ step['eval']
114
+ end
115
+
116
+ def unsupported_step(_scope, step)
117
+ raise Bolt::Error.new("Unsupported plan step", "bolt/unsupported-step", step: step)
118
+ end
119
+
120
+ # This is the method that Puppet calls to evaluate the plan. The name
121
+ # makes more sense for .pp plans.
122
+ def evaluate_block_with_bindings(closure_scope, args_hash, plan)
123
+ plan_result = closure_scope.with_local_scope(args_hash) do |scope|
124
+ plan.steps.each do |step|
125
+ step_result = dispatch_step(scope, step)
126
+
127
+ scope.setvar(step['name'], step_result) if step.key?('name')
128
+ end
129
+
130
+ evaluate_code_blocks(scope, plan.return)
131
+ end
132
+
133
+ throw :return, Puppet::Pops::Evaluator::Return.new(plan_result, nil, nil)
134
+ end
135
+
136
+ # Recursively evaluate any EvaluableString instances in the object.
137
+ def evaluate_code_blocks(scope, value)
138
+ # XXX We should establish a local scope here probably
139
+ case value
140
+ when Array
141
+ value.map { |element| evaluate_code_blocks(scope, element) }
142
+ when Hash
143
+ value.each_with_object({}) do |(k, v), o|
144
+ key = k.is_a?(EvaluableString) ? k.value : k
145
+ o[key] = evaluate_code_blocks(scope, v)
146
+ end
147
+ when EvaluableString
148
+ value.evaluate(scope, @evaluator)
149
+ else
150
+ value
151
+ end
152
+ end
153
+
154
+ # Occasionally the Closure will ask us to evaluate what it assumes are
155
+ # AST objects. Because we've sidestepped the AST, they aren't, so just
156
+ # return the values as already evaluated.
157
+ def evaluate(value, _scope)
158
+ value
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end