bolt 1.3.0 → 1.4.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: d1188441f6f0d4a4012c0e1bedbfeff66dcafa7624911448865e4081950510c3
4
- data.tar.gz: 5add229df85ea63038f8a2492adcd1086ad3d3201280bed7b22fb7cfdaf205a3
3
+ metadata.gz: 8afdc9d66e35557fd6dee9433d0c5dd62f9620fb111a7b56d3ba11e7a0b66e73
4
+ data.tar.gz: d2ea3502f583cfa8cd136f8e04eaebac50364c822f892595eb0782c8e6c6f175
5
5
  SHA512:
6
- metadata.gz: 49cdd374d0607bfd3ff6367d1c9b3be6061df74a7c1ed57940e07b78f9ae7f9fc8e235769fb741b63b85b136f127ee289ac26110b692c47d03d19cf543e0e495
7
- data.tar.gz: 5bdb2cea2de3c6a8696b7e3b76a2ece602d94480f42b30feb8c2531bc93ca5ebadf3ba1b3f6d3bfe9cc5ba0effc0e9d0949729af7df20313d159fcbac554eb03
6
+ metadata.gz: 3c15b22e1ab464ba249372b62161da08c69e31db4c55ff2a89b590fde7abccde114dc0559e1b3b32eae3fcae69ef09afd1f142fe89e1976d682247f4e1e16bdc
7
+ data.tar.gz: d1b82c13cb0faf8b80e5b37bf6800a24a7837e4ed572690257163fcf95da6dcd2a14f25a0f23e7fd33c7e797683e00eda3f0f2933a319785ce92a40f5bdfcc01
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Adds a target to specified inventory group.
6
+ Puppet::Functions.create_function(:add_to_group) do
7
+ # @param targets A pattern or array of patterns identifying a set of targets.
8
+ # @param group The name of the group to add targets to.
9
+ # @example Add new Target to group.
10
+ # Target.new('foo@example.com', 'password' => 'secret').add_to_group('group1')
11
+ # @example Add new target to group by name.
12
+ # add_to_group('bolt:bolt@web.com', 'group1')
13
+ # @example Add an array of targets to group by name.
14
+ # add_to_group(['host1', 'group1', 'winrm://host2:54321'], 'group1')
15
+ # @example Add a comma separated list list of targets to group by name.
16
+ # add_to_group('foo,bar,baz', 'group1')
17
+ dispatch :add_to_group do
18
+ param 'Boltlib::TargetSpec', :targets
19
+ param 'String[1]', :group
20
+ end
21
+
22
+ def add_to_group(targets, group)
23
+ inventory = Puppet.lookup(:bolt_inventory) { nil }
24
+
25
+ unless inventory && Puppet.features.bolt?
26
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
27
+ Puppet::Pops::Issues::TASK_MISSING_BOLT, action: _('process targets through inventory')
28
+ )
29
+ end
30
+
31
+ executor = Puppet.lookup(:bolt_executor) { nil }
32
+ executor&.report_function_call('add_to_group')
33
+
34
+ inventory.add_to_group(inventory.get_targets(targets), group)
35
+ end
36
+ end
@@ -19,7 +19,9 @@ module Bolt
19
19
  inventory_nodes: :cd2,
20
20
  inventory_groups: :cd3,
21
21
  target_nodes: :cd4,
22
- output_format: :cd5
22
+ output_format: :cd5,
23
+ statement_count: :cd6,
24
+ resource_mean: :cd7
23
25
  }.freeze
24
26
 
25
27
  def self.build_client
@@ -82,7 +84,11 @@ module Bolt
82
84
  submit(base_params.merge(screen_view_params))
83
85
  end
84
86
 
85
- def event(category, action, label = nil, value = nil)
87
+ def event(category, action, label: nil, value: nil, **kwargs)
88
+ custom_dimensions = Bolt::Util.walk_keys(kwargs) do |k|
89
+ CUSTOM_DIMENSIONS[k] || raise("Unknown analytics key '#{k}'")
90
+ end
91
+
86
92
  event_params = {
87
93
  # Type
88
94
  t: 'event',
@@ -90,7 +96,7 @@ module Bolt
90
96
  ec: category,
91
97
  # Event Action
92
98
  ea: action
93
- }
99
+ }.merge(custom_dimensions)
94
100
 
95
101
  # Event Label
96
102
  event_params[:el] = label if label
@@ -160,7 +166,7 @@ module Bolt
160
166
  @logger.debug "Skipping submission of '#{screen}' screenview because analytics is disabled"
161
167
  end
162
168
 
163
- def event(category, action, _label = nil, _value = nil)
169
+ def event(category, action, **_kwargs)
164
170
  @logger.debug "Skipping submission of '#{category} #{action}' event because analytics is disabled"
165
171
  end
166
172
 
@@ -134,12 +134,24 @@ module Bolt
134
134
 
135
135
  targets = @inventory.get_targets(args[0])
136
136
 
137
- ast = Puppet::Pops::Serialization::ToDataConverter.convert(apply_body, rich_data: true, symbol_to_string: true)
137
+ apply_ast(apply_body, targets, options, plan_vars)
138
+ end
138
139
 
139
- apply_ast(ast, targets, options, plan_vars)
140
+ # Count the number of top-level statements in the AST.
141
+ def count_statements(ast)
142
+ case ast
143
+ when Puppet::Pops::Model::Program
144
+ count_statements(ast.body)
145
+ when Puppet::Pops::Model::BlockExpression
146
+ ast.statements.count
147
+ else
148
+ 1
149
+ end
140
150
  end
