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
@@ -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