berkshelf 1.3.1 → 1.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,9 @@
1
+ # 1.3.1
2
+ - Support for Vagrant 1.1.x
3
+ - Move Berkshelf Vagrant plugin into it's [own repository](https://github.com/RiotGames/berkshelf-vagrant)
4
+ - Added -d flag to output debug information in berks command
5
+ - Various bug fixes in uploading cookbooks
6
+
1
7
  # 1.2.0
2
8
  - Remove Vagrant as a gem dependency
3
9
  - Remove Chef as a gem dependency
@@ -33,7 +33,7 @@ Gem::Specification.new do |s|
33
33
  s.add_dependency 'mixlib-shellout', '~> 1.1'
34
34
  s.add_dependency 'mixlib-config', '~> 1.1'
35
35
  s.add_dependency 'faraday', '>= 0.8.5'
36
- s.add_dependency 'ridley', '>= 0.8.6'
36
+ s.add_dependency 'ridley', '~> 0.9.0'
37
37
  s.add_dependency 'chozo', '>= 0.6.1'
38
38
  s.add_dependency 'hashie', '>= 2.0.2'
39
39
  s.add_dependency 'minitar'
@@ -1,14 +1,5 @@
1
1
  require 'multi_json'
2
-
3
- # Fix for Facter < 1.7.0 changing LANG to C
4
- # https://github.com/puppetlabs/facter/commit/f77584f4
5
- begin
6
- old_lang = ENV['LANG']
7
- require 'ridley'
8
- ensure
9
- ENV['LANG'] = old_lang
10
- end
11
-
2
+ require 'ridley'
12
3
  require 'chozo/core_ext'
13
4
  require 'active_support/core_ext'
14
5
  require 'archive/tar/minitar'
@@ -46,6 +37,7 @@ module Berkshelf
46
37
  autoload :Git, 'berkshelf/git'
47
38
  autoload :InitGenerator, 'berkshelf/init_generator'
48
39
  autoload :Lockfile, 'berkshelf/lockfile'
40
+ autoload :Logger, 'berkshelf/logger'
49
41
  autoload :Mixin, 'berkshelf/mixin'
50
42
  autoload :Resolver, 'berkshelf/resolver'
51
43
  autoload :UI, 'berkshelf/ui'
@@ -53,8 +45,9 @@ module Berkshelf
53
45
  require 'berkshelf/location'
54
46
 
55
47
  class << self
56
- attr_accessor :ui
48
+ include Berkshelf::Mixin::Logging
57
49
 
50
+ attr_accessor :ui
58
51
  attr_writer :cookbook_store
59
52
 
60
53
  # @return [Pathname]
@@ -79,7 +72,7 @@ module Berkshelf
79
72
  end
80
73
 
81
74
  # @return [Logger]
82
- def log
75
+ def logger
83
76
  Celluloid.logger
84
77
  end
85
78
 
@@ -2,6 +2,7 @@ module Berkshelf
2
2
  # @author Jamie Winsor <reset@riotgames.com>
3
3
  class Berksfile
4
4
  extend Forwardable
5
+ include Berkshelf::Mixin::Logging
5
6
 
6
7
  class << self
7
8
  # @param [String] file
@@ -27,16 +28,16 @@ module Berkshelf
27
28
  # @return [String]
28
29
  # expanded filepath to the vendor directory
29
30
  def vendor(cookbooks, path)
30
- chefignore_file = [
31
- File.join(Dir.pwd, Berkshelf::Chef::Cookbook::Chefignore::FILENAME),
32
- File.join(Dir.pwd, 'cookbooks', Berkshelf::Chef::Cookbook::Chefignore::FILENAME)
33
- ].find { |f| File.exists?(f) }
34
-
35
- chefignore = chefignore_file && Berkshelf::Chef::Cookbook::Chefignore.new(chefignore_file)
31
+ chefignore = nil
36
32
  path = File.expand_path(path)
33
+ scratch = Berkshelf.mktmpdir
34
+
37
35
  FileUtils.mkdir_p(path)
38
36
 