141
151
 
142
- def apply_ast(ast, targets, options, plan_vars = {})
152
+ def apply_ast(raw_ast, targets, options, plan_vars = {})
153
+ ast = Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
154
+
143
155
  notify = proc { |_| nil }
144
156
 
145
157
  r = @executor.log_action('apply catalog', targets) do
@@ -154,12 +166,15 @@ module Bolt
154
166
  result_promises = targets.zip(futures).flat_map do |target, future|
155
167
  @executor.queue_execute([target]) do |transport, batch|
156
168
  @executor.with_node_logging("Applying manifest block", batch) do
169
+ catalog = future.value
170
+ raise future.reason if future.rejected?
171
+
157
172
  arguments = {
158
- 'catalog' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(future.value),
173
+ 'catalog' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(catalog),
159
174
  'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins),
160
175
  '_noop' => options['_noop']
161
176
  }
162
- raise future.reason if future.rejected?
177
+
163
178
  results = transport.batch_task(batch, catalog_apply_task, arguments, options, &notify)
164
179
  Array(results).map { |result| ApplyResult.from_task_result(result) }
165
180
  end
@@ -169,6 +184,10 @@ module Bolt
169
184
  @executor.await_results(result_promises)
170
185
  end
171
186
 
187
+ # Allow for report to exclude event metrics (apply_result doesn't require it to be present)
188
+ resource_counts = r.ok_set.map { |result| result.event_metrics&.fetch('total') }.compact
189
+ @executor.report_apply(count_statements(raw_ast), resource_counts)
190
+
172
191
  if !r.ok && !options['_catch_errors']
173
192
  raise Bolt::ApplyFailure, r
174
193
  end
@@ -165,18 +165,32 @@ module Bolt
165
165
 
166
166
  def report_transport(transport, count)
167
167
  name = transport.class.name.split('::').last.downcase
168
- @analytics&.event('Transport', 'initialize', name, count) unless @reported_transports.include?(name)
168
+ unless @reported_transports.include?(name)
169
+ @analytics&.event('Transport', 'initialize', label: name, value: count)
170
+ end
169
171
  @reported_transports.add(name)
170
172
  end
171
173
 
172
174
  def report_function_call(function)
173
- @analytics&.event('Plan', 'call_function', function)
175
+ @analytics&.event('Plan', 'call_function', label: function)
174
176
  end
175
177
 
176
178
  def report_bundled_content(mode, name)
177
179
  if @bundled_content&.include?(name)
178
- @analytics&.event('Bundled Content', mode, name)
180
+ @analytics&.event('Bundled Content', mode, label: name)
181
+ end
182
+ end
183
+
184
+ def report_apply(statement_count, resource_counts)
185
+ data = { statement_count: statement_count }
186
+
187
+ unless resource_counts.empty?
188
+ sum = resource_counts.inject(0) { |accum, i| accum + i }
189
+ # Intentionally rounded to an integer. High precision isn't useful.
190
+ data[:resource_mean] = sum / resource_counts.length
179
191
  end
192
+
193
+ @analytics&.event('Apply', 'ast', data)
180
194
  end
181
195
 
182
196
  def with_node_logging(description, batch)
@@ -93,6 +93,19 @@ module Bolt
93
93
  targets.map { |t| update_target(t) }
94
94
  end
95
95
 
96
+ def add_to_group(targets, desired_group)
97
+ if group_names.include?(desired_group)
98
+ targets.each do |target|
99
+ if group_names.include?(target.name)
100
+ raise ValidationError.new("Group #{target.name} conflicts with node of the same name", target.name)
101
+ end
102
+ add_node(@groups, target, desired_group)
103
+ end
104
+ else
105
+ raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
106
+ end
107
+ end
108
+
96
109
  def set_var(target, key, value)
97
110
  data = { key => value }
98
111
  set_vars_from_hash(target.name, data)
@@ -243,5 +256,40 @@ module Bolt
243
256
  end
244
257
  end
245
258
  private :set_facts
259
+
260
+ def add_node(current_group, target, desired_group, track = { 'all' => nil })
261
+ if current_group.name == desired_group
262
+ # Group to add to is found
263
+ t_name = target.name
264
+ # Add target to nodes hash
265
+ current_group.nodes[t_name] = { 'name' => t_name }.merge(target.options)
266
+ # Inherit facts, vars, and features from hierarchy
267
+ current_group_data = { facts: current_group.facts, vars: current_group.vars, features: current_group.features }
268
+ data = inherit_data(track, current_group.name, current_group_data)
269
+ set_facts(t_name, @target_facts[t_name] ? data[:facts].merge(@target_facts[t_name]) : data[:facts])
270
+ set_vars_from_hash(t_name, @target_vars[t_name] ? data[:vars].merge(@target_vars[t_name]) : data[:vars])
271
+ data[:features].each do |feature|
272
+ set_feature(target, feature)
273
+ end
274
+ return true
275
+ end
276
+ # Recurse on children Groups if not desired_group
277
+ current_group.groups.each do |child_group|
278
+ track[child_group.name] = current_group
279
+ add_node(child_group, target, desired_group, track)
280
+ end
281
+ end
282
+ private :add_node
283
+
284
+ def inherit_data(track, name, data)
285
+ unless track[name].nil?
286
+ data[:facts] = track[name].facts.merge(data[:facts])
287
+ data[:vars] = track[name].vars.merge(data[:vars])
288
+ data[:features].concat(track[name].features)
289
+ inherit_data(track, track[name].name, data)
290
+ end
291
+ data
292
+ end
293
+ private :inherit_data
246
294
  end
