berkshelf 1.3.1 → 1.4.0.rc1

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.
@@ -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