diff --git a/lib/inspec/profile_context.rb b/lib/inspec/profile_context.rb index 60402b827..cf9ded7f2 100644 --- a/lib/inspec/profile_context.rb +++ b/lib/inspec/profile_context.rb @@ -4,6 +4,7 @@ require 'inspec/rule' require 'inspec/dsl' +require 'inspec/require_loader' require 'securerandom' module Inspec @@ -19,6 +20,7 @@ module Inspec @backend = backend @conf = conf.dup @rules = {} + @require_loader = ::Inspec::RequireLoader.new reload_dsl end @@ -26,7 +28,30 @@ module Inspec def reload_dsl resources_dsl = Inspec::Resource.create_dsl(@backend) ctx = create_context(resources_dsl, rule_context(resources_dsl)) - @profile_context = ctx.new(@backend, @conf) + @profile_context = ctx.new(@backend, @conf, @require_loader) + end + + def load_libraries(libs) + lib_prefix = 'libraries' + File::SEPARATOR + autoloads = [] + + libs.each do |content, source, line| + path = source + if source.start_with?(lib_prefix) + path = source.sub(lib_prefix, '') + autoloads.push(path) if File.dirname(path) == '.' + end + + @require_loader.add(path, content, source, line) + end + + # load all files directly that are flat inside the libraries folder + autoloads.each do |path| + next unless path.end_with?('.rb') + load(*@require_loader.load(path)) unless @require_loader.loaded?(path) + end + + reload_dsl end def load(content, source = nil, line = nil) @@ -100,12 +125,29 @@ module Inspec include Inspec::DSL include resources_dsl - def initialize(backend, conf) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods + def initialize(backend, conf, require_loader) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods @backend = backend @conf = conf + @require_loader = require_loader @skip_profile = false end + # Save the toplevel require method to load all ruby dependencies. + # It is used whenever the `require 'lib'` is not in libraries. + alias_method :__ruby_require, :require + + def require(path) + rbpath = path + '.rb' + return __ruby_require(path) if !@require_loader.exists?(rbpath) + return false if @require_loader.loaded?(rbpath) + + # This is equivalent to calling `require 'lib'` with lib on disk. + # We cannot rely on libraries residing on disk however. + # TODO: Sandboxing. + content, path, line = @require_loader.load(rbpath) + eval(content, TOPLEVEL_BINDING, path, line) # rubocop:disable Lint/Eval + end + define_method :title do |arg| profile_context_owner.set_header(:title, arg) end diff --git a/lib/inspec/require_loader.rb b/lib/inspec/require_loader.rb new file mode 100644 index 000000000..1f5a71ac5 --- /dev/null +++ b/lib/inspec/require_loader.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 +# author: Dominik Richter +# author: Christoph Hartmann + +module Inspec + class RequireLoader + Item = Struct.new(:content, :ref, :line, :loaded) + + def initialize + @contents = {} + end + + def add(path, content, ref, line) + @contents[path] = Item.new(content, ref, line, false) + end + + def load(path) + c = @contents[path] + c.loaded = true + res = [c.content, c.ref, c.line || 1] + yield res if block_given? + res + end + + def exists?(path) + @contents.key?(path) + end + + def loaded?(path) + @contents[path].loaded == true + end + end +end diff --git a/lib/inspec/runner.rb b/lib/inspec/runner.rb index 1bc8472f2..e03a45063 100644 --- a/lib/inspec/runner.rb +++ b/lib/inspec/runner.rb @@ -99,10 +99,7 @@ module Inspec # load all libraries ctx = create_context(options) - libs.each do |lib| - ctx.load(lib[:content].to_s, lib[:ref], lib[:line] || 1) - ctx.reload_dsl - end + ctx.load_libraries(libs.map { |x| [x[:content], x[:ref], x[:line]] }) # hand the context to the profile for further evaluation unless (profile = options['profile']).nil? diff --git a/test/functional/inspec_exec_test.rb b/test/functional/inspec_exec_test.rb index 2e15d770a..1c5c03e20 100644 --- a/test/functional/inspec_exec_test.rb +++ b/test/functional/inspec_exec_test.rb @@ -105,4 +105,12 @@ describe 'inspec exec' do out.stderr.must_equal "This profile requires InSpec version >= 99.0.0. You are running InSpec v#{Inspec::VERSION}.\n" end end + + describe 'with a profile that loads a library and reference' do + let(:out) { inspec('exec ' + File.join(profile_path, 'library')) } + + it 'executes the profile without error' do + out.exit_status.must_equal 0 + end + end end diff --git a/test/unit/mock/profiles/library/controls/filesystem_spec.rb b/test/unit/mock/profiles/library/controls/filesystem_spec.rb new file mode 100644 index 000000000..6f3b625e6 --- /dev/null +++ b/test/unit/mock/profiles/library/controls/filesystem_spec.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 +# copyright: 2015, Chef Software, Inc +# license: All rights reserved + +describe gordon do + it { should be_enabled } +end diff --git a/test/unit/mock/profiles/library/inspec.yml b/test/unit/mock/profiles/library/inspec.yml new file mode 100644 index 000000000..35b69c8e8 --- /dev/null +++ b/test/unit/mock/profiles/library/inspec.yml @@ -0,0 +1,10 @@ +name: complete +title: complete example profile +maintainer: Chef Software, Inc. +copyright: Chef Software, Inc. +copyright_email: support@chef.io +license: Proprietary, All rights reserved +summary: Testing stub +version: 1.0.0 +supports: +- os-family: linux diff --git a/test/unit/mock/profiles/library/libraries/gordonlib.rb b/test/unit/mock/profiles/library/libraries/gordonlib.rb new file mode 100644 index 000000000..f8df1ae16 --- /dev/null +++ b/test/unit/mock/profiles/library/libraries/gordonlib.rb @@ -0,0 +1,2 @@ +module GordonLib +end diff --git a/test/unit/mock/profiles/library/libraries/testlib.rb b/test/unit/mock/profiles/library/libraries/testlib.rb new file mode 100644 index 000000000..4f9e75dbe --- /dev/null +++ b/test/unit/mock/profiles/library/libraries/testlib.rb @@ -0,0 +1,12 @@ +# Library resource + +require 'gordonlib' +require 'hashie' + +class Gordon < Inspec.resource(1) + name 'gordon' + include GordonLib + def enabled? + true + end +end diff --git a/test/unit/profile_context_test.rb b/test/unit/profile_context_test.rb index 99e7bb0cd..0569936fa 100644 --- a/test/unit/profile_context_test.rb +++ b/test/unit/profile_context_test.rb @@ -307,4 +307,39 @@ describe Inspec::ProfileContext do end end end + + describe 'library loading' do + it 'supports simple ruby require statements' do + # Please note: we do discourage the use of Gems in inspec resources at + # this time. Resources should be well packaged whenever possible. + proc { profile.load('Net::POP3') }.must_raise NameError + profile.load_libraries([['require "net/pop"', 'libraries/a.rb']]) + profile.load('Net::POP3').to_s.must_equal 'Net::POP3' + end + + it 'supports loading across the library' do + profile.load_libraries([ + ["require 'a'\nA", 'libraries/b.rb'], + ['module A; end', 'libraries/a.rb'] + ]) + profile.load('A').to_s.must_equal 'A' + end + + it 'fails loading if reference error occur' do + proc { + profile.load_libraries([ + ["require 'a'\nB", 'libraries/b.rb'], + ['module A; end', 'libraries/a.rb'] + ]) + }.must_raise NameError + end + + it 'fails loading if a reference dependency isnt found' do + proc { + profile.load_libraries([ + ["require 'a'\nA", 'libraries/b.rb'], + ]) + }.must_raise LoadError + end + end end