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
@@ -4,8 +4,11 @@ module Berkshelf
4
4
  extend Forwardable
5
5
  include DepSelector
6
6
 
7
- def_delegators :@graph, :package, :packages
7
+ def_delegator :@graph, :package
8
+ def_delegator :@graph, :packages
8
9
 
10
+ # @param [Downloader] downloader
11
+ # @param [Array<CookbookSource>, CookbookSource] sources
9
12
  def initialize(downloader, sources = Array.new)
10
13
  @downloader = downloader
11
14
  @graph = DependencyGraph.new
@@ -18,58 +21,55 @@ module Berkshelf
18
21
  Array(sources).each do |source|
19
22
  add_source(source, false)
20
23
  end
21
- add_sources_dependencies
24
+
25
+ Array(sources).each do |source|
26
+ add_source_dependencies(source)
27
+ end
22
28
  end
23
29
 
30
+ # Add the given source to the collection of sources for this instance
31
+ # of Resolver. By default the dependencies of the given source will also
32
+ # be added as sources to the collection.
33
+ #
24
34
  # @param [Berkshelf::CookbookSource] source
25
35
  # source to add
26
36
  # @param [Boolean] include_dependencies
27
- # if true, after adding the source the dependencies defined in the
28
- # sources metadata will be added to the graph and downloaded
37
+ # adds the dependencies of the given source as sources to the collection of
38
+ # if true. Dependencies will be ignored if false.
29
39
  #
30
- # @return [DepSelector::PackageVersion]
40
+ # @return [Array<CookbookSource>]
31
41
  def add_source(source, include_dependencies = true)
32
- raise DuplicateSourceDefined if has_source?(source.name)
33
-
34
- set_source(source.name, source)
42
+ if has_source?(source)
43
+ raise DuplicateSourceDefined, "A source named '#{source.name}' is already present."
44
+ end
35
45
 
36
- install_or_use_source(source)
46
+ set_source(source)
47
+ use_source(source) || install_source(source)
37
48
 
38
49
  package = add_package(source.name)
39
- package_version = add_version(package, Version.new(source.metadata.version))
50
+ package_version = add_version(package, Version.new(source.cached_cookbook.version))
40
51
 
41
- add_dependencies(package_version, source.dependencies) if include_dependencies
52
+ if include_dependencies
53
+ add_source_dependencies(source)
54
+ end
42
55
 
43
- package_version
56
+ sources
44
57
  end
45
58
 
46
- # @param [DepSelector::PackageVersion] parent_pkgver
47
- # the PackageVersion you would like to add the given dependencies to. In this case
48
- # the package version is a version of a Cookbook.
49
- # @param [Hash] dependencies
50
- # A hash containing Cookbook names for keys and version constraint strings for
51
- # values. This is the same format obtained by sending the 'dependencies' message
52
- # to an instance of Chef::Cookbook::Metadata.
59
+ # Add the dependencies of the given source as sources in the collection of sources
60
+ # on this instance of Resolver. Any dependencies which already have a source in the
61
+ # collection of sources of the same name will not be added to the collection a second
62
+ # time.
53
63
  #
54
- # Example:
55
- # {
56
- # "build-essential" => ">= 0.0.0",
57
- # "ohai" => "~> 1.0.2"
58
- # }
59
- def add_dependencies(parent_pkgver, dependencies)
60
- dependencies.each do |name, constraint|
61
- dep_package = add_package(name)
62
- parent_pkgver.dependencies << Dependency.new(dep_package, VersionConstraint.new(constraint))
63
-
64
- unless has_source?(name)
65
- source = CookbookSource.new(name, constraint)
66
-
67
- install_or_use_source(source)
68
-
69
- dep_pkgver = add_version(dep_package, Version.new(source.metadata.version))
70
- add_dependencies(dep_pkgver, source.dependencies)
71
- set_source(source.name, source)
72
- end
64
+ # @param [CookbookSource] source
65
+ # source to convert dependencies into sources
66
+ #
67
+ # @return [Array<CookbookSource>]
68
+ def add_source_dependencies(source)
69
+ source.cached_cookbook.dependencies.each do |name, constraint|
70
+ next if has_source?(name)
71
+
72
+ add_source(CookbookSource.new(name, constraint))
73
73
  end
74
74
  end
75
75
 
@@ -88,22 +88,24 @@ module Berkshelf
88
88
 
