berkshelf 0.3.3 → 0.3.7

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 (39) hide show
  1. data/Guardfile +1 -1
  2. data/features/install.feature +129 -2
  3. data/features/lockfile.feature +1 -1
  4. data/features/step_definitions/filesystem_steps.rb +12 -0
  5. data/features/update.feature +1 -1
  6. data/features/upload_command.feature +1 -1
  7. data/lib/berkshelf/berksfile.rb +4 -1
  8. data/lib/berkshelf/cached_cookbook.rb +36 -9
  9. data/lib/berkshelf/cli.rb +2 -2
  10. data/lib/berkshelf/cookbook_source.rb +26 -63
  11. data/lib/berkshelf/cookbook_source/git_location.rb +21 -10
  12. data/lib/berkshelf/cookbook_source/location.rb +50 -0
  13. data/lib/berkshelf/cookbook_source/path_location.rb +13 -5
  14. data/lib/berkshelf/cookbook_source/site_location.rb +14 -4
  15. data/lib/berkshelf/cookbook_store.rb +34 -16
  16. data/lib/berkshelf/core_ext/string.rb +12 -0
  17. data/lib/berkshelf/downloader.rb +0 -4
  18. data/lib/berkshelf/errors.rb +41 -1
  19. data/lib/berkshelf/git.rb +83 -14
  20. data/lib/berkshelf/resolver.rb +85 -66
  21. data/lib/berkshelf/version.rb +1 -1
  22. data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +1 -0
  23. data/spec/fixtures/cookbooks/example_metadata_name/metadata.rb +2 -0
  24. data/spec/fixtures/cookbooks/example_metadata_no_name/metadata.rb +1 -0
  25. data/spec/fixtures/cookbooks/example_no_metadata/recipes/default.rb +1 -0
  26. data/spec/support/chef_api.rb +42 -0
  27. data/spec/unit/berkshelf/cached_cookbook_spec.rb +55 -11
  28. data/spec/unit/berkshelf/cookbook_source/git_location_spec.rb +65 -15
  29. data/spec/unit/berkshelf/cookbook_source/location_spec.rb +42 -0
  30. data/spec/unit/berkshelf/cookbook_source/path_location_spec.rb +22 -5
  31. data/spec/unit/berkshelf/cookbook_source/site_location_spec.rb +32 -13
  32. data/spec/unit/berkshelf/cookbook_source_spec.rb +24 -33
  33. data/spec/unit/berkshelf/cookbook_store_spec.rb +47 -7
  34. data/spec/unit/berkshelf/git_spec.rb +97 -12
  35. data/spec/unit/berkshelf/resolver_spec.rb +72 -48
  36. data/spec/unit/berkshelf/uploader_spec.rb +12 -4
  37. metadata +13 -7
  38. data/spec/fixtures/cookbooks/invalid_ruby_files-1.0.0/recipes/default.rb +0 -1
  39. data/spec/fixtures/cookbooks/invalid_template_files-1.0.0/templates/default/broken.erb +0 -1
data/Guardfile CHANGED
@@ -16,7 +16,7 @@ guard 'rspec', :version => 2, :cli => "--color --drb --format Fuubar", :all_on_s
16
16
  watch('spec/spec_helper.rb') { "spec" }
17
17
  end
18
18
 
19
- guard 'cucumber', :cli => "--drb --format pretty --tags ~@wip", :all_on_start => false, :all_after_pass => false, :notification => false do
19
+ guard 'cucumber', :cli => "--drb --format pretty --tags ~@no_run", :all_on_start => false, :all_after_pass => false, :notification => false do
20
20
  watch(%r{^features/.+\.feature$})
21
21
  watch(%r{^features/support/.+$}) { 'features' }
22
22
  watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
@@ -3,7 +3,7 @@ Feature: install cookbooks from a Berksfile
3
3
  I want to be able to run knife berkshelf install to install my cookbooks
4
4
  So that I don't have to download my cookbooks and their dependencies manually
5
5
 
