berkshelf 3.0.0.beta1 → 3.0.0.beta2

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -1
  3. data/CONTRIBUTING.md +2 -0
  4. data/LICENSE +1 -1
  5. data/README.md +1 -1
  6. data/Thorfile +2 -2
  7. data/berkshelf.gemspec +3 -3
  8. data/features/install_command.feature +36 -8
  9. data/features/json_formatter.feature +93 -3
  10. data/features/licenses.feature +1 -1
  11. data/features/lockfile.feature +0 -12
  12. data/features/outdated_command.feature +124 -0
  13. data/features/show_command.feature +44 -25
  14. data/features/step_definitions/chef/config_steps.rb +2 -2
  15. data/features/step_definitions/chef_server_steps.rb +9 -1
  16. data/features/step_definitions/config_steps.rb +1 -1
  17. data/features/step_definitions/filesystem_steps.rb +7 -0
  18. data/features/support/env.rb +2 -1
  19. data/features/update_command.feature +11 -21
  20. data/features/upload_command.feature +45 -1
  21. data/features/vendor_command.feature +83 -0
  22. data/lib/berkshelf.rb +5 -4
  23. data/lib/berkshelf/api_client/remote_cookbook.rb +13 -0
  24. data/lib/berkshelf/berksfile.rb +155 -23
  25. data/lib/berkshelf/chef.rb +0 -1
  26. data/lib/berkshelf/cli.rb +40 -31
  27. data/lib/berkshelf/dependency.rb +14 -4
  28. data/lib/berkshelf/errors.rb +74 -3
  29. data/lib/berkshelf/formatters.rb +12 -1
  30. data/lib/berkshelf/formatters/human_readable.rb +44 -5
  31. data/lib/berkshelf/formatters/json.rb +50 -8
  32. data/lib/berkshelf/installer.rb +8 -8
  33. data/lib/berkshelf/location.rb +17 -0
  34. data/lib/berkshelf/locations/git_location.rb +7 -17
  35. data/lib/berkshelf/locations/mercurial_location.rb +112 -0
  36. data/lib/berkshelf/lockfile.rb +1 -1
  37. data/lib/berkshelf/mercurial.rb +146 -0
  38. data/lib/berkshelf/version.rb +1 -1
  39. data/spec/config/knife.rb +2 -4
  40. data/spec/fixtures/lockfiles/default.lock +0 -1
  41. data/spec/support/chef_api.rb +9 -2
  42. data/spec/support/mercurial.rb +122 -0
  43. data/spec/support/path_helpers.rb +2 -2
  44. data/spec/unit/berkshelf/berksfile_spec.rb +34 -8
  45. data/spec/unit/berkshelf/dependency_spec.rb +0 -7
  46. data/spec/unit/berkshelf/formatters/null_spec.rb +1 -1
  47. data/spec/unit/berkshelf/locations/mercurial_location_spec.rb +150 -0
  48. data/spec/unit/berkshelf/lockfile_spec.rb +0 -12
  49. data/spec/unit/berkshelf/mercurial_spec.rb +173 -0
  50. metadata +32 -110
  51. data/lib/berkshelf/chef/config.rb +0 -68
  52. data/lib/berkshelf/mixin/config.rb +0 -172
  53. data/spec/fixtures/cookbooks/example_metadata_name/metadata.rb +0 -2
  54. data/spec/fixtures/cookbooks/example_metadata_no_name/metadata.rb +0 -1
  55. data/spec/fixtures/cookbooks/example_no_metadata/recipes/default.rb +0 -1
  56. data/spec/fixtures/cookbooks/nginx-0.100.5/README.md +0 -77
  57. data/spec/fixtures/cookbooks/nginx-0.100.5/attributes/default.rb +0 -65
  58. data/spec/fixtures/cookbooks/nginx-0.100.5/definitions/nginx_site.rb +0 -35
  59. data/spec/fixtures/cookbooks/nginx-0.100.5/files/default/mime.types +0 -73
  60. data/spec/fixtures/cookbooks/nginx-0.100.5/files/ubuntu/mime.types +0 -73
  61. data/spec/fixtures/cookbooks/nginx-0.100.5/libraries/nginxlib.rb +0 -1
  62. data/spec/fixtures/cookbooks/nginx-0.100.5/metadata.rb +0 -91
  63. data/spec/fixtures/cookbooks/nginx-0.100.5/providers/defprovider.rb +0 -1
  64. data/spec/fixtures/cookbooks/nginx-0.100.5/recipes/default.rb +0 -59
  65. data/spec/fixtures/cookbooks/nginx-0.100.5/resources/defresource.rb +0 -1
  66. data/spec/fixtures/cookbooks/nginx-0.100.5/templates/default/nginx.pill.erb +0 -15
  67. data/spec/fixtures/cookbooks/nginx-0.100.5/templates/default/plugins/nginx.rb.erb +0 -66
  68. data/spec/fixtures/lockfile_spec/with_lock/Berksfile +0 -1
  69. data/spec/fixtures/lockfile_spec/without_lock/.gitkeep +0 -0
  70. data/spec/fixtures/reset.pem +0 -27
  71. data/spec/unit/chef/config_spec.rb +0 -81
