inspec 0.31.0 → 0.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -2
- data/Gemfile +6 -2
- data/MAINTAINERS.md +3 -1
- data/MAINTAINERS.toml +1 -1
- data/README.md +20 -1
- data/Rakefile +8 -0
- data/docs/cli.rst +18 -2
- data/docs/resources.rst +55 -3
- data/inspec.gemspec +2 -2
- data/lib/bundles/inspec-supermarket/api.rb +1 -0
- data/lib/fetchers/local.rb +12 -1
- data/lib/fetchers/tar.rb +4 -0
- data/lib/fetchers/url.rb +4 -0
- data/lib/inspec/base_cli.rb +17 -0
- data/lib/inspec/cli.rb +33 -12
- data/lib/inspec/dependencies/dependency_set.rb +50 -5
- data/lib/inspec/dependencies/lockfile.rb +94 -0
- data/lib/inspec/dependencies/requirement.rb +93 -53
- data/lib/inspec/dependencies/resolver.rb +53 -170
- data/lib/inspec/dependencies/vendor_index.rb +11 -4
- data/lib/inspec/dsl.rb +23 -15
- data/lib/inspec/errors.rb +1 -7
- data/lib/inspec/log.rb +2 -25
- data/lib/inspec/profile.rb +68 -28
- data/lib/inspec/profile_context.rb +28 -5
- data/lib/inspec/rspec_json_formatter.rb +48 -25
- data/lib/inspec/rule.rb +7 -0
- data/lib/inspec/runner.rb +26 -15
- data/lib/inspec/runner_rspec.rb +2 -6
- data/lib/inspec/shell.rb +35 -26
- data/lib/inspec/version.rb +2 -1
- data/lib/resources/host.rb +13 -6
- data/lib/resources/iis_site.rb +1 -0
- data/lib/resources/os.rb +1 -1
- data/lib/resources/package.rb +22 -6
- data/lib/resources/port.rb +1 -11
- data/lib/resources/service.rb +9 -0
- data/lib/resources/user.rb +8 -8
- metadata +14 -7
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'inspec/dependencies/vendor_index'
|
3
|
+
require 'inspec/dependencies/requirement'
|
3
4
|
require 'inspec/dependencies/resolver'
|
4
5
|
|
5
6
|
module Inspec
|
@@ -10,17 +11,60 @@ module Inspec
|
|
10
11
|
# VendorIndex and the Resolver.
|
11
12
|
#
|
12
13
|
class DependencySet
|
13
|
-
|
14
|
+
#
|
15
|
+
# Return a dependency set given a lockfile.
|
16
|
+
#
|
17
|
+
# @param lockfile [Inspec::Lockfile] A lockfile to generate the dependency set from
|
18
|
+
# @param cwd [String] Current working directory for relative path includes
|
19
|
+
# @param vendor_path [String] Path to the vendor directory
|
20
|
+
#
|
21
|
+
def self.from_lockfile(lockfile, cwd, vendor_path)
|
22
|
+
vendor_index = VendorIndex.new(vendor_path)
|
23
|
+
dep_tree = lockfile.deps.map do |dep|
|
24
|
+
Inspec::Requirement.from_lock_entry(dep, cwd, vendor_index)
|
25
|
+
end
|
14
26
|
|
27
|
+
dep_list = flatten_dep_tree(dep_tree)
|
28
|
+
new(cwd, vendor_path, dep_list)
|
29
|
+
end
|
30
|
+
|
31
|
+
# This is experimental code to test the working of the
|
32
|
+
# dependency loader - perform a proper dependency related search
|
33
|
+
# in the future.
|
34
|
+
#
|
35
|
+
# Flatten tree because that is all we know how to deal with for
|
36
|
+
# right now. Last dep seen for a given name wins right now.
|
37
|
+
def self.flatten_dep_tree(dep_tree)
|
38
|
+
dep_list = {}
|
39
|
+
dep_tree.each do |d|
|
40
|
+
dep_list[d.name] = d
|
41
|
+
dep_list.merge!(flatten_dep_tree(d.dependencies))
|
42
|
+
end
|
43
|
+
dep_list
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :vendor_path
|
47
|
+
attr_writer :dep_list
|
15
48
|
# initialize
|
16
49
|
#
|
17
50
|
# @param cwd [String] current working directory for relative path includes
|
18
51
|
# @param vendor_path [String] path which contains vendored dependencies
|
19
52
|
# @return [dependencies] this
|
20
|
-
def initialize(cwd, vendor_path)
|
53
|
+
def initialize(cwd, vendor_path, dep_list = nil)
|
21
54
|
@cwd = cwd
|
22
|
-
@vendor_path = vendor_path
|
23
|
-
@
|
55
|
+
@vendor_path = vendor_path
|
56
|
+
@dep_list = dep_list
|
57
|
+
end
|
58
|
+
|
59
|
+
def list
|
60
|
+
@dep_list
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_array
|
64
|
+
return [] if @dep_list.nil?
|
65
|
+
@dep_list.map do |_k, v|
|
66
|
+
v.to_hash
|
67
|
+
end.compact
|
24
68
|
end
|
25
69
|
|
26
70
|
#
|
@@ -29,10 +73,11 @@ module Inspec
|
|
29
73
|
#
|
30
74
|
# @param dependencies [Gem::Dependency] list of dependencies
|
31
75
|
# @return [nil]
|
76
|
+
#
|
32
77
|
def vendor(dependencies)
|
33
78
|
return nil if dependencies.nil? || dependencies.empty?
|
34
79
|
@vendor_index ||= VendorIndex.new(@vendor_path)
|
35
|
-
@
|
80
|
+
@dep_list = Resolver.resolve(dependencies, @vendor_index, @cwd)
|
36
81
|
end
|
37
82
|
end
|
38
83
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Inspec
|
5
|
+
class Lockfile
|
6
|
+
# When we finalize this feature, we should set these to 1
|
7
|
+
MINIMUM_SUPPORTED_VERSION = 0
|
8
|
+
CURRENT_LOCKFILE_VERSION = 0
|
9
|
+
|
10
|
+
def self.from_dependency_set(dep_set)
|
11
|
+
lockfile_content = {
|
12
|
+
'lockfile_version' => CURRENT_LOCKFILE_VERSION,
|
13
|
+
'depends' => dep_set.to_array,
|
14
|
+
}
|
15
|
+
new(lockfile_content)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_file(path)
|
19
|
+
parsed_content = YAML.load(File.read(path))
|
20
|
+
version = parsed_content['lockfile_version']
|
21
|
+
fail "No lockfile_version set in #{path}!" if version.nil?
|
22
|
+
validate_lockfile_version!(version.to_i)
|
23
|
+
new(parsed_content)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.validate_lockfile_version!(version)
|
27
|
+
if version < MINIMUM_SUPPORTED_VERSION
|
28
|
+
fail <<EOF
|
29
|
+
This lockfile specifies a lockfile_version of #{version} which is
|
30
|
+
lower than the minimum supported version #{MINIMUM_SUPPORTED_VERSION}.
|
31
|
+
|
32
|
+
Please create a new lockfile for this project by running:
|
33
|
+
|
34
|
+
inspec vendor
|
35
|
+
EOF
|
36
|
+
elsif version == 0
|
37
|
+
# Remove this case once this feature stablizes
|
38
|
+
$stderr.puts <<EOF
|
39
|
+
WARNING: This is a version 0 lockfile. Thank you for trying the
|
40
|
+
experimental dependency management feature. Please be aware you may
|
41
|
+
need to regenerate this lockfile in future versions as the feature is
|
42
|
+
currently in development.
|
43
|
+
EOF
|
44
|
+
elsif version > CURRENT_LOCKFILE_VERSION
|
45
|
+
fail <<EOF
|
46
|
+
This lockfile claims to be version #{version} which is greater than
|
47
|
+
the most recent lockfile version(#{CURRENT_LOCKFILE_VERSION}).
|
48
|
+
|
49
|
+
This may happen if you are using an older version of inspec than was
|
50
|
+
used to create the lockfile.
|
51
|
+
EOF
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :version, :deps
|
56
|
+
def initialize(lockfile_content_hash)
|
57
|
+
version = lockfile_content_hash['lockfile_version']
|
58
|
+
@version = version.to_i
|
59
|
+
parse_content_hash(lockfile_content_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_yaml
|
63
|
+
{
|
64
|
+
'lockfile_version' => CURRENT_LOCKFILE_VERSION,
|
65
|
+
'depends' => @deps,
|
66
|
+
}.to_yaml
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Refactor this to be "version-wise" - i.e. make one dispatch
|
72
|
+
# function for each version so that even if it duplicates code,
|
73
|
+
# it can describe the part of the code that it expects to be
|
74
|
+
# different. Then that dispatch routine can call more well
|
75
|
+
# defined methods like "parse_v0_dependencies" or
|
76
|
+
# "parse_flat_dependencies" or what not as things generally
|
77
|
+
# develop. It does help people easily set breakpoints/track
|
78
|
+
# different entry points of the API.
|
79
|
+
def parse_content_hash(lockfile_content_hash)
|
80
|
+
case version
|
81
|
+
when 0
|
82
|
+
parse_content_hash_0(lockfile_content_hash)
|
83
|
+
else
|
84
|
+
# If we've gotten here, there is likely a mistake in the
|
85
|
+
# lockfile version validation in the constructor.
|
86
|
+
fail "No lockfile parser for version #{version}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def parse_content_hash_0(lockfile_content_hash)
|
91
|
+
@deps = lockfile_content_hash['depends']
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,87 +1,134 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'inspec/fetcher'
|
3
|
+
require 'digest'
|
3
4
|
|
4
5
|
module Inspec
|
5
6
|
#
|
6
7
|
# Inspec::Requirement represents a given profile dependency, where
|
7
8
|
# appropriate we delegate to Inspec::Profile directly.
|
8
9
|
#
|
9
|
-
class Requirement
|
10
|
+
class Requirement # rubocop:disable Metrics/ClassLength
|
10
11
|
attr_reader :name, :dep, :cwd, :opts
|
12
|
+
attr_writer :dependencies
|
13
|
+
|
14
|
+
def self.from_metadata(dep, vendor_index, opts)
|
15
|
+
fail 'Cannot load empty dependency.' if dep.nil? || dep.empty?
|
16
|
+
name = dep[:name] || fail('You must provide a name for all dependencies')
|
17
|
+
version = dep[:version]
|
18
|
+
new(name, version, vendor_index, opts[:cwd], opts.merge(dep))
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_lock_entry(entry, cwd, vendor_index)
|
22
|
+
req = new(entry['name'],
|
23
|
+
entry['version_constraints'],
|
24
|
+
vendor_index,
|
25
|
+
cwd, { url: entry['resolved_source'] })
|
26
|
+
|
27
|
+
locked_deps = []
|
28
|
+
Array(entry['dependencies']).each do |dep_entry|
|
29
|
+
locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, cwd, vendor_index)
|
30
|
+
end
|
31
|
+
|
32
|
+
req.lock_deps(locked_deps)
|
33
|
+
req
|
34
|
+
end
|
11
35
|
|
12
36
|
def initialize(name, version_constraints, vendor_index, cwd, opts)
|
13
37
|
@name = name
|
14
|
-
@
|
15
|
-
|
16
|
-
:runtime)
|
38
|
+
@version_requirement = Gem::Requirement.new(Array(version_constraints))
|
39
|
+
@dep = Gem::Dependency.new(name, @version_requirement, :runtime)
|
17
40
|
@vendor_index = vendor_index
|
18
41
|
@opts = opts
|
19
42
|
@cwd = cwd
|
20
43
|
end
|
21
44
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
45
|
+
def required_version
|
46
|
+
@version_requirement
|
47
|
+
end
|
48
|
+
|
49
|
+
def source_version
|
50
|
+
profile.metadata.params[:version]
|
51
|
+
end
|
52
|
+
|
53
|
+
def source_satisfies_spec?
|
54
|
+
name = profile.metadata.params[:name]
|
55
|
+
version = profile.metadata.params[:version]
|
56
|
+
@dep.match?(name, version)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_hash
|
60
|
+
h = {
|
61
|
+
'name' => name,
|
62
|
+
'resolved_source' => source_url,
|
63
|
+
'version_constraints' => @version_requirement.to_s,
|
64
|
+
}
|
65
|
+
|
66
|
+
if !dependencies.empty?
|
67
|
+
h['dependencies'] = dependencies.map(&:to_hash)
|
68
|
+
end
|
69
|
+
|
70
|
+
if is_vendored?
|
71
|
+
h['content_hash'] = content_hash
|
72
|
+
end
|
73
|
+
h
|
74
|
+
end
|
75
|
+
|
76
|
+
def lock_deps(dep_array)
|
77
|
+
@dependencies = dep_array
|
78
|
+
end
|
79
|
+
|
80
|
+
def is_vendored?
|
81
|
+
@vendor_index.exists?(@name, source_url)
|
82
|
+
end
|
83
|
+
|
84
|
+
def content_hash
|
85
|
+
@content_hash ||= begin
|
86
|
+
archive_path = @vendor_index.archive_entry_for(@name, source_url)
|
87
|
+
fail "No vendored archive path for #{self}, cannot take content hash" if archive_path.nil?
|
88
|
+
Digest::SHA256.hexdigest File.read(archive_path)
|
89
|
+
end
|
25
90
|
end
|
26
91
|
|
27
92
|
def source_url
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@opts[:url]
|
93
|
+
if opts[:path]
|
94
|
+
"file://#{File.expand_path(opts[:path], @cwd)}"
|
95
|
+
elsif opts[:url]
|
96
|
+
opts[:url]
|
33
97
|
end
|
34
98
|
end
|
35
99
|
|
36
100
|
def local_path
|
37
|
-
@local_path ||=
|
38
|
-
|
39
|
-
File.expand_path(opts[:path], @cwd)
|
101
|
+
@local_path ||= if fetcher.class == Fetchers::Local
|
102
|
+
File.expand_path(fetcher.target, @cwd)
|
40
103
|
else
|
41
104
|
@vendor_index.prefered_entry_for(@name, source_url)
|
42
105
|
end
|
43
106
|
end
|
44
107
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
-
:url
|
50
|
-
else
|
51
|
-
fail "Cannot determine source type from #{opts}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def fetcher_class
|
56
|
-
@fetcher_class ||= case source_type
|
57
|
-
when :local_path
|
58
|
-
Fetchers::Local
|
59
|
-
when :url
|
60
|
-
Fetchers::Url
|
61
|
-
else
|
62
|
-
fail "No known fetcher for dependency #{name} with source_type #{source_type}"
|
63
|
-
end
|
64
|
-
|
65
|
-
fail "No fetcher for #{name} (options: #{opts})" if @fetcher_class.nil?
|
66
|
-
@fetcher_class
|
108
|
+
def fetcher
|
109
|
+
@fetcher ||= Inspec::Fetcher.resolve(source_url)
|
110
|
+
fail "No fetcher for #{name} (options: #{opts})" if @fetcher.nil?
|
111
|
+
@fetcher
|
67
112
|
end
|
68
113
|
|
69
114
|
def pull
|
70
|
-
|
71
|
-
|
115
|
+
# TODO(ssd): Dispatch on the class here is gross. Seems like
|
116
|
+
# Fetcher is missing an API we want.
|
117
|
+
if fetcher.class == Fetchers::Local || @vendor_index.exists?(@name, source_url)
|
72
118
|
local_path
|
73
119
|
else
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
120
|
+
@vendor_index.add(@name, source_url, fetcher.archive_path)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def dependencies
|
125
|
+
@dependencies ||= profile.metadata.dependencies.map do |r|
|
126
|
+
Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd)
|
80
127
|
end
|
81
128
|
end
|
82
129
|
|
83
130
|
def to_s
|
84
|
-
dep
|
131
|
+
"#{dep} (#{source_url})"
|
85
132
|
end
|
86
133
|
|
87
134
|
def path
|
@@ -92,12 +139,5 @@ module Inspec
|
|
92
139
|
return nil if path.nil?
|
93
140
|
@profile ||= Inspec::Profile.for_target(path, {})
|
94
141
|
end
|
95
|
-
|
96
|
-
def self.from_metadata(dep, vendor_index, opts)
|
97
|
-
fail 'Cannot load empty dependency.' if dep.nil? || dep.empty?
|
98
|
-
name = dep[:name] || fail('You must provide a name for all dependencies')
|
99
|
-
version = dep[:version]
|
100
|
-
new(name, version, vendor_index, opts[:cwd], opts.merge(dep))
|
101
|
-
end
|
102
142
|
end
|
103
143
|
end
|
@@ -1,188 +1,71 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
# author:
|
3
|
-
|
4
|
-
require 'logger'
|
5
|
-
require 'molinillo'
|
2
|
+
# author: Steven Danna <steve@chef.io>
|
3
|
+
require 'inspec/log'
|
6
4
|
require 'inspec/errors'
|
7
|
-
require 'inspec/dependencies/requirement'
|
8
5
|
|
9
6
|
module Inspec
|
10
7
|
#
|
11
|
-
# Inspec::Resolver is
|
12
|
-
#
|
8
|
+
# Inspec::Resolver is a simple dependency resolver. Unlike Bundler
|
9
|
+
# or Berkshelf, it does not attempt to resolve each named dependency
|
10
|
+
# to a single version. Rather, it traverses down the dependency tree
|
11
|
+
# and:
|
12
|
+
#
|
13
|
+
# - Fetches the dependency from the source
|
14
|
+
# - Checks the presence of cycles, and
|
15
|
+
# - Checks that the specified dependency source satisfies the
|
16
|
+
# specified version constraint
|
17
|
+
#
|
18
|
+
# The full dependency tree is then available for the loader, which
|
19
|
+
# will provide the isolation necessary to support multiple versions
|
20
|
+
# of the same profile being used at runtime.
|
21
|
+
#
|
22
|
+
# Currently the fetching happens somewhat lazily depending on the
|
23
|
+
# implementation of the fetcher being used.
|
13
24
|
#
|
14
25
|
class Resolver
|
15
|
-
def self.resolve(
|
16
|
-
reqs =
|
17
|
-
req = Inspec::Requirement.from_metadata(
|
26
|
+
def self.resolve(dependencies, vendor_index, working_dir)
|
27
|
+
reqs = dependencies.map do |dep|
|
28
|
+
req = Inspec::Requirement.from_metadata(dep, vendor_index, cwd: working_dir)
|
18
29
|
req || fail("Cannot initialize dependency: #{req}")
|
19
30
|
end
|
20
|
-
|
21
|
-
new(vendor_index, opts.merge(cwd: cwd)).resolve(reqs)
|
22
|
-
end
|
23
|
-
|
24
|
-
def initialize(vendor_index, opts = {})
|
25
|
-
@logger = opts[:logger] || Logger.new(nil)
|
26
|
-
@debug_mode = false
|
27
|
-
|
28
|
-
@vendor_index = vendor_index
|
29
|
-
@cwd = opts[:cwd] || './'
|
30
|
-
@resolver = Molinillo::Resolver.new(self, self)
|
31
|
-
@search_cache = {}
|
32
|
-
end
|
33
|
-
|
34
|
-
# Resolve requirements.
|
35
|
-
#
|
36
|
-
# @param requirements [Array(Inspec::requirement)] Array of requirements
|
37
|
-
# @return [Array(String)] list of resolved dependency paths
|
38
|
-
def resolve(requirements)
|
39
|
-
requirements.each(&:pull)
|
40
|
-
@base_dep_graph = Molinillo::DependencyGraph.new
|
41
|
-
@dep_graph = @resolver.resolve(requirements, @base_dep_graph)
|
42
|
-
arr = @dep_graph.map(&:payload)
|
43
|
-
Hash[arr.map { |e| [e.name, e] }]
|
44
|
-
rescue Molinillo::VersionConflict => e
|
45
|
-
raise VersionConflict.new(e.conflicts.keys.uniq, e.message)
|
46
|
-
rescue Molinillo::CircularDependencyError => e
|
47
|
-
names = e.dependencies.sort_by(&:name).map { |d| "profile '#{d.name}'" }
|
48
|
-
raise CyclicDependencyError,
|
49
|
-
'Your profile has requirements that depend on each other, creating '\
|
50
|
-
"an infinite loop. Please remove #{names.count > 1 ? 'either ' : ''} "\
|
51
|
-
"#{names.join(' or ')} and try again."
|
52
|
-
end
|
53
|
-
|
54
|
-
# --------------------------------------------------------------------------
|
55
|
-
# SpecificationProvider
|
56
|
-
|
57
|
-
# Search for the specifications that match the given dependency.
|
58
|
-
# The specifications in the returned array will be considered in reverse
|
59
|
-
# order, so the latest version ought to be last.
|
60
|
-
# @note This method should be 'pure', i.e. the return value should depend
|
61
|
-
# only on the `dependency` parameter.
|
62
|
-
#
|
63
|
-
# @param [Object] dependency
|
64
|
-
# @return [Array<Object>] the specifications that satisfy the given
|
65
|
-
# `dependency`.
|
66
|
-
def search_for(dep)
|
67
|
-
unless dep.is_a?(Inspec::Requirement)
|
68
|
-
fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #search_for(dependency)'
|
69
|
-
end
|
70
|
-
@search_cache[dep] ||= uncached_search_for(dep)
|
71
|
-
end
|
72
|
-
|
73
|
-
def uncached_search_for(dep)
|
74
|
-
# pre-cached and specified dependencies
|
75
|
-
return [dep] unless dep.profile.nil?
|
76
|
-
|
77
|
-
results = @vendor_index.find(dep)
|
78
|
-
return [] unless results.any?
|
79
|
-
|
80
|
-
# TODO: load dep from vendor index
|
81
|
-
# vertex = @dep_graph.vertex_named(dep.name)
|
82
|
-
# locked_requirement = vertex.payload.requirement if vertex
|
83
|
-
fail NotImplementedError, "load dependency #{dep} from vendor index"
|
84
|
-
end
|
85
|
-
|
86
|
-
# Returns the dependencies of `specification`.
|
87
|
-
# @note This method should be 'pure', i.e. the return value should depend
|
88
|
-
# only on the `specification` parameter.
|
89
|
-
#
|
90
|
-
# @param [Object] specification
|
91
|
-
# @return [Array<Object>] the dependencies that are required by the given
|
92
|
-
# `specification`.
|
93
|
-
def dependencies_for(specification)
|
94
|
-
specification.profile.metadata.dependencies.map do |r|
|
95
|
-
Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# Determines whether the given `requirement` is satisfied by the given
|
100
|
-
# `spec`, in the context of the current `activated` dependency graph.
|
101
|
-
#
|
102
|
-
# @param [Object] requirement
|
103
|
-
# @param [DependencyGraph] activated the current dependency graph in the
|
104
|
-
# resolution process.
|
105
|
-
# @param [Object] spec
|
106
|
-
# @return [Boolean] whether `requirement` is satisfied by `spec` in the
|
107
|
-
# context of the current `activated` dependency graph.
|
108
|
-
def requirement_satisfied_by?(requirement, _activated, spec)
|
109
|
-
requirement.matches_spec?(spec) || spec.is_a?(Inspec::Profile)
|
31
|
+
new.resolve(reqs)
|
110
32
|
end
|
111
33
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
def name_for(dependency)
|
119
|
-
unless dependency.is_a?(Inspec::Requirement)
|
120
|
-
fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #name_for(dependency)'
|
34
|
+
def resolve(deps, top_level = true, seen_items = {}, path_string = '')
|
35
|
+
graph = {}
|
36
|
+
if top_level
|
37
|
+
Inspec::Log.debug("Starting traversal of dependencies #{deps.map(&:name)}")
|
38
|
+
else
|
39
|
+
Inspec::Log.debug("Traversing dependency tree of transitive dependency #{deps.map(&:name)}")
|
121
40
|
end
|
122
|
-
dependency.name
|
123
|
-
end
|
124
|
-
|
125
|
-
# @return [String] the name of the source of explicit dependencies, i.e.
|
126
|
-
# those passed to {Resolver#resolve} directly.
|
127
|
-
def name_for_explicit_dependency_source
|
128
|
-
'inspec.yml'
|
129
|
-
end
|
130
41
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
# amount_constrained(dependency), # TODO
|
155
|
-
conflicts[name] ? 0 : 1,
|
156
|
-
# activated.vertex_named(name).payload ? 0 : search_for(dependency).count, # TODO
|
157
|
-
]
|
42
|
+
deps.each do |dep|
|
43
|
+
path_string = if path_string.empty?
|
44
|
+
dep.name
|
45
|
+
else
|
46
|
+
path_string + " -> #{dep.name}"
|
47
|
+
end
|
48
|
+
|
49
|
+
if seen_items.key?(dep.source_url)
|
50
|
+
fail Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{path_string})"
|
51
|
+
else
|
52
|
+
seen_items[dep.source_url] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
if !dep.source_satisfies_spec?
|
56
|
+
fail Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.source_url} has a version #{dep.source_version} which doesn't match #{dep.required_version}"
|
57
|
+
end
|
58
|
+
|
59
|
+
Inspec::Log.debug("Adding #{dep.source_url}")
|
60
|
+
graph[dep.name] = dep
|
61
|
+
if !dep.dependencies.empty?
|
62
|
+
# Recursively resolve any transitive dependencies.
|
63
|
+
resolve(dep.dependencies, false, seen_items.dup, path_string)
|
64
|
+
end
|
158
65
|
end
|
159
|
-
end
|
160
|
-
|
161
|
-
# Returns whether this dependency, which has no possible matching
|
162
|
-
# specifications, can safely be ignored.
|
163
|
-
#
|
164
|
-
# @param [Object] dependency
|
165
|
-
# @return [Boolean] whether this dependency can safely be skipped.
|
166
|
-
def allow_missing?(_dependency)
|
167
|
-
# TODO
|
168
|
-
false
|
169
|
-
end
|
170
|
-
|
171
|
-
# --------------------------------------------------------------------------
|
172
|
-
# UI
|
173
|
-
|
174
|
-
include Molinillo::UI
|
175
|
-
|
176
|
-
# The {IO} object that should be used to print output. `STDOUT`, by default.
|
177
|
-
#
|
178
|
-
# @return [IO]
|
179
|
-
def output
|
180
|
-
self
|
181
|
-
end
|
182
66
|
|
183
|
-
|
184
|
-
|
67
|
+
Inspec::Log.debug('Dependency traversal complete.') if top_level
|
68
|
+
graph
|
185
69
|
end
|
186
|
-
alias puts print
|
187
70
|
end
|
188
71
|
end
|