247
295
  end
@@ -5,7 +5,7 @@ module Bolt
5
5
  # Group is a specific implementation of Inventory based on nested
6
6
  # structured data.
7
7
  class Group
8
- attr_accessor :name, :nodes, :groups, :config, :rest
8
+ attr_accessor :name, :nodes, :groups, :config, :rest, :facts, :vars, :features
9
9
 
10
10
  def initialize(data)
11
11
  @logger = Logging.logger[self]
@@ -180,8 +180,7 @@ module Bolt
180
180
  # Parses a snippet of Puppet manifest code and returns the AST represented
181
181
  # in JSON.
182
182
  def parse_manifest(code, filename)
183
- raw_ast = Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code, filename)
184
- Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
183
+ Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code, filename)
185
184
  rescue Puppet::Error => e
186
185
  raise Bolt::PAL::PALError, "Failed to parse manifest: #{e}"
187
186
  end
@@ -36,5 +36,9 @@ module Bolt
36
36
  def to_json(*args)
37
37
  @value.to_json(*args)
38
38
  end
39
+
40
+ def to_s
41
+ to_json
42
+ end
39
43
  end
40
44
  end
@@ -54,10 +54,7 @@ module Bolt
54
54
  begin
55
55
  require 'net/ssh/krb'
56
56
  rescue LoadError
57
- logger.debug {
58
- "Authentication method 'gssapi-with-mic' is not available. "\
59
- "Please install the kerberos gem with `gem install net-ssh-krb`"
60
- }
57
+ logger.debug("Authentication method 'gssapi-with-mic' (Kerberos) is not available.")
61
58
  end
62
59
 
63
60
  @transport_logger = Logging.logger[Net::SSH]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.3.0'
4
+ VERSION = '1.4.0'
5
5
  end
@@ -38,7 +38,6 @@ require 'bolt/pal'
38
38
  #
39
39
  #
40
40
  # TODO:
41
- # - allow stubbing for commands, scripts and file uploads
42
41
  # - Allow description based stub matching
43
42
  # - Better testing of plan errors
44
43
  # - Better error collection around call counts. Show what stubs exists and more than a single failure
@@ -52,6 +51,36 @@ require 'bolt/pal'
52
51
  # - resultset matchers to help testing canary like plans?
53
52
  # - inventory matchers to help testing plans that change inventory
54
53
  #
54
+ # Stubs:
55
+ # - allow_command(cmd), expect_command(cmd): expect the exact command
56
+ # - allow_script(script), expect_script(script): expect the script as <module>/path/to/file
57
+ # - allow_task(task), expect_task(task): expect the named task
58
+ # - allow_upload(file), expect_upload(file): expect the identified source file
59
+ # - allow_apply_prep: allows `apply_prep` to be invoked in the plan but does not allow modifiers
60
+ # - allow_apply: allows `apply` to be invoked in the plan but does not allow modifiers
61
+ #
62
+ # Stub modifiers:
63
+ # - be_called_times(n): if allowed, fail if the action is called more than 'n' times
64
+ # if expected, fail unless the action is called 'n' times
65
+ # - not_be_called: fail if the action is called
66
+ # - with_targets(targets): target or list of targets that you expect to be passed to the action
67
+ # - with_params(params): list of params and metaparams (or options) that you expect to be passed to the action.
68
+ # Corresponds to the action's last argument.
69
+ # - with_destination(dest): for upload_file, the expected destination path
70
+ # - always_return(value): return a Bolt::ResultSet of Bolt::Result objects with the specified value Hash
71
+ # command and script: only accept 'stdout' and 'stderr' keys
72
+ # upload: does not support this modifier
73
+ # - return_for_targets(targets_to_values): return a Bolt::ResultSet of Bolt::Result objects from the Hash mapping
74
+ # targets to their value Hashes
75
+ # command and script: only accept 'stdout' and 'stderr' keys
76
+ # upload: does not support this modifier
77
+ # - return(&block): invoke the block to construct a Bolt::ResultSet. The blocks parameters differ based on action
78
+ # command: `{ |targets:, command:, params:| ... }`
79
+ # script: `{ |targets:, script:, params:| ... }`
80
+ # task: `{ |targets:, task:, params:| ... }`
81
+ # upload: `{ |targets:, source:, destination:, params:| ... }`
82
+ # - error_with(err): return a failing Bolt::ResultSet, with Bolt::Result objects with the identified err hash
83
+ #
55
84
  # Example:
56
85
  # describe "my_plan" do
57
86
  # it 'should return' do
@@ -87,8 +116,16 @@ require 'bolt/pal'
87
116
  # 'node2' => {'result_key' => 6} })
88
117
  # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(13)
89
118
  # end
119
+ #
120
+ # it 'should construct a custom return value' do
121
+ # expect_task('my_task').return do |targets:, task:, params:|
122
+ # Bolt::ResultSet.new(targets.map { |targ| Bolt::Result.new(targ, {'result_key' => 10'})})
123
+ # end
124
+ # expect(run_plan('my_plan', { 'param1' => 10 })).to eq(10)
125
+ # end
90
126
  # end
