ridley 0.7.0.rc1 → 0.7.0.rc3

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.
@@ -1 +1,2 @@
1
- 1.9.3-p286
1
+ 1.9.3-p327
2
+
@@ -3,6 +3,7 @@ require 'celluloid'
3
3
  require 'faraday'
4
4
  require 'addressable/uri'
5
5
  require 'multi_json'
6
+ require 'solve'
6
7
  require 'active_support/inflector'
7
8
  require 'forwardable'
8
9
  require 'thread'
@@ -21,7 +22,7 @@ JSON.create_id = nil
21
22
 
22
23
  # @author Jamie Winsor <jamie@vialstudios.com>
23
24
  module Ridley
24
- CHEF_VERSION = '10.16.4'.freeze
25
+ CHEF_VERSION = '11.4.0'.freeze
25
26
 
26
27
  autoload :Bootstrapper, 'ridley/bootstrapper'
27
28
  autoload :Client, 'ridley/client'
@@ -71,6 +71,8 @@ module Ridley
71
71
  def_delegator :connection, :client_name
72
72
  def_delegator :connection, :client_name=
73
73
 
74
+ attr_reader :options
75
+
74
76
  attr_accessor :validator_client
75
77
  attr_accessor :validator_path
76
78
  attr_accessor :encrypted_data_bag_secret_path
@@ -103,34 +105,34 @@ module Ridley
103
105
  # URI, String, or Hash of HTTP proxy options
104
106
  def initialize(options = {})
105
107
  log.info { "Ridley starting..." }
