berkshelf 3.0.0.beta6 → 3.0.0.beta7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/features/berksfile.feature +2 -0
  3. data/features/commands/apply.feature +1 -1
  4. data/features/commands/contingent.feature +5 -3
  5. data/features/commands/install.feature +40 -40
  6. data/features/commands/list.feature +42 -20
  7. data/features/commands/outdated.feature +60 -16
  8. data/features/commands/show.feature +51 -8
  9. data/features/commands/update.feature +43 -15
  10. data/features/commands/upload.feature +4 -1
  11. data/features/commands/vendor.feature +27 -0
  12. data/features/json_formatter.feature +20 -8
  13. data/features/lockfile.feature +192 -71
  14. data/generator_files/CHANGELOG.md.erb +5 -0
  15. data/lib/berkshelf/berksfile.rb +166 -139
  16. data/lib/berkshelf/cli.rb +33 -30
  17. data/lib/berkshelf/cookbook_generator.rb +1 -0
  18. data/lib/berkshelf/dependency.rb +64 -14
  19. data/lib/berkshelf/downloader.rb +7 -10
  20. data/lib/berkshelf/errors.rb +59 -11
  21. data/lib/berkshelf/formatters/human_readable.rb +23 -36
  22. data/lib/berkshelf/formatters/json.rb +25 -29
  23. data/lib/berkshelf/installer.rb +111 -122
  24. data/lib/berkshelf/locations/git_location.rb +22 -9
  25. data/lib/berkshelf/locations/mercurial_location.rb +20 -5
  26. data/lib/berkshelf/locations/path_location.rb +22 -7
  27. data/lib/berkshelf/lockfile.rb +435 -203
  28. data/lib/berkshelf/resolver.rb +4 -2
  29. data/lib/berkshelf/source.rb +10 -1
  30. data/lib/berkshelf/version.rb +1 -1
  31. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +3 -4
  32. data/spec/fixtures/lockfiles/2.0.lock +17 -0
  33. data/spec/fixtures/lockfiles/blank.lock +0 -0
  34. data/spec/fixtures/lockfiles/default.lock +18 -10
  35. data/spec/fixtures/lockfiles/empty.lock +3 -0
  36. data/spec/unit/berkshelf/berksfile_spec.rb +31 -74
  37. data/spec/unit/berkshelf/cookbook_generator_spec.rb +4 -0
  38. data/spec/unit/berkshelf/installer_spec.rb +4 -7
  39. data/spec/unit/berkshelf/lockfile_parser_spec.rb +124 -0
  40. data/spec/unit/berkshelf/lockfile_spec.rb +140 -163
  41. metadata +11 -6
  42. data/features/licenses.feature +0 -79
  43. data/features/step_definitions/lockfile_steps.rb +0 -57
@@ -0,0 +1,5 @@
1
+ # <%= name %> cookbook CHANGELOG
2
+ This file is used to list changes made in each version of the <%= name %> cookbook.
3
+
4
+ ## v0.1.0 (<%= Time.now.strftime('%Y-%m-%d') %>)
5
+ - Initial release of <%= name %>
@@ -3,22 +3,23 @@ require_relative "packager"
3
3
  module Berkshelf
4
4
  class Berksfile
5
5
  class << self
6
- # The sources to use if no sources are explicitly provided
6
+ # Instantiate a Berksfile from the given options. This method is used
7
+ # heavily by the CLI to reduce duplication.
7
8
  #
8
- # @return [Array<Berkshelf::Source>]
9
- def default_sources
10
- @default_sources ||= [ Source.new(DEFAULT_API_URL) ]
9
+ # @param (see Berksfile#initialize)
10
+ def from_options(options = {})
11
+ from_file(options[:berksfile], options.slice(:except, :only))
11
12
  end
12
13
 
13
14
  # @param [#to_s] file
14
15
  # a path on disk to a Berksfile to instantiate from
15
16
  #
16
17
  # @return [Berksfile]
17
- def from_file(file)
18
+ def from_file(file, options = {})
18
19
  raise BerksfileNotFound.new(file) unless File.exist?(file)
19
20
 
20
21
  begin
21
- new(file).dsl_eval_file(file)
22
+ new(file, options).dsl_eval_file(file)
22
23
  rescue => ex
23
24
  raise BerksfileReadError.new(ex)
24
25
  end
@@ -38,18 +39,37 @@ module Berkshelf
38
39
  expose_method :cookbook
39
40
  expose_method :group
40
41
 
41
- @@active_group = nil
42
-
43
42
  # @return [String]
44
43
  # The path on disk to the file representing this instance of Berksfile
45
44
  attr_reader :filepath
