Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support only_if in controls #619

Merged
merged 3 commits into from
Apr 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/inspec/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def self.rule_from_check(m, a, b)
# let it run. This happens after everything
# else is merged in.
def self.execute_rule(r, profile_id)
checks = r.instance_variable_get(:@checks)
checks = ::Inspec::Rule.prepare_checks(r)
fid = InspecBaseRule.full_id(r, profile_id)
checks.each do |m, a, b|
# check if the resource is skippable and skipped
Expand Down
2 changes: 1 addition & 1 deletion lib/inspec/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def load_params
impact: rule.impact,
refs: rule.ref,
tags: rule.tag,
checks: rule.instance_variable_get(:@checks),
checks: Inspec::Rule.checks(rule),
code: rule.instance_variable_get(:@__code),
source_location: rule.instance_variable_get(:@__source_location),
group_title: rule.instance_variable_get(:@__group_title),
Expand Down
12 changes: 2 additions & 10 deletions lib/inspec/profile_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,9 @@ def to_s
end

define_method :register_control do |control, &block|
profile_context_owner.register_rule(control, &block) unless control.nil?
::Inspec::Rule.set_skip_rule(control, true) if @skip_profile

# Skip the control if the resource triggered a skip;
if @skip_profile
control.instance_variable_set(:@checks, [])
# TODO: we use os as the carrier here, but should consider
# a separate resource to do skipping
resource = os
resource.skip_resource('Skipped control due to only_if condition.')
control.describe(resource)
end
profile_context_owner.register_rule(control, &block) unless control.nil?
end

# TODO: mock method for attributes; import attribute handling
Expand Down
83 changes: 62 additions & 21 deletions lib/inspec/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ class Rule # rubocop:disable Metrics/ClassLength
def initialize(id, _opts, &block)
@id = id
@impact = nil
@__block = block
@__code = __get_block_source(&block)
@__source_location = __get_block_source_location(&block)
@title = nil
@desc = nil
@refs = []
@tags = {}

# not changeable by the user:
@profile_id = nil
@checks = []
@__block = block
@__code = __get_block_source(&block)
@__source_location = __get_block_source_location(&block)
@__rule_id = nil
@__checks = []
@__skip_rule = nil

# evaluate the given definition
instance_eval(&block) if block_given?
end
Expand Down Expand Up @@ -70,6 +73,15 @@ def tag(*args)
@tags
end

# Skip all checks if only_if is false
#
# @param [Type] &block returns true if tests are added, false otherwise
# @return [nil]
def only_if
return unless block_given?
@__skip_rule ||= !yield
end

# Describe will add one or more tests to this control. There is 2 ways
# of calling it:
#
Expand All @@ -87,25 +99,57 @@ def describe(*values, &block)
dsl = self.class.ancestors[1]
Class.new(DescribeBase) do
include dsl
end.new(method(:add_check))
end.new(method(:__add_check))
else
add_check('describe', values, block)
__add_check('describe', values, block)
end
end

def expect(value, &block)
target = Inspec::Expect.new(value, &block)
add_check('expect', [value], target)
__add_check('expect', [value], target)
target
end

def self.rule_id(rule)
rule.instance_variable_get(:@__rule_id)
end

def self.set_rule_id(rule, value)
rule.instance_variable_set(:@__rule_id, value)
end

def self.checks(rule)
rule.instance_variable_get(:@__checks)
end

def self.skip_status(rule)
rule.instance_variable_get(:@__skip_rule)
end

def self.set_skip_rule(rule, value)
rule.instance_variable_set(:@__skip_rule, value)
end

def self.prepare_checks(rule)
msg = skip_status(rule)
return checks(rule) unless msg
msg = 'Skipped control due to only_if condition.' if msg == true

# TODO: we use os as the carrier here, but should consider
# a separate resource to do skipping
resource = rule.os
resource.skip_resource(msg)
[['describe', [resource], nil]]
end

