berkshelf 0.4.0.rc4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +2 -0
  2. data/Guardfile +6 -3
  3. data/features/default_locations.feature +122 -0
  4. data/features/install.feature +20 -4
  5. data/features/lockfile.feature +1 -6
  6. data/features/update.feature +2 -3
  7. data/generator_files/Berksfile.erb +2 -0
  8. data/generator_files/gitignore.erb +6 -0
  9. data/lib/berkshelf.rb +6 -10
  10. data/lib/berkshelf/berksfile.rb +203 -14
  11. data/lib/berkshelf/cached_cookbook.rb +5 -1
  12. data/lib/berkshelf/cli.rb +4 -0
  13. data/lib/berkshelf/cookbook_source.rb +49 -91
  14. data/lib/berkshelf/cookbook_store.rb +2 -0
  15. data/lib/berkshelf/downloader.rb +71 -51
  16. data/lib/berkshelf/errors.rb +7 -3
  17. data/lib/berkshelf/formatters.rb +6 -6
  18. data/lib/berkshelf/location.rb +171 -0
  19. data/lib/berkshelf/locations/chef_api_location.rb +252 -0
  20. data/lib/berkshelf/locations/git_location.rb +76 -0
  21. data/lib/berkshelf/locations/path_location.rb +38 -0
  22. data/lib/berkshelf/locations/site_location.rb +150 -0
  23. data/lib/berkshelf/lockfile.rb +2 -2
  24. data/lib/berkshelf/resolver.rb +12 -15
  25. data/lib/berkshelf/uploader.rb +2 -9
  26. data/lib/berkshelf/version.rb +1 -1
  27. data/spec/fixtures/lockfile_spec/without_lock/.gitkeep +0 -0
  28. data/spec/support/chef_api.rb +7 -1
  29. data/spec/unit/berkshelf/berksfile_spec.rb +157 -12
  30. data/spec/unit/berkshelf/cached_cookbook_spec.rb +19 -0
  31. data/spec/unit/berkshelf/cookbook_generator_spec.rb +1 -0
  32. data/spec/unit/berkshelf/cookbook_source_spec.rb +25 -35
  33. data/spec/unit/berkshelf/cookbook_store_spec.rb +3 -3
  34. data/spec/unit/berkshelf/downloader_spec.rb +171 -43
  35. data/spec/unit/berkshelf/formatters_spec.rb +13 -16
  36. data/spec/unit/berkshelf/{cookbook_source/location_spec.rb → location_spec.rb} +10 -10
  37. data/spec/unit/berkshelf/{cookbook_source → locations}/chef_api_location_spec.rb +4 -4
  38. data/spec/unit/berkshelf/{cookbook_source → locations}/git_location_spec.rb +8 -8
  39. data/spec/unit/berkshelf/{cookbook_source → locations}/path_location_spec.rb +5 -5
  40. data/spec/unit/berkshelf/{cookbook_source → locations}/site_location_spec.rb +17 -3
  41. data/spec/unit/berkshelf/lockfile_spec.rb +26 -17
  42. data/spec/unit/berkshelf/resolver_spec.rb +6 -5
  43. data/spec/unit/berkshelf/uploader_spec.rb +6 -4
  44. metadata +27 -31
  45. data/lib/berkshelf/cookbook_source/chef_api_location.rb +0 -256
  46. data/lib/berkshelf/cookbook_source/git_location.rb +0 -78
  47. data/lib/berkshelf/cookbook_source/location.rb +0 -167
  48. data/lib/berkshelf/cookbook_source/path_location.rb +0 -40
  49. data/lib/berkshelf/cookbook_source/site_location.rb +0 -149
  50. data/lib/berkshelf/dsl.rb +0 -45
  51. data/lib/berkshelf/tx_result.rb +0 -12
  52. data/lib/berkshelf/tx_result_set.rb +0 -37
  53. data/spec/fixtures/lockfile_spec/without_lock/Berksfile.lock +0 -5
  54. data/spec/unit/berkshelf/dsl_spec.rb +0 -42
  55. data/spec/unit/berkshelf/tx_result_set_spec.rb +0 -77
  56. data/spec/unit/berkshelf/tx_result_spec.rb +0 -21