89
89
  [].tap do |cached_cookbooks|
90
90
  solution.each do |name, version|
91
- source = get_source(name)
92
- cached_cookbooks << CachedCookbook.new(source.name, source.local_path, source.metadata)
91
+ cached_cookbooks << get_source(name).cached_cookbook
93
92
  end
94
93
  end
95
94
  end
96
95
 
97
- # @param [#to_s] source
96
+ # @param [CookbookSource, #to_s] source
98
97
  # name of the source to return
99
98
  #
100
99
  # @return [Berkshelf::CookbookSource]
101
100
  def [](source)
101
+ if source.is_a?(CookbookSource)
102
+ source = source.name
103
+ end
102
104
  @sources[source.to_s]
103
105
  end
104
106
  alias_method :get_source, :[]
105
107
 
106
- # @param [#to_s] source
108
+ # @param [CoobookSource, #to_s] source
107
109
  # the source to test if the resolver has added
108
110
  def has_source?(source)
109
111
  !get_source(source).nil?
@@ -114,28 +116,53 @@ module Berkshelf
114
116
  attr_reader :downloader
115
117
  attr_reader :graph
116
118
 
117
- # @param [#to_s] source
118
- # name of the source to set
119
- # @param [CookbookSource] value
120
- # source to set as value
121
- def []=(source, value)
122
- @sources[source.to_s] = value
119
+ # @param [CookbookSource] source
120
+ def set_source(source)
121
+ @sources[source.name] = source
123
122
  end
124
- alias_method :set_source, :[]=
125
123
 
126
- def install_or_use_source(source)
127
- if downloader.downloaded?(source)
128
- msg = "Using #{source.name} (#{source.metadata.version})"
124
+ # @param [Berkshelf::CookbookSource] source
125
+ #
126
+ # @return [Boolean]
127
+ def install_source(source)
128
+ downloader.download!(source)
129
+ Berkshelf.ui.info "Installing #{source.name} (#{source.cached_cookbook.version}) from #{source.location}"
130
+ end
129
131
 
130
- if source.location.is_a?(CookbookSource::PathLocation)
131
- msg << " at #{source.location}"
132
+ # Use the given source to create a constraint solution if the source has been downloaded or can
133
+ # be satisfied by a cached cookbook that is already present in the cookbook store.
134
+ #
135
+ # @note Git location sources which have not yet been downloaded will not be satisfied by a
136
+ # cached cookbook from the cookbook store.
137
+ #
138
+ # @param [Berkshelf::CookbookSource] source
139
+ #
140
+ # @raise [ConstraintNotSatisfied] if the CachedCookbook does not satisfy the version constraint of
141
+ # this instance of Location.
142
+ # contain a cookbook that satisfies the given version constraint of this instance of
143
+ # CookbookSource.
144
+ #
145
+ # @return [Boolean]
146
+ def use_source(source)
147
+ if source.downloaded?
148
+ cached = source.cached_cookbook
149
+ source.location.validate_cached(cached)
150
+ else
151
+ if source.location.is_a?(CookbookSource::GitLocation)
152
+ return false
132
153
  end
133
154
 
134
- Berkshelf.ui.info msg
135
- else
136
- downloader.download!(source)
137
- Berkshelf.ui.info "Installing #{source.name} (#{source.local_version}) from #{source.location}"
155
+ cached = downloader.cookbook_store.satisfy(source.name, source.version_constraint)
156
+ return false if cached.nil?
157
+
158
+ get_source(source).cached_cookbook = cached
138
159
  end
160
+
161
+ msg = "Using #{cached.cookbook_name} (#{cached.version})"
162
+ msg << " at #{source.location}" if source.location.is_a?(CookbookSource::PathLocation)
163
+ Berkshelf.ui.info msg
164
+
165
+ true
139
166
  end
140
167
 
141
168
  def selector
@@ -148,14 +175,6 @@ module Berkshelf
148
175
  end
149
176
  end
150
177
 
151
- # Add the dependencies of each source to the graph
152
- def add_sources_dependencies
153
- sources.each do |source|
154
- package_version = package(source.name)[Version.new(source.metadata.version)]
155
- add_dependencies(package_version, source.dependencies)
156
- end
157
- end
158
-
159
178
  # @param [String] name
160
179
  # name of the package to add to the graph
