Support vendored profiles in Habitat-packaged profiles

This change adds support in Habitat-packaged profiles for
profiles that depend on other profiles. When `inspec habitat
profile create` or `inspec habitat profile upload` is run,
it will see if the profile's dependencies have been vendored
yet, and if not, it will vendor them before creating the
habitat artifact.

For the git and URL fetchers, more explicit creation of the
target directories for the vendored profiles is done. This
is implicitly done via normal CLI interactions a user may
go through, but in our case, we want to ensure those directories
are there before the fetchers try to write out content.

By adding this support, we also fix a bug experienced in Habitat
where a profile that was packaged before an `inspec exec` was run
for the profile would cause a failure in Habitat. This is caused
by `inspec exec` doing a vendor of the dependencies if necessary
and generating the inspec.lock file. In Habitat, the package dir
is not writable by the hab user and InSpec would fail to run due
to an inability to write out an inspec.lock.

Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
Adam Leff 2017-03-24 16:28:00 -04:00 committed by Dominik Richter
parent 279f07cb1e
commit 8269d0da9e
4 changed files with 101 additions and 2 deletions

View file

@ -37,6 +37,8 @@ module Habitat
validate_habitat_installed
validate_habitat_origin
create_profile_object
verify_profile
vendor_profile_dependencies
copy_profile_to_work_dir
create_plan
create_run_hook
@ -79,7 +81,19 @@ module Habitat
private
def create_profile_object
@profile = Inspec::Profile.for_target(path, backend: Inspec::Backend.create(target: 'mock://'))
@profile = Inspec::Profile.for_target(
path,
cache: Inspec::Cache.new(cache_path.to_s),
backend: Inspec::Backend.create(target: 'mock://'),
)
end
def cache_path
File.join(path, 'vendor')
end
def inspec_lockfile
File.join(path, 'inspec.lock')
end
def verify_profile
@ -92,6 +106,20 @@ module Habitat
Habitat::Log.info('Profile is valid.')
end
def vendor_profile_dependencies
if File.exist?(inspec_lockfile) && Dir.exist?(cache_path)
Habitat::Log.info("Profile's dependencies are already vendored, skipping vendor process.")
else
Habitat::Log.info("Vendoring the profile's dependencies...")
FileUtils.rm_rf(cache_path)
File.delete(inspec_lockfile) if File.exist?(inspec_lockfile)
File.write(inspec_lockfile, profile.generate_lockfile.to_yaml)
# refresh the profile object since the profile now has new files
create_profile_object
end
end
def validate_habitat_installed
Habitat::Log.info('Checking to see if Habitat is installed...')
cmd = Mixlib::ShellOut.new('hab --version')

View file

@ -24,7 +24,7 @@ module Fetchers
# you got to this file during debugging, you may want to look at the
# omnibus source for hints.
#
class Git < Inspec.fetcher(1)
class Git < Inspec.fetcher(1) # rubocop:disable ClassLength
name 'git'
priority 200
@ -44,6 +44,8 @@ module Fetchers
def fetch(dir)
@repo_directory = dir
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
if cloned?
checkout
else

View file

@ -153,7 +153,9 @@ module Fetchers
def download_archive(path)
download_archive_to_temp
final_path = "#{path}#{@archive_type}"
FileUtils.mkdir_p(File.dirname(final_path))
FileUtils.mv(temp_archive_path, final_path)
FileUtils.chmod(0644, final_path)
Inspec::Log.debug("Fetched archive moved to: #{final_path}")
@temp_archive_path = nil
final_path

View file

@ -17,6 +17,73 @@ describe Habitat::Profile do
Habitat::Log.level(:fatal)
end
describe '#verify_profile' do
it 'exits if the profile is not valid' do
profile = mock
profile.stubs(:check).returns(summary: { valid: false })
subject.expects(:profile).returns(profile)
proc { subject.send(:verify_profile) }.must_raise SystemExit
end
it 'does not exist if the profile is valid' do
profile = mock
profile.stubs(:check).returns(summary: { valid: true })
subject.expects(:profile).returns(profile)
subject.send(:verify_profile)
end
end
describe '#vendor_profile_dependencies' do
let(:cache) { '/path/to/cache' }
let(:lockfile) { '/path/to/lock' }
let(:profile) do
profile = mock
profile.stubs(:generate_lockfile).returns(foo: 'bar')
profile
end
before do
subject.expects(:cache_path).at_least_once.returns(cache)
subject.expects(:inspec_lockfile).at_least_once.returns(lockfile)
end
describe 'when lockfile exists and cache dir exists' do
it 'does not generate the lockfile' do
File.expects(:exist?).with(lockfile).returns(true)
Dir.expects(:exist?).with(cache).returns(true)
subject.expects(:profile).returns(profile).never
profile.expects(:generate_lockfile).never
subject.send(:vendor_profile_dependencies)
end
end
describe 'when the lockfile exists but the cache dir does not' do
it 'deletes the lockfile, generates the lockfile, and refreshes the profile object' do
File.expects(:exist?).with(lockfile).returns(true).at_least_once
Dir.expects(:exist?).with(cache).returns(false).at_least_once
FileUtils.expects(:rm_rf).with(cache)
File.expects(:delete).with(lockfile)
subject.expects(:profile).returns(profile)
File.expects(:write).with(lockfile, "---\n:foo: bar\n")
subject.expects(:create_profile_object)
subject.send(:vendor_profile_dependencies)
end
end
describe 'when the lockfile does not exist' do
it 'generates the lockfile, and refreshes the profile object' do
File.expects(:exist?).with(lockfile).returns(false).at_least_once
FileUtils.expects(:rm_rf).with(cache)
subject.expects(:profile).returns(profile)
File.expects(:write).with(lockfile, "---\n:foo: bar\n")
subject.expects(:create_profile_object)
subject.send(:vendor_profile_dependencies)
end
end
end
describe '#validate_habitat_installed' do
it 'exits if hab --version fails' do
cmd = mock