ridley 0.7.0.rc1 → 0.7.0.rc3

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