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