@@ -12,6 +12,16 @@ Feature: Displaying information about a cookbook defined by a Berksfile
12
12
 
13
13
  cookbook 'fake', '1.0.0'
14
14
  """
15
+ And I write to "Berksfile.lock" with:
16
+ """
17
+ {
18
+ "dependencies": {
19
+ "fake": {
20
+ "locked_version": "1.0.0"
21
+ }
22
+ }
23
+ }
24
+ """
15
25
  When I successfully run `berks show fake`
16
26
  Then the output should contain:
17
27
  """
@@ -23,43 +33,52 @@ Feature: Displaying information about a cookbook defined by a Berksfile
23
33
  License: none
24
34
  """
25
35
 
26
- Scenario: When JSON is requested
27
- Given the cookbook store has the cookbooks:
28
- | fake | 1.0.0 |
36
+ Scenario: When the cookbook is not in the Berksfile
37
+ Given I write to "Berksfile" with:
38
+ """
39
+ source "http://localhost:26210"
40
+ """
41
+ When I run `berks show fake`
42
+ Then the output should contain:
43
+ """
44
+ Could not find cookbook(s) 'fake' in any of the configured dependencies. Is it in your Berksfile?
45
+ """
46
+ And the exit status should be "DependencyNotFound"
47
+
48
+ Scenario: When there is no lockfile present
29
49
  And I write to "Berksfile" with:
30
50
  """
31
51
  source "http://localhost:26210"
32
52
 
33
53
  cookbook 'fake', '1.0.0'
34
54
  """
35
- When I successfully run `berks show fake --format json`
55
+ When I run `berks show fake`
36
56
  Then the output should contain:
37
57
  """
38
- {
39
- "cookbooks": [
40
- {
41
- "name": "fake",
42
- "version": "1.0.0",
43
- "description": "A fabulous new cookbook",
44
- "author": "YOUR_COMPANY_NAME",
45
- "email": "YOUR_EMAIL",
46
- "license": "none"
47
- }
48
- ],
49
- "errors": [
50
-
51
- ],
52
- "messages": [
53
- "building universe..."
54
- ]
55
- }
58
+ Could not find cookbook 'fake (>= 0.0.0)'. Try running `berks install` to download and install the missing dependencies.
56
59
  """
60
+ And the exit status should be "LockfileNotFound"
57
61
 
58
- Scenario: When the cookbook is not in the Berksfile
59
- Given I write to "Berksfile" with:
62
+ Scenario: When the cookbook is not installed
63
+ And I write to "Berksfile" with:
60
64
  """
61
65
  source "http://localhost:26210"