6
- Scenario: install cookbooks
6
+ Scenario: installing a Berksfile that contains a source with a default location
7
7
  Given I write to "Berksfile" with:
8
8
  """
9
9
  cookbook "mysql", "1.2.4"
@@ -17,6 +17,83 @@ Feature: install cookbooks from a Berksfile
17
17
  Installing mysql (1.2.4) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
18
18
  Installing openssl (1.0.0) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
19
19
  """
20
+ And the exit status should be 0
21
+
22
+ Scenario: installing a Berksfile that contains the cookbook explicitly desired by a source
23
+ Given the cookbook store has the cookbooks:
24
+ | mysql | 1.2.4 |
25
+ And I write to "Berksfile" with:
26
+ """
27
+ cookbook "mysql", "= 1.2.4"
28
+ """
29
+ When I run the install command
30
+ Then the output should contain:
31
+ """
32
+ Using mysql (1.2.4)
33
+ """
34
+ And the exit status should be 0
35
+
36
+ Scenario: installing a Berksfile that contains a source with dependencies, all of which already have been installed
37
+ Given the cookbook store contains a cookbook "mysql" "1.2.4" with dependencies:
38
+ | openssl | = 1.0.0 |
39
+ | windows | = 1.3.0 |
40
+ | chef_handler | = 1.0.6 |
41
+ And the cookbook store has the cookbooks:
42
+ | openssl | 1.0.0 |
43
+ | windows | 1.3.0 |
44
+ And I write to "Berksfile" with:
45
+ """
46
+ cookbook "mysql", "~> 1.2.0"
47
+ """
48
+ When I run the install command
49
+ Then the output should contain:
50
+ """
51
+ Using mysql (1.2.4)
52
+ Using openssl (1.0.0)
53
+ Using windows (1.3.0)
54
+ Installing chef_handler (1.0.6) from site:
55
+ """
56
+ And the exit status should be 0
57
+
58
+ Scenario: installing a Berksfile that contains a path location
59
+ Given a Berksfile with path location sources to fixtures:
60
+ | example_cookbook | example_cookbook-0.5.0 |
61
+ When I run the install command
62
+ Then the output should contain:
63
+ """
64
+ Using example_cookbook (0.5.0) at path:
65
+ """
66
+ And the exit status should be 0
67
+
68
+ Scenario: installing a Berksfile that contains a Git location
69
+ Given I write to "Berksfile" with:
70
+ """
71
+ cookbook "artifact", git: "git://github.com/RiotGames/artifact-cookbook.git", ref: "0.9.8"
72
+ """
73
+ When I run the install command
74
+ Then the cookbook store should have the cookbooks:
75
+ | artifact | 0.9.8 |
76
+ And the output should contain:
77
+ """
78
+ Installing artifact (0.9.8) from git: 'git://github.com/RiotGames/artifact-cookbook.git' with branch: '0.9.8'
79
+ """
80
+ And the exit status should be 0
81
+
82
+ Scenario: installing a Berksfile that contains an explicit site location
83
+ Given I write to "Berksfile" with:
84
+ """
85
+ cookbook "mysql", "1.2.4", site: "http://cookbooks.opscode.com/api/v1/cookbooks"
86
+ """
87
+ When I run the install command
88
+ Then the cookbook store should have the cookbooks:
89
+ | mysql | 1.2.4 |
90
+ | openssl | 1.0.0 |
91
+ And the output should contain:
92
+ """
93
+ Installing mysql (1.2.4) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
94
+ Installing openssl (1.0.0) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
95
+ """
96
+ And the exit status should be 0
20
97
 
21
98
  Scenario: running install when current project is a cookbook and the 'metadata' is specified
22
99
  Given a cookbook named "sparkle_motion"
@@ -54,7 +131,6 @@ Feature: install cookbooks from a Berksfile
54
131
  """
55
132
  And the CLI should exit with the status code for error "DownloadFailure"
56
133
 
57
- @wip
58
134
  Scenario: running install command with the --shims flag to create a directory of shims
59
135
  Given I write to "Berksfile" with:
60
136
  """