@@ -94,7 +94,6 @@ module Berkshelf
94
94
  attr_reader :manifest
95
95
 
96
96
  def_delegator :@metadata, :version
97
- def_delegator :@metadata, :dependencies
98
97
 
99
98
  def initialize(name, path, metadata)
100
99
  @cookbook_name = name
@@ -125,6 +124,11 @@ module Berkshelf
125
124
  "#{cookbook_name}-#{version}"
126
125
  end
127
126
 
127
+ # @return [Hash]
128
+ def dependencies
129
+ metadata.recommendations.merge(metadata.dependencies)
130
+ end
131
+
128
132
  # @return [Hash]
129
133
  # an hash containing the checksums and expanded file paths of all of the
130
134
  # files found in the instance of CachedCookbook
@@ -59,6 +59,10 @@ module Berkshelf
59
59
  banner: "PATH"
60
60
  desc "install", "Install the Cookbooks specified by a Berksfile or a Berksfile.lock."
61
61
  def install
62
+ unless options[:shims].nil?
63
+ options[:shims] = File.expand_path(options[:shims])
64
+ end
65
+
62
66
  berksfile = ::Berkshelf::Berksfile.from_file(options[:berksfile])
63
67
  berksfile.install(options)
64
68
  end
@@ -2,7 +2,7 @@ module Berkshelf
2
2
  # @author Jamie Winsor <jamie@vialstudios.com>
3
3
  class CookbookSource
4
4
  class << self
5
- @@valid_options = [:group, :locked_version]
5
+ @@valid_options = [:constraint, :locations, :group, :locked_version]
6
6
  @@location_keys = Hash.new
7
7
 
8
8
  # Returns an array of valid options to pass to the initializer
@@ -48,83 +48,62 @@ module Berkshelf
48
48
 
49
49
  @@location_keys
50
50
  end
51
+
52
+ def validate_options(options)
53
+ invalid_options = (options.keys - valid_options)
54
+
55
+ unless invalid_options.empty?
56
+ invalid_options.collect! { |opt| "'#{opt}'" }
57
+ raise InternalError, "Invalid options for Cookbook Source: #{invalid_options.join(', ')}."
58
+ end
59
+
60
+ true
61
+ end
51
62
  end
52
63
 
53
64
  extend Forwardable
54
65
 
55
- # JW TODO: Move locations out of CookbookSource namespace.
56
- # Move all locations into berkshelf/locations/*
57
- # Autorequire all items in berkshelf/locations/
58
- require 'berkshelf/cookbook_source/location'
59
- require 'berkshelf/cookbook_source/site_location'
60
- require 'berkshelf/cookbook_source/git_location'
61
- require 'berkshelf/cookbook_source/path_location'
62
- require 'berkshelf/cookbook_source/chef_api_location'
63
-
64
66
  attr_reader :name
65
- alias_method :to_s, :name
66
-
67
67
  attr_reader :version_constraint
68
68
  attr_reader :groups
69
69
  attr_reader :location
70
70
  attr_accessor :cached_cookbook
71
71
 
72
- def_delegator :@location, :downloaded?
73
-
74
- # @overload initialize(name, version_constraint, options = {})
75
- # @param [#to_s] name
76
- # @param [#to_s] version_constraint
77
- # @param [Hash] options
78
- #
79
- # @option options [String] :git
80
- # the Git URL to clone
81
- # @option options [String] :site
82
- # a URL pointing to a community API endpoint
83
- # @option options [String] :path
84
- # a filepath to the cookbook on your local disk
85
- # @option options [Symbol, Array] :group
86
- # the group or groups that the cookbook belongs to
87
- # @option options [String] :ref
88
- # the commit hash or an alias to a commit hash to clone
89
- # @option options [String] :branch
90
- # same as ref
91
- # @option options [String] :tag
92
- # same as tag
93
- # @option options [String] :locked_version
94
- # @overload initialize(name, options = {})
95
- # @param [#to_s] name
96
- # @param [Hash] options
72
+ # @param [String] name
73
+ # @param [Hash] options
97
74
  #