46
45
 
46
+ # Create a new Berksfile object.
47
+ #
47
48
  # @param [String] path
48
49
  # path on disk to the file containing the contents of this Berksfile
49
- def initialize(path)
50
+ #
51
+ # @option options [Symbol, Array<String>] :except
52
+ # Group(s) to exclude which will cause any dependencies marked as a member of the
53
+ # group to not be installed
54
+ # @option options [Symbol, Array<String>] :only
55
+ # Group(s) to include which will cause any dependencies marked as a member of the
56
+ # group to be installed and all others to be ignored
57
+ def initialize(path, options = {})
50
58
  @filepath = path
51
59
  @dependencies = Hash.new
52
- @sources = Array.new
60
+ @sources = Hash.new
61
+
62
+ if options[:except] && options[:only]
63
+ raise Berkshelf::ArgumentError, 'Cannot specify both :except and :only!'
64
+ elsif options[:except]
65
+ except = Array(options[:except]).collect(&:to_sym)
66
+ @filter = ->(dependency) { (except & dependency.groups).empty? }
67
+ elsif options[:only]
68
+ only = Array(options[:only]).collect(&:to_sym)
69
+ @filter = ->(dependency) { !(only & dependency.groups).empty? }
70
+ else
71
+ @filter = ->(dependency) { true }
72
+ end
53
73
  end
54
74
 
55
75
  # Add a cookbook dependency to the Berksfile to be retrieved and have its dependencies recursively retrieved
@@ -96,17 +116,17 @@ module Berkshelf
96
116
  options[:path] &&= File.expand_path(options[:path], File.dirname(filepath))
97
117
  options[:group] = Array(options[:group])
98
118
 
99
- if @@active_group
100
- options[:group] += @@active_group
119
+ if @active_group
120
+ options[:group] += @active_group
101
121
  end
102
122
 
103
123
  add_dependency(name, constraint, options)
104
124
  end
105
125
 
106
126
  def group(*args)
107
- @@active_group = args
127
+ @active_group = args
108
128
  yield
109
- @@active_group = nil
129
+ @active_group = nil
110
130
  end
111
131
 
112
132
  # Use a Cookbook metadata file to determine additional cookbook dependencies to retrieve. All
@@ -143,13 +163,22 @@ module Berkshelf
143
163
  #
144
164
  # @return [Array<Berkshelf::Source>]
145
165
  def source(api_url)
146
- new_source = Source.new(api_url)
147
- @sources.push(new_source) unless @sources.include?(new_source)
166
+ @sources[api_url] = Source.new(api_url)
148
167
  end
149
168
 
150
169
  # @return [Array<Berkshelf::Source>]
151
170
  def sources
152
- @sources.empty? ? self.class.default_sources : @sources
171
+ if @sources.empty?
172
+ raise NoAPISourcesDefined
173
+ else
174
+ @sources.values
175
+ end
176
+ end
177
+
178
+ # @param [Dependency] dependency
179
+ # the dependency to find the source for
180
+ def source_for(name, version)
181
+ sources.find { |source| source.cookbook(name, version) }
153
182
  end
154
183
 
155
184
  # @todo remove in Berkshelf 4.0
@@ -232,36 +261,27 @@ module Berkshelf
232
261
  @dependencies.has_key?(dependency.to_s)
233
262
  end
234
263
 
235
- # @option options [Symbol, Array] :except
236
- # Group(s) to exclude which will cause any dependencies marked as a member of the
237
- # group to not be installed
238
- # @option options [Symbol, Array] :only
239
- # Group(s) to include which will cause any dependencies marked as a member of the
240
- # group to be installed and all others to be ignored
241
- # @option cookbooks [String, Array] :cookbooks
242
- # Names of the cookbooks to retrieve dependencies for
243
- #
264
+
244
265
  # @return [Array<Berkshelf::Dependency>]