161
180
  def add_package(name)
@@ -1,3 +1,3 @@
1
1
  module Berkshelf
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.7"
3
3
  end
@@ -1,3 +1,4 @@
1
+ name "example_cookbook"
1
2
  maintainer "Josiah Kiehl"
2
3
  maintainer_email "josiah@skirmisher.net"
3
4
  license "DO WHAT YOU WANT CAUSE A PIRATE IS FREE"
@@ -0,0 +1,2 @@
1
+ name "has_metadata"
2
+ version "1.0.0"
@@ -1,3 +1,4 @@
1
+ require 'pathname'
1
2
  require 'chef/rest'
2
3
  require 'chef/cookbook_version'
3
4
 
@@ -17,6 +18,47 @@ module Berkshelf
17
18
  false
18
19
  end
19
20
 
21
+ def generate_cookbook(path, name, version, options = {})
22
+ path = Pathname.new(path)
23
+ cookbook_path = path.join("#{name}-#{version}")
24
+ directories = [
25
+ "recipes",
26
+ "templates/default",
27
+ "files/default",
28
+ "attributes",
29
+ "definitions",
30
+ "providers",
31
+ "resources"
32
+ ]
33
+ files = [
34
+ "recipes/default.rb",
35
+ "templates/default/template.erb",
36
+ "files/default/file.h",
37
+ "attributes/default.rb"
38
+ ]
39
+
40
+ directories.each do |directory|
41
+ FileUtils.mkdir_p(cookbook_path.join(directory))
42
+ end
43
+
44
+ files.each do |file|
45
+ FileUtils.touch(cookbook_path.join(file))
46
+ end
47
+
48
+ metadata = <<-EOF
49
+ name "#{name}"
50
+ version "#{version}"
51
+ EOF
52
+
53
+ if options[:dependencies]
54
+ options[:dependencies].each do |name, constraint|
55
+ metadata << "depends '#{name}', '#{constraint}'\n"
56
+ end
57
+ end
58
+
59
+ File.write(cookbook_path.join("metadata.rb"), metadata)
60
+ end
61
+
20
62
  private
21
63
 
22
64
  def rest
@@ -6,8 +6,44 @@ module Berkshelf
6
6
  subject { CachedCookbook }
7
7
 
8
8
  describe "#from_path" do
9
+ context "given a path that contains a cookbook with a metadata file that contains a name attribute" do
10
+ let(:cookbook_path) { fixtures_path.join("cookbooks", "example_metadata_name") }
11
+
12
+ it "returns an instance of CachedCookbook" do
13
+ subject.from_path(cookbook_path).should be_a(CachedCookbook)
14
+ end
15
+
16
+ it "has a cookbook_name attribute set to what is found in the metadata" do
17
+ subject.from_path(cookbook_path).cookbook_name.should eql("has_metadata")
18
+ end
19
+ end
20
+
21
+ context "given a path that contains a cookbook with a metadata file that does not contain a name attribute" do
22
+ let(:cookbook_path) { fixtures_path.join("cookbooks", "example_metadata_no_name") }
23
+
24
+ it "returns an instnace of CachedCookbook" do
25
+ subject.from_path(cookbook_path).should be_a(CachedCookbook)
26
+ end
27
+
28
+ it "has a cookbook_name attribute set to the basename of the folder" do
29
+ subject.from_path(cookbook_path).cookbook_name.should eql("example_metadata_no_name")
30
+ end
31
+ end
32
+
33
+ context "given a path that does not contain a metadata file" do
34
+ let(:cookbook_path) { fixtures_path.join("cookbooks", "example_no_metadata") }
35
+
36
+ it "raises a CookbookNotFound error" do
37
+ lambda {
38
+ subject.from_path(cookbook_path)
39
+ }.should raise_error(Berkshelf::CookbookNotFound)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "#from_store_path" do
9
45
  before(:each) do
10
- @cached_cb = subject.from_path(fixtures_path.join("cookbooks", "example_cookbook-0.5.0"))
46
+ @cached_cb = subject.from_store_path(fixtures_path.join("cookbooks", "example_cookbook-0.5.0"))
11
47
  end
12
48
 
13
49
  it "returns an instance of CachedCookbook" do
@@ -20,13 +56,13 @@ module Berkshelf
20
56
 
21
57
  context "given a path that does not contain a cookbook" do
