berkshelf 0.4.0.rc4 → 0.4.0

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 (56) hide show
  1. data/.gitignore +2 -0
  2. data/Guardfile +6 -3
  3. data/features/default_locations.feature +122 -0
  4. data/features/install.feature +20 -4
  5. data/features/lockfile.feature +1 -6
  6. data/features/update.feature +2 -3
  7. data/generator_files/Berksfile.erb +2 -0
  8. data/generator_files/gitignore.erb +6 -0
  9. data/lib/berkshelf.rb +6 -10
  10. data/lib/berkshelf/berksfile.rb +203 -14
  11. data/lib/berkshelf/cached_cookbook.rb +5 -1
  12. data/lib/berkshelf/cli.rb +4 -0
  13. data/lib/berkshelf/cookbook_source.rb +49 -91
  14. data/lib/berkshelf/cookbook_store.rb +2 -0
  15. data/lib/berkshelf/downloader.rb +71 -51
  16. data/lib/berkshelf/errors.rb +7 -3
  17. data/lib/berkshelf/formatters.rb +6 -6
  18. data/lib/berkshelf/location.rb +171 -0
  19. data/lib/berkshelf/locations/chef_api_location.rb +252 -0
  20. data/lib/berkshelf/locations/git_location.rb +76 -0
  21. data/lib/berkshelf/locations/path_location.rb +38 -0
  22. data/lib/berkshelf/locations/site_location.rb +150 -0
  23. data/lib/berkshelf/lockfile.rb +2 -2
  24. data/lib/berkshelf/resolver.rb +12 -15
  25. data/lib/berkshelf/uploader.rb +2 -9
  26. data/lib/berkshelf/version.rb +1 -1
  27. data/spec/fixtures/lockfile_spec/without_lock/.gitkeep +0 -0
  28. data/spec/support/chef_api.rb +7 -1
  29. data/spec/unit/berkshelf/berksfile_spec.rb +157 -12
  30. data/spec/unit/berkshelf/cached_cookbook_spec.rb +19 -0
  31. data/spec/unit/berkshelf/cookbook_generator_spec.rb +1 -0
  32. data/spec/unit/berkshelf/cookbook_source_spec.rb +25 -35
  33. data/spec/unit/berkshelf/cookbook_store_spec.rb +3 -3
  34. data/spec/unit/berkshelf/downloader_spec.rb +171 -43
  35. data/spec/unit/berkshelf/formatters_spec.rb +13 -16
  36. data/spec/unit/berkshelf/{cookbook_source/location_spec.rb → location_spec.rb} +10 -10
  37. data/spec/unit/berkshelf/{cookbook_source → locations}/chef_api_location_spec.rb +4 -4
  38. data/spec/unit/berkshelf/{cookbook_source → locations}/git_location_spec.rb +8 -8
  39. data/spec/unit/berkshelf/{cookbook_source → locations}/path_location_spec.rb +5 -5
  40. data/spec/unit/berkshelf/{cookbook_source → locations}/site_location_spec.rb +17 -3
  41. data/spec/unit/berkshelf/lockfile_spec.rb +26 -17
  42. data/spec/unit/berkshelf/resolver_spec.rb +6 -5
  43. data/spec/unit/berkshelf/uploader_spec.rb +6 -4
  44. metadata +27 -31
  45. data/lib/berkshelf/cookbook_source/chef_api_location.rb +0 -256
  46. data/lib/berkshelf/cookbook_source/git_location.rb +0 -78
  47. data/lib/berkshelf/cookbook_source/location.rb +0 -167
  48. data/lib/berkshelf/cookbook_source/path_location.rb +0 -40
  49. data/lib/berkshelf/cookbook_source/site_location.rb +0 -149
  50. data/lib/berkshelf/dsl.rb +0 -45
  51. data/lib/berkshelf/tx_result.rb +0 -12
  52. data/lib/berkshelf/tx_result_set.rb +0 -37
  53. data/spec/fixtures/lockfile_spec/without_lock/Berksfile.lock +0 -5
  54. data/spec/unit/berkshelf/dsl_spec.rb +0 -42
  55. data/spec/unit/berkshelf/tx_result_set_spec.rb +0 -77
  56. data/spec/unit/berkshelf/tx_result_spec.rb +0 -21
