berkshelf 3.0.0.beta6 → 3.0.0.beta7

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/features/berksfile.feature +2 -0
  3. data/features/commands/apply.feature +1 -1
  4. data/features/commands/contingent.feature +5 -3
  5. data/features/commands/install.feature +40 -40
  6. data/features/commands/list.feature +42 -20
  7. data/features/commands/outdated.feature +60 -16
  8. data/features/commands/show.feature +51 -8
  9. data/features/commands/update.feature +43 -15
  10. data/features/commands/upload.feature +4 -1
  11. data/features/commands/vendor.feature +27 -0
  12. data/features/json_formatter.feature +20 -8
  13. data/features/lockfile.feature +192 -71
  14. data/generator_files/CHANGELOG.md.erb +5 -0
  15. data/lib/berkshelf/berksfile.rb +166 -139
  16. data/lib/berkshelf/cli.rb +33 -30
  17. data/lib/berkshelf/cookbook_generator.rb +1 -0
  18. data/lib/berkshelf/dependency.rb +64 -14
  19. data/lib/berkshelf/downloader.rb +7 -10
  20. data/lib/berkshelf/errors.rb +59 -11
  21. data/lib/berkshelf/formatters/human_readable.rb +23 -36
  22. data/lib/berkshelf/formatters/json.rb +25 -29
  23. data/lib/berkshelf/installer.rb +111 -122
  24. data/lib/berkshelf/locations/git_location.rb +22 -9
  25. data/lib/berkshelf/locations/mercurial_location.rb +20 -5
  26. data/lib/berkshelf/locations/path_location.rb +22 -7
  27. data/lib/berkshelf/lockfile.rb +435 -203
  28. data/lib/berkshelf/resolver.rb +4 -2
  29. data/lib/berkshelf/source.rb +10 -1
  30. data/lib/berkshelf/version.rb +1 -1
  31. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +3 -4
  32. data/spec/fixtures/lockfiles/2.0.lock +17 -0
  33. data/spec/fixtures/lockfiles/blank.lock +0 -0
  34. data/spec/fixtures/lockfiles/default.lock +18 -10
  35. data/spec/fixtures/lockfiles/empty.lock +3 -0
  36. data/spec/unit/berkshelf/berksfile_spec.rb +31 -74
  37. data/spec/unit/berkshelf/cookbook_generator_spec.rb +4 -0
  38. data/spec/unit/berkshelf/installer_spec.rb +4 -7
  39. data/spec/unit/berkshelf/lockfile_parser_spec.rb +124 -0
  40. data/spec/unit/berkshelf/lockfile_spec.rb +140 -163
  41. metadata +11 -6
  42. data/features/licenses.feature +0 -79
  43. data/features/step_definitions/lockfile_steps.rb +0 -57
@@ -39,34 +39,30 @@ module Berkshelf
39
39
 
40
40
  # Add a Cookbook installation entry to delayed output
41
41
  #
42
- # @param [String] cookbook
43
- # @param [String] version
44
- # @option options [String] :api_source
45
- # the berkshelf-api source url
46
- # @option options [String] :location_path
47
- # the chef server url for a cookbook's location
48
- def install(cookbook, version, options = {})
49
- cookbooks[cookbook] ||= {}
50
- cookbooks[cookbook][:version] = version
51
-
52
- if options.has_key?(:api_source) && options.has_key?(:location_path)
53
- cookbooks[cookbook][:api_source] = options[:api_source] unless options[:api_source] == Berkshelf::Berksfile::DEFAULT_API_URL
54
- cookbooks[cookbook][:location_path] = options[:location_path] unless options[:api_source] == Berkshelf::Berksfile::DEFAULT_API_URL
42
+ # @param [Source] source
43
+ # the source the dependency is being downloaded from
44
+ # @param [RemoteCookbook] cookbook
45
+ # the cookbook to be downloaded
46
+ def install(source, cookbook)
47
+ cookbooks[cookbook.name] ||= {}
48
+ cookbooks[cookbook.name][:version] = cookbook.version
49
+
50
+ unless source.default?
51
+ cookbooks[cookbook.name][:api_source] = source.uri
52
+ cookbooks[cookbook.name][:location_path] = cookbook.location_path
55
53
  end
56
54
  end
57
55
 
58
56
  # Add a Cookbook use entry to delayed output
59
57
  #
