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
@@ -7,12 +7,24 @@ module Berkshelf
7
7
  attr_accessor :uri
8
8
  attr_accessor :branch
9
9
 
10
- def initialize(name, options)
10
+ alias_method :ref, :branch
11
+ alias_method :tag, :branch
12
+
13
+ # @param [#to_s] name
14
+ # @param [DepSelector::VersionConstraint] version_constraint
15
+ # @param [Hash] options
16
+ def initialize(name, version_constraint, options)
11
17
  @name = name
18
+ @version_constraint = version_constraint
12
19
  @uri = options[:git]
13
20
  @branch = options[:branch] || options[:ref] || options[:tag]
21
+
22
+ Git.validate_uri!(@uri)
14
23
  end
15
24
 
25
+ # @param [#to_s] destination
26
+ #
27
+ # @return [Berkshelf::CachedCookbook]
16
28
  def download(destination)
17
29
  tmp_clone = Dir.mktmpdir
18
30
  ::Berkshelf::Git.clone(uri, tmp_clone)
@@ -27,20 +39,19 @@ module Berkshelf
27
39
  raise CookbookNotFound, msg
28
40
  end
29
41
 
30
- cb_path = File.join(destination, "#{self.name}-#{self.branch}")
31
-
32
- FileUtils.mv(tmp_clone, cb_path, :force => true)
42
+ cb_path = File.join(destination, "#{self.name}-#{Git.rev_parse(tmp_clone)}")
43
+ FileUtils.mv(tmp_clone, cb_path, force: true)
44
+
45
+ cached = CachedCookbook.from_store_path(cb_path)
46
+ validate_cached(cached)
33
47
 
34
- cb_path
35
- rescue Berkshelf::GitError
36
- msg = "Cookbook '#{name}' not found at git: #{uri}"
37
- msg << " with branch '#{branch}'" if branch
38
- raise CookbookNotFound, msg
48
+ set_downloaded_status(true)
49
+ cached
39
50
  end
40
51
 
41
52
  def to_s
42
53
  s = "git: '#{uri}'"
43
- s << " with branch '#{branch}'" if branch
54
+ s << " with branch: '#{branch}'" if branch
44
55
  s
45
56
  end
46
57
 
@@ -0,0 +1,50 @@
1
+ module Berkshelf
2
+ class CookbookSource
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ module Location
5
+ attr_reader :name
6
+ attr_reader :version_constraint
7
+
8
+ # @param [#to_s] name
9
+ def initialize(name, version_constraint)
10
+ @name = name
11
+ @version_constraint = version_constraint
12
+ @downloaded_status = false
13
+ end
14
+
15
+ # @param [#to_s] destination
16
+ #
17
+ # @return [Berkshelf::CachedCookbook]
18
+ def download(destination)
19
+ raise NotImplementedError, "Function must be implemented on includer"
20
+ end
21
+
22
+ # @return [Boolean]
23
+ def downloaded?
24
+ @downloaded_status
25
+ end
26
+
27
+ # Ensures that the given CachedCookbook satisfies the constraint
28
+ #
29
+ # @param [CachedCookbook] cached_cookbook
30
+ #
31
+ # @raise [ConstraintNotSatisfied] if the CachedCookbook does not satisfy the version constraint of
32
+ # this instance of Location.
33
+ #
34
+ # @return [Boolean]
35
+ def validate_cached(cached_cookbook)
36
+ unless version_constraint.include?(cached_cookbook.version)
37
+ raise ConstraintNotSatisfied, "A cookbook satisfying '#{name}' (#{version_constraint}) not found at #{self}"
38
+ end
39
+
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def set_downloaded_status(state)
46
+ @downloaded_status = state
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,17 +6,25 @@ module Berkshelf
6
6
 
7
7
  attr_accessor :path
8
8
 
9
- def initialize(name, options = {})
9
+ # @param [#to_s] name
10
+ # @param [DepSelector::VersionConstraint] version_constraint
11
+ # @param [Hash] options
12
+ def initialize(name, version_constraint, options = {})
10
13
  @name = name
14
+ @version_constraint = version_constraint
11
15
  @path = File.expand_path(options[:path])
16
+ set_downloaded_status(true)
12
17
  end
13
18
 
19
+ # @param [#to_s] destination
20
+ #
21
+ # @return [Berkshelf::CachedCookbook]
14
22
  def download(destination)