91
127
  #
128
+ # See spec/bolt_spec/plan_spec.rb for more examples.
92
129
  module BoltSpec
93
130
  module Plans
94
131
  def self.init
@@ -110,9 +147,11 @@ module BoltSpec
110
147
 
111
148
  # Override in your tests
112
149
  def config
113
- config = Bolt::Config.new(Bolt::Boltdir.new('.'), {})
114
- config.modulepath = modulepath
115
- config
150
+ @config ||= begin
151
+ conf = Bolt::Config.new(Bolt::Boltdir.new('.'), {})
152
+ conf.modulepath = [modulepath].flatten
153
+ conf
154
+ end
116
155
  end
117
156
 
118
157
  # Override in your tests
@@ -120,8 +159,11 @@ module BoltSpec
120
159
  @inventory ||= Bolt::Inventory.new({})
121
160
  end
122
161
 
162
+ # Provided as a class so expectations can be placed on it.
163
+ class MockPuppetDBClient; end
164
+
123
165
  def puppetdb_client
124
- @puppetdb_client ||= mock('puppetdb_client')
166
+ @puppetdb_client ||= MockPuppetDBClient.new
125
167
  end
126
168
 
127
169
  def run_plan(name, params)
@@ -132,30 +174,46 @@ module BoltSpec
132
174
  raise executor.error_message
133
175
  end
134
176
 
135
- executor.assert_call_expectations
177
+ begin
178
+ executor.assert_call_expectations
179
+ rescue StandardError => e
180
+ raise "#{e.message}\nPlan result: #{result}"
181
+ end
136
182
 
137
183
  result
138
184
  end
139
185
 
140
- # Allowed task stubs can be called up to be_called_times number
141
- # of times
142
- def allow_task(task_name)
143
- executor.stub_task(task_name).add_stub
186
+ MOCKED_ACTIONS.each do |action|
187
+ # Allowed action stubs can be called up to be_called_times number of times
188
+ define_method :"allow_#{action}" do |object|
189
+ executor.send(:"stub_#{action}", object).add_stub
190
+ end
191
+
192
+ # Expected action stubs must be called exactly the expected number of times
193
+ # or at least once without be_called_times
194
+ define_method :"expect_#{action}" do |object|
195
+ send(:"allow_#{action}", object).expect_call
196
+ end
197
+
198
+ # This stub will catch any action call if there are no stubs specifically for that task
199
+ define_method :"allow_any_#{action}" do
200
+ executor.send(:"stub_#{action}", :default).add_stub
201
+ end
144
202
  end
145
203
 
146
- # Expected task stubs must be called exactly the expected number of times
147
- # or at least once without be_called_times
148
- def expect_task(task_name)
149
- allow_task(task_name).expect_call
204
+ def allow_apply_prep
205
+ allow_task('puppet_agent::version').always_return('version' => '6.0')
206
+ allow_task('apply_helpers::custom_facts')
207
+ nil
150
208
  end
151
209
 
152
- # This stub will catch any task call if there are no stubs specifically for that task
153
- def allow_any_task
154
- executor.stub_task(:default).add_stub
210
+ def allow_apply
211
+ executor.stub_apply
212
+ nil
155
213
  end
156
214
 
157
215
  # Example helpers to mock other run functions
158
- # The with_targets method makes sense for all stubs
216
+ # The with_targets method makes sense for all stubs
159
217
  # with_params could be reused for options
160
218
  # They probably need special stub methods for other arguments through
161
219
 
@@ -180,7 +238,7 @@ module BoltSpec
180
238
 
181
239
  # intended to be private below here
182
240
  def executor
183
- @executor ||= BoltSpec::Plans::MockExecutor.new
241
+ @executor ||= BoltSpec::Plans::MockExecutor.new(modulepath)
184
242
  end
185
243
  end
186
244
  end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/result'