60
- # @param [String] cookbook
61
- # @param [String] version
62
- # @param [~Location] location
63
- def use(cookbook, version, location = nil)
64
- cookbooks[cookbook] ||= {}
65
- cookbooks[cookbook][:version] = version
66
-
67
- if location && location.is_a?(PathLocation)
68
- cookbooks[cookbook][:metadata] = true if location.metadata?
69
- cookbooks[cookbook][:location] = location.relative_path
58
+ # @param [Dependency] dependency
59
+ def use(dependency)
60
+ cookbooks[dependency.name] ||= {}
61
+ cookbooks[dependency.name][:version] = dependency.cached_cookbook.version
62
+
63
+ if dependency.location.is_a?(PathLocation)
64
+ cookbooks[dependency.name][:metadata] = true if dependency.location.metadata?
65
+ cookbooks[dependency.name][:location] = dependency.location.relative_path
70
66
  end
71
67
  end
72
68
 
@@ -111,13 +107,13 @@ module Berkshelf
111
107
 
112
108
  # Output a list of cookbooks to delayed output
113
109
  #
114
- # @param [Hash<Dependency, CachedCookbook>] list
115
- def list(list)
116
- list.each do |dependency, cookbook|
117
- cookbooks[cookbook.cookbook_name] ||= {}
118
- cookbooks[cookbook.cookbook_name][:version] = cookbook.version
110
+ # @param [Array<Dependency>] dependencies
111
+ def list(dependencies)
112
+ dependencies.each do |dependency, cookbook|
113
+ cookbooks[dependency.name] ||= {}
114
+ cookbooks[dependency.name][:version] = dependency.locked_version.to_s
119
115
  if dependency.location
120
- cookbooks[cookbook.cookbook_name][:location] = dependency.location
116
+ cookbooks[dependency.name][:location] = dependency.location
121
117
  end
122
118
  end
123
119
  end
@@ -2,16 +2,14 @@ require 'berkshelf/api-client'
2
2
 
3
3
  module Berkshelf
4
4
  class Installer
5
- extend Forwardable
6
-
7
5
  attr_reader :berksfile
6
+ attr_reader :lockfile
8
7
  attr_reader :downloader
9
8
 
10
- def_delegator :berksfile, :lockfile
11
-
12
9
  # @param [Berkshelf::Berksfile] berksfile
13
10
  def initialize(berksfile)
14
11
  @berksfile = berksfile
12
+ @lockfile = berksfile.lockfile
15
13
  @downloader = Downloader.new(berksfile)
16
14
  end
17
15
 
@@ -19,6 +17,7 @@ module Berkshelf
19
17
  berksfile.sources.collect do |source|
20
18
  Thread.new do
21
19
  begin
20
+ Berkshelf.formatter.msg("Fetching cookbook index from #{source.uri}...")
22
21
  source.build_universe
23
22
  rescue Berkshelf::APIClientError => ex
24
23
  Berkshelf.formatter.warn "Error retrieving universe from source: #{source}"
@@ -28,146 +27,136 @@ module Berkshelf
28
27
  end.map(&:join)
29
28
  end
30
29
 
31
- # @option options [Array<String>, String] cookbooks
32
- #
33
30
  # @return [Array<Berkshelf::CachedCookbook>]
34
- def run(options = {})
35
- dependencies = lockfile_reduce(berksfile.dependencies(options.slice(:except, :only)))
36
- resolver = Resolver.new(berksfile, dependencies)
37
- lock_deps = []
38
-
39
- dependencies.each do |dependency|
40
- if dependency.scm_location?
41
- Berkshelf.formatter.fetch(dependency)
42
- downloader.download(dependency)
43
- end
44
-
45
- next if (cookbook = dependency.cached_cookbook).nil?
31
+ def run
32
+ reduce_lockfile!
46
33
 
47
- resolver.add_explicit_dependencies(cookbook)
34
+ cookbooks = if lockfile.trusted?
35
+ install_from_lockfile
36
+ else
37
+ install_from_universe
48
38
  end
49
39
 
50
- Berkshelf.formatter.msg("building universe...")
51
- build_universe
40
+ lockfile.graph.update(cookbooks)
41
+ lockfile.update(berksfile.dependencies)
42
+ lockfile.save
52
43
 