39
- scratch = Berkshelf.mktmpdir
37
+ unless (ignore_file = Berkshelf::Chef::Cookbook::Chefignore.find_relative_to(Dir.pwd)).nil?
38
+ chefignore = Berkshelf::Chef::Cookbook::Chefignore.new(ignore_file)
39
+ end
40
+
40
41
  cookbooks.each do |cb|
41
42
  dest = File.join(scratch, cb.cookbook_name, "/")
42
43
  FileUtils.mkdir_p(dest)
@@ -73,10 +74,12 @@ module Berkshelf
73
74
  def_delegator :downloader, :add_location
74
75
  def_delegator :downloader, :locations
75
76
 
77
+ # @param [String] path
78
+ # path on disk to the file containing the contents of this Berksfile
76
79
  def initialize(path)
77
- @filepath = path
78
- @sources = Hash.new
79
- @downloader = Downloader.new(Berkshelf.cookbook_store)
80
+ @filepath = path
81
+ @sources = Hash.new
82
+ @downloader = Downloader.new(Berkshelf.cookbook_store)
80
83
  @cached_cookbooks = nil
81
84
  end
82
85
 
@@ -99,7 +102,8 @@ module Berkshelf
99
102
  # cookbook 'artifact', git: 'git://github.com/RiotGames/artifact-cookbook.git'
100
103
  #
101
104
  # @example a cookbook source that will be retrieved from a Chef API (Chef Server)
102
- # cookbook 'artifact', chef_api: 'https://api.opscode.com/organizations/vialstudios', node_name: 'reset', client_key: '/Users/reset/.chef/knife.rb'
105
+ # cookbook 'artifact', chef_api: 'https://api.opscode.com/organizations/vialstudios',
106
+ # node_name: 'reset', client_key: '/Users/reset/.chef/knife.rb'
103
107
  #
104
108
  # @example a cookbook source that will be retrieved from a Chef API using your Berkshelf config
105
109
  # cookbook 'artifact', chef_api: :config
@@ -225,7 +229,8 @@ module Berkshelf
225
229
  # chef_api :config
226
230
  #
227
231
  # @example using a URL, node_name, and client_key to add a Chef API default location
228
- # chef_api "https://api.opscode.com/organizations/vialstudios", node_name: "reset", client_key: "/Users/reset/.chef/knife.rb"
232
+ # chef_api "https://api.opscode.com/organizations/vialstudios", node_name: "reset",
233
+ # client_key: "/Users/reset/.chef/knife.rb"
229
234
  #
230
235
  # @param [String, Symbol] value
231
236
  # @param [Hash] options
@@ -252,7 +257,8 @@ module Berkshelf
252
257
  # Only raise an exception if the source is a true duplicate
253
258
  groups = (options[:group].nil? || options[:group].empty?) ? [:default] : options[:group]
254
259
  if !(@sources[name].groups & groups).empty?
255
- raise DuplicateSourceDefined, "Berksfile contains multiple sources named '#{name}'. Use only one, or put them in different groups."
260
+ raise DuplicateSourceDefined,
261
+ "Berksfile contains multiple sources named '#{name}'. Use only one, or put them in different groups."
256
262
  end
257
263
  end
258
264
 
@@ -393,7 +399,9 @@ module Berkshelf
393
399
  missing_cookbooks = (options[:cookbooks] - cookbooks.map(&:cookbook_name))
394
400
 
395
401
  unless missing_cookbooks.empty?
396
- raise Berkshelf::CookbookNotFound, "Could not find cookbooks #{missing_cookbooks.collect{|cookbook| "'#{cookbook}'"}.join(', ')} in any of the sources. #{missing_cookbooks.size == 1 ? 'Is it' : 'Are they' } in your Berksfile?"
402
+ msg = "Could not find cookbooks #{missing_cookbooks.collect{|cookbook| "'#{cookbook}'"}.join(', ')}"
403
+ msg << " in any of the sources. #{missing_cookbooks.size == 1 ? 'Is it' : 'Are they' } in your Berksfile?"
404
+ raise Berkshelf::CookbookNotFound, msg
397
405
  end
398
406
 
399
407
  update_lockfile(sources)
@@ -443,64 +451,86 @@ module Berkshelf
443
451
  outdated
444
452
  end
445
453
 
446
- # @option options [String] :server_url
447
- # URL to the Chef API
448
- # @option options [String] :client_name
449
- # name of the client used to authenticate with the Chef API
450
- # @option options [String] :client_key
451
- # filepath to the client's private key used to authenticate with
452
- # the Chef API
453
- # @option options [String] :organization
454
- # the Organization to connect to. This is only used if you are connecting to
455
- # private Chef or hosted Chef
456
- # @option options [Boolean] :force Upload the Cookbook even if the version
457
- # already exists and is frozen on the target Chef Server
458
- # @option options [Boolean] :freeze Freeze the uploaded Cookbook on the Chef
459
- # Server so that it cannot be overwritten
454
+ # @option options [Boolean] :force (false)
455
+ # Upload the Cookbook even if the version already exists and is frozen on the
456
+ # target Chef Server
457
+ # @option options [Boolean] :freeze (true)
458
+ # Freeze the uploaded Cookbook on the Chef Server so that it cannot be overwritten
460
459
  # @option options [Symbol, Array] :except
461
460
  # Group(s) to exclude which will cause any sources marked as a member of the
462
461
  # group to not be installed
463
462
  # @option options [Symbol, Array] :only
464
463
  # Group(s) to include which will cause any sources marked as a member of the
465
464
  # group to be installed and all others to be ignored
466
- # @option cookbooks [String, Array] :cookbooks
465
+ # @option options [String, Array] :cookbooks
467
466
  # Names of the cookbooks to retrieve sources for
468
- # @option options [Hash] :params
469
- # URI query unencoded key/value pairs
470
- # @option options [Hash] :headers
471
- # unencoded HTTP header key/value pairs
472
- # @option options [Hash] :request
473
- # request options
474
- # @option options [Hash] :ssl
475
- # SSL options
476
- # @option options [URI, String, Hash] :proxy
477
- # URI, String, or Hash of HTTP proxy options
467
+ # @option options [Hash] :ssl_verify (true)
468
+ # Disable/Enable SSL verification during uploads
469
+ # @option options [Boolean] :skip_dependencies (false)
470
+ # Skip uploading dependent cookbook(s).
471
+ # @option options [Boolean] :halt_on_frozen (false)
472
+ # Raise a FrozenCookbook error if one of the cookbooks being uploaded is already located
473
+ # on the remote Chef Server and frozen.
478
474
  #
479
475
  # @raise [UploadFailure] if you are uploading cookbooks with an invalid or not-specified client key
476
+ # @raise [Berkshelf::FrozenCookbook]
477
+ # if an attempt to upload a cookbook which has been frozen on the target server is made
478
+ # and the :halt_on_frozen option was true
480
479
  def upload(options = {})
481
- conn = Ridley.new(options)
482
- solution = resolve(options)
480
+ options = options.reverse_merge(
481
+ force: false,
482
+ freeze: true,
483
+ ssl_verify: Berkshelf::Config.instance.ssl.verify,
484
+ skip_dependencies: false,
485
+ halt_on_frozen: false
486
+ )
487
+
488
+ ridley_options = options.slice(:ssl)
489
+ ridley_options[:server_url] = Berkshelf::Config.instance.chef.chef_server_url
490
+ ridley_options[:client_name] = Berkshelf::Config.instance.chef.node_name
491
+ ridley_options[:client_key] = Berkshelf::Config.instance.chef.client_key
492
+ ridley_options[:ssl] = { verify: options[:ssl_verify] }
493
+
494
+ unless ridley_options[:server_url].present?
495
+ raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.server_url"
496
+ end
497
+
498
+ unless ridley_options[:client_name].present?
499
+ raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.node_name"
500
+ end
501
+
502
+ unless ridley_options[:client_key].present?
503
+ raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.client_key"
504
+ end
505
+
506
+ conn = Ridley.new(ridley_options)
507
+ solution = resolve(options)
508
+ upload_opts = options.slice(:force, :freeze)
483
509
 
484
510
  solution.each do |cb|
485
- upload_opts = options.dup
486
- upload_opts[:name] = cb.cookbook_name
511
+ Berkshelf.formatter.upload(cb.cookbook_name, cb.version, conn.server_url)
487
512
 
488
- Berkshelf.formatter.upload cb.cookbook_name, cb.version, upload_opts[:server_url]
489
- conn.cookbook.upload(cb.path, upload_opts)
513
+ begin
514
+ conn.cookbook.upload(cb.path, upload_opts.merge(name: cb.cookbook_name))
515
+ rescue Ridley::Errors::FrozenCookbook => ex
516
+ if options[:halt_on_frozen]
517
+ raise Berkshelf::FrozenCookbook, ex
518
+ end
519
+ end
490
520
  end
491
521
 
492
522
  if options[:skip_dependencies]
493
523
  missing_cookbooks = options.fetch(:cookbooks, nil) - solution.map(&:cookbook_name)
494
524
  unless missing_cookbooks.empty?
495
525
  msg = "Unable to upload cookbooks: #{missing_cookbooks.sort.join(', ')}\n"
496
- msg << "Specified cookbooks must be defined within the Berkshelf file when using the `--skip-dependencies` option"
526
+ msg << "Specified cookbooks must be defined within the Berkshelf file when using the"
527
+ msg << " `--skip-dependencies` option"
497
528
  raise ExplicitCookbookNotFound.new(msg)
498
529
  end
499
530
  end
500
- rescue Ridley::Errors::ClientKeyFileNotFound => e
501
- msg = "Could not upload cookbooks: Missing Chef client key: '#{Berkshelf::Config.instance.chef.client_key}'."
502
- msg << " Generate or update your Berkshelf configuration that contains a valid path to a Chef client key."
503
- raise UploadFailure, msg
531
+ rescue Ridley::Errors::RidleyError => ex
532
+ log_exception(ex)
533
+ raise UploadFailure, ex
504
534
  ensure
505
535
  conn.terminate if conn && conn.alive?
506
536
  end
@@ -515,32 +545,14 @@ module Berkshelf
515
545
  # group to be installed and all others to be ignored
516
546
  # @option cookbooks [String, Array] :cookbooks
517
547
  # Names of the cookbooks to retrieve sources for
548
+ # @option options [Boolean] :skip_dependencies
549
+ # Skip resolving of dependencies
518
550
  #
519
551
  # @return [Array<Berkshelf::CachedCookbooks]
520
552
  def resolve(options = {})
521
553
  resolver(options).resolve
522
554
  end
523
555
 
524
- # Builds a Resolver instance
525
- #
526
- # @option options [Symbol, Array] :except
527
- # Group(s) to exclude which will cause any sources marked as a member of the
528
- # group to not be installed
529
- # @option options [Symbol, Array] :only
530
- # Group(s) to include which will cause any sources marked as a member of the
531
- # group to be installed and all others to be ignored
532
- # @option cookbooks [String, Array] :cookbooks
533
- # Names of the cookbooks to retrieve sources for
534
- #
535
- # @return <Berkshelf::Resolver>
536
- def resolver(options={})
537
- Resolver.new(
538
- self.downloader,
539
- sources: sources(options),
540
- skip_dependencies: options[:skip_dependencies]
541
- )
542
- end
543
-
544
556
  # Reload this instance of Berksfile with the given content. The content
545
557
  # is a string that may contain terms from the included DSL.
546
558
  #
@@ -569,6 +581,28 @@ module Berkshelf
569
581
  File.exist?(Berkshelf::Lockfile::DEFAULT_FILENAME)
570
582
  end
571
583
 
584
+ # Builds a Resolver instance
585
+ #
586
+ # @option options [Symbol, Array] :except
587
+ # Group(s) to exclude which will cause any sources marked as a member of the
588
+ # group to not be installed
589
+ # @option options [Symbol, Array] :only
590
+ # Group(s) to include which will cause any sources marked as a member of the
591
+ # group to be installed and all others to be ignored
592
+ # @option options [String, Array] :cookbooks
593
+ # Names of the cookbooks to retrieve sources for
594
+ # @option options [Boolean] :skip_dependencies
595
+ # Skip resolving of dependencies
596
+ #
597
+ # @return <Berkshelf::Resolver>
598
+ def resolver(options = {})
599
+ Resolver.new(
600
+ self.downloader,
601
+ sources: sources(options),
602
+ skip_dependencies: options[:skip_dependencies]
603
+ )
604
+ end
605
+
572
606
  def write_lockfile(sources)