66
+
67
+ cookbook 'fake', '1.0.0'
68
+ """
69
+ And I write to "Berksfile.lock" with:
70
+ """
71
+ {
72
+ "dependencies": {
73
+ "fake": {
74
+ "locked_version": "1.0.0"
75
+ }
76
+ }
77
+ }
62
78
  """
63
79
  When I run `berks show fake`
64
- Then the output should contain "Cookbook 'fake' is not installed by your Berksfile"
80
+ Then the output should contain:
81
+ """
82
+ Could not find cookbook 'fake (= 1.0.0)'. Try running `berks install` to download and install the missing dependencies.
83
+ """
65
84
  And the exit status should be "CookbookNotFound"
@@ -4,8 +4,8 @@ end
4
4
 
5
5
  Given /^I do not have a Chef config$/ do
6
6
  path = tmp_path.join('knife.rb').to_s
7
- Berkshelf.chef_config = Berkshelf::Chef::Config.new(path)
8
- Berkshelf::Chef::Config.instance.save
7
+ Berkshelf.chef_config = Ridley::Chef::Config.new(path)
8
+ Berkshelf.chef_config.save
9
9
 
10
10
  ENV['BERKSHELF_CHEF_CONFIG'] = path
11
11
  set_env 'BERKSHELF_CHEF_CONFIG', path
@@ -8,10 +8,18 @@ Given /^the Chef Server has cookbooks:$/ do |cookbooks|
8
8
  cookbooks.raw.each do |name, version|
9
9
  purge_cookbook(name, version)
10
10
  cb_path = generate_cookbook(tmp_path, name, version)
11
- upload_cookbook(cb_path)
11
+ upload_cookbook(cb_path, freeze: false, force: true)
12
12
  end
13
13
  end
14
14
 
15
+ Given /^the Chef Server has frozen cookbooks:$/ do |cookbooks|
16
+ cookbooks.raw.each do |name, version|
17
+ purge_cookbook(name, version)
18
+ cb_path = generate_cookbook(tmp_path, name, version)
19
+ upload_cookbook(cb_path, freeze: true, force: true)
20
+ end
21
+ end
22
+
15
23
  Then /^the Chef Server should have the cookbooks:$/ do |cookbooks|
16
24
  cookbooks.raw.each do |name, version|
17
25
  expect(server_has_cookbook?(name, version)).to be_true
@@ -34,7 +34,7 @@ end
34
34
  Then /^a Berkshelf config file should exist at "(.+)" and contain:$/ do |path, table|
35
35
  check_file_presence([path], true)
36
36
 
37
- path = File.expand_path(File.join('tmp', 'aruba', path))
37
+ path = File.join(@dirs.first, path)
38
38
  Berkshelf.config = Berkshelf::Config.from_file(path)
39
39
 
40
40
  table.raw.each do |key, value|
@@ -250,3 +250,10 @@ Then /^the file "(.*?)" in the directory "(.*?)" should not contain:$/ do |file_
250
250
  end
251
251
  }
252
252
  end
253
+
254
+ Then(/^the directory "(.*?)" should contain version "(.*?)" of the "(.*?)" cookbook$/) do |path, version, name|
255
+ cookbook_path = File.join(current_dir, path)
256
+ cookbook = Berkshelf::CachedCookbook.from_path(cookbook_path)
257
+ expect(cookbook.version).to eql(version)
258
+ expect(cookbook.cookbook_name).to eql(name)
259
+ end
@@ -29,6 +29,7 @@ Spork.prefork do
29
29
 
30
30
  Aruba::InProcess.main_class = Berkshelf::Cli::Runner
31
31
  Aruba.process = Aruba::InProcess
32
+ @dirs = ["spec/tmp/aruba"] # set aruba's temporary directory
32
33
 
33
34
  stub_kitchen!
34
35
  clean_tmp_path
@@ -42,7 +43,7 @@ Spork.prefork do
42
43
  options: {
43
44
  url: "http://localhost:#{CHEF_SERVER_PORT}",
44
45
  client_name: "reset",
45
- client_key: fixtures_path.join("reset.pem")
46
+ client_key: File.expand_path("spec/config/berkshelf.pem")
46
47
  }
47
48
  }
48
49
  ]
@@ -23,8 +23,7 @@ Feature: Updating a cookbook defined by a Berksfile
23
23
  {
24
24
  "dependencies":{
25
25
  "berkshelf-cookbook-fixture":{
26
- "locked_version":"0.1.0",
27
- "constraint":"~> 0.1"
26
+ "locked_version":"0.1.0"
28
27
  }
29
28
  }
30
29
  }
@@ -47,12 +46,10 @@ Feature: Updating a cookbook defined by a Berksfile
47
46
  {
48
47
  "dependencies":{
49
48
  "berkshelf-cookbook-fixture":{
50
- "locked_version":"0.1.0",
51
- "constraint":"~> 0.1"
49
+ "locked_version":"0.1.0"
52
50
  },
53
51
  "hostsfile":{
54
- "locked_version":"1.0.1",
55
- "constraint":"= 1.0.1"
52
+ "locked_version":"1.0.1"
56
53
  }
57
54
  }
58
55
  }
@@ -63,12 +60,10 @@ Feature: Updating a cookbook defined by a Berksfile
63
60
  {
64
61
  "dependencies":{
65
62
  "berkshelf-cookbook-fixture":{
66
- "locked_version":"0.2.0",
67
- "constraint":"~> 0.1"
63
+ "locked_version":"0.2.0"
68
64
  },
69
65
  "hostsfile":{
70
- "locked_version":"1.0.1",
71
- "constraint":"~> 1.0.0"
66
+ "locked_version":"1.0.1"
72
67
  }
73
68
  }
74
69
  }
@@ -91,12 +86,10 @@ Feature: Updating a cookbook defined by a Berksfile
91
86
  {
92
87
  "dependencies":{
93
88
  "berkshelf-cookbook-fixture":{
94
- "locked_version":"0.1.0",
95
- "constraint":"~> 0.1"
89
+ "locked_version":"0.1.0"
96
90
  },
97
91
  "hostsfile":{
98
- "locked_version":"1.0.0",
99
- "constraint":"~> 1.0.0"
92
+ "locked_version":"1.0.1"
100
93
  }
101
94
  }
102
95
  }
@@ -107,12 +100,10 @@ Feature: Updating a cookbook defined by a Berksfile
107
100
  {
108
101
  "dependencies":{
109
102
  "berkshelf-cookbook-fixture":{
110
- "locked_version":"0.2.0",
111
- "constraint":"~> 0.1"
103
+ "locked_version":"0.2.0"
112
104
  },
113
105
  "hostsfile":{
114
- "locked_version":"1.0.0",
115
- "constraint":"~> 1.0.0"
106
+ "locked_version":"1.0.1"
116
107
  }
117
108
  }
118
109
  }
@@ -132,8 +123,7 @@ Feature: Updating a cookbook defined by a Berksfile
132
123
  {
133
124
  "dependencies":{
134
125
  "berkshelf-cookbook-fixture":{
135
- "locked_version":"0.1.0",
136
- "constraint":"~> 0.1"
126
+ "locked_version":"0.1.0"
137
127
  }
138
128
  }
139
129
  }
@@ -143,4 +133,4 @@ Feature: Updating a cookbook defined by a Berksfile
143
133
  """