245
- def dependencies(options = {})
246
- cookbooks = Array(options[:cookbooks])
247
- except = Array(options[:except]).collect(&:to_sym)
248
- only = Array(options[:only]).collect(&:to_sym)
249
-
250
- case
251
- when !except.empty? && !only.empty?
252
- raise Berkshelf::ArgumentError, 'Cannot specify both :except and :only'
253
- when !cookbooks.empty?
254
- if !except.empty? && !only.empty?
255
- Berkshelf.ui.warn 'Cookbooks were specified, ignoring :except and :only'
256
- end
257
- @dependencies.values.select { |dependency| cookbooks.include?(dependency.name) }
258
- when !except.empty?
259
- @dependencies.values.select { |dependency| (except & dependency.groups).empty? }
260
- when !only.empty?
261
- @dependencies.values.select { |dependency| !(only & dependency.groups).empty? }
262
- else
263
- @dependencies.values
264
- end
266
+ def dependencies
267
+ @dependencies.values.sort.select(&@filter)
268
+ end
269
+
270
+ #
271
+ # Behaves the same as {Berksfile#dependencies}, but this method returns an
272
+ # array of CachedCookbook objects instead of dependency objects. This method
273
+ # relies on the {Berksfile#retrieve_locked} method to load the proper
274
+ # cached cookbook from the Berksfile + lockfile combination.
275
+ #
276
+ # @see [Berksfile#dependencies]
277
+ # for a description of the +options+ hash
278
+ # @see [Berksfile#retrieve_locked]
279
+ # for a list of possible exceptions that might be raised and why
280
+ #
281
+ # @return [Array<CachedCookbook>]
282
+ #
283
+ def cookbooks
284
+ dependencies.map { |dependency| retrieve_locked(dependency) }
265
285
  end
266
286
 
267
287
  # Find a dependency defined in this berksfile by name.
@@ -336,36 +356,30 @@ module Berkshelf
336
356
  #
337
357
  # 3. Write out a new lockfile.
338
358
  #
339
- # @option options [Symbol, Array] :except
340
- # Group(s) to exclude which will cause any dependencies marked as a member of the
341
- # group to not be installed
342
- # @option options [Symbol, Array] :only
343
- # Group(s) to include which will cause any dependencies marked as a member of the
344
- # group to be installed and all others to be ignored
345
- # @option cookbooks [String, Array] :cookbooks
346
- # Names of the cookbooks to retrieve dependencies for
347
- #
348
359
  # @raise [Berkshelf::OutdatedDependency]
349
360
  # if the lockfile constraints do not satisfy the Berksfile constraints
350
361
  #
351
362
  # @return [Array<Berkshelf::CachedCookbook>]
352
- def install(options = {})
353
- Installer.new(self).run(options)
363
+ def install
364
+ Installer.new(self).run
354
365
  end
355
366
 
356
- # @option options [Symbol, Array] :except
357
- # Group(s) to exclude which will cause any dependencies marked as a member of the
358
- # group to not be installed
359
- # @option options [Symbol, Array] :only
360
- # Group(s) to include which will cause any dependencies marked as a member of the
361
- # group to be installed and all others to be ignored
362
- # @option cookbooks [String, Array] :cookbooks
367
+ # Update the given set of dependencies (or all if no names are given).
368
+ #
369
+ # @option options [String, Array<String>] :cookbooks
363
370
  # Names of the cookbooks to retrieve dependencies for
364
- def update(options = {})
365
- validate_cookbook_names!(options)
371
+ def update(*names)
372
+ validate_cookbook_names!(names)
373
+
374
+ # Calculate the list of cookbooks to unlock
375
+ if names.empty?
376
+ list = dependencies
377
+ else
378
+ list = dependencies.select { |dependency| names.include?(dependency.name) }
379
+ end
366
380
 
367
381
  # Unlock any/all specified cookbooks
368
- dependencies(options).each { |dependency| lockfile.unlock(dependency) }
382
+ list.each { |dependency| lockfile.unlock(dependency) }
369
383
 
370
384
  # NOTE: We intentionally do NOT pass options to the installer
371
385
  self.install
@@ -386,22 +400,7 @@ module Berkshelf
386
400
  # @return [CachedCookbook]
387
401
  # the CachedCookbook that corresponds to the given name parameter
388
402
  def retrieve_locked(dependency)
389
- locked = lockfile.find(dependency.name)
390
-
391
- unless locked
392
- raise CookbookNotFound, "Could not find cookbook '#{dependency.to_s}'."\
393
- " Try running `berks install` to download and install the missing"\
394
- " dependencies."
395
- end
396
-
397
- unless locked.downloaded?
398
- raise CookbookNotFound, "Could not find cookbook '#{locked.to_s}'."\
399
- " Try running `berks install` to download and install the missing"\
400
- " dependencies."
401
- end
402
-
403
- @dependencies[dependency.name] = locked
404
- locked.cached_cookbook
403
+ lockfile.retrieve(dependency)
405
404
  end
406
405
 
407
406
  # The cached cookbooks installed by this Berksfile.
@@ -414,52 +413,54 @@ module Berkshelf
414
413
  # @return [Hash<Berkshelf::Dependency, Berkshelf::CachedCookbook>]
415
414
  # the list of dependencies as keys and the cached cookbook as the value
416
415
  def list
