mirror of
https://github.com/inspec/inspec
synced 2025-02-16 22:18:38 +00:00
CFINSPEC-13: Adds the ability to install and load the gem depedencies specified in the metadata file of profile/resource pack.
Signed-off-by: Vasu1105 <vasundhara.jagdale@chef.io>
This commit is contained in:
parent
c5ac267ab4
commit
e3ef2cb912
15 changed files with 349 additions and 0 deletions
|
@ -28,3 +28,6 @@ require "inspec/base_cli"
|
|||
require "inspec/fetcher"
|
||||
require "inspec/source_reader"
|
||||
require "inspec/resource"
|
||||
|
||||
require "inspec/dependency_loader"
|
||||
require "inspec/dependency_installer"
|
||||
|
|
|
@ -201,6 +201,8 @@ module Inspec
|
|||
long_desc: "Maximum seconds to allow commands to run during execution. A timed out command is considered an error."
|
||||
option :reporter_include_source, type: :boolean, default: false,
|
||||
desc: "Include full source code of controls in the CLI report"
|
||||
option :auto_install_gems, type: :boolean, default: false,
|
||||
desc: "Auto install gem dependencies of profile/resource pack defined in metadata file."
|
||||
end
|
||||
|
||||
def self.help(*args)
|
||||
|
|
69
lib/inspec/dependency_installer.rb
Normal file
69
lib/inspec/dependency_installer.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# This class will install the gem depedencies for profiles etc.
|
||||
# The basic things which is required to install dependencies is the gem path where gem needs to be installed
|
||||
# and the list of gems needs to be installed.
|
||||
require "rubygems/remote_fetcher"
|
||||
require "forwardable" unless defined?(Forwardable)
|
||||
|
||||
module Inspec
|
||||
class DependencyInstaller
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :gem_path, :requested_gems, :dependency_loader
|
||||
|
||||
def_delegator :dependency_loader, :inspec_gem_path
|
||||
|
||||
def initialize(gem_path = nil, requested_gems = [])
|
||||
@dependency_loader = Inspec::DependencyLoader.new
|
||||
@gem_path = gem_path || inspec_gem_path
|
||||
@requested_gems = requested_gems
|
||||
end
|
||||
|
||||
def install
|
||||
requested_gems.each do |requested_gem|
|
||||
version = requested_gem[:version].nil? ? ">= 0" : requested_gem[:version]
|
||||
install_from_remote_gems(requested_gem[:name], { version: version })
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def install_from_remote_gems(requested_gem_name, opts)
|
||||
gem_dependency = Gem::Dependency.new(requested_gem_name, opts[:version] || "> 0")
|
||||
|
||||
# BestSet is rubygems.org API + indexing, APISet is for custom sources
|
||||
sources = if opts[:source]
|
||||
Gem::Resolver::APISet.new(URI.join(opts[:source] + "/api/v1/dependencies"))
|
||||
else
|
||||
Gem::Resolver::BestSet.new
|
||||
end
|
||||
|
||||
begin
|
||||
install_gem_to_gems_dir(gem_dependency, [sources], opts[:update_mode])
|
||||
rescue Gem::RemoteFetcher::FetchError => gem_ex
|
||||
ex = Inspec::GemDependencyInstallError.new(gem_ex.message)
|
||||
ex.gem_name = requested_gem_name
|
||||
raise ex
|
||||
end
|
||||
end
|
||||
|
||||
def install_gem_to_gems_dir(gem_dependency, extra_request_sets = [], update_mode = false)
|
||||
# Solve the dependency (that is, find a way to install the new gem and anything it needs)
|
||||
request_set = Gem::RequestSet.new(gem_dependency)
|
||||
|
||||
begin
|
||||
solution = request_set.resolve
|
||||
rescue Gem::UnsatisfiableDependencyError => gem_ex
|
||||
ex = Inspec::GemDependencyInstallError.new(gem_ex.message)
|
||||
ex.gem_name = gem_dependency.name
|
||||
raise ex
|
||||
end
|
||||
|
||||
# OK, perform the installation.
|
||||
# Ignore deps here, because any needed deps should already be baked into gem_dependency
|
||||
request_set.install_into(gem_path, true, ignore_dependencies: true, document: [])
|
||||
|
||||
# Locate the GemVersion for the new dependency and return it
|
||||
solution.detect { |g| g.name == gem_dependency.name }.version
|
||||
end
|
||||
end
|
||||
end
|
90
lib/inspec/dependency_loader.rb
Normal file
90
lib/inspec/dependency_loader.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
# This class will load the gem depedencies for profiles etc.
|
||||
# The basic things which is required to load gem dependencies is the path from which gems needs to be loaded
|
||||
# and the list of gems needs to be loaded.
|
||||
|
||||
module Inspec
|
||||
class DependencyLoader
|
||||
attr_accessor :gem_path, :gem_list
|
||||
|
||||
# initializes the dependency_loader
|
||||
def initialize(gem_path = nil, gem_list = [])
|
||||
@gem_path = gem_path || inspec_gem_path
|
||||
@gem_list = gem_list
|
||||
end
|
||||
|
||||
def load
|
||||
Gem.path << gem_path
|
||||
Gem.refresh
|
||||
|
||||
gem_list.each do |gem_data|
|
||||
version = gem_data[:version].nil? ? ">= 0" : gem_data[:version]
|
||||
activate_gem_dependency(gem_data[:name], version)
|
||||
end
|
||||
end
|
||||
|
||||
def inspec_gem_path
|
||||
self.class.inspec_gem_path
|
||||
end
|
||||
|
||||
def self.inspec_gem_path
|
||||
require "rbconfig" unless defined?(RbConfig)
|
||||
ruby_abi_version = RbConfig::CONFIG["ruby_version"]
|
||||
# TODO: why are we installing under the api directory for plugins?
|
||||
base_dir = Inspec.config_dir
|
||||
base_dir = File.realpath base_dir if File.exist? base_dir
|
||||
File.join(base_dir, "gems", ruby_abi_version)
|
||||
end
|
||||
|
||||
# Lists all gems found in the inspec_gem_path.
|
||||
# @return [Array[Gem::Specification]] Specs of all gems found.
|
||||
def list_managed_gems
|
||||
Dir.glob(File.join(gem_path, "specifications", "*.gemspec")).map { |p| Gem::Specification.load(p) }
|
||||
end
|
||||
|
||||
def list_installed_gems
|
||||
list_managed_gems
|
||||
end
|
||||
|
||||
def gem_installed?(name)
|
||||
list_installed_gems.detect { |spec| spec.name == name }
|
||||
end
|
||||
|
||||
def gem_version_installed?(name, version)
|
||||
if version.nil?
|
||||
gem_installed?(name)
|
||||
else
|
||||
list_installed_gems.detect { |spec| spec.name == name && spec.version == Gem::Version.new(version) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activate_gem_dependency(name, version_constraint = "> 0")
|
||||
gem_deps = [Gem::Dependency.new(name.to_s, version_constraint)]
|
||||
managed_gem_set = Gem::Resolver::VendorSet.new
|
||||
|
||||
# TODO: Next two lines merge our managed gems with the other gems available
|
||||
# in our "local universe" - which may be the system, or it could be in a Bundler microcosm,
|
||||
# or rbenv, etc. Do we want to merge that, though?
|
||||
distrib_gem_set = Gem::Resolver::CurrentSet.new
|
||||
installed_gem_set = Gem::Resolver.compose_sets(managed_gem_set, distrib_gem_set)
|
||||
|
||||
# So, given what we need, and what we have available, what activations are needed?
|
||||
resolver = Gem::Resolver.new(gem_deps, installed_gem_set)
|
||||
|
||||
begin
|
||||
solution = resolver.resolve
|
||||
rescue Gem::UnsatisfiableDependencyError => gem_ex
|
||||
# If you broke your install, or downgraded to a plugin with a bad gemspec, you could get here.
|
||||
ex = Inspec::GemDependencyLoadError.new(gem_ex.message)
|
||||
raise ex
|
||||
end
|
||||
solution.each do |activation_request|
|
||||
next if activation_request.full_spec.activated?
|
||||
|
||||
activation_request.full_spec.activate
|
||||
# TODO: If we are under Bundler, inform it that we loaded a gem
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,4 +15,11 @@ module Inspec
|
|||
class ConfigError::Invalid < ConfigError; end
|
||||
|
||||
class UserInteractionRequired < Error; end
|
||||
|
||||
class GemDependencyLoadError < Error; end
|
||||
|
||||
class GemDependencyInstallError < Error
|
||||
attr_accessor :gem_name
|
||||
attr_accessor :version
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,6 +51,10 @@ module Inspec
|
|||
params[:depends] || []
|
||||
end
|
||||
|
||||
def gem_dependencies
|
||||
params[:required_gems] || []
|
||||
end
|
||||
|
||||
def supports(sth, version = nil)
|
||||
# Ignore supports with metadata.rb. This file is legacy and the way it
|
||||
# it handles `supports` deprecated. A deprecation warning will be printed
|
||||
|
|
|
@ -13,6 +13,8 @@ require "inspec/dependencies/cache"
|
|||
require "inspec/dependencies/lockfile"
|
||||
require "inspec/dependencies/dependency_set"
|
||||
require "inspec/utils/json_profile_summary"
|
||||
require "inspec/dependency_loader"
|
||||
require "inspec/dependency_installer"
|
||||
|
||||
module Inspec
|
||||
class Profile
|
||||
|
@ -378,6 +380,53 @@ module Inspec
|
|||
@runner_context
|
||||
end
|
||||
|
||||
# Loads the required gems specified in the Profile's metadata file from default inspec gems path i.e. ~/.inspec/gems
|
||||
# else installs and loads them.
|
||||
def load_gem_dependencies
|
||||
gem_dependencies = metadata. gem_dependencies
|
||||
gem_dependencies.each do |gem_data|
|
||||
dependency_loader = DependencyLoader.new
|
||||
gem_version = gem_data[:version].split[1] unless gem_data[:version].nil?
|
||||
if dependency_loader.gem_version_installed?(gem_data[:name], gem_version) || dependency_loader.gem_installed?(gem_data[:name])
|
||||
load_gem_dependency(gem_data)
|
||||
else
|
||||
if Inspec::Config.cached[:auto_install_gems]
|
||||
install_gem_dependency(gem_data)
|
||||
load_gem_dependency(gem_data)
|
||||
else
|
||||
ui = Inspec::UI.new
|
||||
gem_dependencies.each { |gem_dependency| ui.list_item("#{gem_dependency[:name]} #{gem_dependency[:version]}") }
|
||||
choice = ui.prompt.select("Would you like to install profile gem dependencies listed above?", %w{Yes No})
|
||||
if choice == "Yes"
|
||||
Inspec::Config.cached[:auto_install_gems] = true
|
||||
load_gem_dependencies
|
||||
else
|
||||
ui.error "Unable to resolve above listed profile gem dependencies."
|
||||
Inspec::UI.new.exit(:gem_dependency_load_error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Requires gem_data as argument.
|
||||
# gem_dta example: { name: "gem_name", version: "0.0.1"}
|
||||
def load_gem_dependency(gem_data)
|
||||
dependency_loader = DependencyLoader.new(nil, [gem_data])
|
||||
dependency_loader.load
|
||||
rescue Inspec::GemDependencyLoadError => e
|
||||
raise e
|
||||
end
|
||||
|
||||
# Requires gem_data as argument.
|
||||
# gem_dta example: { name: "gem_name", version: "0.0.1"}
|
||||
def install_gem_dependency(gem_data)
|
||||
gem_dependency = DependencyInstaller.new(nil, [gem_data])
|
||||
gem_dependency.install
|
||||
rescue Inspec::GemDependencyInstallError => e
|
||||
raise e
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Inspec::Profile<#{name}>"
|
||||
end
|
||||
|
|
|
@ -105,6 +105,7 @@ module Inspec
|
|||
|
||||
write_lockfile(profile) if @create_lockfile
|
||||
profile.locked_dependencies
|
||||
profile.load_gem_dependencies unless profile.metadata.gem_dependencies.empty?
|
||||
profile_context = profile.load_libraries
|
||||
|
||||
profile_context.dependencies.list.values.each do |requirement|
|
||||
|
|
|
@ -30,6 +30,7 @@ module Inspec
|
|||
EXIT_USAGE_ERROR = 1
|
||||
EXIT_PLUGIN_ERROR = 2
|
||||
EXIT_FATAL_DEPRECATION = 3
|
||||
EXIT_GEM_DEPENDENCY_LOAD_ERROR = 4
|
||||
EXIT_LICENSE_NOT_ACCEPTED = 172
|
||||
EXIT_FAILED_TESTS = 100
|
||||
EXIT_SKIPPED_TESTS = 101
|
||||
|
|
3
test/fixtures/profiles/profile-with-gem-dependency/README.md
vendored
Normal file
3
test/fixtures/profiles/profile-with-gem-dependency/README.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Example InSpec Profile
|
||||
|
||||
This example shows the implementation of an InSpec profile.
|
11
test/fixtures/profiles/profile-with-gem-dependency/controls/example.rb
vendored
Normal file
11
test/fixtures/profiles/profile-with-gem-dependency/controls/example.rb
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "money"
|
||||
|
||||
control "tmp-1.0" do
|
||||
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
||||
m = Money.from_cents(1000, "USD")
|
||||
cents = m.cents
|
||||
|
||||
describe cents do
|
||||
it { should eq 1000 }
|
||||
end
|
||||
end
|
14
test/fixtures/profiles/profile-with-gem-dependency/inspec.yml
vendored
Normal file
14
test/fixtures/profiles/profile-with-gem-dependency/inspec.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: profile-with-gem-dependency
|
||||
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
|
||||
required_gems:
|
||||
- name: money
|
||||
version: ">= 6.15.0"
|
||||
- name: monetize
|
0
test/fixtures/profiles/profile-with-gem-dependency/libraries/.gitkeep
vendored
Normal file
0
test/fixtures/profiles/profile-with-gem-dependency/libraries/.gitkeep
vendored
Normal file
34
test/functional/profile_gem_dependency_test.rb
Normal file
34
test/functional/profile_gem_dependency_test.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
require "functional/helper"
|
||||
|
||||
describe "profile with gem dependencies" do
|
||||
include FunctionalHelper
|
||||
let(:gem_dependency_profiles_path) { File.join(profile_path, "profile-with-gem-dependency") }
|
||||
let(:config_dir_path) { File.expand_path "test/fixtures/config_dirs" }
|
||||
|
||||
def reset_globals
|
||||
ENV["HOME"] = Dir.home
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
reset_globals
|
||||
ENV["HOME"] = File.join(config_dir_path, "profile_gems")
|
||||
end
|
||||
|
||||
after do
|
||||
reset_globals
|
||||
|
||||
if config_dir_path
|
||||
Dir.glob(File.join(config_dir_path, "profile_gems")).each do |path|
|
||||
next if path.end_with? ".gitkeep"
|
||||
|
||||
FileUtils.rm_rf(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "installs the gem dependencies and load them if --auto-install-gems is provided." do
|
||||
out = inspec_with_env("exec #{gem_dependency_profiles_path} --no-create-lockfile --auto-install-gems")
|
||||
_(out.stderr).must_equal ""
|
||||
assert_exit_code 0, out
|
||||
end
|
||||
end
|
61
test/unit/dependency_loader_test.rb
Normal file
61
test/unit/dependency_loader_test.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
require "helper"
|
||||
require "inspec/dependency_loader"
|
||||
require "byebug"
|
||||
|
||||
describe "dependency_loader" do
|
||||
let(:config_dir_path) { File.expand_path "test/fixtures/config_dirs" }
|
||||
let(:gem_list) { [{ name: "inspec-test-fixture", version: "0.1.0" }] }
|
||||
|
||||
def reset_globals
|
||||
ENV["HOME"] = Dir.home
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
reset_globals
|
||||
ENV["HOME"] = File.join(config_dir_path, "test-fixture-1-float/gems/2.7.0")
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
reset_globals
|
||||
end
|
||||
|
||||
let(:gem_path) { [ENV["HOME"]] }
|
||||
let(:dependency_loader) { Inspec::DependencyLoader.new(gem_path, gem_list) }
|
||||
|
||||
describe "load" do
|
||||
it "loads the gem dependency if already installed on the given gem path." do
|
||||
result = dependency_loader.load
|
||||
_(result).must_equal gem_list
|
||||
end
|
||||
|
||||
it "raises error if the gem dependency not exist on the given gem path." do
|
||||
dependency_loader.gem_list = [{ name: "test_gem", version: "0.0.1" }]
|
||||
err = _ { dependency_loader.load }.must_raise Inspec::GemDependencyLoadError
|
||||
_(err.message).must_equal "Unable to resolve dependency: user requested \'test_gem (= 0.0.1)\'"
|
||||
end
|
||||
end
|
||||
|
||||
describe "gem_installed?" do
|
||||
it "returns the list of gems installed if gem already installed" do
|
||||
result = dependency_loader.gem_installed?("inspec-test-fixture")
|
||||
_(result).wont_be_nil
|
||||
end
|
||||
|
||||
it "returns nil if specified gem is not already installed." do
|
||||
result = dependency_loader.gem_installed?("test_gem")
|
||||
_(result).must_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "gem_version_installed?" do
|
||||
it "returns the list of gems installed if gem with specified version is already installed" do
|
||||
result = dependency_loader.gem_version_installed?("inspec-test-fixture", "0.1.0")
|
||||
_(result).wont_be_nil
|
||||
end
|
||||
|
||||
it "returns nil if gem with specified version is not already installed." do
|
||||
result = dependency_loader.gem_version_installed?("test_gem", "0.0.1")
|
||||
_(result).must_be_nil
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue