berkshelf 1.4.6 → 2.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +1 -5
  3. data/CONTRIBUTING.md +3 -1
  4. data/Gemfile +11 -1
  5. data/README.md +16 -0
  6. data/Thorfile +3 -1
  7. data/berkshelf.gemspec +26 -38
  8. data/features/apply_command.feature +32 -0
  9. data/features/configure_command.feature +31 -0
  10. data/features/contingent_command.feature +5 -5
  11. data/features/default_locations.feature +2 -2
  12. data/features/groups_install.feature +19 -20
  13. data/features/info_command.feature +13 -13
  14. data/features/install_command.feature +86 -83
  15. data/features/json_formatter.feature +60 -23
  16. data/features/list_command.feature +5 -11
  17. data/features/lockfile.feature +286 -6
  18. data/features/open_command.feature +8 -4
  19. data/features/outdated_command.feature +8 -15
  20. data/features/package_command.feature +39 -0
  21. data/features/show_command.feature +8 -9
  22. data/features/step_definitions/chef_server_steps.rb +20 -2
  23. data/features/step_definitions/cli_steps.rb +10 -2
  24. data/features/step_definitions/configure_cli_steps.rb +7 -0
  25. data/features/step_definitions/filesystem_steps.rb +19 -14
  26. data/features/step_definitions/json_steps.rb +22 -5
  27. data/features/step_definitions/utility_steps.rb +13 -1
  28. data/features/support/env.rb +10 -23
  29. data/features/update_command.feature +105 -24
  30. data/features/upload_command.feature +0 -14
  31. data/features/vendor_install.feature +3 -3
  32. data/generator_files/Vagrantfile.erb +11 -11
  33. data/lib/berkshelf.rb +6 -5
  34. data/lib/berkshelf/berksfile.rb +267 -99
  35. data/lib/berkshelf/cli.rb +70 -34
  36. data/lib/berkshelf/cli_commands/test_command.rb +11 -0
  37. data/lib/berkshelf/community_rest.rb +1 -1
  38. data/lib/berkshelf/config.rb +19 -2
  39. data/lib/berkshelf/cookbook_source.rb +41 -12
  40. data/lib/berkshelf/cookbook_store.rb +8 -4
  41. data/lib/berkshelf/errors.rb +61 -29
  42. data/lib/berkshelf/formatters.rb +13 -19
  43. data/lib/berkshelf/formatters/human_readable.rb +8 -0
  44. data/lib/berkshelf/formatters/json.rb +12 -1
  45. data/lib/berkshelf/formatters/null.rb +23 -0
  46. data/lib/berkshelf/init_generator.rb +22 -11
  47. data/lib/berkshelf/location.rb +8 -10
  48. data/lib/berkshelf/locations/chef_api_location.rb +4 -5
  49. data/lib/berkshelf/locations/git_location.rb +14 -12
  50. data/lib/berkshelf/locations/path_location.rb +16 -1
  51. data/lib/berkshelf/locations/site_location.rb +1 -3
  52. data/lib/berkshelf/lockfile.rb +230 -33
  53. data/lib/berkshelf/resolver.rb +2 -1
  54. data/lib/berkshelf/ui.rb +4 -8
  55. data/lib/berkshelf/version.rb +1 -1
  56. data/lib/thor/monkies/shell.rb +2 -5
  57. data/spec/fixtures/cassettes/Berkshelf_Resolver/{ClassMethods/_initialize → _initialize}/adds_the_dependencies_of_the_source_as_sources.yml +0 -0
  58. data/spec/fixtures/cookbooks/example_cookbook/.gitignore +2 -0
  59. data/spec/fixtures/cookbooks/example_cookbook/.kitchen.yml +26 -0
  60. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +5 -0
  61. data/spec/fixtures/lockfiles/default.lock +11 -0
  62. data/spec/{config/knife.rb → knife.rb.sample} +2 -2
  63. data/spec/spec_helper.rb +15 -3
  64. data/spec/support/chef_api.rb +19 -5
  65. data/spec/support/chef_server.rb +4 -3
  66. data/spec/support/knife.rb +18 -0
  67. data/spec/unit/berkshelf/berksfile_spec.rb +332 -235
  68. data/spec/unit/berkshelf/cached_cookbook_spec.rb +40 -42
  69. data/spec/unit/berkshelf/chef/cookbook/chefignore_spec.rb +11 -15
  70. data/spec/unit/berkshelf/community_rest_spec.rb +16 -14
  71. data/spec/unit/berkshelf/config_spec.rb +36 -20
  72. data/spec/unit/berkshelf/cookbook_generator_spec.rb +6 -10
  73. data/spec/unit/berkshelf/cookbook_source_spec.rb +244 -183
  74. data/spec/unit/berkshelf/cookbook_store_spec.rb +72 -76
  75. data/spec/unit/berkshelf/core_ext/file_utils_spec.rb +2 -2
  76. data/spec/unit/berkshelf/downloader_spec.rb +137 -157
  77. data/spec/unit/berkshelf/errors_spec.rb +1 -1
  78. data/spec/unit/berkshelf/formatters/null_spec.rb +17 -0
  79. data/spec/unit/berkshelf/formatters_spec.rb +83 -90
  80. data/spec/unit/berkshelf/git_spec.rb +219 -207
  81. data/spec/unit/berkshelf/init_generator_spec.rb +73 -73
  82. data/spec/unit/berkshelf/location_spec.rb +143 -162
  83. data/spec/unit/berkshelf/locations/chef_api_location_spec.rb +94 -89
  84. data/spec/unit/berkshelf/locations/git_location_spec.rb +75 -59
  85. data/spec/unit/berkshelf/locations/path_location_spec.rb +46 -30
  86. data/spec/unit/berkshelf/locations/site_location_spec.rb +4 -4
  87. data/spec/unit/berkshelf/lockfile_spec.rb +185 -1
  88. data/spec/unit/berkshelf/logger_spec.rb +6 -24
  89. data/spec/unit/berkshelf/mixin/logging_spec.rb +6 -8
  90. data/spec/unit/berkshelf/resolver_spec.rb +36 -38
  91. data/spec/unit/berkshelf/ui_spec.rb +9 -10
  92. data/spec/unit/berkshelf_spec.rb +41 -40
  93. data/spec/unit/chef/config_spec.rb +9 -11
  94. metadata +55 -203
  95. data/spec/config/berkshelf.pem +0 -27
  96. data/spec/config/validator.pem +0 -27
@@ -1,55 +1,136 @@
1
- Feature: update
1
+ Feature: update command
2
2
  As a user
3
3
  I want a way to update the versions without clearing out the files I've downloaded
4
4
  So that I can update faster than a clean install
5
5
 
6
- Scenario: knife berkshelf update
6
+ Scenario: updating with the old lockfile format
7
7
  Given I write to "Berksfile" with:
8
8
  """
9
- cookbook "artifact", "0.10.0"
9
+ site :opscode
10
+ cookbook 'berkshelf-cookbook-fixture', '~> 0.1'
10
11
  """
11
12
  Given I write to "Berksfile.lock" with:
12
13
  """
13
- cookbook 'artifact', :locked_version => '0.1.0'
14
+ cookbook 'berkshelf-cookbook-fixture', :locked_version => '0.1.0'
14
15
  """
15
16
  When I successfully run `berks update`
16
- Then the file "Berksfile.lock" should contain exactly:
17
- """
18
- cookbook 'artifact', :locked_version => '0.10.0'
17
+ Then the output should contain "You are using the old lockfile format. Attempting to convert..."
18
+ Then the file "Berksfile.lock" should contain JSON:
19
+ """
20
+ {
21
+ "sha":"b2714a4f9bdf500cb20267067160a0b3c1d8404c",
22
+ "sources":{
23
+ "berkshelf-cookbook-fixture":{
24
+ "locked_version":"0.2.0",
25
+ "constraint":"~> 0.1"
26
+ }
27
+ }
28
+ }
19
29
  """
20
30
 
21
- Scenario: knife berkshelf update a single cookbook
31
+ Scenario: Updating all cookbooks
22
32
  Given I write to "Berksfile" with:
23
33
  """
24
- cookbook "artifact", "0.10.0"
25
- cookbook "build-essential", "~> 1.1.0"
34
+ site :opscode
35
+ cookbook 'berkshelf-cookbook-fixture', '~> 0.1'
36
+ cookbook 'hostsfile', '~> 1.0.0'
26
37
  """
27
38
  Given I write to "Berksfile.lock" with:
28
39
  """
29
- cookbook 'artifact', :locked_version => '0.10.0'
30
- cookbook 'build-essential', :locked_version => '1.1.0'
40
+ {
41
+ "sha":"9d10199aa2652f9e965149c4346db20c78e97553",
42
+ "sources":{
43
+ "berkshelf-cookbook-fixture":{
44
+ "locked_version":"0.1.0",
45
+ "constraint":"~> 0.1"
46
+ },
47
+ "hostsfile":{
48
+ "locked_version":"1.0.1",
49
+ "constraint":"= 1.0.1"
50
+ }
51
+ }
52
+ }
53
+ """
54
+ When I successfully run `berks update`
55
+ Then the file "Berksfile.lock" should contain JSON:
56
+ """
57
+ {
58
+ "sha":"69b2e00e970d2bb6a9b1d09aeb3e6a17ef3df955",
59
+ "sources":{
60
+ "berkshelf-cookbook-fixture":{
61
+ "locked_version":"0.2.0",
62
+ "constraint":"~> 0.1"
63
+ },
64
+ "hostsfile":{
65
+ "locked_version":"1.0.1",
66
+ "constraint":"~> 1.0.0"
67
+ }
68
+ }
69
+ }
31
70
  """
32
- When I successfully run `berks update build-essential`
33
- Then the file "Berksfile.lock" should contain exactly:
71
+
72
+ Scenario: Updating a single cookbook
73
+ Given I write to "Berksfile" with:
34
74
  """
