Skip to content

Commit

Permalink
Do not cache compliance when version unspecified, support 'clear_cache'
Browse files Browse the repository at this point in the history
Fixes Customer Bug 236 (see there for repro)

Currently, when we run `inspec compliance upload my_profile` it is
cached locally in inspec when run. If we update the version in the core
code and run another upload, `inspec compliance upload my_profile` again
it will run the old cached version instead of running a new copy
from automate.

The current workaround is to specify the desired version with
`inspec exec compliance://my_profile/admin#0.1.1`.

The caching happens before we have forward sight into the profile's
contents and only the target name. So the text used to generate the
cache would be `compliance://my_profile/admin` which does not change
version to version.

The fix here simply identifies when we are doing a local `inspec exec
compliance://` (hitting local profiles does not generate a cache) and
skips the cache if there's no version specified. This will eliminate
the unexpected behavior.

Additionally, we have included a `clear_cache` cli method for InSpec,
which should assist the core team and other developers in the future
when debugging edge case issues in InSpec.

Signed-off-by: Nick Schwaderer <nschwaderer@chef.io>
  • Loading branch information
Nick Schwaderer committed Oct 19, 2020
1 parent 38971c7 commit 7fc5caa
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 10 deletions.
25 changes: 23 additions & 2 deletions lib/inspec/cached_fetcher.rb
Expand Up @@ -39,9 +39,12 @@ def cache_key
end

def fetch
if cache.exists?(cache_key)
##
# Plain, local `inspec exec <my_profile>` would not set a cache_key. This
# is set in other cases such as a versioned compliance run.
if cache.exists?(cache_key) && supports_cache?
Inspec::Log.debug "Using cached dependency for #{target}"
[cache.prefered_entry_for(cache_key), false]
[cache.preferred_entry_for(cache_key), false]
else
Inspec::Log.debug "Dependency does not exist in the cache #{target}"
fetcher.fetch(cache.base_path_for(fetcher.cache_key))
Expand All @@ -65,5 +68,23 @@ def assert_cache_sanity!
EOF
raise exception_message if fetcher.resolved_source[:sha256] != target[:sha256]
end

private

##
# Sometimes a cache will not be used. For instance, when the user has
# uploaded to automate without specifying a version.
#
# inspec exec compliance://admin/my_profile#0.2.0 <-- Uses cache
#
# inspec exec compliance://admin/my_profile <-- Does not use cache
#
def supports_cache?
return true if target.is_a?(Hash)
return true if target&.match(%r{^compliance:\/\/.*#(\d|\.)+$})
return true unless target&.include?("compliance")

false
end
end
end
15 changes: 15 additions & 0 deletions lib/inspec/cli.rb
Expand Up @@ -175,6 +175,7 @@ def archive(path)
o[:logger].level = get_log_level(o[:log_level])
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)


# Force vendoring with overwrite when archiving
vendor_options = o.dup
vendor_options[:overwrite] = true
Expand Down Expand Up @@ -393,6 +394,20 @@ def version
end
map %w{-v --version} => :version

desc "clear_cache", "clears the InSpec cache. Useful for debugging."
option :vendor_cache, type: :string,
desc: "Use the given path for caching dependencies. (default: ~/.inspec/cache)"
def clear_cache
o = config
configure_logger(o)

FileUtils.rm_rf File.expand_path(o[:vendor_cache])

o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(o[:log_level])
o[:logger].info "== InSpec cache cleared successfully =="
end

private

def run_command(opts)
Expand Down
2 changes: 1 addition & 1 deletion lib/inspec/dependencies/cache.rb
Expand Up @@ -23,7 +23,7 @@ def initialize(path = nil)
# ignore
end

def prefered_entry_for(key)
def preferred_entry_for(key)
path = base_path_for(key)
if File.directory?(path)
path
Expand Down
2 changes: 1 addition & 1 deletion lib/inspec/runner_rspec.rb
Expand Up @@ -5,7 +5,7 @@
require "inspec/rspec_extensions"

# There be dragons!! Or borgs, or something...
# This file and all its contents cannot be unit-tested. both test-suits
# This file and all its contents cannot be unit-tested. both test-suites
# collide and disable all unit tests that have been added.

module Inspec
Expand Down
27 changes: 27 additions & 0 deletions test/functional/inspec_clear_cache_test.rb
@@ -0,0 +1,27 @@
require "functional/helper"
require "securerandom"

describe "inspec check" do
include FunctionalHelper

parallelize_me!

describe "inspec clear_cache" do
it "clears any existing cache" do
dirname = File.expand_path("~/.inspec/cache")
unless File.directory?(dirname)
FileUtils.mkdir_p(dirname)
end
newfile = "#{dirname}/#{SecureRandom.hex(10)}.txt"
File.write(newfile, SecureRandom.hex(100))

assert !Dir.glob(newfile).empty?

out = inspec("clear_cache")

assert_empty Dir.glob(newfile)
assert_exit_code 0, out
_(out.stdout).must_include "== InSpec cache cleared successfully ==\n"
end
end
end
36 changes: 30 additions & 6 deletions test/unit/cached_fetcher_test.rb
Expand Up @@ -25,8 +25,22 @@
"inputs" => [],
"latest_version" => "" }]
end

before do
InspecPlugins::Compliance::Configuration.expects(:new).returns({ "token" => "123abc", "server" => "https://a2.instance.com" })

@stub_get =
stub_request(
:get,
"https://a2.instance.com/owners/admin/compliance/ssh-baseline/tar"
).with(
headers: {
"Accept" => "*/*",
"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
"Authorization" => "Bearer 123abc",
"User-Agent" => "Ruby",
}
).to_return(status: 200, body: "", headers: {})
end

it "downloads the profile from the compliance service when sha256 not in the cache" do
Expand All @@ -44,19 +58,29 @@
mock_fetch.verify
end

it "does not download the profile when the sha256 exists in the inspec cache" do
it "does not download the profile when the sha256 exists in the inspec cache if version is specified" do
prof = profiles_result[0]
InspecPlugins::Compliance::API.stubs(:profiles).returns(["success", profiles_result])
cache = Inspec::Cache.new
entry_path = cache.base_path_for(prof["sha256"])
mock_prefered_entry_for = Minitest::Mock.new
mock_prefered_entry_for.expect :call, entry_path, [prof["sha256"]]
cf = Inspec::CachedFetcher.new("compliance://#{prof["owner"]}/#{prof["name"]}", cache)
mock_preferred_entry_for = Minitest::Mock.new
mock_preferred_entry_for.expect :call, entry_path, [prof["sha256"]]
cf = Inspec::CachedFetcher.new("compliance://#{prof["owner"]}/#{prof["name"]}#0.1.1", cache)
cache.stubs(:exists?).with(prof["sha256"]).returns(true)
cache.stub(:prefered_entry_for, mock_prefered_entry_for) do
cache.stub(:preferred_entry_for, mock_preferred_entry_for) do
cf.fetch
end
mock_prefered_entry_for.verify
mock_preferred_entry_for.verify
assert_not_requested(@stub_get)
end

it "skips caching on compliance if version unspecified" do
prof = profiles_result[0]
InspecPlugins::Compliance::API.stubs(:profiles).returns(["success", profiles_result])
cache = Inspec::Cache.new
cf = Inspec::CachedFetcher.new("compliance://#{prof["owner"]}/#{prof["name"]}", cache)
cf.fetch
assert_requested(@stub_get)
end
end
end

0 comments on commit 7fc5caa

Please sign in to comment.