bolt 2.26.0 → 2.31.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 +13 -12
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
- data/lib/bolt/analytics.rb +4 -0
- data/lib/bolt/applicator.rb +19 -18
- data/lib/bolt/bolt_option_parser.rb +112 -22
- data/lib/bolt/catalog.rb +1 -1
- data/lib/bolt/cli.rb +210 -174
- data/lib/bolt/config.rb +22 -2
- data/lib/bolt/config/modulepath.rb +30 -0
- data/lib/bolt/config/options.rb +30 -0
- data/lib/bolt/config/transport/options.rb +1 -1
- data/lib/bolt/executor.rb +1 -1
- data/lib/bolt/inventory.rb +11 -10
- data/lib/bolt/logger.rb +26 -19
- data/lib/bolt/module_installer.rb +242 -0
- data/lib/bolt/outputter.rb +4 -0
- data/lib/bolt/outputter/human.rb +77 -17
- data/lib/bolt/outputter/json.rb +21 -6
- data/lib/bolt/outputter/logger.rb +2 -2
- data/lib/bolt/pal.rb +46 -25
- data/lib/bolt/plugin.rb +1 -1
- data/lib/bolt/plugin/module.rb +1 -1
- data/lib/bolt/project.rb +62 -12
- data/lib/bolt/project_migrator.rb +80 -0
- data/lib/bolt/project_migrator/base.rb +39 -0
- data/lib/bolt/project_migrator/config.rb +67 -0
- data/lib/bolt/project_migrator/inventory.rb +67 -0
- data/lib/bolt/project_migrator/modules.rb +198 -0
- data/lib/bolt/puppetfile.rb +149 -0
- data/lib/bolt/puppetfile/installer.rb +43 -0
- data/lib/bolt/puppetfile/module.rb +93 -0
- data/lib/bolt/rerun.rb +1 -1
- data/lib/bolt/result.rb +15 -0
- data/lib/bolt/shell/bash.rb +4 -3
- data/lib/bolt/transport/base.rb +4 -4
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/util.rb +51 -10
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/acl.rb +2 -2
- data/lib/bolt_server/base_config.rb +3 -3
- data/lib/bolt_server/config.rb +1 -1
- data/lib/bolt_server/file_cache.rb +11 -11
- data/lib/bolt_server/transport_app.rb +206 -27
- data/lib/bolt_spec/bolt_context.rb +8 -6
- data/lib/bolt_spec/plans.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +1 -1
- data/lib/bolt_spec/run.rb +1 -1
- metadata +14 -6
- data/lib/bolt/project_migrate.rb +0 -138
- data/lib/bolt_server/pe/pal.rb +0 -67
data/lib/bolt/version.rb
CHANGED
data/lib/bolt_server/acl.rb
CHANGED
@@ -7,7 +7,7 @@ module BoltServer
|
|
7
7
|
class BaseConfig
|
8
8
|
def config_keys
|
9
9
|
%w[host port ssl-cert ssl-key ssl-ca-cert
|
10
|
-
ssl-cipher-suites loglevel logfile
|
10
|
+
ssl-cipher-suites loglevel logfile allowlist projects-dir]
|
11
11
|
end
|
12
12
|
|
13
13
|
def env_keys
|
@@ -98,8 +98,8 @@ module BoltServer
|
|
98
98
|
raise Bolt::ValidationError, "Configured 'ssl-cipher-suites' must be an array of cipher suite names"
|
99
99
|
end
|
100
100
|
|
101
|
-
unless @data['
|
102
|
-
raise Bolt::ValidationError, "Configured '
|
101
|
+
unless @data['allowlist'].nil? || @data['allowlist'].is_a?(Array)
|
102
|
+
raise Bolt::ValidationError, "Configured 'allowlist' must be an array of names"
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
data/lib/bolt_server/config.rb
CHANGED
@@ -7,7 +7,7 @@ require 'bolt/error'
|
|
7
7
|
module BoltServer
|
8
8
|
class Config < BoltServer::BaseConfig
|
9
9
|
def config_keys
|
10
|
-
super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri]
|
10
|
+
super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri projects-dir]
|
11
11
|
end
|
12
12
|
|
13
13
|
def env_keys
|
@@ -64,17 +64,17 @@ module BoltServer
|
|
64
64
|
|
65
65
|
def client
|
66
66
|
@client ||= begin
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
67
|
+
uri = URI(@config['file-server-uri'])
|
68
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
69
|
+
https.use_ssl = true
|
70
|
+
https.ssl_version = :TLSv1_2
|
71
|
+
https.ca_file = @config['ssl-ca-cert']
|
72
|
+
https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
|
73
|
+
https.key = OpenSSL::PKey::RSA.new(ssl_key)
|
74
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
75
|
+
https.open_timeout = @config['file-server-conn-timeout']
|
76
|
+
https
|
77
|
+
end
|
78
78
|
end
|
79
79
|
|
80
80
|
def request_file(path, params, file)
|
@@ -5,6 +5,7 @@ require 'addressable/uri'
|
|
5
5
|
require 'bolt'
|
6
6
|
require 'bolt/error'
|
7
7
|
require 'bolt/inventory'
|
8
|
+
require 'bolt/project'
|
8
9
|
require 'bolt/target'
|
9
10
|
require 'bolt_server/file_cache'
|
10
11
|
require 'bolt/task/puppet_server'
|
@@ -13,7 +14,9 @@ require 'json-schema'
|
|
13
14
|
|
14
15
|
# These are only needed for the `/plans` endpoint.
|
15
16
|
require 'puppet'
|
16
|
-
|
17
|
+
|
18
|
+
# Needed by the `/project_file_metadatas` endpoint
|
19
|
+
require 'puppet/file_serving/fileset'
|
17
20
|
|
18
21
|
module BoltServer
|
19
22
|
class TransportApp < Sinatra::Base
|
@@ -34,6 +37,17 @@ module BoltServer
|
|
34
37
|
transport-winrm
|
35
38
|
].freeze
|
36
39
|
|
40
|
+
# PE_BOLTLIB_PATH is intended to function exactly like the BOLTLIB_PATH used
|
41
|
+
# in Bolt::PAL. Paths and variable names are similar to what exists in
|
42
|
+
# Bolt::PAL, but with a 'PE' prefix.
|
43
|
+
PE_BOLTLIB_PATH = '/opt/puppetlabs/server/apps/bolt-server/pe-bolt-modules'
|
44
|
+
|
45
|
+
# For now at least, we maintain an entirely separate codedir from
|
46
|
+
# puppetserver by default, so that filesync can work properly. If filesync
|
47
|
+
# is not used, this can instead match the usual puppetserver codedir.
|
48
|
+
# See the `orchestrator.bolt.codedir` tk config setting.
|
49
|
+
DEFAULT_BOLT_CODEDIR = '/opt/puppetlabs/server/data/orchestration-services/code'
|
50
|
+
|
37
51
|
def initialize(config)
|
38
52
|
@config = config
|
39
53
|
@schemas = Hash[REQUEST_SCHEMAS.map do |basename|
|
@@ -190,21 +204,82 @@ module BoltServer
|
|
190
204
|
[@executor.run_script(target, file_location, body['arguments'])]
|
191
205
|
end
|
192
206
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
+
# This function is nearly identical to Bolt::Pal's `with_puppet_settings` with the
|
208
|
+
# one difference that we set the codedir to point to actual code, rather than the
|
209
|
+
# tmpdir. We only use this funtion inside the Modulepath initializer so that Puppet
|
210
|
+
# is correctly configured to pull environment configuration correctly. If we don't
|
211
|
+
# set codedir in this way: when we try to load and interpolate the modulepath it
|
212
|
+
# won't correctly load.
|
213
|
+
#
|
214
|
+
# WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
|
215
|
+
def with_pe_pal_init_settings(codedir, environmentpath, basemodulepath)
|
216
|
+
Dir.mktmpdir('pe-bolt') do |dir|
|
217
|
+
cli = []
|
218
|
+
Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
|
219
|
+
dir = setting == :codedir ? codedir : dir
|
220
|
+
cli << "--#{setting}" << dir
|
207
221
|
end
|
222
|
+
cli << "--environmentpath" << environmentpath
|
223
|
+
cli << "--basemodulepath" << basemodulepath
|
224
|
+
Puppet.settings.send(:clear_everything_for_tests)
|
225
|
+
Puppet.initialize_settings(cli)
|
226
|
+
yield
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Use puppet to identify the modulepath from an environment.
|
231
|
+
#
|
232
|
+
# WARNING: THIS FUNCTION SHOULD ONLY BE CALLED INSIDE A SYNCHRONIZED PAL MUTEX
|
233
|
+
def modulepath_from_environment(environment_name)
|
234
|
+
codedir = DEFAULT_BOLT_CODEDIR
|
235
|
+
environmentpath = "#{codedir}/environments"
|
236
|
+
basemodulepath = "#{codedir}/modules:/opt/puppetlabs/puppet/modules"
|
237
|
+
modulepath_dirs = nil
|
238
|
+
with_pe_pal_init_settings(codedir, environmentpath, basemodulepath) do
|
239
|
+
environment = Puppet.lookup(:environments).get!(environment_name)
|
240
|
+
modulepath_dirs = environment.modulepath
|
241
|
+
end
|
242
|
+
modulepath_dirs
|
243
|
+
end
|
244
|
+
|
245
|
+
def in_pe_pal_env(environment)
|
246
|
+
return [400, '`environment` is a required argument'] if environment.nil?
|
247
|
+
@pal_mutex.synchronize do
|
248
|
+
modulepath_obj = Bolt::Config::Modulepath.new(
|
249
|
+
modulepath_from_environment(environment),
|
250
|
+
boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
|
251
|
+
)
|
252
|
+
pal = Bolt::PAL.new(modulepath_obj, nil, nil)
|
253
|
+
yield pal
|
254
|
+
rescue Puppet::Environments::EnvironmentNotFound
|
255
|
+
[400, {
|
256
|
+
"class" => 'bolt/unknown-environment',
|
257
|
+
"message" => "Environment #{environment} not found"
|
258
|
+
}.to_json]
|
259
|
+
rescue Bolt::Error => e
|
260
|
+
[400, e.to_json]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def in_bolt_project(bolt_project)
|
265
|
+
return [400, '`project_ref` is a required argument'] if bolt_project.nil?
|
266
|
+
project_dir = File.join(@config['projects-dir'], bolt_project)
|
267
|
+
return [400, "`project_ref`: #{project_dir} does not exist"] unless Dir.exist?(project_dir)
|
268
|
+
@pal_mutex.synchronize do
|
269
|
+
project = Bolt::Project.create_project(project_dir)
|
270
|
+
bolt_config = Bolt::Config.from_project(project, { log: { 'bolt-debug.log' => 'disable' } })
|
271
|
+
modulepath_object = Bolt::Config::Modulepath.new(
|
272
|
+
bolt_config.modulepath,
|
273
|
+
boltlib_path: [PE_BOLTLIB_PATH, Bolt::Config::Modulepath::BOLTLIB_PATH]
|
274
|
+
)
|
275
|
+
pal = Bolt::PAL.new(modulepath_object, nil, nil, nil, nil, nil, bolt_config.project)
|
276
|
+
context = {
|
277
|
+
pal: pal,
|
278
|
+
config: bolt_config
|
279
|
+
}
|
280
|
+
yield context
|
281
|
+
rescue Bolt::Error => e
|
282
|
+
[400, e.to_json]
|
208
283
|
end
|
209
284
|
end
|
210
285
|
|
@@ -221,14 +296,12 @@ module BoltServer
|
|
221
296
|
plan_info
|
222
297
|
end
|
223
298
|
|
224
|
-
def build_puppetserver_uri(file_identifier, module_name,
|
299
|
+
def build_puppetserver_uri(file_identifier, module_name, parameters)
|
225
300
|
segments = file_identifier.split('/', 3)
|
226
301
|
if segments.size == 1
|
227
302
|
{
|
228
303
|
'path' => "/puppet/v3/file_content/tasks/#{module_name}/#{file_identifier}",
|
229
|
-
'params' =>
|
230
|
-
'environment' => environment
|
231
|
-
}
|
304
|
+
'params' => parameters
|
232
305
|
}
|
233
306
|
else
|
234
307
|
module_segment, mount_segment, name_segment = *segments
|
@@ -241,14 +314,12 @@ module BoltServer
|
|
241
314
|
when 'lib'
|
242
315
|
"/puppet/v3/file_content/plugins/#{name_segment}"
|
243
316
|
end,
|
244
|
-
'params' =>
|
245
|
-
'environment' => environment
|
246
|
-
}
|
317
|
+
'params' => parameters
|
247
318
|
}
|
248
319
|
end
|
249
320
|
end
|
250
321
|
|
251
|
-
def pe_task_info(pal, module_name, task_name,
|
322
|
+
def pe_task_info(pal, module_name, task_name, parameters)
|
252
323
|
# Handle case where task name is simply module name with special `init` task
|
253
324
|
task_name = if task_name == 'init' || task_name.nil?
|
254
325
|
module_name
|
@@ -261,7 +332,7 @@ module BoltServer
|
|
261
332
|
'filename' => file_hash['name'],
|
262
333
|
'sha256' => Digest::SHA256.hexdigest(File.read(file_hash['path'])),
|
263
334
|
'size_bytes' => File.size(file_hash['path']),
|
264
|
-
'uri' => build_puppetserver_uri(file_hash['name'], module_name,
|
335
|
+
'uri' => build_puppetserver_uri(file_hash['name'], module_name, parameters)
|
265
336
|
}
|
266
337
|
end
|
267
338
|
{
|
@@ -271,6 +342,38 @@ module BoltServer
|
|
271
342
|
}
|
272
343
|
end
|
273
344
|
|
345
|
+
def allowed_helper(metadata, allowlist)
|
346
|
+
allowed = allowlist.nil? || allowlist.include?(metadata['name']) ? true : false
|
347
|
+
metadata.merge({ 'allowed' => allowed })
|
348
|
+
end
|
349
|
+
|
350
|
+
def task_list(pal)
|
351
|
+
tasks = pal.list_tasks
|
352
|
+
tasks.map { |task_name, _description| { 'name' => task_name } }
|
353
|
+
end
|
354
|
+
|
355
|
+
def plan_list(pal)
|
356
|
+
plans = pal.list_plans.flatten
|
357
|
+
plans.map { |plan_name| { 'name' => plan_name } }
|
358
|
+
end
|
359
|
+
|
360
|
+
def file_metadatas(pal, module_name, file)
|
361
|
+
pal.in_bolt_compiler do
|
362
|
+
mod = Puppet.lookup(:current_environment).module(module_name)
|
363
|
+
raise ArgumentError, "`module_name`: #{module_name} does not exist" unless mod
|
364
|
+
abs_file_path = mod.file(file)
|
365
|
+
raise ArgumentError, "`file`: #{file} does not exist inside the module's 'files' directory" unless abs_file_path
|
366
|
+
fileset = Puppet::FileServing::Fileset.new(abs_file_path, 'recurse' => 'yes')
|
367
|
+
Puppet::FileServing::Fileset.merge(fileset).collect do |relative_file_path, base_path|
|
368
|
+
metadata = Puppet::FileServing::Metadata.new(base_path, relative_path: relative_file_path)
|
369
|
+
metadata.checksum_type = 'sha256'
|
370
|
+
metadata.links = 'follow'
|
371
|
+
metadata.collect
|
372
|
+
metadata.to_data_hash
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
274
377
|
get '/' do
|
275
378
|
200
|
276
379
|
end
|
@@ -401,12 +504,40 @@ module BoltServer
|
|
401
504
|
end
|
402
505
|
end
|
403
506
|
|
507
|
+
# Fetches the metadata for a single plan
|
508
|
+
#
|
509
|
+
# @param project_ref [String] the project to fetch the plan from
|
510
|
+
get '/project_plans/:module_name/:plan_name' do
|
511
|
+
in_bolt_project(params['project_ref']) do |context|
|
512
|
+
plan_info = pe_plan_info(context[:pal], params[:module_name], params[:plan_name])
|
513
|
+
plan_info = allowed_helper(plan_info, context[:config].project.plans)
|
514
|
+
[200, plan_info.to_json]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
404
518
|
# Fetches the metadata for a single task
|
405
519
|
#
|
406
520
|
# @param environment [String] the environment to fetch the task from
|
407
521
|
get '/tasks/:module_name/:task_name' do
|
408
522
|
in_pe_pal_env(params['environment']) do |pal|
|
409
|
-
|
523
|
+
ps_parameters = {
|
524
|
+
'environment' => params['environment']
|
525
|
+
}
|
526
|
+
task_info = pe_task_info(pal, params[:module_name], params[:task_name], ps_parameters)
|
527
|
+
[200, task_info.to_json]
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Fetches the metadata for a single task
|
532
|
+
#
|
533
|
+
# @param bolt_project_ref [String] the reference to the bolt-project directory to load task metadata from
|
534
|
+
get '/project_tasks/:module_name/:task_name' do
|
535
|
+
in_bolt_project(params['project_ref']) do |context|
|
536
|
+
ps_parameters = {
|
537
|
+
'project' => params['project_ref']
|
538
|
+
}
|
539
|
+
task_info = pe_task_info(context[:pal], params[:module_name], params[:task_name], ps_parameters)
|
540
|
+
task_info = allowed_helper(task_info, context[:config].project.tasks)
|
410
541
|
[200, task_info.to_json]
|
411
542
|
end
|
412
543
|
end
|
@@ -435,13 +566,30 @@ module BoltServer
|
|
435
566
|
end
|
436
567
|
end
|
437
568
|
|
569
|
+
# Fetches the list of plans for a project
|
570
|
+
#
|
571
|
+
# @param project_ref [String] the project to fetch the list of plans from
|
572
|
+
get '/project_plans' do
|
573
|
+
in_bolt_project(params['project_ref']) do |context|
|
574
|
+
plans_response = plan_list(context[:pal])
|
575
|
+
|
576
|
+
# Dig in context for the allowlist of plans from project object
|
577
|
+
plans_response.map! { |metadata| allowed_helper(metadata, context[:config].project.plans) }
|
578
|
+
|
579
|
+
# We structure this array of plans to be an array of hashes so that it matches the structure
|
580
|
+
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
581
|
+
# makes switching between puppetserver and bolt-server easier, which makes changes to switch
|
582
|
+
# to bolt-server smaller/simpler.
|
583
|
+
[200, plans_response.to_json]
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
438
587
|
# Fetches the list of tasks for an environment
|
439
588
|
#
|
440
589
|
# @param environment [String] the environment to fetch the list of tasks from
|
441
590
|
get '/tasks' do
|
442
591
|
in_pe_pal_env(params['environment']) do |pal|
|
443
|
-
|
444
|
-
tasks_response = tasks.map { |task_name, _description| { 'name' => task_name } }.to_json
|
592
|
+
tasks_response = task_list(pal).to_json
|
445
593
|
|
446
594
|
# We structure this array of tasks to be an array of hashes so that it matches the structure
|
447
595
|
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
@@ -451,6 +599,37 @@ module BoltServer
|
|
451
599
|
end
|
452
600
|
end
|
453
601
|
|
602
|
+
# Fetches the list of tasks for a bolt-project
|
603
|
+
#
|
604
|
+
# @param project_ref [String] the project to fetch the list of tasks from
|
605
|
+
get '/project_tasks' do
|
606
|
+
in_bolt_project(params['project_ref']) do |context|
|
607
|
+
tasks_response = task_list(context[:pal])
|
608
|
+
|
609
|
+
# Dig in context for the allowlist of tasks from project object
|
610
|
+
tasks_response.map! { |metadata| allowed_helper(metadata, context[:config].project.tasks) }
|
611
|
+
|
612
|
+
# We structure this array of tasks to be an array of hashes so that it matches the structure
|
613
|
+
# returned by the puppetserver API that serves data like this. Structuring the output this way
|
614
|
+
# makes switching between puppetserver and bolt-server easier, which makes changes to switch
|
615
|
+
# to bolt-server smaller/simpler.
|
616
|
+
[200, tasks_response.to_json]
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
# Implements puppetserver's file_metadatas endpoint for projects.
|
621
|
+
#
|
622
|
+
# @param project_ref [String] the project_ref to fetch the file metadatas from
|
623
|
+
get '/project_file_metadatas/:module_name/*' do
|
624
|
+
in_bolt_project(params['project_ref']) do |context|
|
625
|
+
file = params[:splat].first
|
626
|
+
metadatas = file_metadatas(context[:pal], params[:module_name], file)
|
627
|
+
[200, metadatas.to_json]
|
628
|
+
end
|
629
|
+
rescue ArgumentError => e
|
630
|
+
[400, e.message]
|
631
|
+
end
|
632
|
+
|
454
633
|
error 404 do
|
455
634
|
err = Bolt::Error.new("Could not find route #{request.path}",
|
456
635
|
'boltserver/not-found')
|
@@ -105,7 +105,7 @@ module BoltSpec
|
|
105
105
|
|
106
106
|
# Set the things
|
107
107
|
Puppet[:tasks] = true
|
108
|
-
RSpec.configuration.module_path = [modulepath, Bolt::
|
108
|
+
RSpec.configuration.module_path = [modulepath, Bolt::Config::Modulepath::BOLTLIB_PATH].join(File::PATH_SEPARATOR)
|
109
109
|
opts = {
|
110
110
|
bolt_executor: executor,
|
111
111
|
bolt_inventory: inventory,
|
@@ -142,10 +142,10 @@ module BoltSpec
|
|
142
142
|
# Override in your tests
|
143
143
|
def config
|
144
144
|
@config ||= begin
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
145
|
+
conf = Bolt::Config.default
|
146
|
+
conf.modulepath = [modulepath].flatten
|
147
|
+
conf
|
148
|
+
end
|
149
149
|
end
|
150
150
|
|
151
151
|
def plugins
|
@@ -153,7 +153,9 @@ module BoltSpec
|
|
153
153
|
end
|
154
154
|
|
155
155
|
def pal
|
156
|
-
@pal ||= Bolt::PAL.new(config.modulepath,
|
156
|
+
@pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
|
157
|
+
config.hiera_config,
|
158
|
+
config.project.resource_types)
|
157
159
|
end
|
158
160
|
|
159
161
|
BoltSpec::Plans::MOCKED_ACTIONS.each do |action|
|
data/lib/bolt_spec/plans.rb
CHANGED
@@ -40,7 +40,7 @@ module BoltSpec
|
|
40
40
|
|
41
41
|
def module_file_id(file)
|
42
42
|
modpath = @modulepath.select { |path| file =~ /^#{path}/ }
|
43
|
-
raise "Could not identify
|
43
|
+
raise "Could not identify modulepath containing #{file}: #{modpath}" unless modpath.size == 1
|
44
44
|
|
45
45
|
path = Pathname.new(file)
|
46
46
|
relative = path.relative_path_from(Pathname.new(modpath.first))
|