573
607
  Berkshelf::Lockfile.new(sources).write
574
608
  end
@@ -13,7 +13,7 @@ module Berkshelf
13
13
  cached_name = File.basename(path.to_s).slice(DIRNAME_REGEXP, 1)
14
14
  return nil if cached_name.nil?
15
15
 
16
- from_path(path, name: cached_name)
16
+ from_path(path, name: cached_name)
17
17
  end
18
18
  end
19
19
 
@@ -15,6 +15,21 @@ module Berkshelf::Chef::Cookbook
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  class Chefignore
18
+ class << self
19
+ # Traverse a path in relative context to find a Chefignore file
20
+ #
21
+ # @param [String] path
22
+ # path to traverse
23
+ #
24
+ # @return [String, nil]
25
+ def find_relative_to(path)
26
+ [
27
+ File.join(path, Berkshelf::Chef::Cookbook::Chefignore::FILENAME),
28
+ File.join(path, 'cookbooks', Berkshelf::Chef::Cookbook::Chefignore::FILENAME)
29
+ ].find { |f| File.exists?(f) }
30
+ end
31
+ end
32
+
18
33
  FILENAME = "chefignore".freeze
19
34
  COMMENTS_AND_WHITESPACE = /^\s*(?:#.*)?$/
20
35
 
@@ -31,7 +31,7 @@ module Berkshelf
31
31
  end
32
32
 
33
33
  if @options[:debug]
34
- Berkshelf.log.level = ::Logger::DEBUG
34
+ Berkshelf.logger.level = ::Logger::DEBUG
35
35
  end
36
36
 
37
37
  if @options[:quiet]
@@ -199,53 +199,39 @@ module Berkshelf
199
199
  type: :array,
200
200
  desc: "Only cookbooks that are in these groups.",
201
201
  aliases: "-o"
202
- method_option :freeze,
202
+ method_option :no_freeze,
203
203
  type: :boolean,
204
204
  default: false,
205
- desc: "Freeze the uploaded cookbooks so that they cannot be overwritten"
206
- option :force,
205
+ desc: "Do not freeze uploaded cookbook(s)."
206
+ method_option :force,
207
207
  type: :boolean,
208
208
  default: false,
209
- desc: "Upload cookbook(s) even if a frozen one exists on the target Chef Server"
210
- option :ssl_verify,
209
+ desc: "Upload all cookbook(s) even if a frozen one exists on the Chef Server."
210
+ method_option :ssl_verify,
211
211
  type: :boolean,
212
212
  default: nil,
213
- desc: "Disable/Enable SSL verification when uploading cookbooks"
214
- option :skip_syntax_check,
213
+ desc: "Disable/Enable SSL verification when uploading cookbooks."
214
+ method_option :skip_syntax_check,
215
215
  type: :boolean,
216
216
  default: false,
217
- desc: "Skip Ruby syntax check when uploading cookbooks",
217
+ desc: "Skip Ruby syntax check when uploading cookbooks.",
218
218
  aliases: "-s"
219
- option :skip_dependencies,
219
+ method_option :skip_dependencies,
220
+ type: :boolean,
221
+ desc: "Skip uploading dependent cookbook(s).",
222
+ default: false,
223
+ aliases: "-D"
224
+ method_option :halt_on_frozen,
220
225
  type: :boolean,
221
- desc: 'Do not upload dependencies',
222
226
  default: false,
223
- aliases: '-D'
227
+ desc: "Halt uploading and exit if the Chef Server has a frozen version of the cookbook(s)."
224
228
  desc "upload [COOKBOOKS]", "Upload cookbook(s) specified by a Berksfile to the configured Chef Server."
225
229
  def upload(*cookbook_names)
226
230
  berksfile = ::Berkshelf::Berksfile.from_file(options[:berksfile])
227
231
 
228
- unless Berkshelf::Config.instance.chef.chef_server_url.present?
229
- msg = "Could not upload cookbooks: Missing Chef server_url."
230
- msg << " Generate or update your Berkshelf configuration that contains a valid Chef Server URL."
231
- raise UploadFailure, msg
232
- end
233
-
234
- unless Berkshelf::Config.instance.chef.node_name.present?
235
- msg = "Could not upload cookbooks: Missing Chef node_name."
236
- msg << " Generate or update your Berkshelf configuration that contains a valid Chef node_name."
237
- raise UploadFailure, msg
238
- end
239
-
240
- upload_options = {
241
- server_url: Berkshelf::Config.instance.chef.chef_server_url,
242
- client_name: Berkshelf::Config.instance.chef.node_name,
243
- client_key: Berkshelf::Config.instance.chef.client_key,
244
- ssl: {
245
- verify: (options[:ssl_verify].nil? ? Berkshelf::Config.instance.ssl.verify : options[:ssl_verify])
246
- },
247
- cookbooks: cookbook_names
248
- }.merge(options).symbolize_keys
232
+ upload_options = Hash[options.except(:no_freeze, :berksfile)].symbolize_keys
233
+ upload_options[:cookbooks] = cookbook_names
234
+ upload_options[:freeze] = false if options[:no_freeze]
249
235
 
250
236
  berksfile.upload(upload_options)
251
237
  end
@@ -57,6 +57,11 @@ module Berkshelf
57
57
  raise InternalError, "Invalid options for Cookbook Source: #{invalid_options.join(', ')}."
58
58
  end
59
59
 
60
+ if (options.keys & [:site, :path, :git]).size > 1
61
+ invalid = (options.keys & [:site, :path, :git]).map { |opt| "'#{opt}" }
62
+ raise InternalError, "Cannot specify #{invalid.to_sentence} for a Cookbook Source!"
63
+ end
64
+
60
65
  true
61
66
  end
62
67
  end
@@ -64,13 +69,10 @@ module Berkshelf
64
69
  extend Forwardable
65
70
 
66
71
  attr_reader :name
72
+ attr_reader :options
67
73
  attr_reader :version_constraint
68
- attr_reader :groups
69
- attr_reader :location
70
74
  attr_accessor :cached_cookbook
71
75
 
72
- def_delegator :cached_cookbook, :version, :locked_version
73
-
74
76
  # @param [String] name
75
77
  # @param [Hash] options
76
78
  #
@@ -92,52 +94,70 @@ module Berkshelf
92
94
  # same as tag
93
95
  # @option options [String] :locked_version
94
96
  def initialize(name, options = {})
95
- @name = name
96
- @version_constraint = Solve::Constraint.new(options[:constraint] || ">= 0.0.0")
97
- @groups = []
98
- @cached_cookbook = nil
99
- @location = nil
100
-
101
97
  self.class.validate_options(options)
102
98
 
103
- unless (options.keys & self.class.location_keys.keys).empty?
104
- @location = Location.init(name, version_constraint, options)
105
- end
106
-
107
- if @location.is_a?(PathLocation)
108
- begin
109
- @cached_cookbook = CachedCookbook.from_path(location.path)
110
- rescue IOError
111
- raise Berkshelf::CookbookNotFound
112
- end
113
- end
114
-
99
+ @name = name
115
100
  @locked_version = Solve::Version.new(options[:locked_version]) if options[:locked_version]
101
+ @version_constraint = Solve::Constraint.new(options[:locked_version] || options[:constraint] || ">= 0.0.0")
102
+
103
+ @cached_cookbook, @location = cached_and_location(options)
116
104
 
117
105
  add_group(options[:group]) if options[:group]
118
106
  add_group(:default) if groups.empty?
119
107
  end
120
108
 
121
- def add_group(*groups)
122
- groups = groups.first if groups.first.is_a?(Array)
123
- groups.each do |group|
109
+ def add_group(*local_groups)
110
+ local_groups = local_groups.first if local_groups.first.is_a?(Array)
111
+
112
+ local_groups.each do |group|
124
113
  group = group.to_sym
125
- @groups << group unless @groups.include?(group)
114
+ groups << group unless groups.include?(group)
126
115
  end
127
116
  end
128
117
 
129
118
  # Returns true if the cookbook source has already been downloaded. A cookbook
130
- # source is downloaded when a cached cookbooked is present.
119
+ # source is downloaded when a cached cookbook is present.
131
120
  #
132
121
  # @return [Boolean]
133
122
  def downloaded?
134
123
  !self.cached_cookbook.nil?
135
124
  end
136
125
 
126
+ # Returns true if this CookbookSource has the given group.
127
+ #
128
+ # @return [Boolean]
137
129
  def has_group?(group)
138
130
  groups.include?(group.to_sym)
139
131
  end
140
132
 
133
+ # Get the locked version of this cookbook. First check the instance variable
134
+ # and then resort to the cached_cookbook for the version.
135
+ #
136
+ # This was formerly a delegator, but it would fail if the `@cached_cookbook`
137
+ # was nil or undefined.
138
+ #
139
+ # @return [Solve::Version, nil]
140
+ # the locked version of this cookbook
141
+ def locked_version
142
+ @locked_version ||= cached_cookbook.try(:version)
143
+ end
144
+
145
+ # The location for this CookbookSource, such as a remote Chef Server, the
146
+ # community API, :git, or a :path location. By default, this will be the
147
+ # community API.
148
+ #
149
+ # @return [Berkshelf::Location]
150
+ def location
151
+ @location
152
+ end
153
+
154
+ # The list of groups this CookbookSource belongs to.
155
+ #
156
+ # @return [Array<Symbol>]
157
+ def groups
158
+ @groups ||= []
159
+ end
160
+
141
161
  def to_s
142
162
  msg = "#{self.name} (#{self.version_constraint}) groups: #{self.groups}"
143
163
  msg << " location: #{self.location}" if self.location
@@ -155,5 +175,69 @@ module Berkshelf
155
175
  def to_json
156
176
  MultiJson.dump(self.to_hash, pretty: true)
157
177
  end
178
+
179
+ private
180
+
181
+ # Determine the CachedCookbook and Location information from the given options.
182
+ #
183
+ # @return [Array<CachedCookbook, Location>]
184
+ def cached_and_location(options = {})
185
+ from_path(options) || from_cache(options) || from_default(options)
186
+ end
187
+
188
+ # Attempt to load a CachedCookbook from a local file system path (if the :path
189
+ # option was given). If one is found, the location and cached_cookbook is
190
+ # updated. Otherwise, this method will raise a CookbookNotFound exception.
191
+ #
192
+ # @raises [Berkshelf::CookbookNotFound]
193
+ # if no CachedCookbook exists at the given path
194
+ #
195
+ # @return [Berkshelf::CachedCookbook]
196
+ def from_path(options = {})
197
+ return nil unless options[:path]
198
+
199
+ location = PathLocation.new(name, version_constraint, path: options[:path])
200
+ cached = CachedCookbook.from_path(location.path)
201
+
202
+ [cached, location]
203
+ rescue IOError
204
+ raise Berkshelf::CookbookNotFound
205
+ end
206
+
207
+ # Attempt to load a CachedCookbook from the local CookbookStore. This will save
208
+ # the need to make an http request to download a cookbook we already have cached
209
+ # locally.
210
+ #
211
+ # @return [Berkshelf::CachedCookbook, nil]
212
+ def from_cache(options = {})
213
+ path = File.join(Berkshelf.cookbooks_dir, filename(options))
214
+ return nil unless File.exists?(path)
215
+
216
+ location = PathLocation.new(name, version_constraint, path: path)
217
+ cached = CachedCookbook.from_path(path, name: name)
218
+
219
+ [cached, location]
220
+ end
221
+
222
+ # Use the default location, and a nil CachedCookbook. If there is no location
223
+ # specified,
224
+ #
225
+ # @return [Array<nil, Location>]
226
+ def from_default(options = {})
227
+ if (options.keys & self.class.location_keys.keys).empty?
228
+ location = nil
229
+ else
230
+ location = Location.init(name, version_constraint, options)
231
+ end
232
+
233
+ [nil, location]
234
+ end
235
+
236
+ # The hypothetical location of this CachedCookbook, if it were to exist.
237
+ #
238
+ # @return [String]
239
+ def filename(options = {})
240
+ "#{name}-#{options[:locked_version]}"
241
+ end
158
242
  end
159
243
  end