35
- cookbook 'artifact', :locked_version => '0.10.0'
36
- cookbook 'build-essential', :locked_version => '1.1.2'
75
+ site :opscode
76
+ cookbook 'berkshelf-cookbook-fixture', '~> 0.1'
77
+ cookbook 'hostsfile', '~> 1.0.0'
78
+ """
79
+ And I write to "Berksfile.lock" with:
80
+ """
81
+ {
82
+ "sha":"9d10199aa2652f9e965149c4346db20c78e97553",
83
+ "sources":{
84
+ "berkshelf-cookbook-fixture":{
85
+ "locked_version":"0.1.0",
86
+ "constraint":"~> 0.1"
87
+ },
88
+ "hostsfile":{
89
+ "locked_version":"1.0.0",
90
+ "constraint":"~> 1.0.0"
91
+ }
92
+ }
93
+ }
94
+ """
95
+ And I successfully run `berks update berkshelf-cookbook-fixture`
96
+ Then the file "Berksfile.lock" should contain JSON:
97
+ """
98
+ {
99
+ "sha":"69b2e00e970d2bb6a9b1d09aeb3e6a17ef3df955",
100
+ "sources":{
101
+ "berkshelf-cookbook-fixture":{
102
+ "locked_version":"0.2.0",
103
+ "constraint":"~> 0.1"
104
+ },
105
+ "hostsfile":{
106
+ "locked_version":"1.0.0",
107
+ "constraint":"~> 1.0.0"
108
+ }
109
+ }
110
+ }
37
111
  """
38
112
 
39
- Scenario: knife berkshelf update a cookbook that isn't in the Berksfile
113
+ Scenario: Update a cookbook that doesn't exist
40
114
  Given I write to "Berksfile" with:
41
115
  """
42
- cookbook "artifact", "0.10.0"
43
- cookbook "build-essential", "~> 1.1.0"
116
+ site :opscode
117
+ cookbook 'berkshelf-cookbook-fixture', '~> 0.1'
44
118
  """
45
119
  Given I write to "Berksfile.lock" with:
46
120
  """
47
- cookbook 'artifact', :locked_version => '0.10.0'
48
- cookbook 'build-essential', :locked_version => '1.1.0'
49
- """
50
- When I run `berks update foo`
121
+ {
122
+ "sha":"23150cfe61b7b86882013c8664883058560b899d",
123
+ "sources":{
124
+ "berkshelf-cookbook-fixture":{
125
+ "locked_version":"0.1.0",
126
+ "constraint":"~> 0.1"
127
+ }
128
+ }
129
+ }
130
+ """
131
+ When I run `berks update non-existent-cookbook`
51
132
  Then the output should contain:
52
133
  """
53
- Could not find cookbooks 'foo' in any of the sources. Is it in your Berksfile?
134
+ Could not find cookbooks 'non-existent-cookbook' in any of the sources. Is it in your Berksfile?
54
135
  """
55
136
  And the CLI should exit with the status code for error "CookbookNotFound"
@@ -216,17 +216,3 @@ Feature: upload command
216
216
  | ntp |
217
217
  | vim |
218
218
  And the exit status should be 0
219
-
220
- Scenario: Raise exception uploading an invalid cookbook
221
- Given a cookbook named "cookbook with spaces"
222
- And I write to "Berksfile" with:
223
- """
224
- cookbook 'cookbook with spaces', path: './cookbook with spaces'
225
- """
226
- When I run `berks upload`
227
- Then the output should contain:
228
- """
229
- The cookbook 'cookbook with spaces' has invalid filenames:
230
- """
231
- And the CLI should exit with the status code for error "InvalidCookbookFiles"
232
-
@@ -7,13 +7,13 @@ Feature: install cookbooks to a given vendor path
7
7
  Given I write to "Berksfile" with:
8
8
  """
9
9
  site :opscode
10
- cookbook "artifact", "0.10.0"
10
+ cookbook 'berkshelf-cookbook-fixture', '1.0.0'
11
11
  """
12
12
  When I run the install command with flags:
13
13
  | --path vendor/cookbooks |
14
14
  Then the cookbook store should have the cookbooks:
15
- | artifact | 0.10.0 |
15
+ | berkshelf-cookbook-fixture | 1.0.0 |
16
16
  Then the following directories should exist:
17
17
  | vendor/cookbooks |
18
- | vendor/cookbooks/artifact |
18
+ | vendor/cookbooks/berkshelf-cookbook-fixture |
19
19
  And the exit status should be 0
@@ -9,18 +9,18 @@ Vagrant.configure("2") do |config|
9
9
  config.vm.hostname = "<%= "#{cookbook_name.gsub('_','-')}-berkshelf" %>"
10
10
 
11
11
  # Every Vagrant virtual environment requires a box to build off of.
12
- config.vm.box = "<%= options[:berkshelf_config].vagrant.vm.box %>"
12
+ config.vm.box = "<%= berkshelf_config.vagrant.vm.box %>"
13
13
 
14
14
  # The url from where the 'config.vm.box' box will be fetched if it
15
15
  # doesn't already exist on the user's system.
16
- config.vm.box_url = "<%= options[:berkshelf_config].vagrant.vm.box_url %>"
16
+ config.vm.box_url = "<%= berkshelf_config.vagrant.vm.box_url %>"
17
17
 
18
18
  # Assign this VM to a host-only network IP, allowing you to access it
19
19
  # via the IP. Host-only networks can talk to the host machine as well as
20
20
  # any other machines on the same network, but cannot be accessed (through this
21
21
  # network interface) by any external networks.
22
- <% if options[:berkshelf_config].vagrant.vm.network.hostonly.present? -%>
23
- config.vm.network :private_network, ip: "<%= options[:berkshelf_config].vagrant.vm.network.hostonly %>"
22
+ <% if berkshelf_config.vagrant.vm.network.hostonly.present? -%>
23
+ config.vm.network :private_network, ip: "<%= berkshelf_config.vagrant.vm.network.hostonly %>"
24
24
  <% else %>
25
25
  config.vm.network :private_network, ip: "192.168.33.10"
26
26
  <% end -%>
@@ -28,7 +28,7 @@ Vagrant.configure("2") do |config|
28
28
  # Create a public network, which generally matched to bridged network.
29
29
  # Bridged networks make the machine appear as another physical device on
30
30
  # your network.
31
- <% if options[:berkshelf_config].vagrant.vm.network.bridged -%>
31
+ <% if berkshelf_config.vagrant.vm.network.bridged -%>
32
32
  config.vm.network :public_network
33
33
  <% else %>
34
34
  # config.vm.network :public_network
@@ -37,7 +37,7 @@ Vagrant.configure("2") do |config|
37
37
  # Create a forwarded port mapping which allows access to a specific port
38
38
  # within the machine from a port on the host machine. In the example below,
39
39
  # accessing "localhost:8080" will access port 80 on the guest machine.
40
- <% options[:berkshelf_config].vagrant.vm.forward_port.each do |guest, host| %>
40
+ <% berkshelf_config.vagrant.vm.forward_port.each do |guest, host| %>
41
41
  config.vm.network :forwarded_port, guest: <%= guest %>, host: <%= host %>
42
42
  <% end -%>
43
43
 
@@ -80,11 +80,11 @@ Vagrant.configure("2") do |config|
80
80
  # to skip installing and copying to Vagrant's shelf.
81
81
  # config.berkshelf.except = []
82
82
 
83
- <% if options[:berkshelf_config].vagrant.vm.provision == "chef_client" -%>
83
+ <% if berkshelf_config.vagrant.vm.provision == "chef_client" -%>
84
84
  config.vm.provision :chef_client do |chef|
85
- chef.chef_server_url = "<%= options[:berkshelf_config].chef.chef_server_url %>"
86
- chef.validation_client_name = "<%= options[:berkshelf_config].chef.validation_client_name %>"
87
- chef.validation_key_path = "<%= options[:berkshelf_config].chef.validation_key_path %>"
85
+ chef.chef_server_url = "<%= berkshelf_config.chef.chef_server_url %>"
86
+ chef.validation_client_name = "<%= berkshelf_config.chef.validation_client_name %>"
87
+ chef.validation_key_path = "<%= berkshelf_config.chef.validation_key_path %>"
88
88
 
89
89
  chef.run_list = [
90
90
  <% if options[:chef_minitest] -%>
@@ -93,7 +93,7 @@ Vagrant.configure("2") do |config|
93
93
  "recipe[<%= cookbook_name %>::default]"
94
94
  ]
95
95
  end
96
- <% elsif options[:berkshelf_config].vagrant.vm.provision == "chef_solo" -%>
96
+ <% elsif berkshelf_config.vagrant.vm.provision == "chef_solo" -%>
97
97
  config.vm.provision :chef_solo do |chef|