98
- # @option options [String] :git
99
- # the Git URL to clone
100
- # @option options [String] :site
101
- # a URL pointing to a community API endpoint
102
- # @option options [String] :path
103
- # a filepath to the cookbook on your local disk
104
- # @option options [Symbol, Array] :group
105
- # the group or groups that the cookbook belongs to
106
- # @option options [String] :ref
107
- # the commit hash or an alias to a commit hash to clone
108
- # @option options [String] :branch
109
- # same as ref
110
- # @option options [String] :tag
111
- # same as tag
112
- # @option options [String] :locked_version
113
- def initialize(*args)
114
- options = args.last.is_a?(Hash) ? args.pop : {}
115
- name, constraint = args
116
-
75
+ # @option options [String, Solve::Constraint] constraint
76
+ # version constraint to resolve for this source
77
+ # @option options [String] :git
78
+ # the Git URL to clone
79
+ # @option options [String] :site
80
+ # a URL pointing to a community API endpoint
81
+ # @option options [String] :path
82
+ # a filepath to the cookbook on your local disk
83
+ # @option options [Symbol, Array] :group
84
+ # the group or groups that the cookbook belongs to
85
+ # @option options [String] :ref
86
+ # the commit hash or an alias to a commit hash to clone
87
+ # @option options [String] :branch
88
+ # same as ref
89
+ # @option options [String] :tag
90
+ # same as tag
91
+ # @option options [String] :locked_version
92
+ def initialize(name, options = {})
117
93
  @name = name
118
- @version_constraint = Solve::Constraint.new(constraint || ">= 0.0.0")
94
+ @version_constraint = Solve::Constraint.new(options[:constraint] || ">= 0.0.0")
119
95
  @groups = []
120
96
  @cached_cookbook = nil
97
+ @location = nil
121
98
 
122
- validate_options(options)
99
+ self.class.validate_options(options)
123
100
 
124
- @location = Location.init(name, version_constraint, options)
101
+ unless (options.keys & self.class.location_keys.keys).empty?
102
+ @location = Location.init(name, version_constraint, options)
103
+ end
125
104
 
126
105
  if @location.is_a?(PathLocation)
127
- @cached_cookbook = CachedCookbook.from_path(@location.path)
106
+ @cached_cookbook = CachedCookbook.from_path(location.path)
128
107
  end
129
108
 
130
109
  @locked_version = Solve::Version.new(options[:locked_version]) if options[:locked_version]
@@ -141,22 +120,12 @@ module Berkshelf
141
120
  end
142
121
  end
143
122
 
144
- # @param [String] destination
145
- # destination to download to
146
- #
147
- # @return [Array]
148
- # An array containing the status at index 0 and local path or error message in index 1
123
+ # Returns true if the cookbook source has already been downloaded. A cookbook
124
+ # source is downloaded when a cached cookbooked is present.
149
125
  #
150
- # Example:
151
- # [ :ok, "/tmp/nginx" ]
152
- # [ :error, "Cookbook 'sparkle_motion' not found at site: http://cookbooks.opscode.com/api/v1/cookbooks" ]
153
- def download(destination)
154
- self.cached_cookbook = location.download(destination)
155
-
156
- [ :ok, self.cached_cookbook ]
157
- rescue CookbookNotFound => e
158
- self.cached_cookbook = nil
159
- [ :error, e.message ]
126
+ # @return [Boolean]
127
+ def downloaded?
128
+ !self.cached_cookbook.nil?
160
129
  end
161
130
 
162
131
  def has_group?(group)