53
- cached_cookbooks = resolver.resolve.collect do |name, version, dependency|
54
- lock_deps << dependency
55
- dependency.locked_version ||= Solve::Version.new(version)
56
- if dependency.downloaded?
57
- Berkshelf.formatter.use(dependency.name, dependency.cached_cookbook.version, dependency.location)
58
- dependency.cached_cookbook
59
- else
60
- source = berksfile.sources.find { |source| source.cookbook(name, version) }
61
- remote_cookbook = source.cookbook(name, version)
62
- Berkshelf.formatter.install(name, version, api_source: source, location_type: remote_cookbook.location_type,
63
- location_path: remote_cookbook.location_path)
64
- temp_filepath = downloader.download(name, version)
65
- CookbookStore.import(name, version, temp_filepath)
66
- end
44
+ cookbooks
45
+ end
46
+
47
+ # Install all the dependencies from the lockfile graph.
48
+ #
49
+ # @return [Array<CachedCookbook>]
50
+ # the list of installed cookbooks
51
+ def install_from_lockfile
52
+ locks = lockfile.graph.locks
53
+
54
+ # Only construct the universe if we are going to download things
55
+ unless locks.all? { |_, dependency| dependency.downloaded? }
56
+ build_universe
67
57
  end
68
58
 
69
- verify_licenses!(lock_deps)
70
- lockfile.update(lock_deps)
71
- cached_cookbooks
59
+ locks.sort.collect do |name, dependency|
60
+ install(dependency)
61
+ end
72
62
  end
73
63
 
74
- # Verify that the licenses of all the cached cookbooks fall in the realm of
75
- # allowed licenses from the Berkshelf Config.
76
- #
77
- # @param [Array<Berkshelf::Dependencies>] dependencies
64
+ # Resolve and install the dependencies from the "universe", updating the
65
+ # lockfile appropiately.
78
66
  #
79
- # @raise [Berkshelf::LicenseNotAllowed]
80
- # if the license is not permitted and `raise_license_exception` is true
81
- def verify_licenses!(dependencies)
82
- licenses = Array(Berkshelf.config.allowed_licenses)
83
- return if licenses.empty?
67
+ # @return [Array<CachedCookbook>]
68
+ # the list of installed cookbooks
69
+ def install_from_universe
70
+ dependencies = lockfile.graph.locks.values + berksfile.dependencies
71
+ dependencies = dependencies.inject({}) do |hash, dependency|
72
+ # Fancy way of ensuring no duplicate dependencies are used...
73
+ hash[dependency.name] ||= dependency
74
+ hash
75
+ end.values
76
+
77
+ resolver = Resolver.new(berksfile, dependencies)
78
+
79
+ # Download all SCM locations first, since they might have additional
80
+ # constraints that we don't yet know about
81
+ dependencies.select(&:scm_location?).each do |dependency|
82
+ Berkshelf.formatter.fetch(dependency)
83
+ dependency.download
84
+ end
85
+
86
+ # Unlike when installing from the lockfile, we _always_ need to build
87
+ # the universe when installing from the universe... duh
88
+ build_universe
84
89
 
90
+ # Add any explicit dependencies for already-downloaded cookbooks (like
91
+ # path locations)
85
92
  dependencies.each do |dependency|
86
- next if dependency.location.is_a?(Berkshelf::PathLocation)
87
- cached = dependency.cached_cookbook
93
+ if cookbook = dependency.cached_cookbook
94
+ resolver.add_explicit_dependencies(cookbook)
95
+ end
96
+ end
88
97
 
89
- begin
90
- unless licenses.include?(cached.metadata.license)
91
- raise Berkshelf::LicenseNotAllowed.new(cached)
92
- end
93
- rescue Berkshelf::LicenseNotAllowed => e
94
- if Berkshelf.config.raise_license_exception
95
- FileUtils.rm_rf(cached.path)
96
- raise
97
- end
98
+ resolver.resolve.sort.collect do |dependency|
99
+ install(dependency)
100
+ end
101
+ end
98
102
 
99
- Berkshelf.ui.warn(e.to_s)
100
- end
103
+ # Install a specific dependency.
104
+ #
105
+ # @param [Dependency]
106
+ # the dependency to install
107
+ # @return [CachedCookbook]
108
+ # the installed cookbook
109
+ def install(dependency)
110
+ if dependency.downloaded?
111
+ Berkshelf.formatter.use(dependency)
112
+ dependency.cached_cookbook
113
+ else
114
+ name, version = dependency.name, dependency.locked_version.to_s
115
+ source = berksfile.source_for(name, version)
116
+ cookbook = source.cookbook(name, version)
117
+
118
+ Berkshelf.formatter.install(source, cookbook)
119
+
120
+ stash = downloader.download(name, version)
121
+ CookbookStore.import(name, version, stash)
101
122
  end