98
98
  chef.json = {
99
99
  :mysql => {
@@ -1,9 +1,10 @@
1
- require 'chozo/core_ext'
2
1
  require 'active_support/core_ext'
3
2
  require 'archive/tar/minitar'
3
+ require 'celluloid'
4
+ require 'chozo/core_ext'
4
5
  require 'forwardable'
5
6
  require 'hashie'
6
- require 'multi_json'
7
+ require 'json'
7
8
  require 'pathname'
8
9
  require 'ridley'
9
10
  require 'solve'
@@ -11,7 +12,6 @@ require 'thor'
11
12
  require 'tmpdir'
12
13
  require 'uri'
13
14
  require 'zlib'
14
- require 'celluloid'
15
15
 
16
16
  require 'berkshelf/core_ext'
17
17
  require 'berkshelf/errors'
@@ -56,9 +56,10 @@ module Berkshelf
56
56
  @root ||= Pathname.new(File.expand_path('../', File.dirname(__FILE__)))
57
57
  end
58
58
 
59
- # @return [::Thor::Shell::Color]
59
+ # @return [Thor::Shell::Color, Thor::Shell::Basic]
60
+ # A basic shell on Windows, colored everywhere else
60
61
  def ui
61
- @ui ||= ::Thor::Shell::Color.new
62
+ @ui ||= Thor::Base.shell.new
62
63
  end
63
64
 
64
65
  # Returns the filepath to the location Berskhelf will use for
@@ -83,6 +83,12 @@ module Berkshelf
83
83
  @cached_cookbooks = nil
84
84
  end
85
85
 
86
+ # @return [String]
87
+ # the shasum for the Berksfile
88
+ def sha
89
+ @sha ||= Digest::SHA1.hexdigest File.read(filepath.to_s)
90
+ end
91
+
86
92
  # Add a cookbook source to the Berksfile to be retrieved and have it's dependencies recursively retrieved
87
93
  # and resolved.
88
94
  #
@@ -283,24 +289,31 @@ module Berkshelf
283
289
  @sources.has_key?(source.to_s)
284
290
  end
285
291
 
292
+ # The list of cookbook sources specified in this Berksfile
293
+ #
294
+ # @param [Array] sources
295
+ # the list of sources to filter
296
+ #
286
297
  # @option options [Symbol, Array] :except
287
- # Group(s) to exclude to exclude from the returned Array of sources
298
+ # group(s) to exclude to exclude from the returned Array of sources
288
299
  # group to not be installed
289
300
  # @option options [Symbol, Array] :only
290
- # Group(s) to include which will cause any sources marked as a member of the
301
+ # group(s) to include which will cause any sources marked as a member of the
291
302
  # group to be installed and all others to be ignored
292
303
  # @option cookbooks [String, Array] :cookbooks
293
- # Names of the cookbooks to retrieve sources for
304
+ # names of the cookbooks to retrieve sources for
294
305
  #
295
- # @raise [Berkshelf::ArgumentError] if a value for both :except and :only is provided
306
+ # @raise [Berkshelf::ArgumentError]
307
+ # if a value for both :except and :only is provided
296
308
  #
297
309
  # @return [Array<Berkshelf::CookbookSource>]
310
+ # the list of cookbook sources that match the given options
298
311
  def sources(options = {})
299
- l_sources = @sources.collect { |name, source| source }.flatten
312
+ l_sources = @sources.values
300
313
 
301
- cookbooks = Array(options.fetch(:cookbooks, nil))
302
- except = Array(options.fetch(:except, nil)).collect(&:to_sym)
303
- only = Array(options.fetch(:only, nil)).collect(&:to_sym)
314
+ cookbooks = Array(options[:cookbooks])
315
+ except = Array(options[:except]).collect(&:to_sym)
316
+ only = Array(options[:only]).collect(&:to_sym)
304
317
 
305
318
  case
306
319
  when !except.empty? && !only.empty?
@@ -309,7 +322,7 @@ module Berkshelf
309
322
  if !except.empty? && !only.empty?
310
323
  Berkshelf.ui.warn "Cookbooks were specified, ignoring :except and :only"
311
324
  end
312
- l_sources.select { |source| options[:cookbooks].include?(source.name) }
325
+ l_sources.select { |source| cookbooks.include?(source.name) }
313
326
  when !except.empty?
314
327
  l_sources.select { |source| (except & source.groups).empty? }
315
328
  when !only.empty?
@@ -319,6 +332,16 @@ module Berkshelf
319
332
  end
320
333
  end
321
334
 
335
+ # Find a source defined in this berksfile by name.
336
+ #
337
+ # @param [String] name
338
+ # the name of the cookbook source to search for
339
+ # @return [Berkshelf::CookbookSource, nil]
340
+ # the cookbook source, or nil if one does not exist
341
+ def find(name)
342
+ @sources[name]
343
+ end
344
+
322
345
  # @return [Hash]
323
346
  # a hash containing group names as keys and an array of CookbookSources
324
347
  # that are a member of that group as values
@@ -353,6 +376,33 @@ module Berkshelf
353
376
  end
354
377
  alias_method :get_source, :[]
355
378
 
379
+ # Install the sources listed in the Berksfile, respecting the locked
380
+ # versions in the Berksfile.lock.
381
+ #
382
+ # 1. Check that a lockfile exists. If a lockfile does not exist, all
383
+ # sources are considered to be "unlocked". If a lockfile is specified, a
384
+ # definition is created via the following algorithm:
385
+ #
386
+ # - Compare the SHA of the current Berksfile with the last-known SHA.
387
+ # - If the SHAs match, the Berksfile has not been updated, so we rely
388
+ # solely on the locked sources.
389
+ # - If the SHAs don't match, then the Berksfile has diverged from the
390
+ # lockfile, which means some sources are outdated. For each unlocked
391
+ # source, see if there exists a locked version that still satisfies
392
+ # the version constraint in the Berksfile. If there exists such a
393
+ # source, remove it from the list of unlocked sources. If not, then
394
+ # either a version constraint has changed, or a new source has been
395
+ # added to the Berksfile. In the event that a locked_source exists,
396
+ # but it no longer satisfies the constraint, this method will raise
397
+ # a {Berkshelf::OutdatedCookbookSource}, and inform the user to run
398
+ # <tt>berks update COOKBOOK</tt> to remedy the issue.
399
+ # - Remove any locked sources that no longer exist in the Berksfile
400
+ # (i.e. a cookbook source was removed from the Berksfile).
401
+ #
402
+ # 2. Resolve the collection of locked and unlocked sources.
403
+ #
404
+ # 3. Write out a new lockfile.
405
+ #
356
406
  # @option options [Symbol, Array] :except
357
407
  # Group(s) to exclude which will cause any sources marked as a member of the
358
408
  # group to not be installed
@@ -363,16 +413,26 @@ module Berkshelf
363
413
  # a path to "vendor" the cached_cookbooks resolved by the resolver. Vendoring
364
414
  # is a technique for packaging all cookbooks resolved by a Berksfile.
365
415
  #
416
+ # @raise [Berkshelf::OutdatedCookbookSource]
417
+ # if the lockfile constraints do not satisfy the Berskfile constraints
418
+ # @raise [Berkshelf::ArgumentError]
419
+ # if there are missing or conflicting options
420
+ #
366
421
  # @return [Array<Berkshelf::CachedCookbook>]
367
422
  def install(options = {})
368
- resolver = Resolver.new(self, sources: sources(options))
423
+ if self.sha == lockfile.sha
424
+ local_sources = locked_sources
425
+ else
426
+ local_sources = apply_lockfile(sources(options))
427
+ end
369
428
 
370
- @cached_cookbooks = resolver.resolve
371
- write_lockfile(resolver.sources) unless lockfile_present?
429
+ resolver = resolve(local_sources)
430
+ @cached_cookbooks = resolver[:solution]
431
+ local_sources = resolver[:sources]
372
432
 
373
- if options[:path]
374
- self.class.vendor(@cached_cookbooks, options[:path])
375
- end
433
+ self.class.vendor(@cached_cookbooks, options[:path]) if options[:path]
434
+
435
+ lockfile.update(local_sources, sha: self.sha)
376
436
 
377
437
  self.cached_cookbooks
378
438
  end
@@ -386,25 +446,15 @@ module Berkshelf
386
446
  # @option cookbooks [String, Array] :cookbooks
387
447
  # Names of the cookbooks to retrieve sources for
388
448
  def update(options = {})
389
- resolver = Resolver.new(self, sources: sources(options))
390
-
391
- cookbooks = resolver.resolve
392
- sources = resolver.sources
393
- missing_cookbooks = (options[:cookbooks] - cookbooks.map(&:cookbook_name))
449
+ validate_cookbook_names!(options)
394
450
 
395
- unless missing_cookbooks.empty?
396
- msg = "Could not find cookbooks #{missing_cookbooks.collect{|cookbook| "'#{cookbook}'"}.join(', ')}"
397
- msg << " in any of the sources. #{missing_cookbooks.size == 1 ? 'Is it' : 'Are they' } in your Berksfile?"
398
- raise Berkshelf::CookbookNotFound, msg
399
- end
451
+ # Unlock any/all specified cookbooks
452
+ sources(options).each { |source| lockfile.unlock(source) }
400
453
 
401
- update_lockfile(sources)
454
+ lockfile.reset_sha!
402
455
 
403
- if options[:path]
404
- self.class.vendor(cookbooks, options[:path])
405
- end
406
-
407
- cookbooks
456
+ # NOTE: We intentionally do NOT pass options to the installer
457
+ self.install
408
458
  end
409
459
 
410
460
  # Get a list of all the cookbooks which have newer versions found on the community
@@ -431,7 +481,7 @@ module Berkshelf
431
481
  outdated = Hash.new
432
482
 
433
483
  sources(options).each do |cookbook|
434
- location = cookbook.location || Location.init(cookbook.name, cookbook.version_constraint)
484
+ location = cookbook.location || Location.init(cookbook.name, cookbook.version_constraint, site: :opscode)
435
485
 
436
486
  if location.is_a?(SiteLocation)
437
487
  latest_version = location.latest_version
@@ -476,38 +526,17 @@ module Berkshelf
476
526
  options = options.reverse_merge(
477
527
  force: false,
478
528
  freeze: true,
479
- ssl_verify: Berkshelf::Config.instance.ssl.verify,
480
529
  skip_dependencies: false,
481
530
  halt_on_frozen: false
482
531
  )
483
532
 
484
- ridley_options = options.slice(:ssl)
485
- ridley_options[:server_url] = options[:server_url] || Berkshelf::Config.instance.chef.chef_server_url
486
- ridley_options[:client_name] = Berkshelf::Config.instance.chef.node_name
487
- ridley_options[:client_key] = Berkshelf::Config.instance.chef.client_key
488
- ridley_options[:ssl] = { verify: options[:ssl_verify] }
489
-
490
- unless ridley_options[:server_url].present?
491
- raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.server_url"
492
- end
493
-
494
- unless ridley_options[:client_name].present?
495
- raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.node_name"
496
- end
497
-
498
- unless ridley_options[:client_key].present?
499
- raise UploadFailure, "Missing required attribute in your Berkshelf configuration: chef.client_key"
500
- end
501
-
502
- solution = resolve(options)
533
+ solution = resolve(sources(options), options)[:solution]
503
534
  upload_opts = options.slice(:force, :freeze)
504
- conn = Ridley.new(ridley_options)
535
+ conn = ridley_connection(options)
505
536
 
506
537
  solution.each do |cb|
507
538
  Berkshelf.formatter.upload(cb.cookbook_name, cb.version, conn.server_url)
508
539
 
509
- validate_files!(cb)
510
-
511
540
  begin
512
541
  conn.cookbook.upload(cb.path, upload_opts.merge(name: cb.cookbook_name))
513
542
  rescue Ridley::Errors::FrozenCookbook => ex
@@ -528,27 +557,122 @@ module Berkshelf
528
557
  end
529
558
  rescue Ridley::Errors::RidleyError => ex
530
559
  log_exception(ex)
531
- raise UploadFailure, ex
560
+ raise ChefConnectionError, ex # todo implement
532
561
  ensure
533
562
  conn.terminate if conn && conn.alive?
534
563
  end
535
564
 
565
+ # Resolve this Berksfile and apply the locks found in the generated Berksfile.lock to the
566
+ # target Chef environment
567
+ #
568
+ # @param [String] environment_name
569
+ #
570
+ # @option options [Hash] :ssl_verify (true)
571
+ # Disable/Enable SSL verification during uploads
572
+ #
573
+ # @raise [EnvironmentNotFound] if the target environment was not found
574
+ # @raise [ChefConnectionError] if you are locking cookbooks with an invalid or not-specified client configuration
575
+ def apply(environment_name, options = {})
576
+ conn = ridley_connection(options)
577
+ environment = conn.environment.find(environment_name)
578
+
579
+ if environment
580
+ install
581
+
582
+ environment.cookbook_versions = {}.tap do |cookbook_versions|
583
+ lockfile.sources.each { |source| cookbook_versions[source.name] = source.locked_version }
584
+ end
585
+
586
+ environment.save
587
+ else
588
+ raise EnvironmentNotFound.new(environment_name)
589
+ end
590
+ rescue Ridley::Errors::RidleyError => ex
591
+ raise ChefConnectionError, ex
592
+ ensure
593
+ conn.terminate if conn && conn.alive?
594
+ end
595
+
596
+ # Package the given cookbook for distribution outside of berkshelf. If the
597
+ # name attribute is not given, all cookbooks in the Berksfile will be
598
+ # packaged.
599
+ #
600
+ # @param [String] name
601
+ # the name of the cookbook to package
602
+ # @param [Hash] options
603
+ # a list of options
604
+ #
605
+ # @option options [String] :output
606
+ # the path to output the tarball
607
+ # @option options [Boolean] :skip_dependencies
608
+ # package cookbook dependencies as well
609
+ # @option options [Boolean] :ignore_chefignore
610
+ # do not apply the chefignore file to the packed cookbooks
611
+ #
612
+ # @return [String]
613
+ # the path to the package
614
+ def package(name = nil, options = {})
615
+ tar_name = "#{name || 'package'}.tar.gz"
616
+ output = File.expand_path(File.join(options[:output], tar_name))
617
+
618
+ unless name.nil?
619
+ source = self.find(name)
620
+ raise CookbookNotFound, "Cookbook '#{name}' is not in your Berksfile" unless source
621
+
622
+ package = Berkshelf.ui.mute {
623
+ self.resolve(source, options)[:solution]
624
+ }
625
+ else
626
+ package = Berkshelf.ui.mute {
627
+ self.resolve(sources, options)[:solution]
628
+ }
629
+ end
630
+
631
+ Dir.mktmpdir do |tmp|
632
+ package.each do |cached_cookbook|
633
+ path = cached_cookbook.path.to_s
634
+ destination = File.join(tmp, cached_cookbook.cookbook_name)
635
+
636
+ FileUtils.cp_r(path, destination)
637
+
638
+ unless options[:ignore_chefignore]
639
+ if ignore_file = Berkshelf::Chef::Cookbook::Chefignore.find_relative_to(path)
640
+ chefignore = Berkshelf::Chef::Cookbook::Chefignore.new(ignore_file)
641
+ chefignore.remove_ignores_from(destination) if chefignore
642
+ end
643
+ end
644
+ end
645
+
646
+ FileUtils.mkdir_p(options[:output])
647
+
648
+ Dir.chdir(tmp) do |dir|
649
+ tgz = Zlib::GzipWriter.new(File.open(output, 'wb'))
650
+ Archive::Tar::Minitar.pack('.', tgz)
651
+ end
652
+ end
653
+
654
+ Berkshelf.formatter.package(name, output)
655
+
656
+ output
657
+ end
658
+
536
659
  # Finds a solution for the Berksfile and returns an array of CachedCookbooks.
537
660
  #
538
- # @option options [Symbol, Array] :except
539
- # Group(s) to exclude which will cause any sources marked as a member of the
540
- # group to not be installed
541
- # @option options [Symbol, Array] :only
542
- # Group(s) to include which will cause any sources marked as a member of the
543
- # group to be installed and all others to be ignored
544
- # @option cookbooks [String, Array] :cookbooks
545
- # Names of the cookbooks to retrieve sources for
661
+ # @param [Array<Berkshelf::CookbookSource>] sources
662
+ # Array of cookbook sources to resolve
663
+ #
546
664
  # @option options [Boolean] :skip_dependencies
547
665
  # Skip resolving of dependencies
548
666
  #
549
- # @return [Array<Berkshelf::CachedCookbooks]
550
- def resolve(options = {})
551
- resolver(options).resolve
667
+ # @return [Array<Berkshelf::CachedCookbooks>]
668
+ def resolve(sources = [], options = {})
669
+ resolver = Resolver.new(
670
+ self,
671
+ sources: sources,
672
+ skip_dependencies: options[:skip_dependencies]
673
+ )
674
+
675
+ { solution: resolver.resolve, sources: resolver.sources }
552
676
  end
553
677
 
554
678
  # Reload this instance of Berksfile with the given content. The content
@@ -568,56 +692,100 @@ module Berkshelf
568
692
  self
569
693
  end
570
694
 
695
+ # Get the lockfile corresponding to this Berksfile. This is necessary because
696
+ # the user can specify a different path to the Berksfile. So assuming the lockfile
697
+ # is named "Berksfile.lock" is a poor assumption.
698
+ #
699
+ # @return [Berkshelf::Lockfile]
700
+ # the lockfile corresponding to this berksfile, or a new Lockfile if one does
701
+ # not exist
702
+ def lockfile
703
+ @lockfile ||= Berkshelf::Lockfile.new(self)
704
+ end
705
+
571
706
  private
572
707
 
708
+ def ridley_connection(options = {})
709
+ ridley_options = options.slice(:ssl)
710
+ ridley_options[:server_url] = options[:server_url] || Berkshelf::Config.instance.chef.chef_server_url
711
+ ridley_options[:client_name] = Berkshelf::Config.instance.chef.node_name
712
+ ridley_options[:client_key] = Berkshelf::Config.instance.chef.client_key
713
+ ridley_options[:ssl] = { verify: (options[:ssl_verify] || Berkshelf::Config.instance.ssl.verify) }
714
+
715
+ unless ridley_options[:server_url].present?
716
+ raise ChefConnectionError, "Missing required attribute in your Berkshelf configuration: chef.server_url"
717
+ end
718
+
719
+ unless ridley_options[:client_name].present?
720
+ raise ChefConnectionError, "Missing required attribute in your Berkshelf configuration: chef.node_name"
721
+ end
722
+
723
+ unless ridley_options[:client_key].present?
724
+ raise ChefConnectionError, "Missing required attribute in your Berkshelf configuration: chef.client_key"
725
+ end
726
+
727
+ Ridley.new(ridley_options)
728
+ end
729
+
573
730
  def descendant_directory?(candidate, parent)
574
731
  hack = FileUtils::Entry_.new('/tmp')
575
732
  hack.send(:descendant_diretory?, candidate, parent)
576
733
  end
577
734
 
578
- def lockfile_present?
579
- File.exist?(Berkshelf::Lockfile::DEFAULT_FILENAME)
580
- end
581
-
582
- # Builds a Resolver instance
735
+ # Determine if any cookbooks were specified that aren't in our shelf.
583
736
  #
584
- # @option options [Symbol, Array] :except
585
- # Group(s) to exclude which will cause any sources marked as a member of the
586
- # group to not be installed
587
- # @option options [Symbol, Array] :only
588
- # Group(s) to include which will cause any sources marked as a member of the
589
- # group to be installed and all others to be ignored
590
- # @option options [String, Array] :cookbooks
591
- # Names of the cookbooks to retrieve sources for
592
- # @option options [Boolean] :skip_dependencies
593
- # Skip resolving of dependencies
737
+ # @option options [Array<String>] :cookbooks
738
+ # a list of strings of cookbook names
594
739
  #
595
- # @return <Berkshelf::Resolver>
596
- def resolver(options = {})
597
- Resolver.new(self, sources: sources(options), skip_dependencies: options[:skip_dependencies])
740
+ # @raise [Berkshelf::CookbookNotFound]
741
+ # if a cookbook name is given that does not exist
742
+ def validate_cookbook_names!(options = {})
743
+ missing = (Array(options[:cookbooks]) - sources.map(&:name))
744
+ unless missing.empty?
745
+ raise Berkshelf::CookbookNotFound,
746
+ "Could not find cookbooks #{missing.collect{ |c| "'#{c}'" }.join(', ')} " +
747
+ "in any of the sources. #{missing.size == 1 ? 'Is it' : 'Are they' } in your Berksfile?"
748
+ end
598
749
  end
599
750
 
600
- def write_lockfile(sources)
601
- Berkshelf::Lockfile.new(sources).write
751
+ # The list of sources "locked" by the lockfile.
752
+ #
753
+ # @return [Array<Berkshelf::CookbookSource>]
754
+ # the list of sources in this lockfile
755
+ def locked_sources
756
+ lockfile.sources
602
757
  end
603
758
 
604
- def update_lockfile(sources)
605
- Berkshelf::Lockfile.update!(sources)
759
+ # Merge the locked sources against the given sources.
760
+ #
761
+ # For each the given sources, check if there's a locked version that
762
+ # still satisfies the version constraint. If it does, "lock" that source
763
+ # because we should just use the locked version.
764
+ #
765
+ # If a locked source exists, but doesn't satisfy the constraint, raise a
766
+ # {Berkshelf::OutdatedCookbookSource} and tell the user to run update.
767
+ def apply_lockfile(sources = [])
768
+ sources.collect do |source|
769
+ source_from_lockfile(source) || source
770
+ end
606
771
  end
607
772
 
608
- # Validate that the given cookbook does not have "bad" files. Currently
609
- # this means including spaces in filenames (such as recipes)
610
- #
611
- # @param [Berkshelf::CachedCookbook] cookbook
612
- # the Cookbook to validate
613
- def validate_files!(cookbook)
614
- path = cookbook.path.to_s
773
+ def source_from_lockfile(source)
774
+ locked_source = lockfile.find(source)
775
+
776
+ return nil unless locked_source
615
777
 
616
- files = Dir.glob(File.join(path, '**', '*.rb')).select do |f|
617
- f =~ /[[:space:]]/
778
+ # If there's a locked_version, make sure it's still satisfied
779
+ # by the constraint
780
+ if locked_source.locked_version
781
+ unless source.version_constraint.satisfies?(locked_source.locked_version)
782
+ raise Berkshelf::OutdatedCookbookSource.new(locked_source, source)
783
+ end
618
784
  end
619
785
 
620
- raise Berkshelf::InvalidCookbookFiles.new(cookbook, files) unless files.empty?
786
+ # Update to the new constraint (it might have changed, but still be satisfied)
787
+ locked_source.version_constraint = source.version_constraint
788
+ locked_source
621
789
  end
622
790
  end
623
791
  end