@@ -167,21 +136,10 @@ module Berkshelf
167
136
  @locked_version || cached_cookbook.version
168
137
  end
169
138
 
170
- private
171
-
172
- def set_local_path(path)
173
- @local_path = path
174
- end
175
-
176
- def validate_options(options)
177
- invalid_options = options.keys - self.class.valid_options
178
-
179
- unless invalid_options.empty?
180
- invalid_options.collect! { |opt| "'#{opt}'" }
181
- raise InternalError, "Invalid options for Cookbook Source: #{invalid_options.join(', ')}."
182
- end
183
-
184
- true
185
- end
139
+ def to_s
140
+ msg = "#{self.name} (#{self.version_constraint}) groups: #{self.groups}"
141
+ msg << " location: #{self.location}" if self.location
142
+ msg
143
+ end
186
144
  end
187
145
  end
@@ -3,6 +3,8 @@ require 'fileutils'
3
3
  module Berkshelf
4
4
  # @author Jamie Winsor <jamie@vialstudios.com>
5
5
  class CookbookStore
6
+ # @return [String]
7
+ # filepath to where cookbooks are stored
6
8
  attr_reader :storage_path
7
9
 
8
10
  # Create a new instance of CookbookStore with the given
@@ -3,80 +3,100 @@ module Berkshelf
3
3
  class Downloader
4
4
  extend Forwardable
5
5
 
6
+ DEFAULT_LOCATIONS = [
7
+ {
8
+ type: :site,
9
+ value: Location::OPSCODE_COMMUNITY_API,
10
+ options: Hash.new
11
+ }
12
+ ]
13
+
14
+ # @return [String]
15
+ # a filepath to download cookbook sources to
6
16
  attr_reader :cookbook_store
7
- attr_reader :queue
8
17
 
9
18
  def_delegators :@cookbook_store, :storage_path
10
19
 
11
- def initialize(cookbook_store)
20
+ # @option options [Array<Hash>] locations
21
+ def initialize(cookbook_store, options = {})
12
22
  @cookbook_store = cookbook_store
13
- @queue = []
23
+ @locations = options.fetch(:locations, Array.new)
14
24
  end
15
25
 
16
- # Add a CookbookSource to the downloader's queue
17
- #
18
- # @param [Berkshelf::CookbookSource] source
19
- #
20
- # @return [Array<Berkshelf::CookbookSource>]
21
- # the queue - an array of Berkshelf::CookbookSources
22
- def enqueue(source)
23
- unless validate_source(source)
24
- raise ArgumentError, "Invalid CookbookSource: can only enqueue valid instances of CookbookSource."
25
- end
26
-
27
- @queue << source
26
+ # @return [Array<Hash>]
27
+ # an Array of Hashes representing each default location that can be used to attempt
28
+ # to download cookbook sources which do not have an explicit location. An array of default locations will
29
+ # be used if no locations are explicitly added by the {#add_location} function.
30
+ def locations
31
+ @locations.empty? ? DEFAULT_LOCATIONS : @locations
28
32
  end
29
33
 
30
- # Remove a CookbookSource from the downloader's queue
34
+ # Create a location hash and add it to the end of the array of locations.
31
35
  #
32
- # @param [Berkshelf::CookbookSource] source
36
+ # subject.add_location(:chef_api, "http://chef:8080", node_name: "reset", client_key: "/Users/reset/.chef/reset.pem") =>
37
+ # [ { type: :chef_api, value: "http://chef:8080/", node_name: "reset", client_key: "/Users/reset/.chef/reset.pem" } ]
33
38
  #
34
- # @return [Berkshelf::CookbookSource]
35
- # the CookbookSource removed from the queue
36
- def dequeue(source)
37
- @queue.delete(source)
38
- end
39
-
40
- # Download each CookbookSource in the queue. Upon successful download
41
- # of a CookbookSource it is removed from the queue. If a CookbookSource
42
- # fails to download it remains in the queue.
39
+ # @param [Symbol] type
40
+ # @param [String, Symbol] value
41
+ # @param [Hash] options
43
42
  #