@@ -0,0 +1,252 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class ChefAPILocation
4
+ class << self
5
+ # @param [String] node_name
6
+ #
7
+ # @return [Boolean]
8
+ def validate_node_name(node_name)
9
+ node_name.is_a?(String) && !node_name.empty?
10
+ end
11
+
12
+ # @raise [InvalidChefAPILocation]
13
+ #
14
+ # @see validate_node_name
15
+ def validate_node_name!(node_name)
16
+ unless validate_node_name(node_name)
17
+ raise InvalidChefAPILocation
18
+ end
19
+
20
+ true
21
+ end
22
+
23
+ # @param [String] client_key
24
+ #
25
+ # @return [Boolean]
26
+ def validate_client_key(client_key)
27
+ File.exists?(client_key)
28
+ end
29
+
30
+ # @raise [InvalidChefAPILocation]
31
+ #
32
+ # @see validate_client_key
33
+ def validate_client_key!(client_key)
34
+ unless validate_client_key(client_key)
35
+ raise InvalidChefAPILocation
36
+ end
37
+
38
+ true
39
+ end
40
+
41
+ # @param [String] uri
42
+ #
43
+ # @return [Boolean]
44
+ def validate_uri(uri)
45
+ uri =~ URI.regexp(['http', 'https'])
46
+ end
47
+
48
+ # @raise [InvalidChefAPILocation] if the given object is not a String containing a
49
+ # valid Chef API URI
50
+ #
51
+ # @see validate_uri
52
+ def validate_uri!(uri)
53
+ unless validate_uri(uri)
54
+ raise InvalidChefAPILocation, "'#{uri}' is not a valid Chef API URI."
55
+ end
56
+
57
+ true
58
+ end
59
+ end
60
+
61
+ include Location
62
+
63
+ location_key :chef_api
64
+ valid_options :node_name, :client_key
65
+
66
+ attr_reader :uri
67
+ attr_reader :node_name
68
+ attr_reader :client_key
69
+
70
+ # @param [#to_s] name
71
+ # @param [Solve::Constraint] version_constraint
72
+ # @param [Hash] options
73
+ #
74
+ # @option options [String, Symbol] :chef_api
75
+ # a URL to a Chef API. Alternatively the symbol :knife can be provided
76
+ # which will instantiate this location with the values found in your
77
+ # knife configuration.
78
+ # @option options [String] :node_name
79
+ # the name of the client to use to communicate with the Chef API.
80
+ # Default: Chef::Config[:node_name]
81
+ # @option options [String] :client_key
82
+ # the filepath to the authentication key for the client
83
+ # Default: Chef::Config[:client_key]
84
+ def initialize(name, version_constraint, options = {})
85
+ @name = name
86
+ @version_constraint = version_constraint
87
+ @downloaded_status = false
88
+
89
+ validate_options!(options)
90
+
91
+ if options[:chef_api] == :knife
92
+ begin
93
+ Berkshelf.load_config
94
+ rescue KnifeConfigNotFound => e
95
+ raise KnifeConfigNotFound, "A Knife config is required when ':knife' is given for the value of a 'chef_api' location. #{e}"
96
+ end
97
+ @node_name = Chef::Config[:node_name]
98
+ @client_key = Chef::Config[:client_key]
99
+ @uri = Chef::Config[:chef_server_url]
100
+ else
101
+ @node_name = options[:node_name]
102
+ @client_key = options[:client_key]
103
+ @uri = options[:chef_api]
104
+ end
105
+
106
+ @rest = Chef::REST.new(uri, node_name, client_key)
107
+ end
108
+
109
+ # @param [#to_s] destination
110
+ #
111
+ # @return [Berkshelf::CachedCookbook]
112
+ def download(destination)
113
+ version, uri = target_version
114
+ cookbook = rest.get_rest(uri)
115
+
116
+ scratch = download_files(cookbook.manifest)
117
+
118
+ cb_path = File.join(destination, "#{name}-#{version}")
119
+ FileUtils.mv(scratch, cb_path, force: true)
120
+
121
+ cached = CachedCookbook.from_store_path(cb_path)
122
+ validate_cached(cached)
123
+
124
+ set_downloaded_status(true)
125
+ cached
126
+ end
127
+
128
+ # Returns a hash representing the cookbook versions on at a Chef API for location's cookbook.
129
+ # The keys are version strings and the values are URLs to download the cookbook version.
130
+ #
131
+ # @example
132
+ # {
133
+ # "0.101.2" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2",
134
+ # "0.101.5" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.5"
135
+ # }
136
+ #
137
+ # @return [Hash]
138
+ def versions
139
+ {}.tap do |versions|
140
+ rest.get_rest("cookbooks/#{name}").each do |name, data|
141
+ data["versions"].each do |version_info|
142
+ versions[version_info["version"]] = version_info["url"]
143
+ end
144
+ end
145
+ end
146
+ rescue Net::HTTPServerException => e
147
+ if e.response.code == "404"
148
+ raise CookbookNotFound, "Cookbook '#{name}' not found at chef_api: '#{uri}'"
149
+ else
150
+ raise
151
+ end
152
+ end
153
+
154
+ # Returns an array where the first element is a string representing the latest version of
155
+ # the Cookbook and the second element is the download URL for that version.
156
+ #
157
+ # @example
158
+ # [ "0.101.2" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2" ]
159
+ #
160
+ # @return [Array]
161
+ def latest_version
162
+ graph = Solve::Graph.new
163
+ versions.each do |version, url|
164
+ graph.artifacts(name, version)
165
+ end
166
+ graph.demands(name, ">= 0.0.0")
167
+
168
+ version = Solve.it!(graph)[name]
169
+
170
+ [ version, versions[version] ]
171
+ end
172
+
173
+ def to_s
174
+ "chef_api: '#{uri}'"
175
+ end
176
+
177
+ private
178
+
179
+ attr_reader :rest
180
+
181
+ # Returns an array containing the version and download URL for the cookbook version that
182
+ # should be downloaded for this location.
183
+ #
184
+ # @example
185
+ # [ "0.101.2" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2" ]
186
+ #
187
+ # @return [Array]
188
+ def target_version
189
+ if version_constraint
190
+ solution = self.class.solve_for_constraint(version_constraint, versions)
191
+
192
+ unless solution
193
+ raise NoSolution, "No cookbook version of '#{name}' found at #{self} that would satisfy constraint (#{version_constraint})."
194
+ end
195
+
196
+ solution
197
+ else
198
+ latest_version
199
+ end
200
+ end
201
+
202
+ # Download all of the files in the given manifest to the given destination. If no destination
203
+ # is provided a temporary directory will be created and the files will be downloaded to there.
204
+ #
205
+ # @note
206
+ # the manifest Hash is the same manifest that you get by sending the manifest message to
207
+ # an instance of Chef::CookbookVersion.
208
+ #
209
+ # @param [Hash] manifest
210
+ # @param [String] destination
211
+ #
212
+ # @return [String]
213
+ # the path to the directory containing the files
214
+ def download_files(manifest, destination = Dir.mktmpdir)
215
+ Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment|
216
+ next unless manifest.has_key?(segment)
217
+ manifest[segment].each do |segment_file|
218
+ dest = File.join(destination, segment_file['path'].gsub('/', File::SEPARATOR))
219
+ FileUtils.mkdir_p(File.dirname(dest))
220
+ rest.sign_on_redirect = false
221
+ tempfile = rest.get_rest(segment_file['url'], true)
222
+ FileUtils.mv(tempfile.path, dest)
223
+ end
224
+ end
225
+
226
+ destination
227
+ end
228
+
229
+ # Validates the options hash given to the constructor.
230
+ #
231
+ # @param [Hash] options
232
+ #
233
+ # @raise [InvalidChefAPILocation] if any of the options are missing or their values do not
234
+ # pass validation
235
+ def validate_options!(options)
236
+ if options[:chef_api] == :knife
237
+ return true
238
+ end
239
+
240
+ missing_options = [:node_name, :client_key] - options.keys
241
+
242
+ unless missing_options.empty?
243
+ missing_options.collect! { |opt| "'#{opt}'" }
244
+ raise InvalidChefAPILocation, "Source '#{name}' is a 'chef_api' location with a URL for it's value but is missing options: #{missing_options.join(', ')}."
245
+ end
246
+
247
+ self.class.validate_node_name!(options[:node_name])
248
+ self.class.validate_client_key!(options[:client_key])
249
+ self.class.validate_uri!(options[:chef_api])
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,76 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class GitLocation
4
+ include Location
5
+
6
+ location_key :git
7
+ valid_options :ref, :branch, :tag
8
+
9
+ attr_accessor :uri
10
+ attr_accessor :branch
11
+
12
+ alias_method :ref, :branch
13
+ alias_method :tag, :branch
14
+
15
+ # @param [#to_s] name
16
+ # @param [Solve::Constraint] version_constraint
17
+ # @param [Hash] options
18
+ #
19
+ # @option options [String] :git
20
+ # the Git URL to clone
21
+ # @option options [String] :ref
22
+ # the commit hash or an alias to a commit hash to clone
23
+ # @option options [String] :branch
24
+ # same as ref
25
+ # @option options [String] :tag
26
+ # same as tag
27
+ def initialize(name, version_constraint, options = {})
28
+ @name = name
29
+ @version_constraint = version_constraint
30
+ @uri = options[:git]
31
+ @branch = options[:branch] || options[:ref] || options[:tag]
32
+
33
+ Git.validate_uri!(@uri)
34
+ end
35
+
36
+ # @param [#to_s] destination
37
+ #
38
+ # @return [Berkshelf::CachedCookbook]
39
+ def download(destination)
40
+ tmp_clone = Dir.mktmpdir
41
+ ::Berkshelf::Git.clone(uri, tmp_clone)
42
+ ::Berkshelf::Git.checkout(tmp_clone, branch) if branch
43
+ unless branch
44
+ self.branch = ::Berkshelf::Git.rev_parse(tmp_clone)
45
+ end
46
+
47
+ unless File.chef_cookbook?(tmp_clone)
48
+ msg = "Cookbook '#{name}' not found at git: #{uri}"
49
+ msg << " with branch '#{branch}'" if branch
50
+ raise CookbookNotFound, msg
51
+ end
52
+
53
+ cb_path = File.join(destination, "#{self.name}-#{Git.rev_parse(tmp_clone)}")
54
+ FileUtils.rm_rf(cb_path)
55
+ FileUtils.mv(tmp_clone, cb_path, force: true)
56
+
57
+ cached = CachedCookbook.from_store_path(cb_path)
58
+ validate_cached(cached)
59
+
60
+ set_downloaded_status(true)
61
+ cached
62
+ end
63
+
64
+ def to_s
65
+ s = "git: '#{uri}'"
66
+ s << " with branch: '#{branch}'" if branch
67
+ s
68
+ end
69
+
70
+ private
71
+
72
+ def git
73
+ @git ||= Berkshelf::Git.new(uri)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,38 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class PathLocation
4
+ include Location
5
+
6
+ location_key :path
7
+
8
+ attr_accessor :path
9
+
10
+ # @param [#to_s] name
11
+ # @param [Solve::Constraint] version_constraint
12
+ # @param [Hash] options
13
+ #
14
+ # @option options [String] :path
15
+ # a filepath to the cookbook on your local disk
16
+ def initialize(name, version_constraint, options = {})
17
+ @name = name
18
+ @version_constraint = version_constraint
19
+ @path = File.expand_path(options[:path])
20
+ set_downloaded_status(true)
21
+ end
22
+
23
+ # @param [#to_s] destination
24
+ #
25
+ # @return [Berkshelf::CachedCookbook]
26
+ def download(destination)
27
+ cached = CachedCookbook.from_path(path)
28
+ validate_cached(cached)
29
+
30
+ set_downloaded_status(true)
31
+ cached
32
+ end
33
+
34
+ def to_s
35
+ "path: '#{path}'"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,150 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class SiteLocation
4
+ include Location
5
+
6
+ location_key :site
7
+
8
+ attr_reader :api_uri
9
+ attr_accessor :version_constraint
10
+
11
+ class << self
12
+ # @param [String] target
13
+ # file path to the tar.gz archive on disk
14
+ # @param [String] destination
15
+ # file path to extract the contents of the target to
16
+ def unpack(target, destination)
17
+ Archive::Tar::Minitar.unpack(Zlib::GzipReader.new(File.open(target, 'rb')), destination)
18
+ end
19
+ end
20
+
21
+ # @param [#to_s] name
22
+ # @param [Solve::Constraint] version_constraint
23
+ # @param [Hash] options
24
+ #
25
+ # @option options [String, Symbol] :site
26
+ # a URL pointing to a community API endpoint. Alternatively the symbol :opscode can
27
+ # be provided to initialize a SiteLocation pointing to the Opscode Community Site.
28
+ def initialize(name, version_constraint, options = {})
29
+ @name = name
30
+ @version_constraint = version_constraint
31
+
32
+ @api_uri = if options[:site].nil? || options[:site] == :opscode
33
+ Location::OPSCODE_COMMUNITY_API
34
+ else
35
+ options[:site]
36
+ end
37
+
38
+ @rest = Chef::REST.new(api_uri, false, false)
39
+ end
40
+
41
+ # @param [#to_s] destination
42
+ #
43
+ # @return [Berkshelf::CachedCookbook]
44
+ def download(destination)
45
+ version, uri = target_version
46
+ remote_file = rest.get_rest(uri)['file']
47
+
48
+ downloaded_tf = rest.get_rest(remote_file, true)
49
+
50
+ dir = Dir.mktmpdir
51
+ cb_path = File.join(destination, "#{name}-#{version}")
52
+
53
+ self.class.unpack(downloaded_tf.path, dir)
54
+ FileUtils.mv(File.join(dir, name), cb_path, force: true)
55
+
56
+ cached = CachedCookbook.from_store_path(cb_path)
57
+ validate_cached(cached)
58
+
59
+ set_downloaded_status(true)
60
+ cached
61
+ end
62
+
63
+ # Returns a hash of all the cookbook versions found at communite site URL for the cookbook
64
+ # name of this location.
65
+ #
66
+ # @example
67
+ # {
68
+ # "0.101.2" => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2",
69
+ # "0.101.0" => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_0"
70
+ # }
71
+ #
72
+ # @return [Hash]
73
+ # a hash representing the cookbook versions on at a Chef API for location's cookbook.
74
+ # The keys are version strings and the values are URLs to download the cookbook version.
75
+ def versions
76
+ {}.tap do |versions|
77
+ rest.get_rest(name)['versions'].each do |uri|
78
+ version = version_from_uri(File.basename(uri))
79
+
80
+ versions[version] = uri
81
+ end
82
+ end
83
+ rescue Net::HTTPServerException => e
84
+ if e.response.code == "404"
85
+ raise CookbookNotFound, "Cookbook '#{name}' not found at site: '#{api_uri}'"
86
+ else
87
+ raise
88
+ end
89
+ end
90
+
91
+ # Returns the latest version of the cookbook and it's download link.
92
+ #
93
+ # @example
94
+ # [ "0.101.2" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2" ]
95
+ #
96
+ # @return [Array]
97
+ # an array containing the version and download URL for the cookbook version that
98
+ # should be downloaded for this location.
99
+ def latest_version
100
+ quietly {
101
+ uri = rest.get_rest(name)['latest_version']
102
+
103
+ [ version_from_uri(uri), uri ]
104
+ }
105
+ rescue Net::HTTPServerException => e
106
+ if e.response.code == "404"
107
+ raise CookbookNotFound, "Cookbook '#{name}' not found at site: '#{api_uri}'"
108
+ else
109
+ raise
110
+ end
111
+ end
112
+
113
+ def to_s
114
+ "site: '#{api_uri}'"
115
+ end
116
+
117
+ private
118
+
119
+ attr_reader :rest
120
+
121
+ def uri_escape_version(version)
122
+ version.gsub('.', '_')
123
+ end
124
+
125
+ def version_from_uri(latest_version)
126
+ File.basename(latest_version).gsub('_', '.')
127
+ end
128
+
129
+ # Returns an array containing the version and download URL for the cookbook version that
130
+ # should be downloaded for this location.
131
+ #
132
+ # @example
133
+ # [ "0.101.2" => "https://api.opscode.com/organizations/vialstudios/cookbooks/nginx/0.101.2" ]
134
+ #
135
+ # @return [Array]
136
+ def target_version
137
+ if version_constraint
138
+ solution = self.class.solve_for_constraint(version_constraint, versions)
139
+
140
+ unless solution
141
+ raise NoSolution, "No cookbook version of '#{name}' found at #{self} that would satisfy constraint (#{version_constraint})."
142
+ end
143
+
144
+ solution
145
+ else
146
+ latest_version
147
+ end
148
+ end
149
+ end
150
+ end