librarian-chef-nochef 0.1.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.
@@ -0,0 +1,481 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'digest'
7
+ require 'zlib'
8
+ require 'securerandom'
9
+ require 'archive/tar/minitar'
10
+
11
+ require 'librarian/source/basic_api'
12
+ require 'librarian/chef/manifest_reader'
13
+
14
+ module Librarian
15
+ module Chef
16
+ module Source
17
+ class Site
18
+
19
+ class Line
20
+
21
+ attr_accessor :source, :name
22
+ private :source=, :name=
23
+
24
+ def initialize(source, name)
25
+ self.source = source
26
+ self.name = name
27
+ end
28
+
29
+ def install_version!(version, install_path)
30
+ cache_version_unpacked! version
31
+
32
+ if install_path.exist?
33
+ debug { "Deleting #{relative_path_to(install_path)}" }
34
+ install_path.rmtree
35
+ end
36
+
37
+ unpacked_path = version_unpacked_cache_path(version)
38
+
39
+ debug { "Copying #{relative_path_to(unpacked_path)} to #{relative_path_to(install_path)}" }
40
+ FileUtils.cp_r(unpacked_path, install_path)
41
+ end
42
+
43
+ def manifests
44
+ version_uris.map do |version_uri|
45
+ Manifest.new(source, name, version_uri)
46
+ end
47
+ end
48
+
49
+ def to_version(version_uri)
50
+ version_uri_metadata(version_uri)["version"]
51
+ end
52
+
53
+ def version_dependencies(version)
54
+ version_manifest(version)["dependencies"]
55
+ end
56
+
57
+ private
58
+
59
+ attr_accessor :metadata_cached
60
+ alias metadata_cached? metadata_cached
61
+
62
+ def environment
63
+ source.environment
64
+ end
65
+
66
+ def uri
67
+ @uri ||= URI.parse("#{source.uri}/cookbooks/#{name}")
68
+ end
69
+
70
+ def version_uris
71
+ metadata["versions"]
72
+ end
73
+
74
+ def version_metadata(version)
75
+ version_uri = to_version_uri(version)
76
+ version_uri_metadata(version_uri)
77
+ end
78
+
79
+ def version_uri_metadata(version_uri)
80
+ memo(__method__, version_uri.to_s) do
81
+ cache_version_uri_metadata! version_uri
82
+ parse_local_json(version_uri_metadata_cache_path(version_uri))
83
+ end
84
+ end
85
+
86
+ def version_manifest(version)
87
+ version_uri = to_version_uri(version)
88
+ version_uri_manifest(version_uri)
89
+ end
90
+
91
+ def version_uri_manifest(version_uri)
92
+ memo(__method__, version_uri.to_s) do
93
+ cache_version_uri_unpacked! version_uri
94
+ unpacked_path = version_uri_unpacked_cache_path(version_uri)
95
+ manifest_path = ManifestReader.manifest_path(unpacked_path)
96
+ ManifestReader.read_manifest(name, manifest_path)
97
+ end
98
+ end
99
+
100
+ def metadata
101
+ @metadata ||= begin
102
+ cache_metadata!
103
+ parse_local_json(metadata_cache_path)
104
+ end
105
+ end
106
+
107
+ def to_version_uri(version)
108
+ memo(__method__, version.to_s) do
109
+ cache_version! version
110
+ version_cache_path(version).read
111
+ end
112
+ end
113
+
114
+ def metadata_cached!
115
+ self.metadata_cached = true
116
+ end
117
+
118
+ def cache_path
119
+ @cache_path ||= source.cache_path.join(name)
120
+ end
121
+
122
+ def metadata_cache_path
123
+ @metadata_cache_path ||= cache_path.join("metadata.json")
124
+ end
125
+
126
+ def version_cache_path(version)
127
+ memo(__method__, version.to_s) do
128
+ cache_path.join("version").join(version.to_s)
129
+ end
130
+ end
131
+
132
+ def version_uri_cache_path(version_uri)
133
+ memo(__method__, version_uri.to_s) do
134
+ cache_path.join("version-uri").join(hexdigest(version_uri))
135
+ end
136
+ end
137
+
138
+ def version_metadata_cache_path(version)
139
+ version_uri = to_version_uri(version)
140
+ version_uri_metadata_cache_path(version_uri)
141
+ end
142
+
143
+ def version_uri_metadata_cache_path(version_uri)
144
+ memo(__method__, version_uri.to_s) do
145
+ version_uri_cache_path(version_uri).join("metadata.json")
146
+ end
147
+ end
148
+
149
+ def version_package_cache_path(version)
150
+ version_uri = to_version_uri(version)
151
+ version_uri_package_cache_path(version_uri)
152
+ end
153
+
154
+ def version_uri_package_cache_path(version_uri)
155
+ memo(__method__, version_uri.to_s) do
156
+ version_uri_cache_path(version_uri).join("package.tar.gz")
157
+ end
158
+ end
159
+
160
+ def version_unpacked_cache_path(version)
161
+ version_uri = to_version_uri(version)
162
+ version_uri_unpacked_cache_path(version_uri)
163
+ end
164
+
165
+ def version_uri_unpacked_cache_path(version_uri)
166
+ memo(__method__, version_uri.to_s) do
167
+ version_uri_cache_path(version_uri).join("package")
168
+ end
169
+ end
170
+
171
+ def cache_metadata!
172
+ metadata_cached? and return or metadata_cached!
173
+ cache_remote_json! metadata_cache_path, uri
174
+ end
175
+
176
+ def cache_version_uri_metadata!(version_uri)
177
+ path = version_uri_metadata_cache_path(version_uri)
178
+ path.file? and return
179
+
180
+ cache_remote_json! path, version_uri
181
+ end
182
+
183
+ def cache_version!(version)
184
+ path = version_cache_path(version)
185
+ path.file? and return
186
+
187
+ version_uris.each do |version_uri|
188
+ m = version_uri_metadata(version_uri)
189
+ v = m["version"]
190
+ if version.to_s == v
191
+ write! path, version_uri.to_s
192
+ break
193
+ end
194
+ end
195
+ end
196
+
197
+ def cache_version_package!(version)
198
+ version_uri = to_version_uri(version)
199
+ cache_version_uri_package! version_uri
200
+ end
201
+
202
+ def cache_version_uri_package!(version_uri)
203
+ path = version_uri_package_cache_path(version_uri)
204
+ path.file? and return
205
+
206
+ file_uri = version_uri_metadata(version_uri)["file"]
207
+ cache_remote_object! path, file_uri
208
+ end
209
+
210
+ def cache_version_unpacked!(version)
211
+ version_uri = to_version_uri(version)
212
+ cache_version_uri_unpacked! version_uri
213
+ end
214
+
215
+ def cache_version_uri_unpacked!(version_uri)
216
+ cache_version_uri_package!(version_uri)
217
+
218
+ path = version_uri_unpacked_cache_path(version_uri)
219
+ path.directory? and return
220
+
221
+ package_path = version_uri_package_cache_path(version_uri)
222
+ unpacked_path = version_uri_unpacked_cache_path(version_uri)
223
+
224
+ unpack_package! unpacked_path, package_path
225
+ end
226
+
227
+ def cache_remote_json!(path, uri)
228
+ cache_remote_object!(path, uri, :type => :json)
229
+ end
230
+
231
+ def cache_remote_object!(path, uri, options = { })
232
+ path = Pathname(path)
233
+ uri = to_uri(uri)
234
+ type = options[:type]
235
+
236
+ debug { "Caching #{uri} to #{path}" }
237
+
238
+ response = http_get(uri)
239
+
240
+ object = response.body
241
+ case type
242
+ when :json
243
+ JSON.parse(object) # verify that it's really JSON.
244
+ end
245
+ write! path, object
246
+ end
247
+
248
+ def write!(path, bytes)
249
+ path.dirname.mkpath
250
+ path.open("wb"){|f| f.write(bytes)}
251
+ end
252
+
253
+ def path_detect_gzip?(path)
254
+ Zlib::GzipReader.open(path) { true }
255
+ rescue Zlib::GzipFile::Error
256
+ false
257
+ end
258
+
259
+ def path_detect_tar?(path)
260
+ path_read_bytes_at(path, 257, 8) == "ustar\x0000"
261
+ end
262
+
263
+ def path_read_bytes_at(path, pos, len)
264
+ path = Pathname(path)
265
+ path.stat.size >= pos + len or return
266
+ path.open "rb" do |f|
267
+ f.seek pos ; f.pos == pos or return
268
+ f.read(len)
269
+ end
270
+ end
271
+
272
+ def extract_archive!(source, destination)
273
+ source = Pathname(source)
274
+ destination = Pathname(destination)
275
+
276
+ return extract_archive_targz! source, destination if path_detect_gzip?(source)
277
+ return extract_archive_tar! source, destination if path_detect_tar?(source)
278
+ raise "Unrecognized archive format!"
279
+ end
280
+
281
+ def extract_archive_targz!(source, destination)
282
+ Zlib::GzipReader.open(source) do |input|
283
+ Archive::Tar::Minitar.unpack(input, destination.to_s)
284
+ end
285
+ end
286
+
287
+ def extract_archive_tar!(source, destination)
288
+ source.open "rb" do |input|
289
+ Archive::Tar::Minitar.unpack(input, destination.to_s)
290
+ end
291
+ end
292
+
293
+ def unpack_package!(path, source)
294
+ path = Pathname(path)
295
+ source = Pathname(source)
296
+
297
+ temp = environment.scratch_path.join(SecureRandom.hex(16))
298
+ temp.mkpath
299
+
300
+ debug { "Unpacking #{relative_path_to(source)} to #{relative_path_to(temp)}" }
301
+ extract_archive! source, temp
302
+
303
+ # Cookbook files, as pulled from Opscode Community Site API, are
304
+ # embedded in a subdirectory of the tarball. If created by git archive they
305
+ # can include the subfolder `pax_global_header`, which is ignored.
306
+ subtemps = temp.children
307
+ subtemps.empty? and raise "The package archive was empty!"
308
+ subtemps.delete_if{|pth| pth.to_s[/pax_global_header/]}
309
+ subtemps.size > 1 and raise "The package archive has too many children!"
310
+ subtemp = subtemps.first
311
+ debug { "Moving #{relative_path_to(subtemp)} to #{relative_path_to(path)}" }
312
+ FileUtils.cp_r(subtemp, path)
313
+ FileUtils.rm_rf(subtemp)
314
+ ensure
315
+ temp.rmtree if temp && temp.exist?
316
+ end
317
+
318
+ def parse_local_json(path)
319
+ JSON.parse(path.read)
320
+ end
321
+
322
+ def hexdigest(bytes)
323
+ Digest::MD5.hexdigest(bytes)[0..15]
324
+ end
325
+
326
+ def to_uri(uri)
327
+ uri = URI(uri) unless URI === uri
328
+ uri
329
+ end
330
+
331
+ def debug(*args, &block)
332
+ environment.logger.debug(*args, &block)
333
+ end
334
+
335
+ def relative_path_to(path)
336
+ environment.logger.relative_path_to(path)
337
+ end
338
+
339
+ def http(uri)
340
+ environment.net_http_class(uri.host).new(uri.host, uri.port)
341
+ end
342
+
343
+ def http_get(uri)
344
+ max_redirects = 10
345
+ redirects = []
346
+
347
+ loop do
348
+ debug { "Performing http-get for #{uri}" }
349
+ http = http(uri)
350
+ request = Net::HTTP::Get.new(uri.path)
351
+ response = http.start{|http| http.request(request)}
352
+
353
+ case response
354
+ when Net::HTTPSuccess
355
+ debug { "Responded with success" }
356
+ return response
357
+ when Net::HTTPRedirection
358
+ location = response["Location"]
359
+ debug { "Responded with redirect to #{uri}" }
360
+ redirects.size > max_redirects and raise Error,
361
+ "Could not get #{uri} because too many redirects!"
362
+ redirects.include?(location) and raise Error,
363
+ "Could not get #{uri} because redirect cycle!"
364
+ redirects << location
365
+ uri = URI.parse(location)
366
+ # continue the loop
367
+ else
368
+ raise Error, "Could not get #{uri} because #{response.code} #{response.message}!"
369
+ end
370
+ end
371
+ end
372
+
373
+ def memo(method, *path)
374
+ ivar = "@#{method}".to_sym
375
+ unless memo = instance_variable_get(ivar)
376
+ memo = instance_variable_set(ivar, { })
377
+ end
378
+
379
+ memo.key?(path) or memo[path] = yield
380
+ memo[path]
381
+ end
382
+
383
+ end
384
+
385
+ include Librarian::Source::BasicApi
386
+
387
+ lock_name 'SITE'
388
+ spec_options []
389
+
390
+ attr_accessor :environment, :uri
391
+ private :environment=, :uri=
392
+
393
+ def initialize(environment, uri, options = {})
394
+ self.environment = environment
395
+ self.uri = uri
396
+ end
397
+
398
+ def to_s
399
+ uri
400
+ end
401
+
402
+ def ==(other)
403
+ other &&
404
+ self.class == other.class &&
405
+ self.uri == other.uri
406
+ end
407
+
408
+ def to_spec_args
409
+ [uri, {}]
410
+ end
411
+
412
+ def to_lock_options
413
+ {:remote => uri}
414
+ end
415
+
416
+ def pinned?
417
+ false
418
+ end
419
+
420
+ def unpin!
421
+ end
422
+
423
+ def install!(manifest)
424
+ manifest.source == self or raise ArgumentError
425
+
426
+ name = manifest.name
427
+ version = manifest.version
428
+ install_path = install_path(name)
429
+ line = line(name)
430
+
431
+ info { "Installing #{manifest.name} (#{manifest.version})" }
432
+
433
+ debug { "Installing #{manifest}" }
434
+
435
+ line.install_version! version, install_path
436
+ end
437
+
438
+ # NOTE:
439
+ # Assumes the Opscode Site API responds with versions in reverse sorted order
440
+ def manifests(name)
441
+ line(name).manifests
442
+ end
443
+
444
+ def cache_path
445
+ @cache_path ||= begin
446
+ dir = Digest::MD5.hexdigest(uri)[0..15]
447
+ environment.cache_path.join("source/chef/site/#{dir}")
448
+ end
449
+ end
450
+
451
+ def install_path(name)
452
+ environment.install_path.join(name)
453
+ end
454
+
455
+ def fetch_version(name, version_uri)
456
+ line(name).to_version(version_uri)
457
+ end
458
+
459
+ def fetch_dependencies(name, version, version_uri)
460
+ line(name).version_dependencies(version).map{|k, v| Dependency.new(k, v, nil)}
461
+ end
462
+
463
+ private
464
+
465
+ def line(name)
466
+ @line ||= { }
467
+ @line[name] ||= Line.new(self, name)
468
+ end
469
+
470
+ def info(*args, &block)
471
+ environment.logger.info(*args, &block)
472
+ end
473
+
474
+ def debug(*args, &block)
475
+ environment.logger.debug(*args, &block)
476
+ end
477
+
478
+ end
479
+ end
480
+ end
481
+ end