44
- # @return [TXResultSet]
45
- def download_all
46
- results = TXResultSet.new
47
-
48
- queue.each do |source|
49
- results.add_result download(source)
43
+ # @return [Hash]
44
+ def add_location(type, value, options = {})
45
+ if has_location?(type, value)
46
+ raise DuplicateLocationDefined, "A default '#{type}' location with the value '#{value}' is already defined"
50
47
  end
51
-
52
- results.success.each { |result| dequeue(result.source) }
53
48
 
54
- results
49
+ @locations.push(type: type, value: value, options: options)
55
50
  end
56
51
 
57
- # Downloads the given CookbookSource
58
- #
59
- # @param [CookbookSource] source
60
- # the source to download
52
+ # Checks the list of default locations if a location of the given type and value has already
53
+ # been added and returns true or false.
61
54
  #
62
- # @return [TXResult]
63
- def download(source)
64
- status, message = source.download(storage_path)
65
- TXResult.new(status, message, source)
55
+ # @return [Boolean]
56
+ def has_location?(type, value)
57
+ !@locations.select { |loc| loc[:type] == type && loc[:value] == value }.empty?
66
58
  end
67
59
 
68
- # Downloads the given CookbookSource. Raises a DownloadFailure error
69
- # if the download was not successful.
60
+ # Downloads the given CookbookSource. If the given source does not contain a value for {CookbookSource#location}
61
+ # the default locations of this downloader will be used to attempt to retrieve the source.
70
62
  #
71
63
  # @param [CookbookSource] source
72
64
  # the source to download
73
65
  #
74
- # @return [TXResult]
75
- def download!(source)
76
- result = download(source)
77
- raise DownloadFailure, result.message if result.failed?
78
-
79
- result
66
+ # @return [Array]
67
+ # an array containing the downloaded CachedCookbook and the Location used to download the cookbook
68
+ def download(source)
69
+ cached_cookbook, location = if source.location
70
+ [ source.location.download(storage_path), source.location ]
71
+ else
72
+ cached_cookbook = nil
73
+ location = nil
74
+
75
+ locations.each do |loc|
76
+ location = Location.init(
77
+ source.name,
78
+ source.version_constraint,
79
+ loc[:options].merge(loc[:type] => loc[:value])
80
+ )
81
+ begin
82
+ cached_cookbook = location.download(storage_path)
83
+ break
84
+ rescue
85
+ cached_cookbook, location = nil
86
+ next
87
+ end
88
+ end
89
+
90
+ if cached_cookbook.nil?
91
+ raise CookbookNotFound, "Cookbook '#{source.name}' not found in any of the default locations"
92
+ end
93
+
94
+ [ cached_cookbook, location ]
95
+ end
96
+
97
+ source.cached_cookbook = cached_cookbook
98
+
99
+ [ cached_cookbook, location ]
80
100
  end
81
101
 
82
102
  private
@@ -12,11 +12,15 @@ module Berkshelf
12
12
  end
13
13
 
14
14
  class InternalError < BerkshelfError; status_code(99); end
15
- class MethodNotImplmentedError < ::Berkshelf::InternalError ; end
16
-
15
+ class AbstractFunction < InternalError
16
+ def to_s
17
+ "Function must be implemented on includer"
18
+ end
19
+ end
20
+
17
21
  class BerksfileNotFound < BerkshelfError; status_code(100); end
18
22
  class NoVersionForConstraints < BerkshelfError; status_code(101); end
19
- class DownloadFailure < BerkshelfError; status_code(102); end
23
+ class DuplicateLocationDefined < BerkshelfError; status_code(102); end
20
24
  class CookbookNotFound < BerkshelfError; status_code(103); end
21
25
  class GitError < BerkshelfError
22
26
  status_code(104)
@@ -10,27 +10,27 @@ module Berkshelf
10
10
  end
11
11
 
12
12
  def install(cookbook, version, location)
