inspec 0.31.0 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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