@@ -70,3 +146,54 @@ Feature: install cookbooks from a Berksfile
70
146
  Shims written to:
71
147
  """
72
148
  And the exit status should be 0
149
+
150
+ Scenario: installing a Berksfile that has a Git location source with an invalid Git URI
151
+ Given I write to "Berksfile" with:
152
+ """
153
+ cookbook "nginx", git: "/something/on/disk"
154
+ """
155
+ When I run the install command
156
+ Then the output should contain:
157
+ """
158
+ '/something/on/disk' is not a valid Git URI.
159
+ """
160
+ And the CLI should exit with the status code for error "InvalidGitURI"
161
+
162
+ Scenario: installing when there are sources with duplicate names defined
163
+ Given I write to "Berksfile" with:
164
+ """
165
+ cookbook "artifact"
166
+ cookbook "artifact"
167
+ """
168
+ When I run the install command
169
+ Then the output should contain:
170
+ """
171
+ Berksfile contains two sources named 'artifact'. Remove one and try again.
172
+ """
173
+ And the CLI should exit with the status code for error "DuplicateSourceDefined"
174
+
175
+ Scenario: installing when a git source defines a branch that does not satisfy the version constraint
176
+ Given I write to "Berksfile" with:
177
+ """
178
+ cookbook "artifact", "= 0.9.8", git: "git://github.com/RiotGames/artifact-cookbook.git", ref: "0.10.0"
179
+ """
180
+ When I run the install command
181
+ Then the output should contain:
182
+ """
183
+ A cookbook satisfying 'artifact' (= 0.9.8) not found at git: 'git://github.com/RiotGames/artifact-cookbook.git' with branch: '0.10.0'
184
+ """
185
+ And the CLI should exit with the status code for error "ConstraintNotSatisfied"
186
+
187
+ Scenario: when a git location source is defined and a cookbook of the same name is already cached in the cookbook store
188
+ Given I write to "Berksfile" with:
189
+ """
190
+ cookbook "artifact", git: "git://github.com/RiotGames/artifact-cookbook.git", ref: "0.10.0"
191
+ """
192
+ And the cookbook store has the cookbooks:
193
+ | artifact | 0.10.0 |
194
+ When I run the install command
195
+ Then the output should contain:
196
+ """
197
+ Installing artifact (0.10.0) from git: 'git://github.com/RiotGames/artifact-cookbook.git' with branch: '0.10.0'
198
+ """
199
+ And the exit status should be 0
@@ -17,6 +17,6 @@ Feature: Berksfile.lock
17
17
  cookbook 'ntp', :locked_version => '1.1.8'
18
18
  cookbook 'mysql', :git => 'https://github.com/opscode-cookbooks/mysql.git', :ref => '190c0c2267785b7b9b303369b8a64ed04364d5f9'
19
19
  cookbook 'openssl', :locked_version => '1.0.0'
20
- cookbook 'chef_handler', :locked_version => '1.0.6'
21
20
  cookbook 'windows', :locked_version => '1.3.0'
21
+ cookbook 'chef_handler', :locked_version => '1.0.6'
22
22
  """
@@ -1,6 +1,7 @@
1
1
  require 'aruba/api'
2
2
 
3
3
  World(Aruba::Api)
4
+ World(Berkshelf::RSpec::ChefAPI)
4
5
 
5
6
  Given /^a cookbook named "(.*?)"$/ do |name|
