Allow users to reference resources from dependencies

All resources from deps are added into the control_eval_context used by
the current profile. However, if there is a name conflict, the last
loaded resource wins. The new `require_resource` dsl method allows the
user to do the following:

    require_resource(profile: 'profile_name',
                     resource: 'other',
                    as: 'renamed')

    describe renamed do
      ...
    end

Signed-off-by: Steven Danna <steve@chef.io>
This commit is contained in:
Steven Danna 2016-09-15 08:54:15 +01:00 committed by Christoph Hartmann
parent 31967fa4b2
commit b2146d8758
10 changed files with 138 additions and 12 deletions

View file

@ -92,6 +92,14 @@ module Inspec
res
end
define_method :add_resource do |name, new_res|
resources_dsl.module_exec do
define_method name.to_sym do |*args|
new_res.new(@backend, name.to_s, *args)
end
end
end
define_method :add_resources do |context|
self.class.class_eval do
include context.to_resources_dsl

View file

@ -18,6 +18,15 @@ module Inspec::DSL
alias require_rules require_controls
alias include_rules include_controls
def require_resource(options = {})
fail 'You must specify a specific resource name when calling require_resource()' if options[:resource].nil?
from_profile = options[:profile] || profile_name
target_name = options[:as] || options[:resource]
res = resource_class(from_profile, options[:resource])
add_resource(target_name, res)
end
def self.load_spec_files_for_profile(bind_context, opts, &block)
dependencies = opts[:dependencies]
profile_id = opts[:profile_id]

View file

@ -72,6 +72,9 @@ module Inspec
end
# rubocop:enable Lint/NestedMethodDefinition
if __resource_registry.key?(name)
Inspec::Log.warn("Overwriting resource #{name}. To reference a specific version of #{name} use the resource() method")
end
__resource_registry[name] = cl
end
end

View file

@ -66,6 +66,7 @@ module Inspec
@backend = options[:backend] || Inspec::Backend.create(options)
@source_reader = source_reader
@tests_collected = false
@libraries_loaded = false
Metadata.finalize(@source_reader.metadata, @profile_id)
@runner_context = options[:profile_context] || Inspec::ProfileContext.for_profile(self,
@backend,
@ -125,6 +126,8 @@ module Inspec
end
def load_libraries
return @runner_context if @libraries_loaded
locked_dependencies.each do |d|
c = d.load_libraries
@runner_context.add_resources(c)
@ -135,6 +138,7 @@ module Inspec
end
@runner_context.load_libraries(libs)
@libraries_loaded = true
@runner_context
end

View file

@ -17,7 +17,7 @@ module Inspec
'attributes' => attributes })
end
attr_reader :attributes, :profile_id, :resource_registry
attr_reader :attributes, :profile_id, :resource_registry, :backend
attr_accessor :rules
def initialize(profile_id, backend, conf)
if backend.nil?
@ -28,7 +28,8 @@ module Inspec
@backend = backend
@conf = conf.dup
@rules = {}
@subcontexts = []
@control_subcontexts = []
@lib_subcontexts = []
@require_loader = ::Inspec::RequireLoader.new
@attributes = []
# A local resource registry that only contains resources defined
@ -46,7 +47,7 @@ module Inspec
end
def to_resources_dsl
Inspec::Resource.create_dsl(@backend, @resource_registry)
Inspec::Resource.create_dsl(self)
end
def control_eval_context
@ -66,20 +67,34 @@ module Inspec
@conf['profile'].supports_os?
end
def all_rules
def all_controls
ret = @rules.values
ret += @subcontexts.map(&:all_rules).flatten
ret += @control_subcontexts.map(&:all_rules).flatten
ret
end
alias all_rules all_controls
def subcontext_by_name(name)
found = @lib_subcontexts.find { |c| c.profile_id == name }
if !found
@lib_subcontexts.each do |c|
found = c.subcontext_by_name(name)
break if found
end
end
found
end
def add_resources(context)
@resource_registry.merge!(context.resource_registry)
control_eval_context.add_resources(context)
@lib_subcontexts << context
reload_dsl
end
def add_subcontext(context)
@subcontexts << context
@control_subcontexts << context
end
def load_libraries(libs)

View file