22
58
  it "returns nil" do
23
- subject.from_path(tmp_path).should be_nil
59
+ subject.from_store_path(tmp_path).should be_nil
24
60
  end
25
61
  end
26
62
 
27
63
  context "given a path that does not match the CachedCookbook dirname format" do
28
64
  it "returns nil" do
29
- subject.from_path(fixtures_path.join("cookbooks", "example_cookbook")).should be_nil
65
+ subject.from_store_path(fixtures_path.join("cookbooks", "example_cookbook")).should be_nil
30
66
  end
31
67
  end
32
68
  end
@@ -47,7 +83,7 @@ module Berkshelf
47
83
  end
48
84
 
49
85
  let(:cb_path) { fixtures_path.join("cookbooks", "nginx-0.100.5") }
50
- subject { CachedCookbook.from_path(cb_path) }
86
+ subject { CachedCookbook.from_store_path(cb_path) }
51
87
 
52
88
  describe "#checksums" do
53
89
  it "returns a Hash containing an entry for all matching cookbook files on disk" do
@@ -82,25 +118,33 @@ module Berkshelf
82
118
  end
83
119
 
84
120
  describe "#validate!" do
85
- it "returns true if the cookbook of the given name and version is valid" do
86
- @cb = CachedCookbook.from_path(fixtures_path.join("cookbooks", "example_cookbook-0.5.0"))
121
+ let(:syntax_checker) { double('syntax_checker') }
122
+
123
+ before(:each) do
124
+ subject.stub(:syntax_checker) { syntax_checker }
125
+ end
126
+
127
+ it "asks the syntax_checker to validate the ruby and template files of the cookbook" do
128
+ syntax_checker.should_receive(:validate_ruby_files).and_return(true)
129
+ syntax_checker.should_receive(:validate_templates).and_return(true)
87
130
 
88
- @cb.validate!.should be_true
131
+ subject.validate!
89
132
  end
90
133
 
91
134
  it "raises CookbookSyntaxError if the cookbook contains invalid ruby files" do
92
- @cb = CachedCookbook.from_path(fixtures_path.join("cookbooks", "invalid_ruby_files-1.0.0"))
135
+ syntax_checker.should_receive(:validate_ruby_files).and_return(false)
93
136
 
94
137
  lambda {
95
- @cb.validate!
138
+ subject.validate!
96
139
  }.should raise_error(CookbookSyntaxError)
97
140
  end
98
141
 
99
142
  it "raises CookbookSyntaxError if the cookbook contains invalid template files" do
100
- @cb = CachedCookbook.from_path(fixtures_path.join("cookbooks", "invalid_template_files-1.0.0"))
143
+ syntax_checker.should_receive(:validate_ruby_files).and_return(true)
144
+ syntax_checker.should_receive(:validate_templates).and_return(false)
101
145
 
102
146
  lambda {
103
- @cb.validate!
147
+ subject.validate!
104
148
  }.should raise_error(CookbookSyntaxError)
105
149
  end
106
150
  end
@@ -2,31 +2,45 @@ require 'spec_helper'
2
2
 
3
3
  module Berkshelf
4
4
  describe CookbookSource::GitLocation do
5
- subject { CookbookSource::GitLocation.new("nginx", :git => "git://github.com/opscode-cookbooks/nginx.git") }
5
+ let(:complacent_constraint) { double('comp-vconstraint', include?: true) }
6
+
7
+ describe "ClassMethods" do
8
+ subject { CookbookSource::GitLocation }
9
+
10
+ describe "::initialize" do
11
+ it "raises InvalidGitURI if given an invalid Git URI for options[:git]" do
12
+ lambda {
13
+ subject.new("nginx", complacent_constraint, git: "/something/on/disk")
14
+ }.should raise_error(InvalidGitURI)
15
+ end
16
+ end
17
+ end
18
+
19
+ subject { CookbookSource::GitLocation.new("artifact", complacent_constraint, git: "git://github.com/RiotGames/artifact-cookbook.git") }
6
20
 
7
21
  describe "#download" do
22
+ it "returns an instance of Berkshelf::CachedCookbook" do
23
+ subject.download(tmp_path).should be_a(Berkshelf::CachedCookbook)
24
+ end
25
+
8
26
  it "downloads the cookbook to the given destination" do