144
134
  Could not find cookbook(s) 'non-existent-cookbook' in any of the configured dependencies. Is it in your Berksfile?
145
135
  """
146
- And the exit status should be "CookbookNotFound"
136
+ And the exit status should be "DependencyNotFound"
@@ -100,7 +100,7 @@ Feature: Uploading cookbooks to a Chef Server
100
100
  When I run `berks upload reset`
101
101
  Then the output should contain:
102
102
  """
103
- Failed to upload cookbook 'reset'. Not defined in Berksfile.
103
+ Could not find cookbook(s) 'reset' in any of the configured dependencies. Is it in your Berksfile?
104
104
  """
105
105
  And the exit status should be "DependencyNotFound"
106
106
 
@@ -281,3 +281,47 @@ Feature: Uploading cookbooks to a Chef Server
281
281
  """
282
282
  Uploading fake (0.0.0)
283
283
  """
284
+
285
+ Scenario: When the cookbook already exist
286
+ Given the cookbook store has the cookbooks:
287
+ | fake | 1.0.0 |
288
+ And the Chef Server has frozen cookbooks:
289
+ | fake | 1.0.0 |
290
+ And I write to "Berksfile" with:
291
+ """
292
+ cookbook 'fake', '1.0.0'
293
+ """
294
+ When I successfully run `berks upload`
295
+ Then the output should contain:
296
+ """
297
+ Skipping fake (1.0.0) (already uploaded)
298
+ """
299
+ And the output should contain:
300
+ """
301
+ Skipped uploading some cookbooks because they already existed on the remote server. Re-run with the `--force` flag to force overwrite these cookbooks:
302
+
303
+ * fake (1.0.0)
304
+ """
305
+ And the exit status should be 0
306
+
307
+ Scenario: When the cookbook already exist and is a metadata location
308
+ Given a cookbook named "fake"
309
+ And the cookbook "fake" has the file "Berksfile" with:
310
+ """
311
+ metadata
312
+ """
313
+ When I cd to "fake"
314
+ And the Chef Server has frozen cookbooks:
315
+ | fake | 0.0.0 |
316
+ When I run `berks upload`
317
+ Then the output should contain:
318
+ """
319
+ building universe...
320
+ Using fake (0.0.0) path: '/home/travis/build/RiotGames/berkshelf/spec/tmp/aruba/fake'
321
+ Uploading fake (0.0.0) to: 'http://localhost:26310/'
322
+ Skipping fake (0.0.0) (already uploaded)
323
+ Skipped uploading some cookbooks because they already existed on the remote server. Re-run with the `--force` flag to force overwrite these cookbooks:
324
+
325
+ * fake (0.0.0)
326
+ """
327
+ And the exit status should be 0
@@ -0,0 +1,83 @@
1
+ Feature: Vendoring cookbooks to a directory
2
+ As a CLI user
3
+ I want a command to vendor cookbooks into a directory
4
+ So they are structured similar to a Chef Repository
5
+
6
+ Background:
7
+ Given the Berkshelf API server's cache is empty
8
+ And the Chef Server is empty
9
+
10
+ Scenario: successfully vendoring a Berksfile with multiple cookbook demands
11
+ Given I write to "Berksfile" with:
12
+ """
13
+ source "http://localhost:26210"
14
+
15
+ cookbook 'berkshelf'
16
+ cookbook 'elixir'
17
+ """
18
+ And the Chef Server has cookbooks:
19
+ | berkshelf | 1.0.0 |
20
+ | elixir | 1.0.0 |
21
+ And the Berkshelf API server's cache is up to date
22
+ When I successfully run `berks vendor cukebooks`
23
+ Then the output should contain:
24
+ """
25
+ Vendoring berkshelf (1.0.0) to
26
+ """
27
+ And the output should contain:
28
+ """
29
+ Vendoring elixir (1.0.0) to
30
+ """
31
+ And a directory named "cukebooks/berkshelf" should exist
32
+ And a directory named "cukebooks/elixir" should exist
33
+ And the directory "cukebooks/berkshelf" should contain version "1.0.0" of the "berkshelf" cookbook
34
+ And the directory "cukebooks/elixir" should contain version "1.0.0" of the "elixir" cookbook
35
+
36
+ Scenario: attempting to vendor when no Berksfile is present
37
+ When I run `berks vendor cukebooks`
38
+ Then the exit status should be "BerksfileNotFound"
39
+
40
+ Scenario: vendoring a Berksfile with a metadata demand
41
+ Given a cookbook named "sparkle-motion"
42
+ And the cookbook "sparkle-motion" has the file "Berksfile" with:
43
+ """
44
+ source "http://localhost:26210"
45
+
46
+ metadata
47
+ """
48
+ When I cd to "sparkle-motion"
49
+ And I successfully run `berks vendor cukebooks`
50
+ Then the output should contain:
51
+ """
52
+ Vendoring sparkle-motion (0.0.0) to
53
+ """
54
+ And a directory named "cukebooks/sparkle-motion" should exist
55
+ And the directory "cukebooks/sparkle-motion" should contain version "0.0.0" of the "sparkle-motion" cookbook
56
+
57
+ Scenario: vendoring without an explicit path to vendor into
58
+ Given I write to "Berksfile" with:
59
+ """
60
+ source "http://localhost:26210"
61
+
62
+ cookbook 'berkshelf'
63
+ """
64
+ And the Chef Server has cookbooks:
65
+ | berkshelf | 1.0.0 |
66
+ And the Berkshelf API server's cache is up to date
67
+ When I successfully run `berks vendor`
68
+ And a directory named "berks-cookbooks/berkshelf" should exist
69
+ And the directory "berks-cookbooks/berkshelf" should contain version "1.0.0" of the "berkshelf" cookbook
70
+
71
+ Scenario: vendoring to a directory that already exists
72
+ Given I write to "Berksfile" with:
73
+ """
74
+ source "http://localhost:26210"
75
+
76
+ cookbook 'berkshelf'
77
+ """
78
+ And the Chef Server has cookbooks:
79
+ | berkshelf | 1.0.0 |
80
+ And the Berkshelf API server's cache is up to date
81
+ And a directory named "cukebooks"
82
+ When I run `berks vendor cukebooks`
83
+ And the exit status should be "VendorError"
@@ -67,14 +67,14 @@ module Berkshelf
67
67
 
68
68
  # The Chef configuration file.
69
69
  #
70
- # @return [Berkshelf::Chef::Config]
70
+ # @return [Ridley::Chef::Config]
71
71
  def chef_config
72
- Berkshelf::Chef::Config.instance
72
+ @chef_config ||= Ridley::Chef::Config.new(ENV['BERKSHELF_CHEF_CONFIG'])
73
73
  end
74
74
 
75
- # @param [Berkshelf::Chef::Config]
75
+ # @param [Ridley::Chef::Config]
76
76
  def chef_config=(config)
77
- Berkshelf::Chef::Config.set_config(config)
77
+ @chef_config = config
78
78
  end
79
79
 
80
80
  # Initialize the filepath for the Berkshelf path..
@@ -153,6 +153,7 @@ require_relative 'berkshelf/dependency'
153
153
  require_relative 'berkshelf/downloader'
154
154
  require_relative 'berkshelf/formatters'
155
155
  require_relative 'berkshelf/git'
156
+ require_relative 'berkshelf/mercurial'
156
157
  require_relative 'berkshelf/init_generator'
157
158
  require_relative 'berkshelf/installer'
158
159
  require_relative 'berkshelf/location'
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Berkshelf
2
4
  class APIClient
3
5
  # A representation of cookbook metadata indexed by a Berkshelf API Server. Returned
@@ -37,6 +39,17 @@ module Berkshelf
37
39
  def location_path
38
40
  @attributes[:location_path]
39
41
  end
42
+
43
+ def to_hash
44
+ {
45
+ name: name,
46
+ version: version
47
+ }
48
+ end
49
+
50
+ def to_json(options = {})
51
+ ::JSON.pretty_generate(to_hash, options)
52
+ end
40
53
  end
41
54
  end
42
55
  end
@@ -13,11 +13,13 @@ module Berkshelf
13
13
  #
14
14
  # @return [Berksfile]
15
15
  def from_file(file)
16
- new(file).dsl_eval_file(file)
17
- rescue Errno::ENOENT => ex
18
- raise BerksfileNotFound, "No Berksfile or Berksfile.lock found at: #{file}"
19
- rescue => ex
20
- raise BerksfileReadError.new(ex)
16
+ raise BerksfileNotFound.new(file) unless File.exist?(file)
17
+
18
+ begin
19
+ new(file).dsl_eval_file(file)
20
+ rescue => ex
21
+ raise BerksfileReadError.new(ex)
22
+ end
21
23
  end
22
24
  end
23
25
 
@@ -365,6 +367,40 @@ module Berkshelf
365
367
  self.install
366
368
  end
367
369
 
370
+ # Retrieve information about a given cookbook that is installed by this Berksfile.
371
+ # Unlike {#find}, which returns a dependency, this method returns the corresponding
372
+ # CachedCookbook for the given name.
373
+ #
374
+ # @raise [LockfileNotFound]
375
+ # if there is no lockfile containing that cookbook
376
+ # @raise [CookbookNotFound]
377
+ # if there is a lockfile with a cookbook, but the cookbook is not downloaded
378
+ #
379
+ # @param [String] name
380
+ # the name of the cookbook to find
381
+ #
382
+ # @return [CachedCookbook]
383
+ # the CachedCookbook that corresponds to the given name parameter
384
+ def retrieve_locked(name)
385
+ validate_cookbook_names!(cookbooks: name)
386
+
387
+ locked = lockfile.find(name)
388
+ unless locked
389
+ raise LockfileNotFound, "Could not find cookbook '#{name} (>= 0.0.0)'."\
390
+ " Try running `berks install` to download and install the missing"\
391
+ " dependencies."
392
+ end
393
+
394
+ cookbook = locked.cached_cookbook
395
+ unless cookbook
396
+ raise CookbookNotFound, "Could not find cookbook '#{name} (= #{locked.locked_version})'."\
397
+ " Try running `berks install` to download and install the missing"\
398
+ " dependencies."
399
+ end
400
+
401
+ cookbook
402
+ end
403
+
368
404
  # List of all the cookbooks which have a newer version found at a source that satisfies
369
405
  # the constraints of your dependencies
370
406
  #
@@ -386,7 +422,28 @@ module Berkshelf
386
422
  # #<CachedCookbook name="artifact"> => "0.11.2"
387
423
  # }
388
424
  def outdated(options = {})
389
- raise RuntimeError, "not yet implemented"
425
+ validate_cookbook_names!(options)
426
+
427
+ outdated = {}
428
+ dependencies(options).each do |dependency|
429
+ locked = retrieve_locked(dependency.name)
430
+ outdated[dependency.name] = {}
431
+
432
+ sources.each do |source|
433
+ cookbooks = source.versions(dependency.name)
434
+
435
+ latest = cookbooks.select do |cookbook|
436
+ dependency.version_constraint.satisfies?(cookbook.version) &&
437
+ cookbook.version != locked.version
438
+ end.sort_by { |cookbook| cookbook.version }.last
439
+
440
+ unless latest.nil?
441
+ outdated[dependency.name][source.uri.to_s] = latest
442
+ end
443
+ end
444
+ end
445
+
446
+ outdated.reject { |name, newer| newer.empty? }
390
447
  end
391
448
 
392
449
  # Upload the cookbooks installed by this Berksfile
@@ -417,16 +474,19 @@ module Berkshelf
417
474
  # @raise [Berkshelf::DependencyNotFound]
418
475
  # if one of the given cookbooks is not a dependency defined in the Berksfile
419
476
  # @raise [Berkshelf::FrozenCookbook]
420
- # if an attempt to upload a cookbook which has been frozen on the target server is made
421
- # and the :halt_on_frozen option was true
477
+ # if the cookbook being uploaded is a {metadata} cookbook and is already
478
+ # frozen on the remote Chef Server; indirect dependencies or non-metadata
479
+ # dependencies are just skipped
422
480
  def upload(options = {})
423
- options = options.reverse_merge(force: false, freeze: true, halt_on_frozen: false, cookbooks: [])
481
+ options = {
482
+ force: false,
483
+ freeze: true,
484
+ halt_on_frozen: false,
485
+ skip_dependencies: false,
486
+ cookbooks: [],
487
+ }.merge(options)
424
488
 
425
- options[:cookbooks].each do |cookbook|
426
- unless dependency = find(cookbook)
427
- raise DependencyNotFound, "Failed to upload cookbook '#{cookbook}'. Not defined in Berksfile."
428
- end
429
- end
489
+ validate_cookbook_names!(options)
430
490
 
431
491
  cached_cookbooks = install(options)
432
492
  cached_cookbooks = filter_to_upload(cached_cookbooks, options[:cookbooks]) if options[:cookbooks]
@@ -495,9 +555,12 @@ module Berkshelf
495
555
  cached_cookbooks.each { |cookbook| validate_files!(cookbook) }
496
556
 
497
557
  Dir.mktmpdir do |tmp|
558
+ cookbooks_dir = File.join(tmp, 'cookbooks')
559
+ FileUtils.mkdir_p(cookbooks_dir)
560
+
498
561
  cached_cookbooks.each do |cookbook|
499
562
  path = cookbook.path.to_s
500
- destination = File.join(tmp, cookbook.cookbook_name)
563
+ destination = File.join(cookbooks_dir, cookbook.cookbook_name)
501
564
 
502
565
  FileUtils.cp_r(path, destination)
503
566
 
@@ -522,6 +585,61 @@ module Berkshelf
522
585
  output
523
586
  end
524
587
 
588
+ # Install the Berksfile or Berksfile.lock and then copy the cached cookbooks into
589
+ # directories within the given destination matching their name.
590
+ #
591
+ # @param [String] destination
592
+ # filepath to vendor cookbooks to
593
+ #
594
+ # @option options [Symbol, Array] :except
595
+ # Group(s) to exclude which will cause any dependencies marked as a member of the
596
+ # group to not be installed
597
+ # @option options [Symbol, Array] :only
598
+ # Group(s) to include which will cause any dependencies marked as a member of the
599
+ # group to be installed and all others to be ignored
600
+ #
601
+ # @return [String, nil]
602
+ # the expanded path cookbooks were vendored to or nil if nothing was vendored
603
+ def vendor(destination, options = {})
604
+ destination = File.expand_path(destination)
605
+
606
+ if Dir.exist?(destination)
607
+ raise VendorError, "destination already exists #{destination}. Delete it and try again or use a " +
608
+ "different filepath."
609
+ end
610
+
611
+ scratch = Berkshelf.mktmpdir
612
+ chefignore = nil
613
+ cached_cookbooks = install(options.slice(:except, :only))
614
+
615
+ if cached_cookbooks.empty?
616
+ return nil
617
+ end
618
+
619
+ if ignore_file = Berkshelf::Chef::Cookbook::Chefignore.find_relative_to(Dir.pwd)
620
+ chefignore = Berkshelf::Chef::Cookbook::Chefignore.new(ignore_file)
621
+ end
622
+
623
+ cached_cookbooks.each do |cookbook|
624
+ Berkshelf.formatter.vendor(cookbook, destination)
625
+ cookbook_destination = File.join(scratch, cookbook.cookbook_name, '/')
626
+ FileUtils.mkdir_p(cookbook_destination)
627
+
628
+ # Dir.glob does not support backslash as a File separator
629
+ src = cookbook.path.to_s.gsub('\\', '/')
630
+ files = Dir.glob(File.join(src, '*'))
631
+
632
+ if chefignore
633
+ files = chefignore.remove_ignores_from(files)
634
+ end
635
+
636
+ FileUtils.cp_r(files, cookbook_destination)
637
+ end
638
+
639
+ FileUtils.mv(scratch, destination)
640
+ destination
641
+ end
642
+
525
643
  # Get the lockfile corresponding to this Berksfile. This is necessary because
526
644
  # the user can specify a different path to the Berksfile. So assuming the lockfile
527
645
  # is named "Berksfile.lock" is a poor assumption.
@@ -536,22 +654,37 @@ module Berkshelf
536
654
  private
537
655
 
538
656
  def do_upload(cookbooks, options = {})
539
- upload_opts = options.slice(:force, :freeze)
657
+ @skipped = []
540
658
 
541
659
  ridley_connection(options) do |conn|
542
660
  cookbooks.each do |cookbook|
543
- Berkshelf.formatter.upload(cookbook.cookbook_name, cookbook.version, conn.server_url)
661
+ Berkshelf.formatter.upload(cookbook, conn)
544
662
  validate_files!(cookbook)
545
663
 
546
664
  begin
547
- conn.cookbook.upload(cookbook.path, upload_opts.merge(name: cookbook.cookbook_name))
665
+ conn.cookbook.upload(cookbook.path, {
666
+ force: options[:force],
667
+ freeze: options[:freeze],
668
+ name: cookbook.cookbook_name,
669
+ })
548
670
  rescue Ridley::Errors::FrozenCookbook => ex
549
671
  if options[:halt_on_frozen]
550
- raise Berkshelf::FrozenCookbook, ex
672
+ raise Berkshelf::FrozenCookbook.new(cookbook)
551
673
  end
674
+
675
+ Berkshelf.formatter.skip(cookbook, conn)
676
+ @skipped << cookbook
552
677
  end
553
678
  end
554
679
  end
680
+
681
+ unless @skipped.empty?
682
+ Berkshelf.formatter.msg "Skipped uploading some cookbooks because they" <<
683
+ " already existed on the remote server. Re-run with the `--force`" <<
684
+ " flag to force overwrite these cookbooks:" <<
685
+ "\n\n" <<
686
+ " * " << @skipped.map { |c| "#{c.cookbook_name} (#{c.version})" }.join("\n * ")
687
+ end
555
688
  end
556
689
 
557
690
  # Filter the cookbooks to upload based on a set of given names. The dependencies of a cookbook
@@ -608,14 +741,13 @@ module Berkshelf
608
741
  # @option options [Array<String>] :cookbooks
609
742
  # a list of strings of cookbook names
610
743
  #
611
- # @raise [Berkshelf::CookbookNotFound]
744
+ # @raise [Berkshelf::DependencyNotFound]
612
745
  # if a cookbook name is given that does not exist
613
746
  def validate_cookbook_names!(options = {})
614
747
  missing = (Array(options[:cookbooks]) - dependencies.map(&:name))
748
+
615
749
  unless missing.empty?
616
- raise Berkshelf::CookbookNotFound,
617
- "Could not find cookbook(s) #{missing.collect{ |c| "'#{c}'" }.join(', ')} " +
618
- "in any of the configured dependencies. #{missing.size == 1 ? 'Is it' : 'Are they' } in your Berksfile?"
750
+ raise Berkshelf::DependencyNotFound.new(missing)
619
751
  end
620
752
  end
621
753