@ -6,6 +6,8 @@
require 'inspec/plugins'
module Inspec
class ProfileNotFound < StandardError; end
class Resource
def self.default_registry
@default_registry ||= {}
@ -25,9 +27,22 @@ module Inspec
#
# @param backend [BackendRunner] exposing the target to resources
# @return [ResourcesDSL]
def self.create_dsl(backend, my_registry = registry)
# need the local name, to use it in the module creation further down
def self.create_dsl(profile_context)
backend = profile_context.backend
my_registry = profile_context.resource_registry
Module.new do
define_method :resource_class do |profile_name, resource_name|
inner_context = if profile_name == profile_context.profile_id
profile_context
else
profile_context.subcontext_by_name(profile_name)
end
fail ProfileNotFound, "Cannot find profile named: #{profile_name}" if inner_context.nil?
inner_context.resource_registry[resource_name]
end
my_registry.each do |id, r|
define_method id.to_sym do |*args|
r.new(backend, id.to_s, *args)

View file

@ -176,6 +176,15 @@ Test Summary: \e[32m2 successful\e[0m, \e[31m0 failures\e[0m, \e[37m0 skipped\e[
end
end
describe 'using namespaced resources' do
it 'works' do
out = inspec('exec ' + File.join(profile_path, 'dependencies', 'resource-namespace'))
out.stderr.must_equal ''
out.exit_status.must_equal 0
out.stdout.force_encoding(Encoding::UTF_8).must_include "Summary: \e[32m5 successful\e[0m, \e[31m0 failures\e[0m, \e[37m0 skipped\e[0m\n"
end
end
describe "with a 2-level dependency tree" do
it 'correctly runs tests from the whole tree' do
out = inspec('exec ' + File.join(profile_path, 'dependencies', 'inheritance'))

View file

@ -0,0 +1,27 @@
# encoding: utf-8
# copyright: 2015, The Authors
# license: All rights reserved
require_resource(profile: 'profile_c', resource: 'gordon_config', as: 'gordy_config')
describe gordy_config do
its('version') { should eq('1.0') }
end
control 'whichgordon' do
describe gordy_config do
its('version') { should eq('1.0') }
end
describe gordon_config do
its('version') { should eq('2.0') }
end
describe gordy_config do
its('version') { should eq(gordy_config.version) }
end
describe gordon_config do
its('version') { should eq(gordon_config.version) }
end
end

View file

@ -0,0 +1,13 @@
name: resource-namespace
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: All Rights Reserved
summary: An InSpec Compliance Profile
version: 0.1.0
depends:
- name: profile_a
path: '../profile_a'
- name: profile_d
path: '../profile_d'

View file

@ -2,7 +2,6 @@
# author: Steven Danna
require 'helper'
require 'rspec/core'
require 'inspec/control_eval_context'
describe Inspec::ControlEvalContext do
@ -11,7 +10,6 @@ describe Inspec::ControlEvalContext do
"wombat"
end
end
Inspec::Log.level = :debug
let(:control_content) { <<EOF
control 'foo' do
@ -44,11 +42,36 @@ EOF
profile_context.stubs(:current_load).returns({file: "<test content>"})
eval_context.instance_eval(control_content)
profile_context.all_rules.each do |rule|
# Turn each rule into an example group and run it, none of the example content should raise an
# exception
# Turn each rule into an example group and run it, none of the
# example content should raise an exception
Inspec::Rule.prepare_checks(rule).each do |m, a, b|
# if we require this at the top level, none of the other tests
# in this file will run. itsfine.jpg
require 'rspec/core'
RSpec::Core::ExampleGroup.describe(*a, &b).run
end
end
end
describe "#resource" do
let(:resource_dsl) { Inspec::Resource.create_dsl(profile_context) }
let(:inner_context) { Inspec::ProfileContext.new('inner-context', backend, {}) }
let(:control_content) do <<EOF
resource('profile_a', 'foobar')
EOF
end
it "fails if the requested profile can't be found" do
assert_raises(Inspec::ProfileNotFound) {
eval_context.instance_eval(control_content).must_raise
}
end
it "returns the resource from a subcontext" do
profile_context.expects(:subcontext_by_name).with('profile_a').returns(inner_context)
inner_context.expects(:resource_registry).returns({'foobar' => mock(:new => 'newfoo')})
eval_context.instance_eval(control_content).must_equal "newfoo"
end
end
end