def self.merge(dst, src)
if src.id != dst.id
# TODO: register an error, this case should not happen
return
end
sp = src.instance_variable_get(:@profile_id)
dp = dst.instance_variable_get(:@profile_id)
sp = rule_id(src)
dp = rule_id(dst)
if sp != dp
# TODO: register an error, this case should not happen
return
Expand All @@ -117,10 +161,10 @@ def self.merge(dst, src)
# merge indirect fields
# checks defined in the source will completely eliminate
# all checks that were defined in the destination
sc = src.instance_variable_get(:@checks)
unless sc.nil? || sc.empty?
dst.instance_variable_set(:@checks, sc)
end
sc = checks(src)
dst.instance_variable_set(:@__checks, sc) unless sc.empty?
sr = skip_status(src)
set_skip_rule(dst, sr) unless sr.nil?
end

# Get the full id consisting of profile id + rule id
Expand All @@ -140,11 +184,8 @@ def self.full_id(profile_id, rule)
return nil
end
end
pid = rule.instance_variable_get(:@profile_id)
if pid.nil?
rule.instance_variable_set(:@profile_id, profile_id)
pid = profile_id
end
pid = rule_id(rule)
pid = set_rule_id(rule, profile_id) if pid.nil?

# if we don't have a profile id, just return the rule's ID
return rid if pid.nil? or pid.empty?
Expand All @@ -154,8 +195,8 @@ def self.full_id(profile_id, rule)

private

def add_check(describe_or_expect, values, block)
@checks.push([describe_or_expect, values, block])
def __add_check(describe_or_expect, values, block)
@__checks.push([describe_or_expect, values, block])
end

# Idio(ma)tic unindent
Expand Down
2 changes: 1 addition & 1 deletion lib/inspec/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def get_check_example(method_name, arg, block)

def register_rule(rule_id, rule)
@rules[rule_id] = rule
checks = rule.instance_variable_get(:@checks)
checks = ::Inspec::Rule.prepare_checks(rule)
examples = checks.map do |m, a, b|
get_check_example(m, a, b)
end.flatten.compact
Expand Down
46 changes: 44 additions & 2 deletions test/unit/profile_context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def get_rule
end

def get_checks
get_rule.instance_variable_get(:@checks)
Inspec::Rule.prepare_checks(get_rule)
end

it 'must be able to load empty content' do
Expand Down Expand Up @@ -161,6 +161,18 @@ def load(call)
get_checks.length.must_equal 1
get_checks[0][1][0].must_be_nil
end

it 'doesnt overwrite falsy only_ifs' do
profile.load(if_false + if_true + control)
get_checks.length.must_equal 1
get_checks[0][1][0].resource_skipped.must_equal 'Skipped control due to only_if condition.'
end

it 'doesnt overwrite falsy only_ifs' do
profile.load(if_true + if_false + control)
get_checks.length.must_equal 1
get_checks[0][1][0].resource_skipped.must_equal 'Skipped control due to only_if condition.'
end
end

it 'provides the control keyword in the global DSL' do
Expand Down Expand Up @@ -189,7 +201,7 @@ def get_rule
it 'doesnt add any checks if none are provided' do
profile.load("rule #{rule_id.inspect}")
rule = profile.rules[rule_id]
rule.instance_variable_get(:@checks).must_equal([])
::Inspec::Rule.prepare_checks(rule).must_equal([])
end

describe 'supports empty describe blocks' do
Expand Down Expand Up @@ -264,5 +276,35 @@ def get_rule
check[2].must_be_kind_of Proc
end
end

describe 'with only_if' do
it 'provides the only_if keyword' do
profile.load(format(context_format, 'only_if'))
get_checks.must_equal([])
end

it 'skips with only_if == false' do
profile.load(format(context_format, 'only_if { false }'))
get_checks.length.must_equal 1
get_checks[0][1][0].resource_skipped.must_equal 'Skipped control due to only_if condition.'
end

it 'does nothing with only_if == false' do
profile.load(format(context_format, 'only_if { true }'))
get_checks.length.must_equal 0
end

it 'doesnt overwrite falsy only_ifs' do
profile.load(format(context_format, "only_if { false }\nonly_if { true }"))
get_checks.length.must_equal 1
get_checks[0][1][0].resource_skipped.must_equal 'Skipped control due to only_if condition.'
end

it 'doesnt overwrite falsy only_ifs' do
profile.load(format(context_format, "only_if { true }\nonly_if { false }"))
get_checks.length.must_equal 1
get_checks[0][1][0].resource_skipped.must_equal 'Skipped control due to only_if condition.'
end
end
end
end