4
+ require 'bolt/util'
5
+
6
+ module BoltSpec
7
+ module Plans
8
+ # Nothing in the ActionDouble is 'public'
9
+ class ActionDouble
10
+ def initialize(action_stub)
11
+ @stubs = []
12
+ @action_stub = action_stub
13
+ end
14
+
15
+ def process(*args)
16
+ matches = @stubs.select { |s| s.matches(*args) }
17
+ unless matches.empty?
18
+ matches[0].call(*args)
19
+ end
20
+ end
21
+
22
+ def assert_called(object)
23
+ @stubs.each { |s| s.assert_called(object) }
24
+ end
25
+
26
+ def add_stub
27
+ stub = Plans.const_get(@action_stub).new
28
+ @stubs.unshift stub
29
+ stub
30
+ end
31
+ end
32
+
33
+ class ActionStub
34
+ attr_reader :invocation
35
+
36
+ def initialize(expect = false)
37
+ @calls = 0
38
+ @expect = expect
39
+ @expected_calls = 1
40
+ # invocation spec
41
+ @invocation = {}
42
+ # return value
43
+ @data = { default: {} }
44
+ end
45
+
46
+ def assert_called(object)
47
+ satisfied = if @expect
48
+ (@expected_calls.nil? && @calls > 0) || @calls == @expected_calls
49
+ else
50
+ @expected_calls.nil? || @calls <= @expected_calls
51
+ end
52
+ unless satisfied
53
+ unless (times = @expected_calls)
54
+ times = @expect ? "at least one" : "any number of"
55
+ end
56
+ message = "Expected #{object} to be called #{times} times"
57
+ message += " with targets #{@invocation[:targets]}" if @invocation[:targets]
58
+ message += " with parameters #{parameters}" if parameters
59
+ raise message
60
+ end
61
+ end
62
+
63
+ # This changes the stub from an allow to an expect which will validate
64
+ # that it has been called.
65
+ def expect_call
66
+ @expect = true
67
+ self
68
+ end
69
+
70
+ # Used to create a valid Bolt::Result object from result data.
71
+ def default_for(target)
72
+ case @data[:default]
73
+ when Bolt::Error
74
+ Bolt::Result.from_exception(target, @data[:default])
75
+ when Hash
76
+ result_for(target, Bolt::Util.walk_keys(@data[:default], &:to_sym))
77
+ else
78
+ raise 'Default result must be a Hash'
79
+ end
80
+ end
81
+
82
+ def check_resultset(result_set, object)
83
+ unless result_set.is_a?(Bolt::ResultSet)
84
+ raise "Return block for #{object} did not return a Bolt::ResultSet"
85
+ end
86
+ result_set
87
+ end
88
+
89
+ # Below here are the intended 'public' methods of the stub
90
+
91
+ # Restricts the stub to only match invocations with
92
+ # the correct targets
93
+ def with_targets(targets)
94
+ targets = [targets] unless targets.is_a? Array
95
+ @invocation[:targets] = targets.map do |target|
96
+ if target.is_a? String
97
+ target
98
+ else
99
+ target.name
100
+ end
101
+ end
102
+ self
103
+ end
104
+
105
+ # limit the maximum number of times an allow stub may be called or
106
+ # specify how many times an expect stub must be called.
107
+ def be_called_times(times)
108
+ @expected_calls = times
109
+ self
110
+ end
111
+
112
+ # error if the stub is called at all.
113
+ def not_be_called
114
+ @expected_calls = 0
115
+ self
116
+ end
117
+
118
+ def return(&block)
119
+ raise "Cannot set return values and return block." if @data_set
120
+ @return_block = block
121
+ self
122
+ end
123
+
124
+ # Set different result values for each target. May use string or symbol keys, but allowed key names
125
+ # are restricted based on action.
126
+ def return_for_targets(data)
127
+ data.each_with_object(@data) do |(target, result), hsh|
128
+ raise "Mocked results must be hashes: #{target}: #{result}" unless result.is_a? Hash
129
+ hsh[target] = result_for(Bolt::Target.new(target), Bolt::Util.walk_keys(result, &:to_sym))
130
+ end
131
+ raise "Cannot set return values and return block." if @return_block
132
+ @data_set = true
133
+ self
134
+ end
135
+
136
+ # Set a default return value for all targets, specific targets may be overridden with return_for_targets.
137
+ # Follows the same rules for data as return_for_targets.
138
+ def always_return(data)
139
+ @data[:default] = data
140
+ @data_set = true
141
+ self
142
+ end
143
+
144
+ # Set a default error result for all targets.
145
+ def error_with(data)
146
+ data = Bolt::Util.walk_keys(data, &:to_s)
147
+ if data['msg'] && data['kind'] && (data.keys - %w[msg kind details issue_code]).empty?
148
+ @data[:default] = Bolt::Error.new(data['msg'], data['kind'], data['details'], data['issue_code'])
149
+ else
150
+ STDERR.puts "In the future 'error_with()' may require msg and kind, and " \
151
+ "optionally accept only details and issue_code."
152
+ @data[:default] = data
153
+ end
154
+ @data_set = true
155
+ self
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ require_relative 'action_stubs/command_stub'
162
+ require_relative 'action_stubs/script_stub'
163
+ require_relative 'action_stubs/task_stub'
164
+ require_relative 'action_stubs/upload_stub'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltSpec
4
+ module Plans
5
+ class CommandStub < ActionStub
6
+ def matches(targets, _command, options)
7
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
8
+ return false
9
+ end
10
+
11
+ if @invocation[:options] && options != @invocation[:options]
12
+ return false
13
+ end
14
+
15
+ true
16
+ end
17
+
18
+ def call(targets, command, options)
19
+ @calls += 1
20
+ if @return_block
21
+ check_resultset(@return_block.call(targets: targets, command: command, params: options), command)
22
+ else
23
+ Bolt::ResultSet.new(targets.map { |target| @data[target.name] || default_for(target) })
24
+ end
25
+ end
26
+
27
+ def parameters
28
+ @invocation[:options]
29
+ end
30
+
31
+ def result_for(target, stdout: '', stderr: '')
32
+ Bolt::Result.for_command(target, stdout, stderr, 0)
33
+ end
34
+
35
+ # Public methods
36
+
37
+ def with_params(params)
38
+ @invocation[:options] = params
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltSpec
4
+ module Plans
5
+ class ScriptStub < ActionStub
6
+ def matches(targets, _script, arguments, options)
7
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
8
+ return false
9
+ end
10
+
11
+ if @invocation[:arguments] && arguments != @invocation[:arguments]
12
+ return false
13
+ end
14
+
15
+ if @invocation[:options] && options != @invocation[:options]
16
+ return false
17
+ end
18
+
19
+ true
20
+ end
21
+
22
+ def call(targets, script, arguments, options)
23
+ @calls += 1
24
+ if @return_block
25
+ # Merge arguments and options into params to match puppet function signature.
26
+ params = options.merge('arguments' => arguments)
27
+ check_resultset(@return_block.call(targets: targets, script: script, params: params), script)
28
+ else
29
+ Bolt::ResultSet.new(targets.map { |target| @data[target.name] || default_for(target) })
30
+ end
31
+ end
32
+
33
+ def parameters
34
+ @invocation[:arguments] + @invocation[:options]
35
+ end
36
+
37
+ def result_for(target, stdout: '', stderr: '')
38
+ Bolt::Result.for_command(target, stdout, stderr, 0)
39
+ end
40
+
41
+ # Public methods
42
+
43
+ def with_params(params)
44
+ @invocation[:arguments] = params['arguments']
45
+ @invocation[:options] = params.select { |k, _v| k.start_with?('_') }
46
+ self
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltSpec
4
+ module Plans
5
+ class TaskStub < ActionStub
6
+ def matches(targets, _task, arguments, options)
7
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
8
+ return false
9
+ end
10
+
11
+ if @invocation[:arguments] && arguments != @invocation[:arguments]
12
+ return false
13
+ end
14
+
15
+ if @invocation[:options] && options != @invocation[:options]
16
+ return false
17
+ end
18
+
19
+ true
20
+ end
21
+
22
+ def call(targets, task, arguments, options)
23
+ @calls += 1
24
+ if @return_block
25
+ # Merge arguments and options into params to match puppet function signature.
26
+ check_resultset(@return_block.call(targets: targets, task: task, params: arguments.merge(options)), task)
27
+ else
28
+ Bolt::ResultSet.new(targets.map { |target| @data[target.name] || default_for(target) })
29
+ end
30
+ end
31
+
32
+ def parameters
33
+ @invocation[:arguments] + @invocation[:options]
34
+ end
35
+
36
+ # Allow any data.
37
+ def result_for(target, data)
38
+ Bolt::Result.new(target, value: Bolt::Util.walk_keys(data, &:to_s))
39
+ end
40
+
41
+ # Public methods
42
+
43
+ # Restricts the stub to only match invocations with certain parameters.
44
+ # All parameters must match exactly.
45
+ def with_params(params)
46
+ @invocation[:arguments] = params.reject { |k, _v| k.start_with?('_') }
47
+ @invocation[:options] = params.select { |k, _v| k.start_with?('_') }
48
+ self
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltSpec
4
+ module Plans
5
+ class UploadStub < ActionStub
6
+ def matches(targets, _source, destination, options)
7
+ if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
8
+ return false
9
+ end
10
+
11
+ if @invocation[:destination] && destination != @invocation[:destination]
12
+ return false
13
+ end
14
+
15
+ if @invocation[:options] && options != @invocation[:options]
16
+ return false
17
+ end
18
+
19
+ true
20
+ end
21
+
22
+ def call(targets, source, destination, options)
23
+ @calls += 1
24
+ if @return_block
25
+ results = @return_block.call(targets: targets, source: source, destination: destination, params: options)
26
+ check_resultset(results, source)
27
+ else
28
+ results = targets.map do |target|
29
+ if @data[:default].is_a?(Bolt::Error)
30
+ default_for(target)
31
+ else
32
+ Bolt::Result.for_upload(target, source, destination)
33
+ end
34
+ end
35
+ Bolt::ResultSet.new(results)
36
+ end
37
+ end
38
+
39
+ def parameters
40
+ @invocation[:options]
41
+ end
42
+
43
+ def result_for(_target, _data)
44
+ raise 'Upload result cannot be changed'
45
+ end
46
+
47
+ # Public methods
48
+
49
+ def always_return(_data)
50
+ raise 'Upload result cannot be changed'
51
+ end
52
+
53
+ def with_destination(destination)
54
+ @invocation[:destination] = destination
55
+ self
56
+ end
57
+
58
+ def with_params(params)
59
+ @invocation[:options] = params
60
+ self
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,191 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt_spec/plans/action_stubs'
3
4
  require 'bolt/error'
