berkshelf 3.0.0.beta6 → 3.0.0.beta7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/features/berksfile.feature +2 -0
  3. data/features/commands/apply.feature +1 -1
  4. data/features/commands/contingent.feature +5 -3
  5. data/features/commands/install.feature +40 -40
  6. data/features/commands/list.feature +42 -20
  7. data/features/commands/outdated.feature +60 -16
  8. data/features/commands/show.feature +51 -8
  9. data/features/commands/update.feature +43 -15
  10. data/features/commands/upload.feature +4 -1
  11. data/features/commands/vendor.feature +27 -0
  12. data/features/json_formatter.feature +20 -8
  13. data/features/lockfile.feature +192 -71
  14. data/generator_files/CHANGELOG.md.erb +5 -0
  15. data/lib/berkshelf/berksfile.rb +166 -139
  16. data/lib/berkshelf/cli.rb +33 -30
  17. data/lib/berkshelf/cookbook_generator.rb +1 -0
  18. data/lib/berkshelf/dependency.rb +64 -14
  19. data/lib/berkshelf/downloader.rb +7 -10
  20. data/lib/berkshelf/errors.rb +59 -11
  21. data/lib/berkshelf/formatters/human_readable.rb +23 -36
  22. data/lib/berkshelf/formatters/json.rb +25 -29
  23. data/lib/berkshelf/installer.rb +111 -122
  24. data/lib/berkshelf/locations/git_location.rb +22 -9
  25. data/lib/berkshelf/locations/mercurial_location.rb +20 -5
  26. data/lib/berkshelf/locations/path_location.rb +22 -7
  27. data/lib/berkshelf/lockfile.rb +435 -203
  28. data/lib/berkshelf/resolver.rb +4 -2
  29. data/lib/berkshelf/source.rb +10 -1
  30. data/lib/berkshelf/version.rb +1 -1
  31. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +3 -4
  32. data/spec/fixtures/lockfiles/2.0.lock +17 -0
  33. data/spec/fixtures/lockfiles/blank.lock +0 -0
  34. data/spec/fixtures/lockfiles/default.lock +18 -10
  35. data/spec/fixtures/lockfiles/empty.lock +3 -0
  36. data/spec/unit/berkshelf/berksfile_spec.rb +31 -74
  37. data/spec/unit/berkshelf/cookbook_generator_spec.rb +4 -0
  38. data/spec/unit/berkshelf/installer_spec.rb +4 -7
  39. data/spec/unit/berkshelf/lockfile_parser_spec.rb +124 -0
  40. data/spec/unit/berkshelf/lockfile_spec.rb +140 -163
  41. metadata +11 -6
  42. data/features/licenses.feature +0 -79
  43. data/features/step_definitions/lockfile_steps.rb +0 -57
@@ -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)