106
- options = options.reverse_merge(
108
+ @options = options.reverse_merge(
107
109
  ssh: Hash.new
108
110
  ).deep_symbolize_keys
109
- self.class.validate_options(options)
111
+ self.class.validate_options(@options)
110
112
 
111
- @ssh = options[:ssh]
112
- @validator_client = options[:validator_client]
113
+ @ssh = @options[:ssh]
114
+ @validator_client = @options[:validator_client]
113
115
 
114
- options[:client_key] = File.expand_path(options[:client_key])
116
+ @options[:client_key] = File.expand_path(@options[:client_key])
115
117
 
116
- if options[:validator_path]
117
- @validator_path = File.expand_path(options[:validator_path])
118
+ if @options[:validator_path]
119
+ @validator_path = File.expand_path(@options[:validator_path])
118
120
  end
119
121
 
120
- if options[:encrypted_data_bag_secret_path]
121
- @encrypted_data_bag_secret_path = File.expand_path(options[:encrypted_data_bag_secret_path])
122
+ if @options[:encrypted_data_bag_secret_path]
123
+ @encrypted_data_bag_secret_path = File.expand_path(@options[:encrypted_data_bag_secret_path])
122
124
  end
123
125
 
124
- unless options[:client_key].present? && File.exist?(options[:client_key])
125
- raise Errors::ClientKeyFileNotFound, "client key not found at: '#{options[:client_key]}'"
126
+ unless @options[:client_key].present? && File.exist?(@options[:client_key])
127
+ raise Errors::ClientKeyFileNotFound, "client key not found at: '#{@options[:client_key]}'"
126
128
  end
127
129
 
128
130
  super(Celluloid::Registry.new)
129
131
  pool(Ridley::Connection, size: 4, args: [
130
- options[:server_url],
131
- options[:client_name],
132
- options[:client_key],
133
- options.slice(*Connection::VALID_OPTIONS)
132
+ @options[:server_url],
133
+ @options[:client_name],
134
+ @options[:client_key],
135
+ @options.slice(*Connection::VALID_OPTIONS)
134
136
  ], as: :connection_pool)
135
137
  end
136
138
 
@@ -1,3 +1,6 @@
1
+ require 'open-uri'
2
+ require 'tempfile'
3
+
1
4
  module Ridley
2
5
  # @author Jamie Winsor <jamie@vialstudios.com>
3
6
  class Connection < Faraday::Connection
@@ -15,9 +18,9 @@ module Ridley
15
18
  attr_reader :client_key
16
19
  attr_reader :client_name
17
20
 
18
- # @param [String] :server_url
19
- # @param [String] :client_name
20
- # @param [String] :client_key
21
+ # @param [String] server_url
22
+ # @param [String] client_name
23
+ # @param [String] client_key
21
24
  #
22
25
  # @option options [Hash] :params
23
26
  # URI query unencoded key/value pairs
@@ -77,8 +80,52 @@ module Ridley
77
80
  api_type == :foss
78
81
  end
79
82
 
83
+ # Override Faraday::Connection#run_request to catch exceptions from {Ridley::Middleware} that
84
+ # we expect. Caught exceptions are re-raised with Celluloid#abort so we don't crash the connection.
85
+ def run_request(*args)
86
+ super
87
+ rescue Errors::HTTPError => ex
88
+ abort(ex)
89
+ end
90
+
80
91
  def server_url
81
92
  self.url_prefix.to_s
82
93
  end
94
+
95
+ # Stream the response body of a remote URL to a file on the local file system
96
+ #
97
+ # @param [String] target
98
+ # a URL to stream the response body from
99
+ # @param [String] destination
100
+ # a location on disk to stream the content of the response body to
101
+ def stream(target, destination)
102
+ FileUtils.mkdir_p(File.dirname(destination))
103
+
104
+ target = Addressable::URI.parse(target)
105
+ headers = Middleware::ChefAuth.authentication_headers(
106
+ client_name,
107
+ client_key,
108
+ http_method: "GET",
109
+ host: target.host,
110
+ path: target.path
111
+ )
112
+
113
+ unless ssl[:verify]
114
+ headers.merge!(ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
115
+ end
116
+
117
+ local = Tempfile.new('ridley-stream')
118
+ local.binmode
119
+
120
+ open(target, 'rb', headers) do |remote|
121
+ local.write(remote.read)
122
+ end
123
+
124
+ FileUtils.mv(local.path, destination)
125
+ rescue OpenURI::HTTPError => ex
126
+ abort(ex)
127
+ ensure
128
+ local.close(true) unless local.nil?
129
+ end
83
130
  end
84
131
  end
@@ -5,6 +5,7 @@ module Ridley
5
5
  class InternalError < RidleyError; end
6
6
  class ArgumentError < InternalError; end
7
7
 
8
+ class ResourceNotFound < RidleyError; end
8
9
  class ValidatorNotFound < RidleyError; end
9
10
 
10
11
  class InvalidResource < RidleyError
@@ -20,6 +21,18 @@ module Ridley
20
21
  alias_method :to_s, :message
21
22
  end
22
23
 
24
+ class UnknownCookbookFileType < RidleyError
25
+ attr_reader :type
26
+
27
+ def initialize(type)
28
+ @type = type
29
+ end
30
+
31
+ def to_s
32
+ "filetype: '#{type}'"
33
+ end
34
+ end
35
+
23
36
  class BootstrapError < RidleyError; end
24
37
  class ClientKeyFileNotFound < BootstrapError; end
25
38
  class EncryptedDataBagSecretNotFound < BootstrapError; end
@@ -4,6 +4,45 @@ module Ridley
4
4
  module Middleware
5
5
  # @author Jamie Winsor <jamie@vialstudios.com>
6
6
  class ChefAuth < Faraday::Middleware
7
+ class << self
8
+ include Mixlib::Authentication
9
+
10
+ # Generate authentication headers for a request to a Chef Server
11
+ #
12
+ # @param [String] client_name
13
+ # @param [String] client_key
14
+ #
15
+ # @option options [String] :host
16
+ #
17
+ # @see {#signing_object} for options
18
+ def authentication_headers(client_name, client_key, options = {})
19
+ rsa_key = OpenSSL::PKey::RSA.new(File.read(client_key))
20
+ headers = signing_object(client_name, options).sign(rsa_key).merge(host: options[:host])
21
+ headers.inject({}) { |memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo }
22
+ end
23
+
24
+ # Create a signing object for a Request to a Chef Server
25
+ #
26
+ # @param [String] client_name
27
+ #
28
+ # @option options [String] :http_method
29
+ # @option options [String] :path
30
+ # @option options [String] :body
31
+ # @option options [Time] :timestamp
32
+ #
33
+ # @return [SigningObject]
34
+ def signing_object(client_name, options = {})
35
+ options = options.reverse_merge(
36
+ body: String.new,
37
+ timestamp: Time.now.utc.iso8601
38
+ )
39
+ options[:user_id] = client_name
40
+ options[:proto_version] = "1.0"
41
+
42
+ SignedHeaderAuth.signing_object(options)
43
+ end
44
+ end
45
+
7
46
  include Ridley::Logging
8
47
 
9
48
  attr_reader :client_name
@@ -12,24 +51,23 @@ module Ridley
12
51
  def initialize(app, client_name, client_key)
13
52
  super(app)
14
53
  @client_name = client_name
15
- @client_key = OpenSSL::PKey::RSA.new(File.read(client_key))
54
+ @client_key = client_key
16
55
  end
17
56
 
18
57
  def call(env)
19
- sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
58
+ signing_options = {
20
59
  http_method: env[:method],
21
- host: env[:url].host,
60
+ host: env[:url].host || "localhost",
22
61
  path: env[:url].path,
23
- body: env[:body] || '',
24
- timestamp: Time.now.utc.iso8601,
25
- user_id: client_name
26
- )
27
- authentication_headers = sign_obj.sign(client_key)
62
+ body: env[:body] || ''
63
+ }
64
+ authentication_headers = self.class.authentication_headers(client_name, client_key, signing_options)
65
+
28
66
  env[:request_headers] = default_headers.merge(env[:request_headers]).merge(authentication_headers)
29
67
  env[:request_headers] = env[:request_headers].merge('Content-Length' => env[:body].bytesize.to_s) if env[:body]
30
68
 
31
- log.debug { "Performing Authenticated Chef Request: "}
32
- log.debug { env }
69
+ log.debug { "==> performing authenticated Chef request as '#{client_name}'"}
70
+ log.debug { "request env: #{env}"}
33
71
 
34
72
  @app.call(env)
35
73
  end
@@ -14,12 +14,14 @@ module Ridley
14
14
  end
15
15
  end
16
16
 
17
+ include Ridley::Logging
18
+
17
19
  def on_complete(env)
18
- Ridley.log.debug("Handling Chef Response")
19
- Ridley.log.debug(env)
20
+ log.debug { "==> handling Chef response" }
21
+ log.debug { "request env: #{env}" }
20
22
 
21
23
  unless self.class.success?(env)
22
- Ridley.log.debug("Error encounted in Chef Response")
24
+ log.debug { "** error encounted in Chef response" }
23
25
  raise Errors::HTTPError.fabricate(env)
24
26
  end
25
27
  end
@@ -51,7 +51,7 @@ module Ridley
51
51
  # @return [String]
52
52
  def response_type(env)
53
53
  if env[:response_headers][CONTENT_TYPE].nil?
54
- log.debug "Response did not specify a content type."
54
+ log.debug { "response did not specify a content type" }
55
55
  return "text/html"
56
56
  end
57
57
 
@@ -98,10 +98,10 @@ module Ridley
98
98
  log.debug(env)
99
99
 
100
100
  if self.class.json_response?(env)
101
- log.debug("Parsing Chef Response body as JSON")
101
+ log.debug { "==> parsing Chef response body as JSON" }
102
102
  env[:body] = self.class.parse(env[:body])
103
103
  else
104
- log.debug("Chef Response did not contain a JSON body")
104
+ log.debug { "==> Chef response did not contain a JSON body" }
105
105
  end
106
106
  end
107
107
  end
@@ -67,7 +67,7 @@ module Ridley
67
67
  # @return [nil, Object]
68
68
  def find(client, object)
69
69
  find!(client, object)
70
- rescue Errors::HTTPNotFound
70
+ rescue Errors::ResourceNotFound
71
71
  nil
72
72
  end
73
73
 
@@ -81,6 +81,8 @@ module Ridley
81
81
  def find!(client, object)
82
82
  chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
83
83
  new(client, client.connection.get("#{self.resource_path}/#{chef_id}").body)
84
+ rescue Errors::HTTPNotFound => ex
85
+ raise Errors::ResourceNotFound, ex
84
86
  end
85
87
 
86
88
  # @param [Ridley::Client] client
@@ -90,7 +92,7 @@ module Ridley
90
92
  def create(client, object)
91
93
  resource = new(client, object.to_hash)
92
94
  new_attributes = client.connection.post(self.resource_path, resource.to_json).body
93
- resource.attributes = resource.attributes.deep_merge(new_attributes)
95
+ resource.mass_assign(resource._attributes_.deep_merge(new_attributes))
94
96
  resource
95
97
  end
96
98
 
@@ -147,7 +149,7 @@ module Ridley
147
149
  def save
148
150
  raise Errors::InvalidResource.new(self.errors) unless valid?
149
151
 
150
- mass_assign(self.class.create(client, self).attributes)
152
+ mass_assign(self.class.create(client, self)._attributes_)
151
153
  true
152
154
  rescue Errors::HTTPConflict
153
155
  self.update
@@ -164,7 +166,7 @@ module Ridley
164
166
  def update
165
167
  raise Errors::InvalidResource.new(self.errors) unless valid?
166
168
 
167
- mass_assign(self.class.update(client, self).attributes)
169
+ mass_assign(self.class.update(client, self)._attributes_)
168
170
  true
169
171
  end
170
172
 
@@ -172,7 +174,7 @@ module Ridley
172
174
  #
173
175
  # @return [Object]
174
176
  def reload
175
- mass_assign(self.class.find(client, self).attributes)
177
+ mass_assign(self.class.find(client, self)._attributes_)
176
178
  self
177
179
  end
178
180
 
@@ -182,7 +184,7 @@ module Ridley
182
184
  end
183
185
 
184
186
  def to_s
185
- self.attributes
187
+ "#{self.chef_id}: #{self._attributes_}"
186
188
  end
187
189
 
188
190
  # @param [Object] other
@@ -67,7 +67,7 @@ module Ridley
67
67
  # of connection. Only OHC/OPC requires the json_class attribute is not present.
68
68
  def to_json
69
69
  if client.connection.hosted?
70
- attributes.except(:json_class).to_json
70
+ to_hash.except(:json_class).to_json
71
71
  else
72
72
  super
73
73
  end
@@ -2,16 +2,87 @@ module Ridley
2
2
  # @author Jamie Winsor <jamie@vialstudios.com>
3
3
  class CookbookResource < Ridley::Resource
4
4
  class << self
5
+ # List all of the cookbooks and their versions present on the remote
6
+ #
7
+ # @example return value
8
+ # {
9
+ # "ant" => [
10
+ # "0.10.1"
11
+ # ],
12
+ # "apache2" => [
13
+ # "1.4.0"
14
+ # ]
15
+ # }
16
+ #
17
+ # @param [Ridley::Client] client
18
+ #
19
+ # @return [Hash]
20
+ # a hash containing keys which represent cookbook names and values which contain
21
+ # an array of strings representing the available versions
22
+ def all(client)
23
+ response = client.connection.get(self.resource_path).body
24
+
25
+ {}.tap do |cookbooks|
26
+ response.each do |name, details|
27
+ cookbooks[name] = details["versions"].collect { |version| version["version"] }
28
+ end
29
+ end
30
+ end
31
+
5
32
  def create(*args)
6
33
  raise NotImplementedError
7
34
  end
8
35
 
9
- def delete(*args)
10
- raise NotImplementedError
36
+ # Delete a cookbook of the given name and version on the remote Chef server
37
+ #
38
+ # @param [Ridley::Client] client
39
+ # @param [String] name
40
+ # @param [String] version
41
+ #
42
+ # @option options [Boolean] purge (false)
43
+ #
44
+ # @return [Boolean]
45
+ def delete(client, name, version, options = {})
46
+ options = options.reverse_merge(purge: false)
47
+ url = "#{self.resource_path}/#{name}/#{version}"
48
+ url += "?purge=true" if options[:purge]
49
+
50
+ client.connection.delete(url).body
51
+ true
52
+ rescue Errors::HTTPNotFound
53
+ true
11
54
  end
12
55
 
13
- def delete_all(*args)
14
- raise NotImplementedError
56
+ # Delete all of the versions of a given cookbook on the remote Chef server
57
+ #
58
+ # @param [Ridley::Client] client
59
+ # @param [String] name
60
+ # name of the cookbook to delete
61
+ #
62
+ # @option options [Boolean] purge (false)
63
+ def delete_all(client, name, options = {})
64
+ versions(client, name).each do |version|
65
+ delete(client, name, version, options)
66
+ end
67
+ end
68
+
69
+ # Download the entire cookbook
70
+ #
71
+ # @param [Ridley::Client] client
72
+ # @param [String] name
73
+ # @param [String] version
74
+ # @param [String] destination (Dir.mktmpdir)
75
+ # the place to download the cookbook too. If no value is provided the cookbook
76
+ # will be downloaded to a temporary location
77
+ #
78
+ # @return [String]
79
+ # the path to the directory the cookbook was downloaded to
80
+ def download(client, name, version, destination = Dir.mktmpdir)
81
+ cookbook = find(client, name, version)
82
+
83
+ unless cookbook.nil?
84
+ cookbook.download(destination)
85
+ end
15
86
  end
16
87
 
17
88
  # @param [Ridley::Client] client
@@ -19,7 +90,7 @@ module Ridley
19
90
  # @param [String] version
20
91
  #
21
92
  # @return [nil, CookbookResource]
22
- def find(client, object, version = nil)
93
+ def find(client, object, version)
23
94
  find!(client, object, version)
24
95
  rescue Errors::HTTPNotFound
25
96
  nil
@@ -33,15 +104,40 @@ module Ridley
33
104
  # if a resource with the given chef_id is not found
34
105
  #
35
106
  # @return [CookbookResource]
36
- def find!(client, object, version = nil)
37
- chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
38
- fetch_uri = "#{self.resource_path}/#{chef_id}"
39
-
40
- unless version.nil?
41
- fetch_uri = File.join(fetch_uri, version)
42
- end
107
+ def find!(client, object, version)
108
+ chef_id = object.respond_to?(:chef_id) ? object.chef_id : object
109
+ new(client, client.connection.get("#{self.resource_path}/#{chef_id}/#{version}").body)
110
+ end
111
+
112
+ # Return the latest version of the given cookbook found on the remote Chef server
113
+ #
114
+ # @param [Ridley::Client] client
115
+ # @param [String] name
116
+ #
117
+ # @return [String, nil]
118
+ def latest_version(client, name)
119
+ ver = versions(client, name).collect do |version|
120
+ Solve::Version.new(version)
121
+ end.sort.last
43
122
 
44
- new(client, client.connection.get(fetch_uri).body)
123
+ ver.nil? ? nil : ver.to_s
124
+ end
125
+
126
+ # Return the version of the given cookbook which best stasifies the given constraint
127
+ #
128
+ # @param [Ridley::Client] client
129
+ # @param [String] name
130
+ # name of the cookbook
131
+ # @param [String, Solve::Constraint] constraint
132
+ # constraint to solve for
133
+ #
134
+ # @return [CookbookResource, nil]
135
+ # returns the cookbook resource for the best solution or nil if no solution exists
136
+ def satisfy(client, name, constraint)
137
+ version = Solve::Solver.satisfy_best(constraint, versions(client, name)).to_s
138
+ find(client, name, version)
139
+ rescue Solve::Errors::NoSolutionError
140
+ nil
45
141
  end
46
142
 
47
143
  # Save a new Cookbook Version of the given name, version with the
@@ -71,8 +167,39 @@ module Ridley
71
167
  def update(*args)
72
168
  raise NotImplementedError
73
169
  end
170
+
171
+ # Return a list of versions for the given cookbook present on the remote Chef server
172
+ #
173
+ # @param [Ridley::Client] client
174
+ # @param [String] name
175
+ #
176
+ # @example
177
+ # versions(client, "nginx") => [ "1.0.0", "1.2.0" ]
178
+ #
179
+ # @return [Array<String>]
180
+ def versions(client, name)
181
+ response = client.connection.get("#{self.resource_path}/#{name}").body
182
+
183
+ response[name]["versions"].collect do |cb_ver|
184
+ cb_ver["version"]
185
+ end
186
+ end
74
187
  end
75
188
 
189
+ include Ridley::Logging
190
+
191
+ FILE_TYPES = [
192
+ :resources,
193
+ :providers,
194
+ :recipes,
195
+ :definitions,
196
+ :libraries,
197
+ :attributes,
198
+ :files,
199
+ :templates,
200
+ :root_files
201
+ ].freeze
202
+
76
203
  set_chef_id "name"
77
204
  set_chef_type "cookbook"
78
205
  set_chef_json_class "Chef::Cookbook"
@@ -81,41 +208,166 @@ module Ridley
81
208
  attribute :name,
82
209
  required: true
83
210
 
84
- # Broken until resolved: https://github.com/reset/chozo/issues/17
85
- # attribute :attributes,
86
- # type: Array
211
+ attribute :attributes,
212
+ type: Array,
213
+ default: Array.new
87
214
 
88
215
  attribute :cookbook_name,
89
216
  type: String
90
217
 
91
218
  attribute :definitions,
92
- type: Array
219
+ type: Array,
220
+ default: Array.new
93
221
 
94
222
  attribute :files,
95
- type: Array
223
+ type: Array,
224
+ default: Array.new
96
225
 
97
226
  attribute :libraries,
98
- type: Array
227
+ type: Array,
228
+ default: Array.new
99
229
 
100
230
  attribute :metadata,
101
231
  type: Hashie::Mash
102
232
 
103
233
  attribute :providers,
104
- type: Array
234
+ type: Array,
235
+ default: Array.new
105
236
 
106
237
  attribute :recipes,
107
- type: Array
238
+ type: Array,
239
+ default: Array.new
108
240
 
109
241
  attribute :resources,
110
- type: Array
242
+ type: Array,
243
+ default: Array.new
111
244
 
112
245
  attribute :root_files,
113
- type: Array
246
+ type: Array,
247
+ default: Array.new
114
248
 
115
249
  attribute :templates,
116
- type: Array
250
+ type: Array,
251
+ default: Array.new
117
252
 
118
253
  attribute :version,
119
254
  type: String
255
+
256
+ # Download the entire cookbook
257
+ #
258
+ # @param [String] destination (Dir.mktmpdir)
259
+ # the place to download the cookbook too. If no value is provided the cookbook
260
+ # will be downloaded to a temporary location
261
+ #
262
+ # @return [String]
263
+ # the path to the directory the cookbook was downloaded to
264
+ def download(destination = Dir.mktmpdir)
265
+ destination = File.expand_path(destination)
266
+ log.debug { "downloading cookbook: '#{name}'" }
267
+
268
+ FILE_TYPES.each do |filetype|
269
+ next unless manifest.has_key?(filetype)
270
+
271
+ manifest[filetype].each do |file|
272
+ file_destination = File.join(destination, file[:path].gsub('/', File::SEPARATOR))
273
+ FileUtils.mkdir_p(File.dirname(file_destination))
274
+ download_file(filetype, file[:name], file_destination)
275
+ end
276
+ end
277
+
278
+ destination
279
+ end
280
+
281
+ # Download a single file from a cookbook
282
+ #
283
+ # @param [#to_sym] filetype
284
+ # the type of file to download. These are broken up into the following types in Chef:
285
+ # - attribute (unsupported until resolved https://github.com/reset/chozo/issues/17)
286
+ # - definition
287
+ # - file
288
+ # - library
289
+ # - provider
290
+ # - recipe
291
+ # - resource
292
+ # - root_file
293
+ # - template
294
+ # these types are where the files are stored in your cookbook's structure. For example, a
295
+ # recipe would be stored in the recipes directory while a root_file is stored at the root
296
+ # of your cookbook
297
+ # @param [String] name
298
+ # name of the file to download
299
+ # @param [String] destination
300
+ # where to download the file to
301
+ #
302
+ # @return [nil]
303
+ def download_file(filetype, name, destination)
304
+ download_fun(filetype).call(name, destination)
305
+ end
306
+
307
+ # A hash containing keys for all of the different cookbook filetypes with values
308
+ # representing each file of that type this cookbook contains
309
+ #
310
+ # @example
311
+ # {
312
+ # root_files: [
313
+ # {
314
+ # :name => "afile.rb",
315
+ # :path => "files/ubuntu-9.10/afile.rb",
316
+ # :checksum => "2222",
317
+ # :specificity => "ubuntu-9.10"
318
+ # },
319
+ # ],
320
+ # templates: [ manifest_record1, ... ],
321
+ # ...
322
+ # }
323
+ #
324
+ # @return [Hash]
325
+ def manifest
326
+ {}.tap do |manifest|
327
+ FILE_TYPES.each do |filetype|
328
+ manifest[filetype] = get_attribute(filetype)
329
+ end
330
+ end
331
+ end
332
+
333
+ def to_s
334
+ "#{name}: #{manifest}"
335
+ end
336
+
337
+ private
338
+
339
+ # Return a lambda for downloading a file from the cookbook of the given type
340
+ #
341
+ # @param [#to_sym] filetype
342
+ #
343
+ # @return [lambda]
344
+ # a lambda which takes to parameters: target and path. Target is the URL to download from
345
+ # and path is the location on disk to steam the contents of the remote URL to.
346
+ def download_fun(filetype)
347
+ collection = case filetype.to_sym
348
+ when :attribute, :attributes; method(:attributes)
349
+ when :definition, :definitions; method(:definitions)
350
+ when :file, :files; method(:files)
351
+ when :library, :libraries; method(:libraries)
352
+ when :provider, :providers; method(:providers)
353
+ when :recipe, :recipes; method(:recipes)
354
+ when :resource, :resources; method(:resources)
355
+ when :root_file, :root_files; method(:root_files)
356
+ when :template, :templates; method(:templates)
357
+ else
358
+ raise Errors::UnknownCookbookFileType.new(filetype)
359
+ end
360
+
361
+ ->(target, destination) {
362
+ files = collection.call # JW: always chaining .call.find results in a nil value. WHY?
363
+ file = files.find { |f| f[:name] == target }
364
+ return nil if file.nil?
365
+
366
+ destination = File.expand_path(destination)
367
+ log.debug { "downloading '#{filetype}' file: #{file} to: '#{destination}'" }
368
+
369
+ client.connection.stream(file[:url], destination)
370
+ }
371
+ end
120
372
  end
121
373
  end