15
- unless File.chef_cookbook?(path)
16
- raise CookbookNotFound, "Cookbook '#{name}' not found at path: '#{path}'"
17
- end
23
+ cached = CachedCookbook.from_path(path)
24
+ validate_cached(cached)
18
25
 
19
- path
26
+ set_downloaded_status(true)
27
+ cached
20
28
  end
21
29
 
22
30
  def to_s
@@ -53,14 +53,20 @@ module Berkshelf
53
53
  end
54
54
  end
55
55
 
56
- def initialize(name, options = {})
56
+ # @param [#to_s] name
57
+ # @param [DepSelector::VersionConstraint] version_constraint
58
+ # @param [Hash] options
59
+ def initialize(name, version_constraint, options = {})
57
60
  options[:site] ||= OPSCODE_COMMUNITY_API
58
61
 
59
62
  @name = name
60
- @version_constraint = options[:version_constraint]
63
+ @version_constraint = version_constraint
61
64
  @api_uri = options[:site]
62
65
  end
63
66
 
67
+ # @param [#to_s] destination
68
+ #
69
+ # @return [Berkshelf::CachedCookbook]
64
70
  def download(destination)
65
71
  version, uri = target_version
66
72
  remote_file = rest.get_rest(uri)['file']
@@ -70,9 +76,13 @@ module Berkshelf
70
76
  cb_path = File.join(destination, "#{name}-#{version}")
71
77
 
72
78
  self.class.unpack(downloaded_tf.path, dir)
73
- FileUtils.mv(File.join(dir, name), cb_path, :force => true)
79
+ FileUtils.mv(File.join(dir, name), cb_path, force: true)
74
80
 
75
- cb_path
81
+ cached = CachedCookbook.from_store_path(cb_path)
82
+ validate_cached(cached)
83
+
84
+ set_downloaded_status(true)
85
+ cached
76
86
  end
77
87
 
78
88
  # @return [Array]
@@ -3,6 +3,9 @@ require 'fileutils'
3
3
  module Berkshelf
4
4
  # @author Jamie Winsor <jamie@vialstudios.com>
5
5
  class CookbookStore
6
+ include DepSelector
7
+ include DepSelector::Exceptions
8
+
6
9
  attr_reader :storage_path
7
10
 
8
11
  # Create a new instance of CookbookStore with the given
@@ -24,24 +27,29 @@ module Berkshelf
24
27
  # @param [String] version
25
28
  # version of the Cookbook you want to retrieve
26
29
  #
27
- # @return [Berkshelf::CachedCookbook]
30
+ # @return [Berkshelf::CachedCookbook, nil]
28
31
  def cookbook(name, version)
29
- return nil unless downloaded?(name, version)
30
-
31
32
  path = cookbook_path(name, version)
32
- CachedCookbook.from_path(path)
33
+ return nil unless path.cookbook?
34
+
35
+ CachedCookbook.from_store_path(path)
33
36
  end
34
37
 
35
- # Returns an array of all of the Cookbooks that have been cached
36
- # to the storage_path of this instance of CookbookStore.
38
+ # Returns an array of the Cookbooks that have been cached to the
39
+ # storage_path of this instance of CookbookStore. Passing the filter
40
+ # option will return only the CachedCookbooks whose name match the
41
+ # filter.
37
42
  #
38
43
  # @return [Array<Berkshelf::CachedCookbook>]
39
- def cookbooks
44
+ def cookbooks(filter = nil)
40
45
  [].tap do |cookbooks|
41
46
  storage_path.each_child do |p|
42
- cached_cookbook = CachedCookbook.from_path(p)
47
+ cached_cookbook = CachedCookbook.from_store_path(p)
43
48
 
44
- cookbooks << cached_cookbook if cached_cookbook
49
+ next unless cached_cookbook
50
+ next if filter && cached_cookbook.cookbook_name != filter
51
+
52
+ cookbooks << cached_cookbook
45
53
  end
46
54
  end
47
55
  end
@@ -57,15 +65,25 @@ module Berkshelf
57
65
  storage_path.join("#{name}-#{version}")
58
66
  end
59
67
 
60
- # Returns true if the Cookbook of the given name and verion is downloaded
61
- # to this instance of CookbookStore.
68
+ # Return a CachedCookbook matching the best solution for the given name and
69
+ # constraint. Nil is returned if no matching CachedCookbook is found.
62
70
  #