13
- raise MethodNotImplmentedError, "#install must be implemented on #{self.class}"
13
+ raise AbstractFunction, "#install must be implemented on #{self.class}"
14
14
  end
15
15
 
16
16
  def use(cookbook, version, path = nil)
17
- raise MethodNotImplmentedError, "#install must be implemented on #{self.class}"
17
+ raise AbstractFunction, "#install must be implemented on #{self.class}"
18
18
  end
19
19
 
20
20
  def upload(cookbook, version, chef_server_url)
21
- raise MethodNotImplmentedError, "#upload must be implemented on #{self.class}"
21
+ raise AbstractFunction, "#upload must be implemented on #{self.class}"
22
22
  end
23
23
 
24
24
  def shims_written(directory)
25
- raise MethodNotImplmentedError, "#shims_written must be implemented on #{self.class}"
25
+ raise AbstractFunction, "#shims_written must be implemented on #{self.class}"
26
26
  end
27
27
 
28
28
  def msg(message)
29
- raise MethodNotImplmentedError, "#msg must be implemented on #{self.class}"
29
+ raise AbstractFunction, "#msg must be implemented on #{self.class}"
30
30
  end
31
31
 
32
32
  def error(message)
33
- raise MethodNotImplmentedError, "#error must be implemented on #{self.class}"
33
+ raise AbstractFunction, "#error must be implemented on #{self.class}"
34
34
  end
35
35
  end
36
36
  end
