berkshelf 0.4.0.rc4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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