inspec 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- attr_reader :list, :vendor_path
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 || File.join(Dir.home, '.inspec', 'cache')
23
- @list = nil
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
- @list = Resolver.resolve(dependencies, @vendor_index, @cwd)
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
- @dep = Gem::Dependency.new(name,
15
- Gem::Requirement.new(Array(version_constraints)),
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 matches_spec?(spec)
23
- params = spec.profile.metadata.params
24
- @dep.match?(params[:name], params[:version])
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
- case source_type
29
- when :local_path
30
- "file://#{File.expand_path(opts[:path])}"
31
- when :url
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 ||= case source_type
38
- when :local_path
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 source_type
46
- @source_type ||= if @opts[:path]
47
- :local_path
48
- elsif opts[:url]
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
- case source_type
71
- when :local_path
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
- if @vendor_index.exists?(@name, source_url)
75
- local_path
76
- else
77
- archive = fetcher_class.download_archive(source_url)
78
- @vendor_index.add(@name, source_url, archive.path)
79
- end
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.to_s
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: Dominik Richter
3
- # author: Christoph Hartmann
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 responsible for recursively resolving all the
12
- # depenendencies for a given top-level dependency set.
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(requirements, vendor_index, cwd, opts = {})
16
- reqs = requirements.map do |req|
17
- req = Inspec::Requirement.from_metadata(req, vendor_index, cwd: cwd)
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
- # Returns the name for the given `dependency`.
113
- # @note This method should be 'pure', i.e. the return value should depend
114
- # only on the `dependency` parameter.
115
- #
116
- # @param [Object] dependency
117
- # @return [String] the name for the given `dependency`.
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
- # @return [String] the name of the source of 'locked' dependencies, i.e.
132
- # those passed to {Resolver#resolve} directly as the `base`
133
- def name_for_locking_dependency_source
134
- 'inspec.lock'
135
- end
136
-
137
- # Sort dependencies so that the ones that are easiest to resolve are first.
138
- # Easiest to resolve is (usually) defined by:
139
- # 1) Is this dependency already activated?
140
- # 2) How relaxed are the requirements?
141
- # 3) Are there any conflicts for this dependency?
142
- # 4) How many possibilities are there to satisfy this dependency?
143
- #
144
- # @param [Array<Object>] dependencies
145
- # @param [DependencyGraph] activated the current dependency graph in the
146
- # resolution process.
147
- # @param [{String => Array<Conflict>}] conflicts
148
- # @return [Array<Object>] a sorted copy of `dependencies`.
149
- def sort_dependencies(dependencies, activated, conflicts)
150
- dependencies.sort_by do |dependency|
151
- name = name_for(dependency)
152
- [
153
- activated.vertex_named(name).payload ? 0 : 1,
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
- def print(what = '')
184
- @logger.info(what)
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