4
5
  require 'bolt/result_set'
5
6
  require 'bolt/result'
7
+ require 'pathname'
6
8
  require 'set'
7
9
 
8
10
  module BoltSpec
9
11
  module Plans
10
- class UnexpectedInvocation < ArgumentError; end
11
-
12
- # Nothing in the TaskDouble is 'public'
13
- class TaskDouble
14
- def initialize
15
- @stubs = []
16
- end
17
-
18
- def process(targets, task, arguments, options)
19
- # TODO: should we bother matching at all? or just call each
20
- # stub until one works?
21
- matches = @stubs.select { |s| s.matches(targets, task, arguments, options) }
22
- unless matches.empty?
23
- matches[0].call(targets, task, arguments, options)
24
- end
25
- end
12
+ MOCKED_ACTIONS = %i[command script task upload].freeze
26
13
 
27
- def assert_called(taskname)
28
- @stubs.each { |s| s.assert_called(taskname) }
29
- end
30
-
31
- def add_stub
32
- stub = TaskStub.new
33
- @stubs.unshift stub
34
- stub
35
- end
36
- end
14
+ class UnexpectedInvocation < ArgumentError; end
37
15
 
38
- class TaskStub
39
- attr_reader :invocation
16
+ # Nothing on the executor is 'public'
17
+ class MockExecutor
18
+ attr_reader :noop, :error_message
19
+ attr_accessor :run_as
40
20
 