63
- # @param [String] name
64
- # @param [String] version
71
+ # @param [#to_s] name
72
+ # @param [DepSelector::VersionConstraint] constraint
65
73
  #
66
- # @return [Boolean]
67
- def downloaded?(name, version)
68
- cookbook_path(name, version).cookbook?
74
+ # @return [Berkshelf::CachedCookbook, nil]
75
+ def satisfy(name, constraint)
76
+ graph = DependencyGraph.new
77
+ selector = Selector.new(graph)
78
+ package = graph.package(name)
79
+ solution_constraints = [ SolutionConstraint.new(graph.package(name), constraint) ]
80
+
81
+ cookbooks(name).each { |cookbook| package.add_version(Version.new(cookbook.version)) }
82
+ name, version = quietly { selector.find_solution(solution_constraints).first }
83
+
84
+ cookbook(name, version)
85
+ rescue InvalidSolutionConstraints, NoSolutionExists
86
+ nil
69
87
  end
70
88
 
71
89
  private
@@ -0,0 +1,12 @@
1
+ class String
2
+ # Separates the string on the given separator and prepends the given
3
+ # value to each and returns a new string from the result.
4
+ #
5
+ # @param [String] separator
6
+ # @param [String] value
7
+ #
8
+ # @return [String]
9
+ def prepend_each(separator, value)
10
+ lines(separator).collect { |x| value + x }.join
11
+ end
12
+ end
@@ -79,10 +79,6 @@ module Berkshelf
79
79
  result
80
80
  end
81
81
 
82
- def downloaded?(source)
83
- source.downloaded? || cookbook_store.downloaded?(source.name, source.local_version)
84
- end
85
-
86
82
  private
87
83
 
88
84
  def validate_source(source)
@@ -1,21 +1,61 @@
1
1
  module Berkshelf
2
2
  class BerkshelfError < StandardError
3
3
  class << self
4
+ # @param [Integer] code
4
5
  def status_code(code)
5
6
  define_method(:status_code) { code }
6
7
  define_singleton_method(:status_code) { code }
7
8
  end
8
9
  end
10
+
11
+ alias_method :message, :to_s
9
12
  end
10
13
 
11
14
  class BerksfileNotFound < BerkshelfError; status_code(100); end
12
15
  class NoVersionForConstraints < BerkshelfError; status_code(101); end
13
16
  class DownloadFailure < BerkshelfError; status_code(102); end
14
17
  class CookbookNotFound < BerkshelfError; status_code(103); end
15
- class GitError < BerkshelfError; status_code(104); end
18
+ class GitError < BerkshelfError
19
+ status_code(104)
20
+ attr_reader :stderr
21
+
22
+ def initialize(stderr)
23
+ @stderr = stderr
24
+ end
25
+
26
+ def to_s
27
+ out = "An error occured during Git execution:\n"
28
+ out << stderr.prepend_each("\n", "\t")
29
+ end
30
+ end
31
+
16
32
  class DuplicateSourceDefined < BerkshelfError; status_code(105); end
17
33
  class NoSolution < BerkshelfError; status_code(106); end
18
34
  class CookbookSyntaxError < BerkshelfError; status_code(107); end
19
35
  class UploadFailure < BerkshelfError; status_code(108); end
20
36
  class KnifeConfigNotFound < BerkshelfError; status_code(109); end
37
+
38
+ class InvalidGitURI < BerkshelfError
39
+ status_code(110)
40
+ attr_reader :uri
41
+
42
+ # @param [String] uri
43
+ def initialize(uri)
44
+ @uri = uri
45
+ end
46
+
47
+ def to_s
48
+ "'#{uri}' is not a valid Git URI."
49
+ end
50
+ end
51
+
52
+ class GitNotFound < BerkshelfError
53
+ status_code(110)
54
+
55
+ def to_s
56
+ "Could not find a Git executable in your path. Please add it and try again."
57
+ end
58
+ end
59
+
60
+ class ConstraintNotSatisfied < BerkshelfError; status_code(111); end
21
61
  end
@@ -1,39 +1,71 @@
1
+ require 'uri'
2
+ require 'mixlib/shellout'
3
+
1
4
  module Berkshelf
5
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
6
  class Git