102
123
  end
103
124
 
104
125
  private
105
126
 
106
- # Returns an instance of `Berkshelf::Dependency` with an equality constraint matching
107
- # the locked version of the dependency in the lockfile.
108
- #
109
- # If no matching dependency is found in the lockfile then nil is returned.
110
- #
111
- # @param [Berkshelf:Dependency] dependency
112
- #
113
- # @return [Berkshelf::Dependency, nil]
114
- def dependency_from_lockfile(dependency)
115
- locked = lockfile.find(dependency)
116
-
117
- return nil unless locked
118
-
119
- # If there's a locked_version, make sure it's still satisfied
120
- # by the constraint
121
- if locked.locked_version
122
- unless dependency.version_constraint.satisfies?(locked.locked_version)
123
- raise Berkshelf::OutdatedDependency.new(locked, dependency)
127
+ # Iterate over each top-level dependency defined in the lockfile and
128
+ # check if that dependency is still defined in the Berksfile.
129
+ #
130
+ # If the dependency is no longer present in the Berksfile, it is "safely"
131
+ # removed using {Lockfile#unlock} and {Lockfile#remove}. This prevents
132
+ # the lockfile from "leaking" dependencies when they have been removed
133
+ # from the Berksfile, but still remained locked in the lockfile.
134
+ #
135
+ # If the dependency exists, a constraint comparison is conducted to verify
136
+ # that the locked dependency still satisifes the original constraint. This
137
+ # handles the edge case where a user has updated or removed a constraint
138
+ # on a dependency that already existed in the lockfile.
139
+ #
140
+ # @raise [OutdatedDependency]
141
+ # if the constraint exists, but is no longer satisifed by the existing
142
+ # locked version
143
+ #
144
+ # @return [Array<Dependency>]
145
+ def reduce_lockfile!
146
+ lockfile.dependencies.each do |dependency|
147
+ if berksfile.dependencies.map(&:name).include?(dependency.name)
148
+ locked = lockfile.graph.find(dependency)
149
+ next if locked.nil?
150
+
151
+ unless dependency.version_constraint.satisfies?(locked.version)
152
+ raise OutdatedDependency.new(locked, dependency)
124
153
  end
154
+ else
155
+ lockfile.unlock(dependency)
125
156
  end
126
-
127
- locked.version_constraint = Solve::Constraint.new("= #{locked.locked_version}")
128
- locked
129
- end
130
-
131
- # Merge the locked dependencies against the given dependencies.
132
- #
133
- # For each the given dependencies, check if there's a locked version that
134
- # still satisfies the version constraint. If it does, "lock" that dependency
135
- # because we should just use the locked version.
136
- #
137
- # If a locked dependency exists, but doesn't satisfy the constraint, raise a
138
- # {Berkshelf::OutdatedDependency} and tell the user to run update.
139
- #
140
- # Never use the locked constraint for a dependency with a {PathLocation}
141
- #
142
- # @param [Array<Berkshelf::Dependency>] dependencies
143
- #
144
- # @return [Array<Berkshelf::Dependency>]
145
- def lockfile_reduce(dependencies = [])
146
- {}.tap do |h|
147
- (dependencies + lockfile.dependencies).each do |dependency|
148
- next if h.has_key?(dependency.name)
149
-
150
- if dependency.path_location?
151
- result = dependency
152
- else
153
- result = dependency_from_lockfile(dependency) || dependency
154
- end
155
-
156
- h[result.name] = result
157
- end
158
- end.values
159
157
  end
160
158
 
161
- # The list of dependencies "locked" by the lockfile.
162
- #
163
- # @return [Array<Berkshelf::Dependency>]
164
- # the list of dependencies in this lockfile
165
- def locked_dependencies
166
- lockfile.dependencies
167
- end
168
-
169
- def reduce_scm_locations(dependencies)
170
- dependencies.select { |dependency| SCM_LOCATIONS.include?(dependency.class.location_key) }
171
- end
159
+ lockfile.save
160
+ end
172
161
  end
173
162
  end
@@ -68,7 +68,7 @@ module Berkshelf
68
68
 
69
69
  tmp_path = rel ? File.join(repo_path, rel) : repo_path
70
70
  unless File.chef_cookbook?(tmp_path)
71
- msg = "Cookbook '#{dependency.name}' not found at git: #{to_display}"
71
+ msg = "Cookbook '#{dependency.name}' not found at #{to_s}"
72
72
  msg << " at path '#{rel}'" if rel
73
73
  raise CookbookNotFound, msg
74
74
  end
@@ -90,18 +90,31 @@ module Berkshelf
90
90
  end
91
91
  end
92
92
 
93
+ def ==(other)
94
+ other.is_a?(GitLocation) &&
95
+ other.uri == uri &&
96
+ other.branch == branch &&
97
+ other.ref == ref &&
98
+ other.rel == rel
99
+ end
100
+
93
101
  def to_s
94
- "#{self.class.location_key}: #{to_display}"
102
+ if rel
103
+ "#{uri} (at #{branch || ref[0...7]}/#{rel})"
104
+ else
105
+ "#{uri} (at #{branch || ref[0...7]})"
106
+ end
95
107
  end
96
108
 
97
- private
109
+ def to_lock
110
+ out = " git: #{uri}\n"
111
+ out << " branch: #{branch}\n" if branch
112
+ out << " ref: #{ref}\n" if ref
113
+ out << " rel: #{rel}\n" if rel
114
+ out
115
+ end
98
116
 
99
- def to_display
100
- info = checkout_info
101
- s = "'#{uri}' with #{info[:kind]}: '#{info[:rev]}'"
102
- s << " at ref: '#{ref}'" if ref && (info[:kind] != "ref" || ref != info[:rev])
103
- s
104
- end
117
+ private
105
118
 
106
119
  def cached?(destination)
107
120
  revision_path(destination) && File.exists?(revision_path(destination))
@@ -49,8 +49,7 @@ module Berkshelf
49
49
 
50
50
  tmp_path = rel ? File.join(repo_path, rel) : repo_path
51
51
  unless File.chef_cookbook?(tmp_path)
52
- msg = "Cookbook '#{dependency.name}' not found at hg: #{uri}"
53
- msg << " with rev '#{rev}'" if rev
52
+ msg = "Cookbook '#{dependency.name}' not found at #{to_s}"
54
53
  msg << " at path '#{rel}'" if rel
55
54
  raise CookbookNotFound, msg
56
55
  end
@@ -72,10 +71,26 @@ module Berkshelf
72
71
  end
73
72
  end
74
73
 
74
+ def ==(other)
75
+ other.is_a?(MercurialLocation) &&
76
+ other.uri == uri &&
77
+ other.rev == rev &&
78
+ other.rel == rel
79
+ end
80
+
75
81
  def to_s
76
- s = "#{self.class.location_key}: '#{uri}'"
77
- s << " at rev: '#{rev}'" if rev
78
- s
82
+ if rel
83
+ "#{uri} (at #{rev}/#{rel})"
84
+ else
85
+ "#{uri} (at #{rev})"
86
+ end
87
+ end
88
+
89
+ def to_lock
90
+ out = " hg: #{uri}\n"
91
+ out << " rev: #{rev}\n" if rev
92
+ out << " rel: #{rel}\n" if rel
93
+ out
79
94
  end
80
95
 
81
96
  private
@@ -49,6 +49,15 @@ module Berkshelf
49
49
  "./#{new_path}"
50
50
  end
51
51
 
52
+ #
53
+ # The expanded path of this path on disk, relative to the berksfile.
54
+ #
55
+ # @return [String]
56
+ #
57
+ def expanded_path
58
+ relative_path(dependency.berksfile.filepath)
59
+ end
60
+
52
61
  # Valid if the path exists and is readable
53
62
  #
54
63
  # @return [Boolean]
@@ -60,14 +69,20 @@ module Berkshelf
60
69
  super.merge(value: self.path)
61
70
  end
62
71
 
63
- # The string representation of this PathLocation
64
- #
65
- # @example
66
- # loc.to_s #=> artifact (1.4.0) at path: '/Users/Seth/Dev/artifact'
67
- #
68
- # @return [String]
72
+ def ==(other)
73
+ other.is_a?(PathLocation) &&
74
+ other.metadata? == metadata? &&
75
+ other.expanded_path == expanded_path
76
+ end
77
+
78
+ def to_lock
79
+ out = " path: #{relative_path(dependency.berksfile.filepath)}\n"
80
+ out << " metadata: true\n" if metadata?
81
+ out
82
+ end
83
+
69
84
  def to_s
70
- "#{self.class.location_key}: '#{File.expand_path(path)}'"
85
+ "source at #{relative_path(dependency.berksfile.filepath)}"
71
86
  end
72
87
  end
73
88
  end