41
- def initialize(expect = false)
42
- @calls = 0
43
- @expect = expect
44
- @expected_calls = 1
45
- # invocation spec
46
- @invocation = {}
47
- # return value
48
- @data = { default: {} }
21
+ def initialize(modulepath)
22
+ @noop = false
23
+ @run_as = nil
24
+ @error_message = nil
25
+ @allow_apply = false
26
+ @modulepath = [modulepath].flatten.map { |path| File.absolute_path(path) }
27
+ MOCKED_ACTIONS.each { |action| instance_variable_set(:"@#{action}_doubles", {}) }
49
28
  end
50
29
 
51
- def matches(targets, _task, arguments, options)
52
- if @invocation[:targets] && Set.new(@invocation[:targets]) != Set.new(targets.map(&:name))
53
- return false
54
- end
30
+ def module_file_id(file)
31
+ modpath = @modulepath.select { |path| file =~ /^#{path}/ }
32
+ raise "Could not identify module path containing #{file}: #{modpath}" unless modpath.size == 1
55
33
 
56
- if @invocation[:arguments] && arguments != @invocation[:arguments]
57
- return false
58
- end
59
-
60
- if @invocation[:options] && options != @invocation[:options]
61
- return false
62
- end
63
-
64
- true
34
+ path = Pathname.new(file)
35
+ relative = path.relative_path_from(Pathname.new(modpath.first))
36
+ segments = relative.to_path.split('/')
37
+ ([segments[0]] + segments[2..-1]).join('/')
65
38
  end
66
39
 
67
- def call(targets, task, arguments, options)
68
- @calls += 1
69
- if @return_block
70
- # Merge arguments and options into params to match puppet function signature.
71
- result_set = @return_block.call(targets: targets, task: task, params: arguments.merge(options))
72
- unless result_set.is_a?(Bolt::ResultSet)
73
- raise "Return block for #{task} did not return a Bolt::ResultSet"
74
- end
75
- result_set
76
- else
77
- results = targets.map do |target|
78
- val = @data[target.name] || @data[:default]
79
- Bolt::Result.new(target, value: val)
80
- end
81
- Bolt::ResultSet.new(results)
40
+ def run_command(targets, command, options = {})
41
+ result = nil
42
+ if (doub = @command_doubles[command] || @command_doubles[:default])
43
+ result = doub.process(targets, command, options)
82
44
  end
83
- end
84
-
85
- def assert_called(taskname)
86
- satisfied = if @expect
87
- (@expected_calls.nil? && @calls > 0) || @calls == @expected_calls
88
- else
89
- @expected_calls.nil? || @calls <= @expected_calls
90
- end
91
- unless satisfied
92
- unless (times = @expected_calls)
93
- times = @expect ? "at least one" : "any number of"
94
- end
95
- message = "Expected #{taskname} to be called #{times} times"
96
- message += " with targets #{@invocation[:targets]}" if @invocation[:targets]
97
- message += " with parameters #{@invocations[:parameters]}" if @invocation[:parameters]
98
- raise message
45
+ unless result
46
+ targets = targets.map(&:name)
47
+ @error_message = "Unexpected call to 'run_command(#{command}, #{targets}, #{options})'"
48
+ raise UnexpectedInvocation, @error_message
99
49
  end
50
+ result
100
51
  end
101
52
 
102
- # This changes the stub from an allow to an expect which will validate
103
- # that it has been called.
104
- def expect_call
105
- @expect = true
106
- self
107
- end
108
-
109
- # Below here are the intended 'public' methods of the stub
110
-
111
- # Restricts the stub to only match invocations with
112
- # the correct targets
113
- def with_targets(targets)
114
- targets = [targets] unless targets.is_a? Array
115
- @invocation[:targets] = targets.map do |target|
116
- if target.is_a? String
117
- target
118
- else
119
- target.name
120
- end
53
+ def run_script(targets, script_path, arguments, options = {})
54
+ script = module_file_id(script_path)
55
+ result = nil
56
+ if (doub = @script_doubles[script] || @script_doubles[:default])
57
+ result = doub.process(targets, script, arguments, options)
121
58
  end
122
- self
123
- end
124
-
125
- # Restricts the stub to only match invocations with certain parameters
126
- # All parameters must match exactly and since arguments and options are
127
- # treated differently at the executor this won't work with some '_*' options
128
- # TODO: Fix handling of '_*' options probably by breaking them into other helpers
129
- def with_params(params)
130
- @invocation[:parameters] = params
131
- @invocation[:arguments] = params.reject { |k, _v| k.start_with?('_') }
132
- @invocation[:options] = params.select { |k, _v| k.start_with?('_') }
133
- self
134
- end
135
-
136
- # limit the maximum number of times an allow stub may be called or
137
- # specify how many times an expect stub must be called.
138
- def be_called_times(times)
139
- @expected_calls = times
140
- self
141
- end
142
-
143
- # error if the stub is called at all.
144
- def not_be_called
145
- @expected_calls = 0
146
- self
147
- end
148
-
149
- def return(&block)
150
- raise "Cannot set return values and return block." if @data_set
151
- @return_block = block
152
- self
153
- end
154
-
155
- # Set different result values for each target
156
- def return_for_targets(data)
157
- data.each do |target, result|
158
- raise "Mocked results must be hashes: #{target}: #{result}" unless result.is_a? Hash
59
+ unless result
60
+ targets = targets.map(&:name)
61
+ params = options.merge('arguments' => arguments)
62
+ @error_message = "Unexpected call to 'run_script(#{script}, #{targets}, #{params})'"
63
+ raise UnexpectedInvocation, @error_message
159
64
  end
160
- raise "Cannot set return values and return block." if @return_block
161
- @data = data
162
- @data_set = true
163
- self
164
- end
165
-
166
- # Set a default return value for all targets, specific targets may be overridden with return_for_targets
167
- def always_return(default_data)
168
- return_for_targets(default: default_data)
169
- end
170
-
171
- # Set a default error result for all targets.
172
- def error_with(error_data)
173
- always_return("_error" => error_data)
174
- end
175
- end
176
-
177
- # Nothing on the executor is 'public'
178
- class MockExecutor
179
- attr_reader :noop, :error_message
180
-
181
- def initialize
182
- @noop = false
183
- @task_doubles = {}
184
- @allow_any_task = true
185
- @error_message = nil
65
+ result
186
66
  end
187
67
 
188
- def run_task(targets, task, arguments, options)
68
+ def run_task(targets, task, arguments, options = {})
189
69
  result = nil
190
70
  if (doub = @task_doubles[task.name] || @task_doubles[:default])
191
71
  result = doub.process(targets, task.name, arguments, options)
@@ -199,18 +79,44 @@ module BoltSpec
199
79
  result
200
80
  end
201
81
 
82
+ def upload_file(targets, source_path, destination, options = {})
83
+ source = module_file_id(source_path)
84
+ result = nil
85
+ if (doub = @upload_doubles[source] || @upload_doubles[:default])
86
+ result = doub.process(targets, source, destination, options)
87
+ end
88
+ unless result
89
+ targets = targets.map(&:name)
90
+ @error_message = "Unexpected call to 'upload_file(#{source}, #{destination}, #{targets}, #{options})'"
91
+ raise UnexpectedInvocation, @error_message
92
+ end
93
+ result
94
+ end
95
+
202
96
  def assert_call_expectations
203
- @task_doubles.map do |taskname, doub|
204
- doub.assert_called(taskname)
97
+ MOCKED_ACTIONS.each do |action|
98
+ instance_variable_get(:"@#{action}_doubles").map do |object, doub|
99
+ doub.assert_called(object)
100
+ end
101
+ end
102
+ end
103
+
104
+ MOCKED_ACTIONS.each do |action|
105
+ define_method(:"stub_#{action}") do |object|
106
+ instance_variable_get(:"@#{action}_doubles")[object] ||= ActionDouble.new(:"#{action.capitalize}Stub")
205
107
  end
206
108
  end
207
109
 
208
- def stub_task(task_name)
209
- @task_doubles[task_name] ||= TaskDouble.new
110
+ def stub_apply
111
+ @allow_apply = true
210
112
  end
211
113
 
212
114
  def wait_until_available(targets, _options)
213
- targets.map { |target| Bolt::Result.new(target) }
115
+ Bolt::ResultSet.new(targets.map { |target| Bolt::Result.new(target) })
116
+ end
117
+
118
+ def log_action(*_args)
119
+ yield
214
120
  end
215
121
 
216
122
  def log_plan(_plan_name)
@@ -224,6 +130,24 @@ module BoltSpec
224
130
  def report_function_call(_function); end
225
131
 
226
132
  def report_bundled_content(_mode, _name); end
133
+
134
+ def report_apply(_statements, _resources); end
135
+
136
+ # Mocked for Apply so it does not compile and execute.
137
+ def with_node_logging(_description, targets)
138
+ raise "Unexpected call to apply(#{targets})" unless @allow_apply
139
+ end
140
+
141
+ def queue_execute(targets)
142
+ raise "Unexpected call to apply(#{targets})" unless @allow_apply
143
+ targets
144
+ end
145
+
146
+ def await_results(promises)
147
+ raise "Unexpected call to apply(#{targets})" unless @allow_apply
148
+ Bolt::ResultSet.new(promises.map { |target| Bolt::ApplyResult.new(target) })
149
+ end
150
+ # End Apply mocking
227
151
  end
228
152
  end
229
153
  end
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: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-14 00:00:00.000000000 Z
11
+ date: 2018-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -283,6 +283,7 @@ files:
283
283
  - bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb
284
284
  - bolt-modules/boltlib/lib/puppet/datatypes/target.rb
285
285
  - bolt-modules/boltlib/lib/puppet/functions/add_facts.rb
286
+ - bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb
286
287
  - bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb
287
288
  - bolt-modules/boltlib/lib/puppet/functions/facts.rb
288
289
  - bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb
@@ -360,6 +361,11 @@ files:
360
361
  - lib/bolt_server/schemas/winrm-run_task.json
361
362
  - lib/bolt_server/transport_app.rb
362
363
  - lib/bolt_spec/plans.rb
364
+ - lib/bolt_spec/plans/action_stubs.rb
365
+ - lib/bolt_spec/plans/action_stubs/command_stub.rb
366
+ - lib/bolt_spec/plans/action_stubs/script_stub.rb
367
+ - lib/bolt_spec/plans/action_stubs/task_stub.rb
368
+ - lib/bolt_spec/plans/action_stubs/upload_stub.rb
363
369
  - lib/bolt_spec/plans/mock_executor.rb
364
370
  - lib/bolt_spec/run.rb
365
371
  - libexec/apply_catalog.rb