@@ -0,0 +1,171 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ module Location
4
+ OPSCODE_COMMUNITY_API = 'http://cookbooks.opscode.com/api/v1/cookbooks'.freeze
5
+
6
+ module ClassMethods
7
+ # Register the location key for the including source location with CookbookSource
8
+ #
9
+ # @param [Symbol] key
10
+ def location_key(key)
11
+ CookbookSource.add_location_key(key, self)
12
+ end
13
+
14
+ # Register a valid option or multiple options with the CookbookSource class
15
+ #
16
+ # @param [Symbol] opts
17
+ def valid_options(*opts)
18
+ Array(opts).each do |opt|
19
+ CookbookSource.add_valid_option(opt)
20
+ end
21
+ end
22
+
23
+ # Returns an array where the first element is string representing the best version
24
+ # for the given constraint and the second element is the URI to where the corresponding
25
+ # version of the Cookbook can be downloaded from
26
+ #
27
+ # @example:
28
+ # constraint = Solve::Constraint.new("~> 0.101.2")
29
+ # versions = {
30
+ # "1.0.0" => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/1_0_0",
31
+ # "2.0.0" => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/2_0_0"
32
+ # }
33
+ #
34
+ # subject.solve_for_constraint(versions, constraint) =>
35
+ # [ "2.0.0", "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/2_0_0" ]
36
+ #
37
+ # @param [String, Solve::Constraint] constraint
38
+ # version constraint to solve for
39
+ #
40
+ # @param [Hash] versions
41
+ # a hash where the keys are a string representing a cookbook version and the values
42
+ # are the download URL for the cookbook version.
43
+ #
44
+ # @return [Array, nil]
45
+ def solve_for_constraint(constraint, versions)
46
+ graph = Solve::Graph.new
47
+ name = "none"
48
+
49
+ versions.each do |version, uri|
50
+ graph.artifacts(name, version)
51
+ end
52
+
53
+ graph.demands(name, constraint)
54
+ result = Solve.it(graph)
55
+
56
+ return nil if result.nil?
57
+
58
+ version = result[name]
59
+
60
+ [ version, versions[version] ]
61
+ end
62
+ end
63
+
64
+ class << self
65
+ def included(base)
66
+ base.send :extend, ClassMethods
67
+ end
68
+
69
+ # Creates a new instance of a Class implementing Location with the given name and
70
+ # constraint. Which Class to instantiated is determined by the values in the given
71
+ # options Hash. Source Locations have an associated location_key registered with
72
+ # CookbookSource. If your options Hash contains a key matching one of these location_keys
73
+ # then the Class who registered that location_key will be instantiated. If you do not
74
+ # provide an option with a matching location_key a SiteLocation class will be
75
+ # instantiated.
76
+ #
77
+ # @example
78
+ # Location.init("nginx", ">= 0.0.0", git: "git://github.com/RiotGames/artifact-cookbook.git") =>
79
+ # instantiates a GitLocation
80
+ #
81
+ # Location.init("nginx", ">= 0.0.0", path: "/Users/reset/code/nginx-cookbook") =>
82
+ # instantiates a PathLocation
83
+ #
84
+ # Location.init("nginx", ">= 0.0.0", site: "http://cookbooks.opscode.com/api/v1/cookbooks") =>
85
+ # instantiates a SiteLocation
86
+ #
87
+ # Location.init("nginx", ">= 0.0.0", chef_api: "https://api.opscode.com/organizations/vialstudios") =>
88
+ # instantiates a ChefAPILocation
89
+ #
90
+ # Location.init("nginx", ">= 0.0.0") =>
91
+ # instantiates a SiteLocation
92
+ #
93
+ # @param [String] name
94
+ # @param [String, Solve::Constraint] constraint
95
+ # @param [Hash] options
96
+ #
97
+ # @return [SiteLocation, PathLocation, GitLocation, ChefAPILocation]
98
+ def init(name, constraint, options = {})
99
+ klass = klass_from_options(options)
100
+
101
+ klass.new(name, constraint, options)
102
+ end
103
+
104
+ private
105
+
106
+ def klass_from_options(options)
107
+ location_keys = (options.keys & CookbookSource.location_keys.keys)
108
+ if location_keys.length > 1
109
+ location_keys.collect! { |opt| "'#{opt}'" }
110
+ raise InternalError, "Only one location key (#{CookbookSource.location_keys.keys.join(', ')}) may be specified. You gave #{location_keys.join(', ')}."
111
+ end
112
+
113
+ if location_keys.empty?
114
+ SiteLocation
115
+ else
116
+ CookbookSource.location_keys[location_keys.first]
117
+ end
118
+ end
119
+ end
120
+
121
+ attr_reader :name
122
+ attr_reader :version_constraint
123
+
124
+ # @param [#to_s] name
125
+ # @param [Solve::Constraint] version_constraint
126
+ # @param [Hash] options
127
+ def initialize(name, version_constraint, options = {})
128
+ @name = name
129
+ @version_constraint = version_constraint
130
+ @downloaded_status = false
131
+ end
132
+
133
+ # @param [#to_s] destination
134
+ #
135
+ # @return [Berkshelf::CachedCookbook]
136
+ def download(destination)
137
+ raise AbstractFunction
138
+ end
139
+
140
+ # @return [Boolean]
141
+ def downloaded?
142
+ @downloaded_status
143
+ end
144
+
145
+ # Ensures that the given CachedCookbook satisfies the constraint
146
+ #
147
+ # @param [CachedCookbook] cached_cookbook
148
+ #
149
+ # @raise [ConstraintNotSatisfied] if the CachedCookbook does not satisfy the version constraint of
150
+ # this instance of Location.
151
+ #
152
+ # @return [Boolean]
153
+ def validate_cached(cached_cookbook)
154
+ unless version_constraint.satisfies?(cached_cookbook.version)
155
+ raise ConstraintNotSatisfied, "A cookbook satisfying '#{name}' (#{version_constraint}) not found at #{self}"
156
+ end
157
+
158
+ true
159
+ end
160
+
161
+ private
162
+
163
+ def set_downloaded_status(state)
164
+ @downloaded_status = state
165
+ end
166
+ end
167
+ end
168
+
169
+ Dir["#{File.dirname(__FILE__)}/locations/*.rb"].sort.each do |path|
170
+ require "berkshelf/locations/#{File.basename(path, '.rb')}"
171
+ end