417
- Hash[*dependencies.sort.collect { |dependency| [dependency, retrieve_locked(dependency)] }.flatten]
416
+ validate_lockfile_present!
417
+ validate_lockfile_trusted!
418
+ validate_dependencies_installed!
419
+
420
+ lockfile.graph.locks.values
418
421
  end
419
422
 
420
- # List of all the cookbooks which have a newer version found at a source that satisfies
421
- # the constraints of your dependencies
422
- #
423
- # @option options [Symbol, Array] :except
424
- # Group(s) to exclude which will cause any dependencies marked as a member of the
425
- # group to not be installed
426
- # @option options [Symbol, Array] :only
427
- # Group(s) to include which will cause any dependencies marked as a member of the
428
- # group to be installed and all others to be ignored
429
- # @option cookbooks [String, Array] :cookbooks
430
- # Whitelist of cookbooks to to check for updated versions for
423
+ # List of all the cookbooks which have a newer version found at a source
424
+ # that satisfies the constraints of your dependencies.
431
425
  #
432
426
  # @return [Hash]
433
- # a hash of cached cookbooks and their latest version. An empty hash is returned
434
- # if there are no newer cookbooks for any of your dependencies
427
+ # a hash of cached cookbooks and their latest version. An empty hash is
428
+ # returned if there are no newer cookbooks for any of your dependencies
435
429
  #
436
430
  # @example
437
431
  # berksfile.outdated #=> {
438
432
  # #<CachedCookbook name="artifact"> => "0.11.2"
439
433
  # }
440
- def outdated(options = {})
441
- validate_cookbook_names!(options)
442
-
443
- outdated = {}
444
- dependencies(options).each do |dependency|
445
- locked = retrieve_locked(dependency)
446
- outdated[dependency.name] = {}
434
+ def outdated(*names)
435
+ validate_lockfile_present!
436
+ validate_lockfile_trusted!
437
+ validate_dependencies_installed!
438
+ validate_cookbook_names!(names)
439
+
440
+ # Calculate the list of cookbooks to unlock
441
+ if names.empty?
442
+ list = dependencies
443
+ else
444
+ list = dependencies.select { |dependency| names.include?(dependency.name) }
445
+ end
447
446
 
447
+ lockfile.graph.locks.inject({}) do |hash, (name, dependency)|
448
448
  sources.each do |source|
449
- cookbooks = source.versions(dependency.name)
449
+ cookbooks = source.versions(name)
450
450
 
451
451
  latest = cookbooks.select do |cookbook|
452
452
  dependency.version_constraint.satisfies?(cookbook.version) &&
453
- cookbook.version != locked.version
453
+ cookbook.version.to_s != dependency.locked_version.to_s
454
454
  end.sort_by { |cookbook| cookbook.version }.last
455
455
 
456
456
  unless latest.nil?
457
- outdated[dependency.name][source.uri.to_s] = latest
457
+ hash[name] ||= {}
458
+ hash[name][source.uri.to_s] = latest
458
459
  end
459
460
  end
460
- end
461
461
 
462
- outdated.reject { |name, newer| newer.empty? }
462
+ hash
463
+ end
463
464
  end
464
465
 
465
466
  # Upload the cookbooks installed by this Berksfile
@@ -469,12 +470,6 @@ module Berkshelf
469
470
  # target Chef Server
470
471
  # @option options [Boolean] :freeze (true)
471
472
  # Freeze the uploaded Cookbook on the Chef Server so that it cannot be overwritten
472
- # @option options [Symbol, Array] :except
473
- # Group(s) to exclude which will cause any dependencies marked as a member of the
474
- # group to not be installed
475
- # @option options [Symbol, Array] :only
476
- # Group(s) to include which will cause any dependencies marked as a member of the
477
- # group to be installed and all others to be ignored
478
473
  # @option options [String, Array] :cookbooks
479
474
  # Names of the cookbooks to retrieve dependencies for
480
475
  # @option options [Hash] :ssl_verify (true)
@@ -506,9 +501,9 @@ module Berkshelf
506
501
  validate: true
507
502
  }.merge(options)
508
503
 
509
- validate_cookbook_names!(options)
504
+ validate_cookbook_names!(options[:cookbooks])
510
505
 
511
- cached_cookbooks = install(options)
506
+ cached_cookbooks = install
512
507
  cached_cookbooks = filter_to_upload(cached_cookbooks, options[:cookbooks]) if options[:cookbooks]
513
508
  do_upload(cached_cookbooks, options)
514
509
  end
@@ -520,24 +515,17 @@ module Berkshelf
520
515
  # @param [String] path
521
516
  # the path where the tarball will be created
522
517
  #
