berkshelf 0.3.3 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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