6
7
  steps %{
@@ -21,6 +22,17 @@ Given /^the cookbook "(.*?)" has the file "(.*?)" with:$/ do |cookbook_name, fil
21
22
  write_file(File.join(cookbook_name, file_name), content)
22
23
  end
23
24
 
25
+ Given /^the cookbook store has the cookbooks:$/ do |cookbooks|
26
+ cookbooks.raw.each do |name, version|
27
+ generate_cookbook(cookbook_store, name, version)
28
+ end
29
+ end
30
+
31
+ Given /^the cookbook store contains a cookbook "(.*?)" "(.*?)" with dependencies:$/ do |name, version, dependencies|
32
+ generate_cookbook(cookbook_store, name, version, dependencies: dependencies.raw)
33
+ end
34
+
35
+
24
36
  Then /^the cookbook store should have the cookbooks:$/ do |cookbooks|
25
37
  cookbooks.raw.each do |name, version|
26
38
  cookbook_store.should have_structure {
@@ -18,6 +18,6 @@ Feature: update
18
18
  """
19
19
  cookbook 'mysql', :locked_version => '1.2.6'
20
20
  cookbook 'openssl', :locked_version => '1.0.0'
21
- cookbook 'chef_handler', :locked_version => '1.0.6'
22
21
  cookbook 'windows', :locked_version => '1.3.0'
22
+ cookbook 'chef_handler', :locked_version => '1.0.6'
23
23
  """
@@ -3,7 +3,7 @@ Feature: upload command
3
3
  I need a way to upload cookbooks to a Chef server that I have installed into my Bookshelf
4
4
  So they are available to Chef clients
5
5
 
6
- @slow_process @wip
6
+ @no_run @slow_process
7
7
  Scenario: running the upload command when the Sources in the Berksfile are already installed
8
8
  Given I write to "Berksfile" with:
9
9
  """
@@ -47,7 +47,9 @@ module Berkshelf
47
47
  #
48
48
  # @return [Array<Berkshelf::CookbookSource]
49
49
  def add_source(source)
50
- raise DuplicateSourceDefined if has_source?(source)
50
+ if has_source?(source)
51
+ raise DuplicateSourceDefined, "Berksfile contains two sources named '#{source.name}'. Remove one and try again."
52
+ end
51
53
  @sources[source.to_s] = source
52
54
  end
53
55
 
@@ -186,6 +188,7 @@ module Berkshelf
186
188
  FileUtils.mkdir_p(path)
187
189
  cached_cookbooks.each do |cached_cookbook|
188
190
  destination = File.expand_path(File.join(path, cached_cookbook.cookbook_name))
191
+ FileUtils.rm_rf(destination)
189
192
  begin
190
193
  FileUtils.ln_r(cached_cookbook.path, destination, force: true)
191
194
  rescue ArgumentError
@@ -5,22 +5,48 @@ module Berkshelf
5
5
  # @author Jamie Winsor <jamie@vialstudios.com>
6
6
  class CachedCookbook
7
7
  class << self
8
- # @param [String] path
8
+ # Creates a new instance of Berkshelf::CachedCookbook from a path on disk that
9
+ # contains a Cookbook. The name of the Cookbook will be determined first by the
10
+ # name attribute of the metadata.rb file if it is present. If the name attribute
11
+ # has not been set the Cookbook name will be determined by the basename of the
12
+ # given filepath.
13
+ #
14
+ # @param [#to_s] path
15
+ # a path on disk to the location of a Cookbook
16
+ #
17
+ # @return [Berkshelf::CachedCookbook]
18
+ def from_path(path)
19
+ path = Pathname.new(path)
20
+ metadata = Chef::Cookbook::Metadata.new
21
+
22
+ begin
23
+ metadata.from_file(path.join("metadata.rb").to_s)
24
+ rescue IOError
25
+ raise CookbookNotFound, "No 'metadata.rb' file found at: '#{path}'"
26
+ end
27
+
28
+ name = metadata.name.empty? ? File.basename(path) : metadata.name
29
+
30
+ new(name, path, metadata)
31
+ end
32
+
33
+ # @param [#to_s] path
9
34
  # a path on disk to the location of a Cookbook downloaded by the Downloader
10
35
  #
11
36
  # @return [CachedCookbook]
12
37
  # an instance of CachedCookbook initialized by the contents found at the
13
38
  # given path.
14
- def from_path(path)
15
- matchdata = File.basename(path.to_s).match(DIRNAME_REGEXP)
16
- return nil if matchdata.nil?
17
-
18
- cached_name = matchdata[1]
39
+ def from_store_path(path)
40
+ path = Pathname.new(path)
41
+ cached_name = File.basename(path.to_s).slice(DIRNAME_REGEXP, 1)
42
+ return nil if cached_name.nil?
19
43
 
20
44
  metadata = Chef::Cookbook::Metadata.new
21
45
 
22
- if path.join("metadata.rb").exist?
46
+ begin
23
47
  metadata.from_file(path.join("metadata.rb").to_s)
48
+ rescue IOError
49
+ raise CookbookNotFound, "No 'metadata.rb' file found at: '#{path}'"
24
50
  end
25
51
 
26
52
  new(cached_name, path, metadata)
@@ -37,7 +63,7 @@ module Berkshelf
37
63
  end
38
64
  end
39
65
 
40
- DIRNAME_REGEXP = /^(.+)-(\d+\.\d+\.\d+)$/
66
+ DIRNAME_REGEXP = /^(.+)-(.+)$/
41
67
  CHEF_TYPE = "cookbook_version".freeze
42
68
  CHEF_JSON_CLASS = "Chef::CookbookVersion".freeze
43
69
 
@@ -67,7 +93,8 @@ module Berkshelf
67
93
  # }
68
94
  attr_reader :manifest
69
95
 
70
- def_delegators :@metadata, :version
96
+ def_delegator :@metadata, :version
97
+ def_delegator :@metadata, :dependencies
71
98
 
72
99
  def initialize(name, path, metadata)
73
100
  @cookbook_name = name
@@ -8,7 +8,6 @@ module Berkshelf
8
8
  super
9
9
  # JW TODO: Replace Chef::Knife::UI with our own UI class
10
10
  ::Berkshelf.ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
11
- load_config
12
11
  @options = options.dup # unfreeze frozen options Hash from Thor
13
12
  rescue BerkshelfError => e
14
13
  Berkshelf.ui.fatal e
@@ -24,7 +23,7 @@ module Berkshelf
24
23
 
25
24
  class_option :config,
26
25
  type: :string,
27
- default: File.expand_path("~/.chef/knife.rb"),
26
+ default: File.expand_path(ENV["CHEF_CONFIG"] || "~/.chef/knife.rb"),
28
27
  desc: "Path to Knife or Chef configuration to use.",
29
28
  aliases: "-c",
30
29
  banner: "PATH"
@@ -96,6 +95,7 @@ module Berkshelf
96
95
  desc: "Upload all cookbooks even if a frozen one exists on the target Chef Server"
97
96
  desc "upload", "Upload the Cookbooks specified by a Berksfile or a Berksfile.lock to a Chef Server."
98
97
  def upload
98
+ load_config
99
99
  berksfile = ::Berkshelf::Berksfile.from_file(options[:berksfile])
100
100
  berksfile.upload(Chef::Config[:chef_server_url], options)
101
101
  rescue BerkshelfError => e
@@ -1,18 +1,9 @@
1
1
  module Berkshelf
2
2
  # @author Jamie Winsor <jamie@vialstudios.com>
3
3
  class CookbookSource
4
- module Location
5
- attr_reader :name
6
-
7
- def initialize(name)
8
- @name = name
9
- end
10
-
11
- def download(destination)
12
- raise NotImplementedError, "Function must be implemented on includer"
13
- end
14
- end
4
+ extend Forwardable
15
5
 
6
+ autoload :Location, 'berkshelf/cookbook_source/location'
16
7
  autoload :SiteLocation, 'berkshelf/cookbook_source/site_location'
17
8
  autoload :GitLocation, 'berkshelf/cookbook_source/git_location'
18
9
  autoload :PathLocation, 'berkshelf/cookbook_source/path_location'
@@ -20,49 +11,52 @@ module Berkshelf
20
11
  LOCATION_KEYS = [:git, :path, :site]
21
12
 
22
13
  attr_reader :name
14
+ alias_method :to_s, :name
15
+
23
16
  attr_reader :version_constraint
24
17
  attr_reader :groups
25
18
  attr_reader :location
26
- attr_reader :local_path
19
+ attr_accessor :cached_cookbook
27
20
 
28
- # TODO: describe how the options on this function work.
29
- #
30
- # @param [String] name
31
- # @param [String] version_constraint (optional)
32
- # @param [Hash] options (optional)
21
+ def_delegator :@location, :downloaded?
22
+
23
+ # @overload initialize(name, version_constraint, options = {})
24
+ # @param [#to_s] name
25
+ # @param [#to_s] version_constraint
26
+ # @param [Hash] options
27
+ # @overload initialize(name, options = {})
28
+ # @param [#to_s] name
29
+ # @param [Hash] options
33
30
  def initialize(*args)
34
31
  options = args.last.is_a?(Hash) ? args.pop : {}
35
32
  name, constraint = args
36
33
 
37
34
  @name = name
38
- @version_constraint = DepSelector::VersionConstraint.new(constraint)
35
+ @version_constraint = DepSelector::VersionConstraint.new(constraint || ">= 0.0.0")
39
36
  @groups = []
40
- @local_path = nil
37
+ @cached_cookbook = nil
41
38
 
42
39
  if (options.keys & LOCATION_KEYS).length > 1
43
40
  raise ArgumentError, "Only one location key (#{LOCATION_KEYS.join(', ')}) may be specified"
44
41
  end
45
42
 
46
- options[:version_constraint] = version_constraint if version_constraint
47
-
48
43
  @location = case
49
44
  when options[:git]
50
- GitLocation.new(name, options)
45
+ GitLocation.new(name, version_constraint, options)
51
46
  when options[:path]
52
- loc = PathLocation.new(name, options)
53
- set_local_path loc.path
47
+ loc = PathLocation.new(name, version_constraint, options)
48
+ @cached_cookbook = CachedCookbook.from_path(loc.path)
54
49
  loc
55
50
  when options[:site]
56
- SiteLocation.new(name, options)
51
+ SiteLocation.new(name, version_constraint, options)
57
52
  else
58
- SiteLocation.new(name, options)
53
+ SiteLocation.new(name, version_constraint, options)
59
54
  end
60
55
 
61
56
  @locked_version = DepSelector::Version.new(options[:locked_version]) if options[:locked_version]
62
57
 
63
58
  add_group(options[:group]) if options[:group]
64
59
  add_group(:default) if groups.empty?
65
- set_downloaded_status(false)
66
60
  end
67
61
 
68
62
  def add_group(*groups)
@@ -83,55 +77,24 @@ module Berkshelf
83
77
  # [ :ok, "/tmp/nginx" ]
84
78
  # [ :error, "Cookbook 'sparkle_motion' not found at site: http://cookbooks.opscode.com/api/v1/cookbooks" ]
85
79
  def download(destination)
86
- set_local_path location.download(destination)
87
- [ :ok, local_path ]
80
+ self.cached_cookbook = location.download(destination)
81
+
82
+ [ :ok, self.cached_cookbook ]
88
83
  rescue CookbookNotFound => e
89
- set_local_path = nil
84
+ self.cached_cookbook = nil
90
85
  [ :error, e.message ]
91
86
  end
92
87
 
93
- def downloaded?
94
- !local_path.nil?
95
- end
96
-
97
- def metadata
98
- return nil unless local_path
99
-
100
- cookbook_metadata = Chef::Cookbook::Metadata.new
101
- cookbook_metadata.from_file(File.join(local_path, "metadata.rb"))
102
- cookbook_metadata
103
- end
104
-
105
- def to_s
106
- name
107
- end
108
-
109
88
  def has_group?(group)
110
89
  groups.include?(group.to_sym)
111
90
  end
112
91
 
113
- def dependencies
114
- return nil unless metadata
115
-
116
- metadata.dependencies
117
- end
118
-
119
- def local_version
120
- return nil unless metadata
121
-
122
- metadata.version
123
- end
124
-
125
92
  def locked_version
126
- @locked_version || local_version
93
+ @locked_version || cached_cookbook.version
127
94
  end
128
95
 
129
96
  private
130
97
 
131
- def set_downloaded_status(state)
132
- @downloaded_state = state
133
- end
134
-
135
98
  def set_local_path(path)
136
99
  @local_path = path
137
100
  end