523
- # @option options [Symbol, Array] :except
524
- # Group(s) to exclude which will cause any dependencies marked as a member of the
525
- # group to not be installed
526
- # @option options [Symbol, Array] :only
527
- # Group(s) to include which will cause any dependencies marked as a member of the
528
- # group to be installed and all others to be ignored
529
- #
530
518
  # @raise [Berkshelf::PackageError]
531
519
  #
532
520
  # @return [String]
533
521
  # the path to the package
534
- def package(path, options = {})
522
+ def package(path)
535
523
  packager = Packager.new(path)
536
524
  packager.validate!
537
525
 
538
526
  outdir = Dir.mktmpdir do |temp_dir|
539
527
  source = Berkshelf.ui.mute do
540
- vendor(File.join(temp_dir, "cookbooks"), options.slice(:only, :except))
528
+ vendor(File.join(temp_dir, 'cookbooks'))
541
529
  end
542
530
  packager.run(source)
543
531
  end
@@ -552,16 +540,9 @@ module Berkshelf
552
540
  # @param [String] destination
553
541
  # filepath to vendor cookbooks to
554
542
  #
555
- # @option options [Symbol, Array] :except
556
- # Group(s) to exclude which will cause any dependencies marked as a member of the
557
- # group to not be installed
558
- # @option options [Symbol, Array] :only
559
- # Group(s) to include which will cause any dependencies marked as a member of the
560
- # group to be installed and all others to be ignored
561
- #
562
543
  # @return [String, nil]
563
544
  # the expanded path cookbooks were vendored to or nil if nothing was vendored
564
- def vendor(destination, options = {})
545
+ def vendor(destination)
565
546
  destination = File.expand_path(destination)
566
547
 
567
548
  if Dir.exist?(destination)
@@ -569,9 +550,12 @@ module Berkshelf
569
550
  "different filepath."
570
551
  end
571
552
 
553
+ # Ensure the parent directory exists, in case a nested path was given
554
+ FileUtils.mkdir_p(File.expand_path(File.join(destination, '..')))
555
+
572
556
  scratch = Berkshelf.mktmpdir
573
557
  chefignore = nil
574
- cached_cookbooks = install(options.slice(:except, :only))
558
+ cached_cookbooks = install
575
559
 
576
560
  return nil if cached_cookbooks.empty?
577
561
 
@@ -686,15 +670,58 @@ module Berkshelf
686
670
  cookbooks
687
671
  end
688
672
 
673
+ # Ensure the lockfile is present on disk.
674
+ #
675
+ # @raise [LockfileNotFound]
676
+ # if the lockfile does not exist on disk
677
+ #
678
+ # @return [true]
679
+ def validate_lockfile_present!
680
+ raise LockfileNotFound unless lockfile.present?
681
+ true
682
+ end
683
+
684
+ # Ensure that all dependencies defined in the Berksfile exist in this
685
+ # lockfile.
686
+ #
687
+ # @raise [LockfileOutOfSync]
688
+ # if there are dependencies specified in the Berksfile which do not
689
+ # exist (or are not satisifed by) the lockfile
690
+ #
691
+ # @return [true]
692
+ def validate_lockfile_trusted!
693
+ raise LockfileOutOfSync unless lockfile.trusted?
694
+ true
695
+ end
696
+
697
+ # Ensure that all dependencies in the lockfile are installed on this
698
+ # system. You should validate that the lockfile can be trusted before
699
+ # using this method.
700
+ #
701
+ # @raise [DependencyNotInstalled]
702
+ # if the dependency in the lockfile is not in the Berkshelf shelf on
703
+ # this system
704
+ #
705
+ # @return [true]
706
+ def validate_dependencies_installed!
707
+ lockfile.graph.locks.each do |_, dependency|
708
+ unless dependency.downloaded?
709
+ raise DependencyNotInstalled.new(dependency)
710
+ end
711
+ end
712
+
713
+ true
714
+ end
715
+
689
716
  # Determine if any cookbooks were specified that aren't in our shelf.
690
717
  #
691
- # @option options [Array<String>] :cookbooks
692
- # a list of strings of cookbook names
718
+ # @param [Array<String>] names
719
+ # a list of cookbook names
693
720
  #
694
721
  # @raise [Berkshelf::DependencyNotFound]
695
722
  # if a cookbook name is given that does not exist
696
- def validate_cookbook_names!(options = {})
697
- missing = (Array(options[:cookbooks]) - dependencies.map(&:name))
723
+ def validate_cookbook_names!(names)
724
+ missing = names - dependencies.map(&:name)
698
725
 
699
726
  unless missing.empty?
700
727
  raise Berkshelf::DependencyNotFound.new(missing)