9
- subject.download(tmp_path)
10
- # have to set outside of custom rspec matcher block
11
- name, branch = subject.name, subject.branch
27
+ cached_cookbook = subject.download(tmp_path)
12
28
 
13
29
  tmp_path.should have_structure {
14
- directory "#{name}-#{branch}" do
30
+ directory "#{cached_cookbook.cookbook_name}-#{Git.rev_parse(cached_cookbook.path)}" do
15
31
  file "metadata.rb"
16
32
  end
17
33
  }
18
34
  end
19
35
 
20
- it "returns the path to the cookbook" do
21
- result = subject.download(tmp_path)
22
- # have to set outside of custom rspec matcher block
23
- name, branch = subject.name, subject.branch
36
+ it "sets the downloaded status to true" do
37
+ subject.download(tmp_path)
24
38
 
25
- result.should eql(tmp_path.join("#{name}-#{branch}").to_s)
39
+ subject.should be_downloaded
26
40
  end
27
41
 
28
42
  context "given no ref/branch/tag options is given" do
29
- subject { CookbookSource::GitLocation.new("nginx", :git => "git://github.com/opscode-cookbooks/nginx.git") }
43
+ subject { CookbookSource::GitLocation.new("nginx", complacent_constraint, git: "git://github.com/opscode-cookbooks/nginx.git") }
30
44
 
31
45
  it "sets the branch attribute to the HEAD revision of the cloned repo" do
32
46
  subject.download(tmp_path)
@@ -36,17 +50,17 @@ module Berkshelf
36
50
  end
37
51
 
38
52
  context "given a git repo that does not exist" do
39
- subject { CookbookSource::GitLocation.new("doesnot_exist", :git => "git://github.com/RiotGames/thisrepo_does_not_exist.git") }
53
+ subject { CookbookSource::GitLocation.new("doesnot_exist", complacent_constraint, git: "git://github.com/RiotGames/thisrepo_does_not_exist.git") }
40
54
 
41
- it "raises a CookbookNotFound error" do
55
+ it "raises a GitError" do
42
56
  lambda {
43
57
  subject.download(tmp_path)
44
- }.should raise_error(CookbookNotFound)
58
+ }.should raise_error(GitError)
45
59
  end
46
60
  end
47
61
 
48
62
  context "given a git repo that does not contain a cookbook" do
49
- subject { CookbookSource::GitLocation.new("doesnot_exist", :git => "git://github.com/RiotGames/berkshelf.git") }
63
+ subject { CookbookSource::GitLocation.new("doesnot_exist", complacent_constraint, git: "git://github.com/RiotGames/berkshelf.git") }
50
64
 
51
65
  it "raises a CookbookNotFound error" do
52
66
  lambda {
@@ -54,6 +68,42 @@ module Berkshelf
54
68
  }.should raise_error(CookbookNotFound)
55
69
  end
56
70
  end
71
+
72
+ context "given the content at the Git repo does not satisfy the version constraint" do
73
+ subject do
74
+ CookbookSource::GitLocation.new("nginx",
75
+ double('constraint', include?: false),
76
+ git: "git://github.com/opscode-cookbooks/nginx.git"
77
+ )
78
+ end
79
+
80
+ it "raises a ConstraintNotSatisfied error" do
81
+ lambda {
82
+ subject.download(tmp_path)
83
+ }.should raise_error(ConstraintNotSatisfied)
84
+ end
85
+ end
86
+
87
+ context "given a value for ref that is a tag or branch and not a commit hash" do
88
+ subject do
89
+ CookbookSource::GitLocation.new("artifact",
90
+ complacent_constraint,
91
+ git: "git://github.com/RiotGames/artifact-cookbook.git",
92
+ ref: "0.9.8"
93
+ )
94
+ end
95
+
96
+ let(:commit_hash) { "d7be334b094f497f5cce4169a8b3012bf7b27bc3" }
97
+
98
+ before(:each) { Git.should_receive(:rev_parse).and_return(commit_hash) }
99
+
100
+ it "returns a cached cookbook with a path that contains the commit hash it is pointing to" do
101
+ cached_cookbook = subject.download(tmp_path)
102
+ expected_path = tmp_path.join("#{cached_cookbook.cookbook_name}-#{commit_hash}")
103
+
104
+ cached_cookbook.path.should eql(expected_path)
105
+ end
106
+ end
57
107
  end
58
108
  end
59
109
  end