7
+ GIT_REGEXP = URI.regexp(%w{ https git })
8
+ SSH_REGEXP = /(.+)@(.+):(.+)\/(.+)\.git/
9
+
3
10
  class << self
11
+ # @overload git(commands)
12
+ # Shellout to the Git executable on your system with the given commands.
13
+ #
14
+ # @param [Array<String>]
15
+ #
16
+ # @return [String]
17
+ # the output of the execution of the Git command
4
18
  def git(*command)
5
- out = quietly {
6
- %x{ #{git_cmd} #{command.join(' ')} }
7
- }
8
- error_check
19
+ cmd = Mixlib::ShellOut.new(git_cmd, *command)
20
+ cmd.run_command
21
+
22
+ unless cmd.exitstatus == 0
23
+ raise GitError.new(cmd.stderr)
24
+ end
9
25
 
10
- out.chomp
26
+ cmd.stdout.chomp
11
27
  end
12
28
 
29
+ # Clone a remote Git repository to disk
30
+ #
31
+ # @param [String] uri
32
+ # a Git URI to clone
33
+ # @param [#to_s] destination
34
+ # a local path on disk to clone to
35
+ #
36
+ # @return [String]
37
+ # the destination the URI was cloned to
13
38
  def clone(uri, destination = Dir.mktmpdir)
14
39
  git("clone", uri, destination.to_s)
15
40
 
16
- error_check
17
-
18
41
  destination
19
42
  end
20
43
 
44
+ # Checkout the given reference in the given repository
45
+ #
46
+ # @param [String] repo_path
47
+ # path to a Git repo on disk
48
+ # @param [String] ref
49
+ # reference to checkout
21
50
  def checkout(repo_path, ref)
22
51
  Dir.chdir repo_path do
23
52
  git("checkout", "-q", ref)
24
53
  end
25
54
  end
26
55
 
56
+ # @param [Strin] repo_path
27
57
  def rev_parse(repo_path)
28
58
  Dir.chdir repo_path do
29
59
  git("rev-parse", "HEAD")
30
60
  end
31
61
  end
32
62
 
63
+ # Return an absolute path to the Git executable on your system
33
64
  #
34
- # This is to defeat aliases/shell functions called 'git' and a number of
35
- # other problems.
65
+ # @return [String]
66
+ # absolute path to git executable
36
67
  #
68
+ # @raise [GitNotFound] if executable is not found in system path
37
69
  def find_git
38
70
  git_path = nil
39
71
  ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
@@ -50,21 +82,58 @@ module Berkshelf
50
82
  end
51
83
 
52
84
  unless git_path
53
- raise "Could not find git. Please ensure it is in your path."
85
+ raise GitNotFound
54
86
  end
55
87
 
56
88
  return git_path
57
89
  end
58
90
 
91
+ # Determines if the given URI is a valid Git URI. A valid Git URI is a string
92
+ # containing the location of a Git repository by either the Git protocol,
93
+ # SSH protocol, or HTTPS protocol.
94
+ #
95
+ # @example Valid Git protocol URI
96
+ # "git://github.com/reset/thor-foodcritic.git"
97
+ # @example Valid HTTPS URI
98
+ # "https://github.com/reset/solve.git"
99
+ # @example Valid SSH protocol URI
100
+ # "git@github.com:reset/solve.git"
101
+ #
102
+ # @param [String] uri
103
+ #
104
+ # @return [Boolean]
105
+ def validate_uri(uri)
106
+ unless uri.is_a?(String)
107
+ return false
108
+ end
109
+
110
+ unless uri.slice(SSH_REGEXP).nil?
111
+ return true
112
+ end
113
+
114
+ unless uri.slice(GIT_REGEXP).nil?
115
+ return true
116
+ end
117
+
118
+ false
119
+ end
120
+
121
+ # @raise [InvalidGitURI] if the given object is not a String containing a valid Git URI
122
+ #
123
+ # @see validate_uri
124
+ def validate_uri!(uri)
125
+ unless validate_uri(uri)
126
+ raise InvalidGitURI.new(uri)
127
+ end
128
+
129
+ true
130
+ end
131
+
59
132
  private
60
133
 
61
134
  def git_cmd
62
135
  @git_cmd ||= find_git
63
136
  end
64
-
65
- def error_check
66
- raise Berkshelf::GitError, "Did not succeed executing git; check the output above." unless $?.success?
67
- end
68
137
  end
69
138
  end
70
139
  end