mirror of
https://github.com/inspec/inspec
synced 2024-11-26 14:40:26 +00:00
Merge pull request #3818 from inspec/ja/rewrite-habitat-plugin
Rewrite inspec-habitat plugin
This commit is contained in:
commit
74e2f72ed5
22 changed files with 923 additions and 435 deletions
5
lib/plugins/inspec-habitat/Berksfile
Normal file
5
lib/plugins/inspec-habitat/Berksfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
source 'https://supermarket.chef.io'
|
||||
|
||||
group :integration do
|
||||
cookbook 'inspec_habitat_fixture', path: 'test/cookbooks/inspec_habitat_fixture/'
|
||||
end
|
150
lib/plugins/inspec-habitat/README.md
Normal file
150
lib/plugins/inspec-habitat/README.md
Normal file
|
@ -0,0 +1,150 @@
|
|||
# InSpec Habitat Plugin
|
||||
|
||||
## Summary
|
||||
|
||||
This plugin allows you to do the following:
|
||||
1. Add Habitat configuration to a profile
|
||||
2. Create/Upload a Habitat package from an InSpec profile
|
||||
|
||||
Creating a [Habitat](https://www.habitat.sh/) package from an InSpec profile
|
||||
allows you to execute that profile as a service (via a Habitat Supervisor) on
|
||||
any Linux based platform.
|
||||
|
||||
When running as a service, an InSpec report will be created in JSON format (by
|
||||
default at `/hab/svc/YOUR_SERVICE/logs/inspec_last_run.json`). Additionally, a
|
||||
log of the last run will be located at
|
||||
`/hab/svc/YOUR_SERVICE/logs/inspec_log.txt` and CLI output is viewable in
|
||||
the Supervisor logs by default. You can also configure this service to report
|
||||
to [Chef Automate](https://www.chef.io/automate/).
|
||||
|
||||
See below for usage instructions.
|
||||
|
||||
## Plugin Usage
|
||||
|
||||
### Adding Habitat Configuration to an InSpec Profile
|
||||
|
||||
Run the following command:
|
||||
|
||||
```
|
||||
inspec habitat profile setup PATH
|
||||
```
|
||||
|
||||
This will create the following files:
|
||||
- habitat/plan.sh (Provides build time instructions to Habitat)
|
||||
- habitat/default.toml (Used to configure the running Habitat service)
|
||||
- habitat/hooks/run (Shell script to execute this profile as a service)
|
||||
- habitat/config/inspec_exec_config.json (JSON for `inspec exec` CLI options)
|
||||
|
||||
### Creating a Habitat Package
|
||||
|
||||
> This command requires Habitat to be installed and configured. For instructions
|
||||
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
|
||||
|
||||
Run the following command:
|
||||
|
||||
```
|
||||
inspec habitat profile create PATH
|
||||
```
|
||||
|
||||
This command will:
|
||||
- Create a Habitat artifact (`.hart` file).
|
||||
|
||||
> NOTE: If you are fetching packages from Chef Automate see
|
||||
[below](#Integrating-with-Chef-Automate).
|
||||
|
||||
### Uploading a Habitat Package
|
||||
|
||||
> This command requires Habitat to be installed and configured. For instructions
|
||||
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
|
||||
|
||||
Run the following command:
|
||||
|
||||
```
|
||||
inspec habitat profile upload PATH
|
||||
```
|
||||
|
||||
This command will:
|
||||
- Create a Habitat artifact (`.hart` file).
|
||||
- Upload the Habitat artifact to [bldr.habitat.sh](bldr.habitat.sh).
|
||||
|
||||
> NOTE: If you are fetching packages from Chef Automate see
|
||||
[below](#Integrating-with-Chef-Automate).
|
||||
|
||||
## Habitat Package Usage
|
||||
|
||||
> This command requires Habitat to be installed and configured. For instructions
|
||||
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
|
||||
|
||||
General usage instructions for using Habitat packages can be found
|
||||
[here](https://www.habitat.sh/docs/using-habitat/#Using-Habitat-Packages).
|
||||
|
||||
Installing the package from a HART file:
|
||||
|
||||
```
|
||||
# See Habitat docs for more info. The below is for testing only.
|
||||
hab pkg install PATH_TO_CREATED_HART_FILE
|
||||
hab sup run YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
|
||||
```
|
||||
|
||||
Installing the package from the Public Builder Depot:
|
||||
|
||||
```
|
||||
# See Habitat docs for more info. The below is for testing only.
|
||||
hab pkg install YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
|
||||
hab sup run YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
|
||||
```
|
||||
|
||||
## Integrating with Chef Automate
|
||||
|
||||
### Fetching Profiles from Chef Automate During Build
|
||||
|
||||
Fetching profiles from Chef Automate requires authentication.
|
||||
|
||||
Run the following commands prior to creating/uploading your Habitat package:
|
||||
|
||||
```
|
||||
# Remove -k if you are not using a self-signed certificate
|
||||
inspec compliance login -k --user USER --token API_TOKEN https://AUTOMATE_FQDN
|
||||
export HAB_STUDIO_SECRET_COMPLIANCE_CREDS=$(cat ~/.inspec/compliance/config.json)
|
||||
```
|
||||
|
||||
### Sending InSpec Reports to Chef Automate
|
||||
|
||||
After running your Habitat package as a service you can configure it to report
|
||||
to Chef Automate via a
|
||||
[configuration update](https://www.habitat.sh/docs/using-habitat/#config-updates).
|
||||
|
||||
For example, create a TOML file (config.toml) that matches the below:
|
||||
|
||||
```
|
||||
[automate]
|
||||
url = 'https://chef-automate.test'
|
||||
token = 'TOKEN'
|
||||
user = 'admin'
|
||||
```
|
||||
|
||||
Then apply it like so:
|
||||
|
||||
```
|
||||
# The '1' here is the config version (increment this with each change)
|
||||
hab config apply inspec-profile-PROFILE_NAME.default 1 /path/to/config.toml
|
||||
```
|
||||
|
||||
This will apply the configuration to all services in the service group. For
|
||||
more info on service groups see the
|
||||
[Habitat docs](https://www.habitat.sh/docs/using-habitat/#service-groups)
|
||||
|
||||
## Testing
|
||||
|
||||
Lint, unit, and functional tests are ran from the root of the InSpec source:
|
||||
|
||||
```
|
||||
bundle exec rake test
|
||||
```
|
||||
|
||||
To execute the integration tests (Test Kitchen + Vagrant + VirtualBox) run the
|
||||
following from the directory containing this README.md:
|
||||
|
||||
```
|
||||
bundle exec kitchen test
|
||||
```
|
28
lib/plugins/inspec-habitat/kitchen.yml
Normal file
28
lib/plugins/inspec-habitat/kitchen.yml
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
driver:
|
||||
name: vagrant
|
||||
|
||||
provisioner:
|
||||
name: chef_solo
|
||||
sudo: true
|
||||
|
||||
verifier:
|
||||
name: inspec
|
||||
|
||||
platforms:
|
||||
- name: ubuntu-18.04
|
||||
|
||||
lifecycle:
|
||||
# Build the InSpec gem so it is available to install during `kitchen converge`
|
||||
pre_create:
|
||||
- cd ../../../ && gem build inspec.gemspec
|
||||
- mv ../../../inspec-*.gem test/cookbooks/inspec_habitat_fixture/files/inspec-local.gem
|
||||
post_converge:
|
||||
- local: sleep 10 # Wait for Habitat to load/run hab service before `verify`
|
||||
|
||||
suites:
|
||||
- name: default
|
||||
run_list:
|
||||
- recipe[inspec_habitat_fixture]
|
||||
attributes:
|
||||
|
|
@ -10,21 +10,21 @@ module InspecPlugins
|
|||
"#{basename} habitat profile #{command.usage}"
|
||||
end
|
||||
|
||||
desc 'create PATH', 'Create a one-time Habitat artifact for the profile found at PATH'
|
||||
desc 'create PATH', 'Create a Habitat artifact for the profile found at PATH'
|
||||
option :output_dir, type: :string, required: false,
|
||||
desc: 'Directory in which to save the generated Habitat artifact. Default: current directory'
|
||||
def create(path)
|
||||
InspecPlugins::Habitat::Profile.create(path, options)
|
||||
desc: 'Output directory for the Habitat artifact. Default: current directory'
|
||||
def create(path = '.')
|
||||
InspecPlugins::Habitat::Profile.new(path, options).create
|
||||
end
|
||||
|
||||
desc 'setup PATH', 'Configure the profile at PATH for Habitat, including a plan and hooks'
|
||||
def setup(path)
|
||||
InspecPlugins::Habitat::Profile.setup(path)
|
||||
def setup(path = '.')
|
||||
InspecPlugins::Habitat::Profile.new(path, options).setup
|
||||
end
|
||||
|
||||
desc 'upload PATH', 'Create a one-time Habitat artifact for the profile found at PATH, and upload it to a Habitat Depot'
|
||||
def upload(path)
|
||||
InspecPlugins::Habitat::Profile.upload(path, options)
|
||||
desc 'upload PATH', 'Create then upload a Habitat artifact for the profile found at PATH to the Habitat Builder Depot'
|
||||
def upload(path = '.')
|
||||
InspecPlugins::Habitat::Profile.new(path, options).upload
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,238 +1,226 @@
|
|||
# encoding: utf-8
|
||||
# author: Adam Leff
|
||||
|
||||
require 'inspec/profile_vendor'
|
||||
require 'mixlib/shellout'
|
||||
require 'tomlrb'
|
||||
require 'ostruct'
|
||||
|
||||
module InspecPlugins
|
||||
module Habitat
|
||||
class Profile
|
||||
attr_reader :options, :path, :profile
|
||||
|
||||
def self.create(path, options = {})
|
||||
creator = new(path, options)
|
||||
hart_file = creator.create
|
||||
creator.copy(hart_file)
|
||||
ensure
|
||||
creator.delete_work_dir
|
||||
end
|
||||
|
||||
def self.setup(path)
|
||||
new(path).setup
|
||||
end
|
||||
|
||||
def self.upload(path, options = {})
|
||||
uploader = new(path, options)
|
||||
uploader.upload
|
||||
ensure
|
||||
uploader.delete_work_dir
|
||||
end
|
||||
|
||||
attr_reader :logger
|
||||
def initialize(path, options = {})
|
||||
@path = path
|
||||
@options = options
|
||||
@cli_config = nil
|
||||
|
||||
log_level = options.fetch('log_level', 'info')
|
||||
@log = Inspec::Log
|
||||
@log.level(log_level.to_sym)
|
||||
@logger = Inspec::Log
|
||||
logger.level(options.fetch(:log_level, 'info').to_sym)
|
||||
end
|
||||
|
||||
def create
|
||||
@log.info("Creating a Habitat artifact for profile: #{path}")
|
||||
logger.info("Creating a Habitat artifact for '#{@path}'...")
|
||||
|
||||
validate_habitat_installed
|
||||
validate_habitat_origin
|
||||
create_profile_object
|
||||
verify_profile
|
||||
vendor_profile_dependencies
|
||||
copy_profile_to_work_dir
|
||||
create_habitat_directories(work_dir)
|
||||
create_plan(work_dir)
|
||||
create_run_hook(work_dir)
|
||||
create_default_config(work_dir)
|
||||
# Need to create working directory first so `ensure` doesn't error
|
||||
working_dir = create_working_dir
|
||||
|
||||
# returns the path to the .hart file in the work directory
|
||||
build_hart
|
||||
habitat_config = read_habitat_config
|
||||
verify_habitat_setup(habitat_config)
|
||||
|
||||
output_dir = @options[:output_dir] || Dir.pwd
|
||||
unless File.directory?(output_dir)
|
||||
exit_with_error("Output directory #{output_dir} is not a directory " \
|
||||
'or does not exist.')
|
||||
end
|
||||
|
||||
duplicated_profile = duplicate_profile(@path, working_dir)
|
||||
prepare_profile!(duplicated_profile)
|
||||
|
||||
hart_file = build_hart(working_dir, habitat_config)
|
||||
|
||||
logger.debug("Copying artifact to #{output_dir}...")
|
||||
destination = File.join(output_dir, File.basename(hart_file))
|
||||
FileUtils.cp(hart_file, destination)
|
||||
|
||||
logger.info("Habitat artifact '#{@destination}' created.")
|
||||
destination
|
||||
rescue => e
|
||||
@log.debug(e.backtrace.join("\n"))
|
||||
exit_with_error(
|
||||
'Unable to generate Habitat artifact.',
|
||||
"#{e.class} -- #{e.message}",
|
||||
)
|
||||
logger.debug(e.backtrace.join("\n"))
|
||||
exit_with_error('Unable to create Habitat artifact.')
|
||||
ensure
|
||||
if Dir.exist?(working_dir)
|
||||
logger.debug("Deleting working directory #{working_dir}")
|
||||
FileUtils.rm_rf(working_dir)
|
||||
end
|
||||
end
|
||||
|
||||
def copy(hart_file)
|
||||
validate_output_dir
|
||||
def setup(profile = profile_from_path(@path))
|
||||
path = profile.root_path
|
||||
logger.debug("Setting up #{path} for Habitat...")
|
||||
|
||||
@log.info("Copying artifact to #{output_dir}...")
|
||||
copy_hart(hart_file)
|
||||
plan_file = File.join(path, 'habitat', 'plan.sh')
|
||||
logger.info("Generating Habitat plan at #{plan_file}...")
|
||||
vars = {
|
||||
profile: profile,
|
||||
habitat_origin: read_habitat_config['origin'],
|
||||
}
|
||||
create_file_from_template(plan_file, 'plan.sh.erb', vars)
|
||||
|
||||
run_hook_file = File.join(path, 'habitat', 'hooks', 'run')
|
||||
logger.info("Generating a Habitat run hook at #{run_hook_file}...")
|
||||
create_file_from_template(run_hook_file, 'hooks/run.erb')
|
||||
|
||||
default_toml = File.join(path, 'habitat', 'default.toml')
|
||||
logger.info("Generating a Habitat default.toml at #{default_toml}...")
|
||||
create_file_from_template(default_toml, 'default.toml.erb')
|
||||
|
||||
config = File.join(path, 'habitat', 'config', 'inspec_exec_config.json')
|
||||
logger.info("Generating #{config} for `inspec exec`...")
|
||||
create_file_from_template(config, 'config/inspec_exec_config.json.erb')
|
||||
end
|
||||
|
||||
def upload
|
||||
validate_habitat_auth_token
|
||||
hart_file = create
|
||||
upload_hart(hart_file)
|
||||
habitat_config = read_habitat_config
|
||||
|
||||
if habitat_config['auth_token'].nil?
|
||||
exit_with_error(
|
||||
'Unable to determine Habitat auth token for uploading.',
|
||||
'Run `hab setup` or set the HAB_AUTH_TOKEN environment variable.',
|
||||
)
|
||||
end
|
||||
|
||||
# Run create command to create habitat artifact
|
||||
hart = create
|
||||
|
||||
logger.info("Uploading Habitat artifact #{hart}...")
|
||||
upload_hart(hart, habitat_config)
|
||||
logger.info("Habitat artifact #{hart} uploaded.")
|
||||
rescue => e
|
||||
@log.debug(e.backtrace.join("\n"))
|
||||
exit_with_error(
|
||||
'Unable to upload Habitat artifact.',
|
||||
"#{e.class} -- #{e.message}",
|
||||
)
|
||||
end
|
||||
|
||||
def delete_work_dir
|
||||
@log.debug("Deleting work directory #{work_dir}")
|
||||
FileUtils.rm_rf(work_dir) if Dir.exist?(work_dir)
|
||||
end
|
||||
|
||||
def setup
|
||||
@log.info("Setting up profile at #{path} for Habitat...")
|
||||
create_profile_object
|
||||
verify_profile
|
||||
vendor_profile_dependencies
|
||||
create_habitat_directories(path)
|
||||
create_plan(path)
|
||||
create_run_hook(path)
|
||||
create_default_config(path)
|
||||
logger.debug(e.backtrace.join("\n"))
|
||||
exit_with_error('Unable to upload Habitat artifact.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_profile_object
|
||||
@profile = Inspec::Profile.for_target(
|
||||
def create_working_dir
|
||||
working_dir = Dir.mktmpdir
|
||||
logger.debug("Generated working directory #{working_dir}")
|
||||
working_dir
|
||||
end
|
||||
|
||||
def duplicate_profile(path, working_dir)
|
||||
profile = profile_from_path(path)
|
||||
copy_profile_to_working_dir(profile, working_dir)
|
||||
profile_from_path(working_dir)
|
||||
end
|
||||
|
||||
def prepare_profile!(profile)
|
||||
vendored_profile = vendor_profile_dependencies!(profile)
|
||||
verify_profile(vendored_profile)
|
||||
setup(vendored_profile)
|
||||
end
|
||||
|
||||
def profile_from_path(path)
|
||||
Inspec::Profile.for_target(
|
||||
path,
|
||||
backend: Inspec::Backend.create(Inspec::Config.mock),
|
||||
)
|
||||
end
|
||||
|
||||
def verify_profile
|
||||
@log.info('Checking to see if the profile is valid...')
|
||||
def copy_profile_to_working_dir(profile, working_dir)
|
||||
logger.debug('Copying profile contents to the working directory...')
|
||||
profile.files.each do |profile_file|
|
||||
next if File.extname(profile_file) == '.hart'
|
||||
|
||||
unless profile.check[:summary][:valid]
|
||||
exit_with_error('Profile check failed. Please fix the profile before creating a Habitat artifact.')
|
||||
end
|
||||
|
||||
@log.info('Profile is valid.')
|
||||
end
|
||||
|
||||
def vendor_profile_dependencies
|
||||
profile_vendor = Inspec::ProfileVendor.new(path)
|
||||
if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
|
||||
@log.info("Profile's dependencies are already vendored, skipping vendor process.")
|
||||
else
|
||||
@log.info("Vendoring the profile's dependencies...")
|
||||
profile_vendor.vendor!
|
||||
|
||||
@log.info('Ensuring all vendored content has read permissions...')
|
||||
profile_vendor.make_readable
|
||||
|
||||
# refresh the profile object since the profile now has new files
|
||||
create_profile_object
|
||||
end
|
||||
end
|
||||
|
||||
def validate_habitat_installed
|
||||
@log.info('Checking to see if Habitat is installed...')
|
||||
cmd = Mixlib::ShellOut.new('hab --version')
|
||||
cmd.run_command
|
||||
exit_with_error('Unable to run Habitat commands.', cmd.stderr) if cmd.error?
|
||||
end
|
||||
|
||||
def validate_habitat_origin
|
||||
exit_with_error(
|
||||
'Unable to determine Habitat origin name.',
|
||||
'Run `hab setup` or set the HAB_ORIGIN environment variable.',
|
||||
) if habitat_origin.nil?
|
||||
end
|
||||
|
||||
def validate_habitat_auth_token
|
||||
exit_with_error(
|
||||
'Unable to determine Habitat auth token for publishing.',
|
||||
'Run `hab setup` or set the HAB_AUTH_TOKEN environment variable.',
|
||||
) if habitat_auth_token.nil?
|
||||
end
|
||||
|
||||
def validate_output_dir
|
||||
exit_with_error("Output directory #{output_dir} is not a directory or does not exist.") unless
|
||||
File.directory?(output_dir)
|
||||
end
|
||||
|
||||
def work_dir
|
||||
return @work_dir if @work_dir
|
||||
|
||||
@work_dir ||= Dir.mktmpdir('inspec-habitat-exporter')
|
||||
@log.debug("Generated work directory #{@work_dir}")
|
||||
|
||||
@work_dir
|
||||
end
|
||||
|
||||
def create_habitat_directories(parent_directory)
|
||||
[
|
||||
File.join(parent_directory, 'habitat'),
|
||||
File.join(parent_directory, 'habitat', 'hooks'),
|
||||
].each do |dir|
|
||||
Dir.mkdir(dir) unless Dir.exist?(dir)
|
||||
end
|
||||
end
|
||||
|
||||
def copy_profile_to_work_dir
|
||||
@log.info('Copying profile contents to the work directory...')
|
||||
profile.files.each do |f|
|
||||
src = File.join(profile.root_path, f)
|
||||
dst = File.join(work_dir, f)
|
||||
if File.directory?(f)
|
||||
@log.debug("Creating directory #{dst}")
|
||||
src = File.join(profile.root_path, profile_file)
|
||||
dst = File.join(working_dir, profile_file)
|
||||
if File.directory?(profile_file)
|
||||
logger.debug("Creating directory #{dst}")
|
||||
FileUtils.mkdir_p(dst)
|
||||
else
|
||||
@log.debug("Copying file #{src} to #{dst}")
|
||||
logger.debug("Copying file #{src} to #{dst}")
|
||||
FileUtils.cp_r(src, dst)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_plan(directory)
|
||||
plan_file = File.join(directory, 'habitat', 'plan.sh')
|
||||
@log.info("Generating Habitat plan at #{plan_file}...")
|
||||
File.write(plan_file, plan_contents)
|
||||
def verify_profile(profile)
|
||||
logger.debug('Checking to see if the profile is valid...')
|
||||
|
||||
unless profile.check[:summary][:valid]
|
||||
exit_with_error('Profile check failed. Please fix the profile ' \
|
||||
'before creating a Habitat artifact.')
|
||||
end
|
||||
|
||||
logger.debug('Profile is valid.')
|
||||
end
|
||||
|
||||
def create_run_hook(directory)
|
||||
run_hook_file = File.join(directory, 'habitat', 'hooks', 'run')
|
||||
@log.info("Generating a Habitat run hook at #{run_hook_file}...")
|
||||
File.write(run_hook_file, run_hook_contents)
|
||||
def vendor_profile_dependencies!(profile)
|
||||
profile_vendor = Inspec::ProfileVendor.new(profile.root_path)
|
||||
if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
|
||||
logger.debug("Profile's dependencies are already vendored, skipping " \
|
||||
'vendor process.')
|
||||
else
|
||||
logger.debug("Vendoring the profile's dependencies...")
|
||||
profile_vendor.vendor!
|
||||
|
||||
logger.debug('Ensuring all vendored content has read permissions...')
|
||||
profile_vendor.make_readable
|
||||
end
|
||||
|
||||
# Return new profile since it has changed
|
||||
Inspec::Profile.for_target(
|
||||
profile.root_path,
|
||||
backend: Inspec::Backend.create(Inspec::Config.mock),
|
||||
)
|
||||
end
|
||||
|
||||
def create_default_config(directory)
|
||||
default_toml = File.join(directory, 'habitat', 'default.toml')
|
||||
@log.info("Generating Habitat's default.toml configuration...")
|
||||
File.write(default_toml, 'sleep_time = 300')
|
||||
def verify_habitat_setup(habitat_config)
|
||||
logger.debug('Checking to see if Habitat is installed...')
|
||||
cmd = Mixlib::ShellOut.new('hab --version')
|
||||
cmd.run_command
|
||||
if cmd.error?
|
||||
exit_with_error('Unable to run Habitat commands.', cmd.stderr)
|
||||
end
|
||||
|
||||
if habitat_config['origin'].nil?
|
||||
exit_with_error(
|
||||
'Unable to determine Habitat origin name.',
|
||||
'Run `hab setup` or set the HAB_ORIGIN environment variable.',
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_hart
|
||||
@log.info('Building our Habitat artifact...')
|
||||
def create_file_from_template(file, template, vars = {})
|
||||
FileUtils.mkdir_p(File.dirname(file))
|
||||
template_path = File.join(__dir__, '../../templates/habitat', template)
|
||||
contents = ERB.new(File.read(template_path))
|
||||
.result(OpenStruct.new(vars).instance_eval { binding })
|
||||
File.write(file, contents)
|
||||
end
|
||||
|
||||
def build_hart(working_dir, habitat_config)
|
||||
logger.debug('Building our Habitat artifact...')
|
||||
|
||||
env = {
|
||||
'TERM' => 'vt100',
|
||||
'HAB_ORIGIN' => habitat_origin,
|
||||
'HAB_ORIGIN' => habitat_config['origin'],
|
||||
'HAB_NONINTERACTIVE' => 'true',
|
||||
}
|
||||
|
||||
env['RUST_LOG'] = 'debug' if @log.level == :debug
|
||||
env['RUST_LOG'] = 'debug' if logger.level == :debug
|
||||
|
||||
# TODO: Would love to use Mixlib::ShellOut here, but it doesn't
|
||||
# seem to preserve the STDIN tty, and docker gets angry.
|
||||
Dir.chdir(work_dir) do
|
||||
Dir.chdir(working_dir) do
|
||||
unless system(env, 'hab pkg build .')
|
||||
exit_with_error('Unable to build the Habitat artifact.')
|
||||
end
|
||||
end
|
||||
|
||||
hart_files = Dir.glob(File.join(work_dir, 'results', '*.hart'))
|
||||
hart_files = Dir.glob(File.join(working_dir, 'results', '*.hart'))
|
||||
|
||||
if hart_files.length > 1
|
||||
exit_with_error('More than one Habitat artifact was created which was not expected.')
|
||||
exit_with_error('More than one Habitat artifact was created which ' \
|
||||
'was not expected.')
|
||||
elsif hart_files.empty?
|
||||
exit_with_error('No Habitat artifact was created.')
|
||||
end
|
||||
|
@ -240,21 +228,16 @@ module InspecPlugins
|
|||
hart_files.first
|
||||
end
|
||||
|
||||
def copy_hart(working_dir_hart)
|
||||
hart_basename = File.basename(working_dir_hart)
|
||||
dst = File.join(output_dir, hart_basename)
|
||||
FileUtils.cp(working_dir_hart, dst)
|
||||
def upload_hart(hart_file, habitat_config)
|
||||
logger.debug("Uploading '#{hart_file}' to the Habitat Builder Depot...")
|
||||
|
||||
dst
|
||||
end
|
||||
|
||||
def upload_hart(hart_file)
|
||||
@log.info('Uploading the Habitat artifact to our Depot...')
|
||||
config = habitat_config
|
||||
|
||||
env = {
|
||||
'TERM' => 'vt100',
|
||||
'HAB_AUTH_TOKEN' => habitat_auth_token,
|
||||
'HAB_AUTH_TOKEN' => config['auth_token'],
|
||||
'HAB_NONINTERACTIVE' => 'true',
|
||||
'HAB_ORIGIN' => config['origin'],
|
||||
'TERM' => 'vt100',
|
||||
}
|
||||
|
||||
env['HAB_DEPOT_URL'] = ENV['HAB_DEPOT_URL'] if ENV['HAB_DEPOT_URL']
|
||||
|
@ -269,124 +252,25 @@ module InspecPlugins
|
|||
)
|
||||
end
|
||||
|
||||
@log.info('Upload complete!')
|
||||
logger.debug('Upload complete!')
|
||||
end
|
||||
|
||||
def habitat_origin
|
||||
ENV['HAB_ORIGIN'] || habitat_cli_config['origin']
|
||||
end
|
||||
|
||||
def habitat_auth_token
|
||||
ENV['HAB_AUTH_TOKEN'] || habitat_cli_config['auth_token']
|
||||
end
|
||||
|
||||
def habitat_cli_config
|
||||
return @cli_config if @cli_config
|
||||
|
||||
config_file = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
|
||||
return {} unless File.exist?(config_file)
|
||||
|
||||
@cli_config = Tomlrb.load_file(config_file)
|
||||
end
|
||||
|
||||
def output_dir
|
||||
options[:output_dir] || Dir.pwd
|
||||
def read_habitat_config
|
||||
cli_toml = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
|
||||
cli_toml = '/hab/etc/cli.toml' unless File.exist?(cli_toml)
|
||||
cli_config = File.exist?(cli_toml) ? Tomlrb.load_file(cli_toml) : {}
|
||||
cli_config['origin'] ||= ENV['HAB_ORIGIN']
|
||||
cli_config['auth_token'] ||= ENV['HAB_AUTH_TOKEN']
|
||||
cli_config
|
||||
end
|
||||
|
||||
def exit_with_error(*errors)
|
||||
errors.each do |error_msg|
|
||||
@log.error(error_msg)
|
||||
logger.error(error_msg)
|
||||
end
|
||||
|
||||
exit 1
|
||||
end
|
||||
|
||||
def package_name
|
||||
"inspec-profile-#{profile.name}"
|
||||
end
|
||||
|
||||
def plan_contents
|
||||
plan = <<~EOL
|
||||
pkg_name=#{package_name}
|
||||
pkg_version=#{profile.version}
|
||||
pkg_origin=#{habitat_origin}
|
||||
pkg_deps=(chef/inspec)
|
||||
EOL
|
||||
|
||||
plan += "pkg_license='#{profile.metadata.params[:license]}'\n\n" if profile.metadata.params[:license]
|
||||
|
||||
plan += <<~EOL
|
||||
do_setup_environment() {
|
||||
ARCHIVE_PATH="$HAB_CACHE_SRC_PATH/$pkg_dirname/$pkg_name-$pkg_version.tar.gz"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
if [ ! -f $PLAN_CONTEXT/../inspec.yml ]; then
|
||||
exit_with 'Cannot find inspec.yml. Please build from profile root.' 1
|
||||
fi
|
||||
|
||||
local profile_files=($(ls $PLAN_CONTEXT/../ -I habitat -I results))
|
||||
local profile_location="$HAB_CACHE_SRC_PATH/$pkg_dirname/build"
|
||||
mkdir -p $profile_location
|
||||
|
||||
build_line "Copying profile files to $profile_location"
|
||||
cp -R ${profile_files[@]} $profile_location
|
||||
|
||||
build_line "Archiving $ARCHIVE_PATH"
|
||||
inspec archive "$HAB_CACHE_SRC_PATH/$pkg_dirname/build" \
|
||||
-o $ARCHIVE_PATH \
|
||||
--overwrite
|
||||
}
|
||||
|
||||
do_install() {
|
||||
mkdir -p $pkg_prefix/profiles
|
||||
cp $ARCHIVE_PATH $pkg_prefix/profiles
|
||||
}
|
||||
EOL
|
||||
|
||||
plan
|
||||
end
|
||||
|
||||
def run_hook_contents
|
||||
<<~EOL
|
||||
#!{{pkgPathFor "core/bash"}}/bin/bash
|
||||
|
||||
# Redirect stderr to stdout
|
||||
# This will be captured by Habitat and viewable via `journalctl`
|
||||
# NOTE: We might want log to "{{pkg.svc_path}}/logs" and handle rotation
|
||||
exec 2>&1
|
||||
|
||||
# InSpec will try to create a .cache directory in the user's home directory
|
||||
# so this needs to be someplace writeable by the hab user
|
||||
export HOME={{pkg.svc_var_path}}
|
||||
|
||||
RESULTS_DIR="{{pkg.svc_var_path}}/inspec_results"
|
||||
RESULTS_FILE="${RESULTS_DIR}/{{pkg.name}}.json"
|
||||
|
||||
# Create a directory for InSpec reporter output
|
||||
mkdir -p $(dirname $RESULTS_FILE)
|
||||
|
||||
while true; do
|
||||
echo "Executing InSpec for {{pkg.ident}}"
|
||||
inspec exec "{{pkg.path}}/profiles/*" --reporter=json > ${RESULTS_FILE}
|
||||
|
||||
EXIT_STATUS=$?
|
||||
if [ $EXIT_STATUS -eq 0 ]; then
|
||||
echo "InSpec run completed successfully."
|
||||
elif [ $EXIT_STATUS -eq 100 ]; then
|
||||
echo "InSpec run completed successfully, with at least 1 failed test"
|
||||
elif [ $EXIT_STATUS -eq 101 ]; then
|
||||
echo "InSpec run completed successfully, with skipped tests and no failures"
|
||||
else
|
||||
echo "InSpec run did not complete successfully. Exited with status: $?"
|
||||
fi
|
||||
echo "Results located here: ${RESULTS_FILE}"
|
||||
|
||||
echo "Sleeping for {{cfg.sleep_time}} seconds"
|
||||
sleep {{cfg.sleep_time}}
|
||||
done
|
||||
EOL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"target_id": "{{ sys.member_id }}",
|
||||
"reporter": {
|
||||
"cli": {
|
||||
"stdout": {{cfg.report_to_stdout}}
|
||||
},
|
||||
"json": {
|
||||
"file": "{{pkg.svc_path}}/logs/inspec_last_run.json"
|
||||
}{{#if cfg.automate.token ~}},
|
||||
"automate" : {
|
||||
"url": "{{cfg.automate.url}}/data-collector/v0/",
|
||||
"token": "{{cfg.automate.token}}",
|
||||
"node_name": "{{ sys.hostname }}",
|
||||
"verify_ssl": false
|
||||
}{{/if ~}}
|
||||
}
|
||||
{{#if cfg.automate.token }},
|
||||
"compliance": {
|
||||
"server" : "{{cfg.automate.url}}",
|
||||
"token" : "{{cfg.automate.token}}",
|
||||
"user" : "{{cfg.automate.user}}",
|
||||
"insecure" : true,
|
||||
"ent" : "automate"
|
||||
}{{/if }}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
interval = 300
|
||||
report_to_stdout = true
|
||||
|
||||
# Uncomment and replace values to report to Automate.
|
||||
# This can also be applied at runtime via `hab config apply`
|
||||
#[automate]
|
||||
#url = 'https://chef-automate.test'
|
||||
#token = 'TOKEN'
|
||||
#user = 'admin'
|
32
lib/plugins/inspec-habitat/templates/habitat/hooks/run.erb
Normal file
32
lib/plugins/inspec-habitat/templates/habitat/hooks/run.erb
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
|
||||
exec 2>&1
|
||||
|
||||
CONFIG="{{pkg.svc_config_path}}/inspec_exec_config.json"
|
||||
INTERVAL="{{cfg.interval}}"
|
||||
LOG_FILE="{{pkg.svc_path}}/logs/inspec_log.txt"
|
||||
PROFILE_IDENT="{{pkg.origin}}/{{pkg.name}}"
|
||||
PROFILE_PATH="{{pkg.path}}/{{pkg.name}}-{{pkg.version}}.tar.gz"
|
||||
|
||||
while true; do
|
||||
echo "Executing ${PROFILE_IDENT}"
|
||||
exec inspec exec ${PROFILE_PATH} --json-config ${CONFIG} 2>&1 | tee ${LOG_FILE}
|
||||
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 1 ]; then
|
||||
echo "InSpec run failed."
|
||||
else
|
||||
echo "InSpec run completed successfully."
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "No controls failed or were skipped."
|
||||
elif [ $exit_code -eq 100 ]; then
|
||||
echo "At least 1 control failed."
|
||||
elif [ $exit_code -eq 101 ]; then
|
||||
echo "No controls failed but at least 1 skipped."
|
||||
fi
|
||||
fi
|
||||
echo "Results are logged here: ${LOG_FILE}"
|
||||
|
||||
echo "Sleeping for ${INTERVAL} seconds"
|
||||
sleep ${INTERVAL}
|
||||
done
|
85
lib/plugins/inspec-habitat/templates/habitat/plan.sh.erb
Normal file
85
lib/plugins/inspec-habitat/templates/habitat/plan.sh.erb
Normal file
|
@ -0,0 +1,85 @@
|
|||
pkg_name=<%= "inspec-profile-#{profile.name}" %>
|
||||
pkg_version=<%= profile.version %>
|
||||
pkg_origin=<%= habitat_origin %>
|
||||
pkg_deps=(chef/inspec)
|
||||
pkg_build_deps=(chef/inspec core/jq-static)
|
||||
pkg_svc_user=root
|
||||
<%= "pkg_license='#{profile.metadata.params[:license]}'" if profile.metadata.params[:license]%>
|
||||
|
||||
do_before() {
|
||||
# Exit with error if not in the directory with 'inspec.yml'.
|
||||
# This can happen if someone does 'hab studio enter' from within the
|
||||
# 'habitat/' directory.
|
||||
if [ ! -f "$PLAN_CONTEXT/../inspec.yml" ]; then
|
||||
message="ERROR: Cannot find inspec.yml."
|
||||
message="$message Please build from the profile root"
|
||||
build_line "$message"
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Execute an 'inspec compliance login' if a profile needs to be fetched from
|
||||
# the Automate server
|
||||
if [ "$(grep "compliance: " "$PLAN_CONTEXT/../inspec.yml")" ]; then
|
||||
_do_compliance_login;
|
||||
fi
|
||||
}
|
||||
|
||||
do_setup_environment() {
|
||||
set_buildtime_env PROFILE_CACHE_DIR "$HAB_CACHE_SRC_PATH/$pkg_dirname"
|
||||
set_buildtime_env ARCHIVE_NAME "$pkg_name-$pkg_version.tar.gz"
|
||||
|
||||
# InSpec loads `pry` which tries to expand `~`. This fails if HOME isn't set.
|
||||
set_runtime_env HOME "$pkg_svc_var_path"
|
||||
|
||||
# InSpec will create a `.inspec` directory in the user's home directory.
|
||||
# This overrides that to write to a place within the running service's path.
|
||||
# NOTE: Setting HOME does the same currently. This is here to be explicit.
|
||||
set_runtime_env INSPEC_CONFIG_DIR "$pkg_svc_var_path"
|
||||
}
|
||||
|
||||
do_unpack() {
|
||||
# Change directory to where the profile files are
|
||||
pushd "$PLAN_CONTEXT/../" > /dev/null
|
||||
|
||||
# Get a list of all files in the profile except those that are Habitat related
|
||||
profile_files=($(ls -I habitat -I results -I "*.hart"))
|
||||
|
||||
mkdir -p "$PROFILE_CACHE_DIR" > /dev/null
|
||||
|
||||
# Copy just the profile files to the profile cache directory
|
||||
cp -r ${profile_files[@]} "$PROFILE_CACHE_DIR"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
inspec archive "$PROFILE_CACHE_DIR" \
|
||||
--overwrite \
|
||||
-o "$PROFILE_CACHE_DIR/$ARCHIVE_NAME"
|
||||
}
|
||||
|
||||
do_install() {
|
||||
cp "$PROFILE_CACHE_DIR/$ARCHIVE_NAME" "$pkg_prefix"
|
||||
}
|
||||
|
||||
_do_compliance_login() {
|
||||
if [ -z $COMPLIANCE_CREDS ]; then
|
||||
message="ERROR: Please perform an 'inspec compliance login' and set"
|
||||
message="$message \$HAB_STUDIO_SECRET_COMPLIANCE_CREDS to the contents of"
|
||||
message="$message '~/.inspec/compliance/config.json'"
|
||||
build_line "$message"
|
||||
return 1
|
||||
fi
|
||||
|
||||
user=$(echo $COMPLIANCE_CREDS | jq .user | sed 's/"//g')
|
||||
token=$(echo $COMPLIANCE_CREDS | jq .token | sed 's/"//g')
|
||||
automate_server=$(echo $COMPLIANCE_CREDS | \
|
||||
jq .server | \
|
||||
sed 's/\/api\/v0//' | \
|
||||
sed 's/"//g'
|
||||
)
|
||||
insecure=$(echo $COMPLIANCE_CREDS | jq .insecure)
|
||||
inspec compliance login --insecure $insecure \
|
||||
--user $user \
|
||||
--token $token \
|
||||
$automate_server
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
source 'https://supermarket.chef.io'
|
||||
metadata
|
|
@ -0,0 +1,3 @@
|
|||
# InSpec Habitat Fixture
|
||||
|
||||
Used to setup a server for testing the `inspec-habitat` plugin
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/expect -f
|
||||
|
||||
set timeout -1
|
||||
|
||||
spawn hab setup
|
||||
|
||||
expect "Connect to an on-premises bldr instance?"
|
||||
send -- "No\r"
|
||||
|
||||
expect "Set up a default origin?"
|
||||
send -- "Yes\r"
|
||||
|
||||
expect "Default origin name"
|
||||
send -- "vagrant\r"
|
||||
|
||||
expect "Create an origin key for `vagrant'?"
|
||||
send -- "Yes\r"
|
||||
|
||||
expect "Set up a default Habitat personal access token?"
|
||||
send -- "No\r"
|
||||
|
||||
expect "Set up a default Habitat Supervisor CtlGateway secret?"
|
||||
send -- "No\r"
|
||||
|
||||
expect "Enable analytics?"
|
||||
send -- "No\r"
|
||||
|
||||
expect eof
|
|
@ -0,0 +1,9 @@
|
|||
name 'inspec_habitat_fixture'
|
||||
maintainer 'The Authors'
|
||||
maintainer_email 'you@example.com'
|
||||
license 'All Rights Reserved'
|
||||
description 'Used for testing the inspec-habitat plugin'
|
||||
version '0.1.0'
|
||||
chef_version '>= 13.0'
|
||||
|
||||
depends 'habitat'
|
|
@ -0,0 +1,61 @@
|
|||
#
|
||||
# Cookbook:: kitchen_setup_cookbook
|
||||
# Recipe:: default
|
||||
#
|
||||
# Copyright:: 2019, The Authors, All Rights Reserved.
|
||||
|
||||
package %w(ruby ruby-dev gcc g++ make expect)
|
||||
|
||||
base_dir = '/home/vagrant'
|
||||
|
||||
cookbook_file "#{base_dir}/inspec-local.gem" do
|
||||
source 'inspec-local.gem'
|
||||
action :create
|
||||
end
|
||||
|
||||
gem_package 'inspec' do
|
||||
source "#{base_dir}/inspec-local.gem"
|
||||
subscribes :install, "cookbook_file[#{base_dir}/inspec-local.gem]", :immediately
|
||||
end
|
||||
|
||||
cookbook_file "#{base_dir}/hab_setup.exp" do
|
||||
source 'hab_setup.exp'
|
||||
mode '0755'
|
||||
action :create
|
||||
end
|
||||
|
||||
hab_install 'install habitat'
|
||||
hab_sup 'setup hab supervisor'
|
||||
|
||||
execute 'setup hab cli' do
|
||||
command "#{base_dir}/hab_setup.exp"
|
||||
live_stream true
|
||||
not_if { ::File.exist?('/hab/etc/cli.toml') }
|
||||
not_if { ::File.exist?('~/.hab/etc/cli.toml') }
|
||||
end
|
||||
|
||||
execute 'create inspec profile for testing' do
|
||||
command "inspec init profile #{base_dir}/hab_test_profile"
|
||||
live_stream true
|
||||
creates "#{base_dir}/hab_test_profile"
|
||||
end
|
||||
|
||||
directory "#{base_dir}/output"
|
||||
|
||||
execute 'create hart file from profile' do
|
||||
command "inspec habitat profile create #{base_dir}/hab_test_profile --output_dir '#{base_dir}/output'"
|
||||
live_stream true
|
||||
not_if "find #{base_dir}/output | grep vagrant-inspec-profile-hab_test_profile-0.1.0-.*.hart"
|
||||
end
|
||||
|
||||
execute 'install vagrant/inspec-profile-hab_test_profile' do
|
||||
command "hab pkg install #{base_dir}/output/*.hart"
|
||||
live_stream true
|
||||
not_if 'hab pkg list --origin vagrant | grep inspec-profile'
|
||||
end
|
||||
|
||||
execute 'load vagrant/inspec-profile-hab_test_profile into supervisor' do
|
||||
command 'hab svc load vagrant/inspec-profile-hab_test_profile'
|
||||
live_stream true
|
||||
not_if 'sudo hab svc status | grep "vagrant/inspec-profile-hab_test_profile"'
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
require_relative '../../../shared/core_plugin_test_helper.rb'
|
||||
require 'fileutils'
|
||||
|
||||
class ProfileCli < MiniTest::Test
|
||||
include CorePluginFunctionalHelper
|
||||
|
||||
def setup
|
||||
@tmpdir = Dir.mktmpdir
|
||||
@habitat_profile = File.join(@tmpdir, 'habitat-profile')
|
||||
run_inspec_process('init profile ' + @habitat_profile)
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.remove_entry_secure(@tmpdir)
|
||||
end
|
||||
|
||||
def test_setup_subcommand
|
||||
result = run_inspec_process('habitat profile setup ' + @habitat_profile + ' --log-level debug')
|
||||
|
||||
# Command runs without error
|
||||
assert_empty result.stderr
|
||||
assert_equal 0, result.exit_status
|
||||
|
||||
# Command creates only expected files
|
||||
base_dir = File.join(@tmpdir, 'habitat-profile', 'habitat')
|
||||
files = %w{
|
||||
default.toml
|
||||
plan.sh
|
||||
config
|
||||
config/inspec_exec_config.json
|
||||
hooks
|
||||
hooks/run
|
||||
}
|
||||
actual_files = Dir.glob(File.join(base_dir, '**/*'))
|
||||
expected_files = files.map { |x| File.join(base_dir, x) }
|
||||
assert_equal actual_files.sort, expected_files.sort
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
# Habitat InSpec Test Profile
|
||||
|
||||
This profile is used to test `inspec habitat profile` commands
|
|
@ -0,0 +1,40 @@
|
|||
control 'inspec-habitat-create' do
|
||||
title 'Create command'
|
||||
|
||||
output_hart_dir = '/home/vagrant/output'
|
||||
find_hart_output = command("find #{output_hart_dir} -name '*.hart'").stdout
|
||||
hart_files = find_hart_output.split("\n")
|
||||
|
||||
hab_profile_path = '/home/vagrant/hab_test_profile'
|
||||
find_profile_files_command = "find #{hab_profile_path} -type f -printf '%f\n'"
|
||||
profile_files = command(find_profile_files_command).stdout.split("\n").sort
|
||||
expected_files = %w{
|
||||
.gitkeep
|
||||
README.md
|
||||
example.rb
|
||||
inspec.yml
|
||||
}
|
||||
|
||||
describe '`inspec habitat profile create`' do
|
||||
it 'should create exactly 1 hart file' do
|
||||
expect(hart_files.length).to eq(1)
|
||||
end
|
||||
it 'does not add any extra files to a default generated profile' do
|
||||
expect(profile_files).to eq(expected_files)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
control 'inspec-habitat-service' do
|
||||
title 'inspec-profile-hab_test_profile service'
|
||||
describe 'The running service' do
|
||||
it 'should create a log file' do
|
||||
log = '/hab/svc/inspec-profile-hab_test_profile/logs/inspec_log.txt'
|
||||
expect(file(log).exist?).to be(true)
|
||||
end
|
||||
it 'should create a JSON file for the last run' do
|
||||
log = '/hab/svc/inspec-profile-hab_test_profile/logs/inspec_last_run.json'
|
||||
JSON.parse(file(log).content)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
name: inspec_habitat
|
||||
title: InSpec Profile
|
||||
maintainer: The Authors
|
||||
copyright: The Authors
|
||||
copyright_email: you@example.com
|
||||
license: Apache-2.0
|
||||
summary: An InSpec Compliance Profile
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
|
@ -0,0 +1,3 @@
|
|||
# Example InSpec Profile
|
||||
|
||||
This profile is used for unit testing the `inspec-habitat` profile
|
|
@ -0,0 +1,7 @@
|
|||
control 'example' do
|
||||
impact 0.7
|
||||
title 'Example control'
|
||||
describe 'example' do
|
||||
it { should cmp 'example' }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
name: example_profile
|
||||
title: InSpec Profile
|
||||
maintainer: The Authors
|
||||
copyright: The Authors
|
||||
copyright_email: you@example.com
|
||||
license: Apache-2.0
|
||||
summary: An InSpec Compliance Profile
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
|
@ -1,184 +1,240 @@
|
|||
require 'mixlib/log'
|
||||
require 'ostruct'
|
||||
require 'fileutils'
|
||||
require 'minitest/autorun'
|
||||
require 'mocha/setup'
|
||||
require_relative '../../lib/inspec-habitat/profile.rb'
|
||||
|
||||
describe InspecPlugins::Habitat::Profile do
|
||||
let(:profile) do
|
||||
OpenStruct.new(
|
||||
name: 'my_profile',
|
||||
version: '1.2.3',
|
||||
files: %w(file1 file2)
|
||||
class InspecPlugins::Habitat::ProfileTest < MiniTest::Unit::TestCase
|
||||
def setup
|
||||
@tmpdir = Dir.mktmpdir
|
||||
|
||||
@output_dir = File.join(@tmpdir, 'output')
|
||||
FileUtils.mkdir(@output_dir)
|
||||
|
||||
@fake_hart_file = FileUtils.touch(File.join(@tmpdir, 'fake-hart.hart'))[0]
|
||||
|
||||
# Path from `__FILE__` needed to support running tests in `inspec/inspec`
|
||||
@test_profile_path = File.join(
|
||||
File.expand_path(File.dirname(__FILE__)),
|
||||
'../',
|
||||
'support',
|
||||
'example_profile'
|
||||
)
|
||||
@test_profile = Inspec::Profile.for_target(
|
||||
@test_profile_path,
|
||||
backend: Inspec::Backend.create(Inspec::Config.mock),
|
||||
)
|
||||
end
|
||||
|
||||
let(:subject) { InspecPlugins::Habitat::Profile.new('/path/to/profile', { 'log_level' => 'fatal' }) }
|
||||
@hab_profile = InspecPlugins::Habitat::Profile.new(
|
||||
@test_profile_path,
|
||||
{ output_dir: @output_dir },
|
||||
)
|
||||
|
||||
@mock_hab_config = {
|
||||
'auth_token' => 'FAKETOKEN',
|
||||
'origin' => 'fake_origin',
|
||||
}
|
||||
|
||||
before do
|
||||
Inspec::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
|
||||
def after_run
|
||||
FileUtils.remove_entry_secure(@tmpdir)
|
||||
end
|
||||
|
||||
describe '#vendor_profile_dependencies' do
|
||||
let(:profile_vendor) do
|
||||
profile_vendor = mock
|
||||
profile_vendor.stubs(:lockfile).returns(lockfile)
|
||||
profile_vendor.stubs(:cache_path).returns(cache_path)
|
||||
profile_vendor
|
||||
end
|
||||
let(:lockfile) { mock }
|
||||
let(:cache_path) { mock }
|
||||
def test_create_raises_if_output_dir_does_not_exist
|
||||
profile = InspecPlugins::Habitat::Profile.new(
|
||||
@test_profile_path,
|
||||
{
|
||||
output_dir: '/not/a/real/path',
|
||||
log_level: 'fatal',
|
||||
},
|
||||
)
|
||||
|
||||
before do
|
||||
Inspec::ProfileVendor.expects(:new).returns(profile_vendor)
|
||||
end
|
||||
assert_raises(SystemExit) { profile.create }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
|
||||
describe 'when lockfile exists and cache dir exists' do
|
||||
it 'does not vendor the dependencies' do
|
||||
lockfile.expects(:exist?).returns(true)
|
||||
cache_path.expects(:exist?).returns(true)
|
||||
profile_vendor.expects(:vendor!).never
|
||||
profile_vendor.expects(:make_readable).never
|
||||
subject.send(:vendor_profile_dependencies)
|
||||
def test_create
|
||||
file_count = Dir.glob(File.join(@test_profile_path, '**/*')).count
|
||||
|
||||
@hab_profile.stub :read_habitat_config, @mock_hab_config do
|
||||
@hab_profile.stub :verify_habitat_setup, nil do
|
||||
@hab_profile.stub :build_hart, @fake_hart_file do
|
||||
@hab_profile.create
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when the lockfile exists but the cache dir does not' do
|
||||
it 'vendors the dependencies and refreshes the profile object' do
|
||||
lockfile.expects(:exist?).returns(true)
|
||||
cache_path.expects(:exist?).returns(false)
|
||||
profile_vendor.expects(:vendor!)
|
||||
profile_vendor.expects(:make_readable)
|
||||
subject.expects(:create_profile_object)
|
||||
# It should not modify target profile
|
||||
new_file_count = Dir.glob(File.join(@test_profile_path, '**/*')).count
|
||||
assert_equal new_file_count, file_count
|
||||
|
||||
subject.send(:vendor_profile_dependencies)
|
||||
end
|
||||
# It should create 1 Habitat artifact
|
||||
output_files = Dir.glob(File.join(@output_dir, '**/*'))
|
||||
assert_equal 1, output_files.count
|
||||
assert_equal 'fake-hart.hart', File.basename(output_files.first)
|
||||
end
|
||||
|
||||
def test_create_rasies_if_habitat_is_not_installed
|
||||
cmd = MiniTest::Mock.new
|
||||
cmd.expect(:error?, true)
|
||||
cmd.expect(:run_command, nil)
|
||||
|
||||
Mixlib::ShellOut.stub :new, cmd, 'hab --version' do
|
||||
assert_raises(SystemExit) { @hab_profile.create }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
|
||||
describe 'when the lockfile does not exist' do
|
||||
it 'vendors the dependencies and refreshes the profile object' do
|
||||
lockfile.expects(:exist?).returns(false)
|
||||
profile_vendor.expects(:vendor!)
|
||||
profile_vendor.expects(:make_readable)
|
||||
subject.expects(:create_profile_object)
|
||||
cmd.verify
|
||||
end
|
||||
|
||||
subject.send(:vendor_profile_dependencies)
|
||||
def test_upload
|
||||
@hab_profile.stub :read_habitat_config, @mock_hab_config do
|
||||
@hab_profile.stub :create, @fake_hart_file do
|
||||
@hab_profile.stub :upload_hart, nil do
|
||||
@hab_profile.upload
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_habitat_installed' do
|
||||
it 'exits if hab --version fails' do
|
||||
cmd = mock
|
||||
cmd.stubs(:error?).returns(true)
|
||||
cmd.stubs(:run_command)
|
||||
cmd.stubs(:stdout)
|
||||
cmd.stubs(:stderr)
|
||||
Mixlib::ShellOut.expects(:new).with('hab --version').returns(cmd)
|
||||
proc { subject.send(:validate_habitat_installed) }.must_raise SystemExit
|
||||
def test_upload_raises_if_no_habitat_auth_token_is_found
|
||||
@hab_profile.stub :read_habitat_config, {} do
|
||||
assert_raises(SystemExit) { @hab_profile.upload }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_habitat_origin' do
|
||||
it 'does not exit if the origin key exists' do
|
||||
subject.expects(:habitat_origin).returns('12345')
|
||||
subject.send(:validate_habitat_origin)
|
||||
end
|
||||
|
||||
it 'exits if no origin key exists' do
|
||||
subject.expects(:habitat_origin).returns(nil)
|
||||
proc { subject.send(:validate_habitat_origin) }.must_raise SystemExit
|
||||
def test_create_working_dir
|
||||
Dir.stub :mktmpdir, '/tmp/fakedir' do
|
||||
assert_equal '/tmp/fakedir', @hab_profile.send(:create_working_dir)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_habitat_auth_token' do
|
||||
it 'does not exit if the auth_token exists' do
|
||||
subject.expects(:habitat_auth_token).returns('12345')
|
||||
subject.send(:validate_habitat_auth_token)
|
||||
end
|
||||
def test_duplicate_profile
|
||||
current_profile = @test_profile
|
||||
duplicated_profile = @hab_profile.send(:duplicate_profile,
|
||||
@test_profile_path,
|
||||
@tmpdir)
|
||||
assert duplicated_profile.is_a?(Inspec::Profile)
|
||||
assert duplicated_profile.sha256 == current_profile.sha256.to_s
|
||||
refute_same duplicated_profile.root_path, current_profile.root_path
|
||||
end
|
||||
|
||||
it 'exits if no auth_token exists' do
|
||||
subject.expects(:habitat_auth_token).returns(nil)
|
||||
proc { subject.send(:validate_habitat_auth_token) }.must_raise SystemExit
|
||||
def test_profile_from_path
|
||||
profile = @hab_profile.send(:profile_from_path, @test_profile_path)
|
||||
assert profile.is_a?(Inspec::Profile)
|
||||
end
|
||||
|
||||
def test_copy_profile_to_working_dir
|
||||
duplicated_profile = @hab_profile.send(:duplicate_profile,
|
||||
@test_profile_path,
|
||||
@tmpdir)
|
||||
|
||||
dst = File.join(@tmpdir, 'working_dir')
|
||||
FileUtils.mkdir_p(dst)
|
||||
@hab_profile.send(:copy_profile_to_working_dir, duplicated_profile, dst)
|
||||
|
||||
expected_files = %w{
|
||||
README.md
|
||||
inspec.yml
|
||||
example.rb
|
||||
}
|
||||
|
||||
actual_files = Dir.glob(File.join(dst, '**/*')).map do |path|
|
||||
next unless File.file?(path)
|
||||
File.basename(path)
|
||||
end.compact
|
||||
|
||||
assert(actual_files.sort == expected_files.sort)
|
||||
end
|
||||
|
||||
def test_verify_profile_raises_if_profile_is_not_valid
|
||||
bad_profile_path = File.join(@tmpdir, 'bad_profile')
|
||||
FileUtils.mkdir_p(File.join(bad_profile_path))
|
||||
FileUtils.touch(File.join(bad_profile_path, 'inspec.yml'))
|
||||
bad_profile = Inspec::Profile.for_target(
|
||||
bad_profile_path,
|
||||
backend: Inspec::Backend.create(Inspec::Config.mock),
|
||||
)
|
||||
assert_raises(SystemExit) { @hab_profile.send(:verify_profile, bad_profile) }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
|
||||
def test_vendor_profile_dependencies_does_not_vendor_if_already_vendored
|
||||
mock_lock_file = MiniTest::Mock.new
|
||||
mock_lock_file.expect(:exist?, true)
|
||||
mock_cache_path = MiniTest::Mock.new
|
||||
mock_cache_path.expect(:exist?, true)
|
||||
|
||||
mock = MiniTest::Mock.new
|
||||
mock.expect(:lockfile, mock_lock_file)
|
||||
mock.expect(:cache_path, mock_cache_path)
|
||||
|
||||
Inspec::ProfileVendor.stub :new, mock do
|
||||
new_profile = @hab_profile.send(:vendor_profile_dependencies!,
|
||||
@test_profile)
|
||||
assert new_profile.is_a?(Inspec::Profile)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_hart' do
|
||||
before do
|
||||
subject.expects(:work_dir).at_least_once.returns(Dir.tmpdir)
|
||||
end
|
||||
def test_vendor_profile_dependencies
|
||||
mock_lock_file = MiniTest::Mock.new
|
||||
mock_lock_file.expect(:exist?, false)
|
||||
|
||||
it 'exits if the build fails' do
|
||||
subject.expects(:system).returns(false)
|
||||
proc { subject.send(:build_hart) }.must_raise SystemExit
|
||||
end
|
||||
mock = MiniTest::Mock.new
|
||||
mock.expect(:lockfile, mock_lock_file)
|
||||
mock.expect(:vendor!, nil)
|
||||
mock.expect(:make_readable, nil)
|
||||
|
||||
it 'exits if more than one hart is created' do
|
||||
subject.expects(:system).returns(true)
|
||||
Dir.expects(:glob).returns(%w(hart1 hart2))
|
||||
proc { subject.send(:build_hart) }.must_raise SystemExit
|
||||
end
|
||||
|
||||
it 'exits if more than no hart is created' do
|
||||
subject.expects(:system).returns(true)
|
||||
Dir.expects(:glob).returns([])
|
||||
proc { subject.send(:build_hart) }.must_raise SystemExit
|
||||
end
|
||||
|
||||
it 'returns the hart filename' do
|
||||
subject.expects(:system).returns(true)
|
||||
Dir.expects(:glob).returns(%w(hart1))
|
||||
subject.send(:build_hart).must_equal('hart1')
|
||||
Inspec::ProfileVendor.stub :new, mock do
|
||||
new_profile = @hab_profile.send(:vendor_profile_dependencies!,
|
||||
@test_profile)
|
||||
assert new_profile.is_a?(Inspec::Profile)
|
||||
end
|
||||
mock.verify
|
||||
end
|
||||
|
||||
describe '#upload_hart' do
|
||||
it 'exits if the upload failed' do
|
||||
env = {
|
||||
'TERM' => 'vt100',
|
||||
'HAB_AUTH_TOKEN' => 'my_token',
|
||||
'HAB_NONINTERACTIVE' => 'true',
|
||||
}
|
||||
def test_verify_habitat_setup_raises_if_hab_version_errors
|
||||
mock = MiniTest::Mock.new
|
||||
mock.expect(:run_command, nil)
|
||||
mock.expect(:error?, true)
|
||||
mock.expect(:stderr, 'This would be an error message')
|
||||
|
||||
cmd = mock
|
||||
cmd.stubs(:run_command)
|
||||
cmd.stubs(:error?).returns(true)
|
||||
cmd.stubs(:stdout)
|
||||
cmd.stubs(:stderr)
|
||||
|
||||
subject.expects(:habitat_auth_token).returns('my_token')
|
||||
Mixlib::ShellOut.expects(:new).with("hab pkg upload my_hart", env: env).returns(cmd)
|
||||
proc { subject.send(:upload_hart, 'my_hart') }.must_raise SystemExit
|
||||
Mixlib::ShellOut.stub(:new, mock) do
|
||||
assert_raises(SystemExit) { @hab_profile.send(:verify_habitat_setup, {}) }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
mock.verify
|
||||
end
|
||||
|
||||
describe '#habitat_cli_config' do
|
||||
it 'returns an empty hash if the CLI config does not exist' do
|
||||
File.expects(:exist?).with(File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')).returns(false)
|
||||
subject.send(:habitat_cli_config).must_equal({})
|
||||
end
|
||||
def test_verify_habitat_setup_raises_if_not_habitat_origin
|
||||
mock = MiniTest::Mock.new
|
||||
mock.expect(:run_command, nil)
|
||||
mock.expect(:error?, false)
|
||||
|
||||
it 'returns parsed TOML from the hab config file' do
|
||||
config_file = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
|
||||
File.expects(:exist?).with(config_file).returns(true)
|
||||
Tomlrb.expects(:load_file).with(config_file).returns(foo: 1)
|
||||
subject.send(:habitat_cli_config).must_equal(foo: 1)
|
||||
Mixlib::ShellOut.stub(:new, mock) do
|
||||
assert_raises(SystemExit) { @hab_profile.send(:verify_habitat_setup, {}) }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
mock.verify
|
||||
end
|
||||
|
||||
# TODO: Figure out how to stub system()
|
||||
# def test_build_hart
|
||||
# end
|
||||
|
||||
def test_upload_hart_raises_if_hab_pkg_upload_fails
|
||||
mock = MiniTest::Mock.new
|
||||
mock.expect(:run_command, nil)
|
||||
mock.expect(:error?, true)
|
||||
mock.expect(:stdout, 'This would contain output from `hab`')
|
||||
mock.expect(:stderr, 'This would be an error message')
|
||||
|
||||
Mixlib::ShellOut.stub(:new, mock) do
|
||||
assert_raises(SystemExit) { @hab_profile.send(:upload_hart, @fake_hart_file, {}) }
|
||||
# TODO: Figure out how to capture and validate `Inspec::Log.error`
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue