berkshelf 3.0.0.beta6 → 3.0.0.beta7

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