devstructure 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,10 @@
1
+ # The [DevStructure API](http://docs.devstructure.com/api) allows
2
+ # read and write access to blueprints, and soon other parts of the
3
+ # service.
1
4
  require 'devstructure'
2
5
  require 'devstructure/blueprint'
3
6
 
4
- # HERE BE DRAGONS The 1.4.3 version of the JSON gem is crap-tastic,
7
+ # *HERE BE DRAGONS* The 1.4.3 version of the JSON gem is crap-tastic,
5
8
  # which forces me to pin to 1.4.2, which in turn forces me to load
6
9
  # RubyGems manually here, which in turn pretty much makes me cry.
7
10
  require 'rubygems'
@@ -13,13 +16,19 @@ require 'net/https'
13
16
  require 'openssl'
14
17
  require 'uri'
15
18
 
19
+ # We subclass `Net::HTTP` so as not to take away a user's ability to
20
+ # shoot themselves in the foot.
16
21
  class DevStructure::API < Net::HTTP
17
22
 
18
- # Undo Net::HTTP's allocator hijacking.
23
+ # Undo `Net::HTTP`'s allocator hijacking.
19
24
  class << self
20
25
  alias new newobj
21
26
  end
22
27
 
28
+ # No one really has access to a DevStructure API token these days,
29
+ # save for the one that's installed on each DevStructure server.
30
+ # The default token is the one created by concatenating `/etc/token`
31
+ # and `~/.token`.
23
32
  def initialize(token=nil)
24
33
  @newimpl = true
25
34
  uri = URI.parse("https://api.devstructure.com")
@@ -38,11 +47,15 @@ class DevStructure::API < Net::HTTP
38
47
  end
39
48
  end
40
49
 
50
+ # This is the lazy man's way of building the URI for a request.
41
51
  def path(*args)
42
52
  args.reject! { |arg| arg.nil? }
43
53
  "/#{args.join("/")}"
44
54
  end
45
55
 
56
+ # These are the only two headers that are required when accessing the
57
+ # API. Others such as `Content-Type` and `Content-Length` will be
58
+ # added as necessary, either explicitly or by `Net::HTTP`.
46
59
  def headers
47
60
  {
48
61
  "Authorization" => "Token token=\"#{@token}\"",
@@ -50,14 +63,22 @@ class DevStructure::API < Net::HTTP
50
63
  }
51
64
  end
52
65
 
66
+ # `Blueprints` objects represent a particular user's blueprint collection
67
+ # but is supremely lazy about fetching it.
53
68
  class Blueprints
54
69
  include DevStructure
55
70
 
71
+ # This ugly constructor should be eschewed in favor of `API#blueprints`.
56
72
  def initialize(api, username=nil)
57
73
  @api = api
58
74
  @username = username
59
75
  end
60
76
 
77
+ # The `method_missing` handler allows natural-looking code like
78
+ # `blueprints.foo.bar` that look like the URIs they're referencing
79
+ # behind the scenes. These are limited to reads when taken all the
80
+ # way to referencing a specific blueprint. Just referencing a user
81
+ # this way still allows writes.
61
82
  def method_missing(symbol, *args)
62
83
  if @username
63
84
  get symbol.to_s
@@ -67,6 +88,9 @@ class DevStructure::API < Net::HTTP
67
88
  end
68
89
  end
69
90
 
91
+ # The API version of
92
+ # [`blueprint-list`(1)](http://devstructure.github.com/contractor/blueprint-list.1.html).
93
+ # It returns an array of `DevStructure::Blueprint` objects.
70
94
  def list
71
95
  @api.start unless @api.started?
72
96
  response = @api.get(@api.path("blueprints", @username), @api.headers)
@@ -76,6 +100,9 @@ class DevStructure::API < Net::HTTP
76
100
  end
77
101
  end
78
102
 
103
+ # The API version of
104
+ # [`blueprint-show`(1)](http://devstructure.github.com/contractor/blueprint-show.1.html).
105
+ # It returns a `DevStructure::Blueprint` object.
79
106
  def get(name, token=nil)
80
107
  @api.start unless @api.started?
81
108
  response = @api.get(@api.path("blueprints", @username, name, token),
@@ -85,6 +112,12 @@ class DevStructure::API < Net::HTTP
85
112
  Blueprint.new(hash["name"], hash)
86
113
  end
87
114
 
115
+ # The API version of
116
+ # [`blueprint-create`(1)](http://devstructure.github.com/contractor/blueprint-create.1.html).
117
+ # It encodes and POSTs a blueprint and uploads to S3 any source tarballs
118
+ # referenced. The S3 upload credentials come back as part of the API
119
+ # response. Each set of headers is only valid for that particular
120
+ # upload and even then only for a few minutes.
88
121
  def post(name, options={})
89
122
  @api.start unless @api.started?
90
123
  params = {}
@@ -113,6 +146,7 @@ class DevStructure::API < Net::HTTP
113
146
  response
114
147
  end
115
148
 
149
+ # Delete a blueprint and all its versions.
116
150
  def delete(name, options={})
117
151
  @api.start unless @api.started?
118
152
  @api.delete(@api.path("blueprints", @username, name), @api.headers)
@@ -120,6 +154,7 @@ class DevStructure::API < Net::HTTP
120
154
 
121
155
  end
122
156
 
157
+ # Grab a particular user's blueprint collection.
123
158
  def blueprints(username=nil)
124
159
  Blueprints.new(self, username)
125
160
  end
@@ -1,13 +1,20 @@
1
+ # A `Blueprint` is just a `Hash` with some convenient additions, the first
2
+ # of which is a set of instance methods that access the innards of the
3
+ # blueprint itself (rather than the metadata). `Blueprint`s come from
4
+ # `DevStructure::API` and can actually save themselves if changes are made.
1
5
  require 'devstructure'
2
6
  require 'devstructure/api'
7
+ require 'devstructure/chef'
3
8
  require 'devstructure/puppet'
4
9
 
5
10
  require 'base64'
11
+ require 'stringio'
6
12
 
7
13
  class DevStructure::Blueprint < Hash
8
14
  include DevStructure
9
- include DevStructure::Puppet
10
15
 
16
+ # Blueprints are usually created from a name and a hash, being gathered
17
+ # from the URL and response of a DevStructure API request.
11
18
  def initialize(name, hash)
12
19
  super nil
13
20
  clear
@@ -18,37 +25,61 @@ class DevStructure::Blueprint < Hash
18
25
 
19
26
  attr_accessor :name
20
27
 
28
+ # Attempt to save the blueprint, returning directly from the API. This
29
+ # is purely for convenience.
21
30
  def save(ds=nil)
22
31
  ds ||= API.new
23
32
  ds.blueprints(owner).post(name, self)
24
33
  end
25
34
 
35
+ # The outer layers of the hash are not arbitrarily settable. Blueprints
36
+ # have a defined set of fields and accepting anything beyond that would
37
+ # be confusing.
26
38
  private :[]=
27
39
 
40
+ # This is the preferred method for reading the blueprint metadata but
41
+ # hash-like access will work just fine, you'll just have to use strings.
28
42
  def method_missing(symbol, *args)
29
43
  self[symbol.to_s]
30
44
  end
31
45
 
46
+ # The architecture of the server used to create a blueprint will be
47
+ # "amd64" or "i386" if it's set at all. When possible, the architecture
48
+ # is left unspecified to allow blueprints to be applied to any server.
32
49
  def arch
33
50
  self["blueprint"]["_arch"]
34
51
  end
35
52
 
53
+ # The files, packages, and sources hashes are completely described in
54
+ # [`blueprint`(5)](http://devstructure.github.com/contractor/blueprint.5.html).
36
55
  def files
37
56
  self["blueprint"]["_files"]
38
57
  end
39
-
40
58
  def packages
41
59
  self["blueprint"]["_packages"]
42
60
  end
43
-
44
61
  def sources
45
62
  self["blueprint"]["_sources"]
46
63
  end
47
64
 
65
+ # Subtracting one blueprint from another is an important strategy used
66
+ # to keep superfluous packages managed by `apt` out of blueprints in
67
+ # most cases. Improvements in the 1.1.0 release reduced the need for
68
+ # this optimization but it doesn't hurt.
69
+ #
70
+ # It takes three passes through the package tree. The first two
71
+ # remove superfluous packages and the final one accounts for some
72
+ # special dependencies by adding them back.
48
73
  def -(other)
49
74
  b = Marshal.load(Marshal.dump(self))
50
75
 
51
- # First pass removes all duplicates except managers.
76
+ # The first pass removes all duplicate packages that are not
77
+ # themselves managers. The introduction of multiple-version
78
+ # complicates this slightly. For each package, each version
79
+ # that appears in the other blueprint is removed from this
80
+ # blueprint. After that is finished, this blueprint is
81
+ # normalized. If no versions remain, the package is removed.
82
+ # If only one version remains, it is converted to a string.
52
83
  other.walk(
53
84
  :package => lambda { |manager, command, package, version|
54
85
  return if b.packages[package]
@@ -66,7 +97,8 @@ class DevStructure::Blueprint < Hash
66
97
  }
67
98
  )
68
99
 
69
- # Second pass removes managers that manage no packages.
100
+ # The second pass removes managers that manage no packages, which
101
+ # is a potential side effect of the first pass.
70
102
  b.walk(
71
103
  :package => lambda { |manager, command, package, version|
72
104
  return unless b.packages[package]
@@ -75,12 +107,19 @@ class DevStructure::Blueprint < Hash
75
107
  }
76
108
  )
77
109
 
78
- # Third pass adds back special dependencies like ruby*-dev.
110
+ # The third pass adds back special dependencies like `ruby*-dev`.
111
+ # It isn't apparent by the rules above that a manager like RubyGems
112
+ # needs more than just itself to function. In some sense, this
113
+ # might be considered a missing dependency in the Debian archive
114
+ # but in reality, it's only _likely_ that you need `ruby*-dev` to
115
+ # use `rubygems*`.
116
+ #
117
+ # The only dependency that fits this bill currently is in fact
118
+ # RubyGems, which gets `ruby*-dev` added for the matching version
119
+ # of the Ruby language.
79
120
  b.walk(
80
121
  :after => lambda { |manager, command|
81
122
  case manager
82
-
83
- # Match a version of ruby*-dev to a version of rubygems*.
84
123
  when /^rubygems(\d+\.\d+(?:\.\d+)?)$/
85
124
  b.packages["apt"]["_packages"]["ruby#{$1}-dev"] =
86
125
  packages["apt"]["_packages"]["ruby#{$1}-dev"]
@@ -92,32 +131,47 @@ class DevStructure::Blueprint < Hash
92
131
  b
93
132
  end
94
133
 
134
+ # Everyone loves a good disclaimer. This one's chock full of useful
135
+ # links so folks can find their way back to DevStructure when the
136
+ # need strikes. Marketing!
95
137
  def disclaimer
96
- puts <<EOF
97
- #
98
- # #{owner}'s #{name}
99
- # http#{token ? "s" : ""}://devstructure.com/blueprints/#{owner}/#{name}
138
+ disclaimer = <<EOF
139
+ \#
140
+ \# #{owner}'s #{name}
141
+ \# http#{token ? "s" : ""}://devstructure.com/blueprints/#{owner}/#{name}
100
142
  EOF
101
143
  if token
102
- puts <<EOF
103
- #
104
- # This blueprint is private. You may share it by adding trusted users
105
- # or by sharing this secret URL:
106
- # https://devstructure.com/blueprints/#{owner}/#{name}/#{token}
144
+ disclaimer << <<EOF
145
+ \#
146
+ \# This blueprint is private. You may share it by adding trusted users
147
+ \# or by sharing this secret URL:
148
+ \# https://devstructure.com/blueprints/#{owner}/#{name}/#{token}
107
149
  EOF
108
150
  end
109
- puts <<EOF
110
- #
111
- # This file was automatically generated by ruby-devstructure(7).
112
- #
151
+ disclaimer << <<EOF
152
+ \#
153
+ \# This file was automatically generated by ruby-devstructure(7).
154
+ \#
113
155
  EOF
156
+ disclaimer
114
157
  end
115
158
 
159
+ # This is the POSIX shell code generator.
116
160
  def sh
117
- disclaimer
118
-
119
- # Packages.
161
+ puts disclaimer
162
+
163
+ # First come packages. The algorithm used to walk the package tree
164
+ # is pluggable and in this case we only need to take action as each
165
+ # package name is encountered. Each manager is annotated with a
166
+ # shell command suitable for installing packages in `printf`(3)-style.
167
+ #
168
+ # RubyGems is, once again, a special case. Ubuntu ships with a very
169
+ # old version so we take the liberty of upgrading it anytime it is
170
+ # encountered.
120
171
  walk(
172
+ :before => lambda { |manager, command|
173
+ puts "apt-get update" if "apt" == manager
174
+ },
121
175
  :package => lambda { |manager, command, package, version|
122
176
  return if manager == package
123
177
  printf "#{command}\n", package, version
@@ -129,7 +183,9 @@ EOF
129
183
  }
130
184
  )
131
185
 
132
- # System files.
186
+ # Plaintext files are written to their final destination using `cat`(1)
187
+ # and heredoc syntax. Binary files are written using `base64`(1) and
188
+ # symbolic links are placed using `ln`(1). Shocking stuff.
133
189
  files.sort.each do |pathname, content|
134
190
  if Hash == content.class
135
191
  if content["_target"]
@@ -146,12 +202,15 @@ EOF
146
202
  end
147
203
  eof = "EOF"
148
204
  eof << "EOF" while content =~ /^#{eof}$/
205
+ puts "mkdir -p #{File.dirname(pathname)}"
149
206
  puts "#{command} >#{pathname} <<#{eof}"
150
207
  puts content
151
208
  puts eof
152
209
  end if files
153
210
 
154
- # Source tarballs.
211
+ # Source tarballs are downloaded, extracted, and removed. Generally,
212
+ # the directory in question is `/usr/local` but the future could hold
213
+ # anything.
155
214
  sources.sort.each do |dirname, filename|
156
215
  puts "wget http://s3.amazonaws.com/blueprint-sources/#{filename}"
157
216
  puts "tar xf #{filename} -C #{dirname}"
@@ -160,111 +219,155 @@ EOF
160
219
 
161
220
  end
162
221
 
222
+ # This is the Puppet code generator.
163
223
  def puppet
164
- disclaimer
165
- manifest = Manifest.new(@name)
166
- manifest << Exec.defaults(:path => ENV["PATH"].split(":"))
224
+ puts disclaimer
225
+ manifest = Puppet::Manifest.new(@name)
226
+
227
+ # Right out of the gate, we set a default `PATH` because Puppet does not.
228
+ manifest << Puppet::Exec.defaults(:path => ENV["PATH"].split(":"))
167
229
 
168
- # Map managers to their manager.
230
+ # We need a pre-built map of each manager to its own manager so we can
231
+ # easily declare dependencies within the Puppet manifest tree.
169
232
  managers = {}
170
233
  walk(:package => lambda { |manager, command, package, version|
171
234
  managers[package] = manager if packages[package] && manager != package
172
235
  })
173
236
 
174
- # Packages.
237
+ # Each manager's packages are grouped to create a series of subclasses
238
+ # that allow dependency management to be handled coursely using Puppet
239
+ # classes. Because of Puppet's rules about resource name uniqueness,
240
+ # we have to check that managers aren't managing themselves and bail
241
+ # early.
242
+ #
243
+ # As always, RubyGems is getting ready to get bossy so we're prepared
244
+ # by having the version of Ruby in question available.
175
245
  walk(
176
246
  :before => lambda { |manager, command|
177
247
  p = packages[manager]["_packages"]
178
248
  return if 0 == p.length || 1 == p.length && p[manager]
249
+ if "apt" == manager
250
+ manifest << Puppet::Exec.new("apt-get update",
251
+ :before => Puppet::Class["apt"])
252
+ end
179
253
  },
180
254
  :package => lambda { |manager, command, package, version|
181
255
  command =~ /gem(\d+\.\d+(?:\.\d+)?) install/
182
256
  ruby_version = $1
183
257
  case manager
184
258
 
259
+ # `apt` packages are natively supported by Puppet so they're
260
+ # very easy.
185
261
  when "apt"
186
262
  unless manager == package
187
- manifest[manager] << Package.new(package, :ensure => version)
263
+ manifest[manager] << Puppet::Package.new(package,
264
+ :ensure => version
265
+ )
188
266
  end
189
267
 
190
- # Update RubyGems.
268
+ # However, if this package happens to be RubyGems, we introduce
269
+ # a couple of new resources that update it to the latest version.
191
270
  if package =~ /^rubygems(\d+\.\d+(?:\.\d+)?)$/
192
271
  v = $1
193
272
  prereq = if "1.8" == v
194
- manifest[manager] << Package.new("rubygems-update",
195
- :require => Package["rubygems1.8"],
273
+ manifest[manager] << Puppet::Package.new("rubygems-update",
274
+ :require => Puppet::Package["rubygems1.8"],
196
275
  :provider => :gem,
197
276
  :ensure => :latest
198
277
  )
199
- Package["rubygems1.8"]
278
+ Puppet::Package["rubygems1.8"]
200
279
  else
201
- manifest[manager] << Exec.new(
280
+ manifest[manager] << Puppet::Exec.new(
202
281
  "/usr/bin/gem#{v} install --no-rdoc --no-ri rubygems-update",
203
282
  :alias => "rubygems-update-#{v}",
204
- :require => Package["rubygems#{v}"]
283
+ :require => Puppet::Package["rubygems#{v}"]
205
284
  )
206
- Exec["rubygems-update-#{v}"]
285
+ Puppet::Exec["rubygems-update-#{v}"]
207
286
  end
208
- manifest[manager] << Exec.new(
287
+ manifest[manager] << Puppet::Exec.new(
209
288
  "/usr/bin/ruby#{v} /usr/bin/update_rubygems",
210
289
  :path => %W(/usr/bin /var/lib/gems/#{v}/bin),
211
290
  :require => prereq
212
291
  )
213
292
  end
214
293
 
215
- # Only 1.8 can use the native gem package provider.
294
+ # RubyGems 1.8 is supported well enough by Puppet to include a
295
+ # native provider, which means it is almost as simple as `apt`.
216
296
  when "rubygems1.8"
217
297
  options = {
218
298
  :require => [
219
299
  Puppet::Class[managers[manager]],
220
- Package["rubygems1.8"],
300
+ Puppet::Package["rubygems1.8"],
221
301
  ],
222
302
  :provider => :gem,
223
303
  :ensure => version
224
304
  }
225
- manifest[manager] << Package.new(package, options)
305
+ manifest[manager] << Puppet::Package.new(package, options)
226
306
 
227
- # Other gems use exec resources.
307
+ # Other RubyGems versions use `exec` resources for installation
308
+ # but follow a predictable enough directory layout that we can
309
+ # avoid running the `gem` command after the first run with an
310
+ # inexpensive check to see if the directory exists.
228
311
  when /rubygems/
229
- manifest[manager] << Exec.new(
230
- sprintf("#{command}", package, version),
312
+ manifest[manager] << Puppet::Exec.new(
313
+ sprintf(command, package, version),
231
314
  :creates =>
232
315
  "/usr/lib/ruby/gems/#{ruby_version}/gems/#{package}-#{version}",
233
316
  :require => [
234
317
  Puppet::Class[managers[manager]],
235
- Package[manager],
318
+ Puppet::Package[manager],
236
319
  ]
237
320
  )
238
321
 
322
+ # Python packages follow a far less predictable directory naming
323
+ # scheme so we aren't able to optimize them like RubyGems. They
324
+ # use `exec` resources and leave it to the Python tools (probably
325
+ # `easy_install`).
239
326
  when /python/
240
- manifest[manager] << Exec.new(sprintf("#{command}", package),
327
+ manifest[manager] << Puppet::Exec.new(sprintf(command, package),
241
328
  :require => [
242
329
  Puppet::Class[managers[manager]],
243
- Package[manager, "python-setuptools"],
330
+ Puppet::Package[manager, "python-setuptools"],
244
331
  ]
245
332
  )
246
333
 
247
- # TODO Expand Java, Erlang, etc.
334
+ # It would be at this point where we would tackle Java, Erlang,
335
+ # and every other possible package manager. Slow and steady.
248
336
 
337
+ # As a last resort, we execute the command exactly as the shell
338
+ # code generator would.
249
339
  else
250
- manifest[manager] << Exec.new(sprintf("#{command}", package,
340
+ manifest[manager] << Puppet::Exec.new(sprintf(command, package,
251
341
  version), :require => [
252
342
  Puppet::Class[managers[manager]],
253
- Package[manager],
343
+ Puppet::Package[manager],
254
344
  ]
255
345
  )
256
346
  end
257
347
  }
258
348
  )
259
349
 
260
- # System files.
350
+ # System files must be placed after packages are installed so we set
351
+ # Puppet's dependencies to put all packages ahead of all files.
352
+ #
353
+ # File content is handled much like the shell version except base 64
354
+ # decoding takes place here in Ruby rather than in the `base64`(1)
355
+ # tool. Resources for all parent directories are created ahead of
356
+ # any file. Puppet's autorequire mechanism will ensure that a file's
357
+ # parent directories are realized before the file itself.
261
358
  if files && 0 < files.length
262
- manifest << Package.defaults(
359
+ manifest << Puppet::Package.defaults(
263
360
  :before => files.sort.map { |pathname, content|
264
361
  Puppet::File[pathname]
265
362
  }
266
363
  )
267
364
  files.sort.each do |pathname, content|
365
+ dirnames = File.dirname(pathname).split("/")
366
+ dirnames.shift
367
+ (0..(dirnames.length - 1)).each do |i|
368
+ manifest << Puppet::File.new("/#{dirnames[0..i].join("/")}",
369
+ :ensure => :directory)
370
+ end
268
371
  if Hash == content.class
269
372
  if content["_target"]
270
373
  manifest << Puppet::File.new(pathname,
@@ -276,18 +379,23 @@ EOF
276
379
  end
277
380
  manifest << Puppet::File.new(pathname,
278
381
  :content => content,
279
- :ensure => :present
382
+ :ensure => :file
280
383
  )
281
384
  end
282
385
  end
283
386
 
284
- # Source tarballs.
387
+ # Source tarballs are downloaded, extracted, and removed in one fell
388
+ # swoop. Puppet 2.6 introduced the requirement to wrap compound commands
389
+ # such as this in a shell invocation. This is a pretty direct Puppet
390
+ # equivalent to the shell version above.
285
391
  if sources && 0 < sources.length
286
- manifest << Package.defaults(
287
- :before => sources.sort.map { |dirname, filename| Exec[pathname] }
392
+ manifest << Puppet::Package.defaults(
393
+ :before => sources.sort.map { |dirname, filename|
394
+ Puppet::Exec[pathname]
395
+ }
288
396
  )
289
397
  sources.sort.each do |dirname, filename|
290
- manifest << Exec.new(filename,
398
+ manifest << Puppet::Exec.new(filename,
291
399
  :command => "/bin/sh -c 'wget http://s3.amazonaws.com/blueprint-sources/#{filename}; tar xf #{filename}; rm #{filename}'",
292
400
  :cwd => dirname
293
401
  )
@@ -297,57 +405,153 @@ EOF
297
405
  puts manifest
298
406
  end
299
407
 
300
- def chef
301
- puts "# No soup for you."
302
- raise NotImplementedError, "Contractor::Blueprint#chef", caller
408
+ # This is the Chef code generator. It is the first of its kind in
409
+ # creating a tarball rather than a single plaintext file. Because of
410
+ # this added output complexity, an `IO`-like object is accepted and
411
+ # returned after being used by the underlying `Chef::Cookbook`.
412
+ def chef(io=StringIO.new)
413
+ cookbook = Chef::Cookbook.new(@name, disclaimer)
414
+
415
+ # First come packages, traversed in the same order as with the shell
416
+ # code generator but passed off to the cookbook.
417
+ walk(
418
+ :before => lambda { |manager, command|
419
+ cookbook.execute "apt-get update" if "apt" == manager
420
+ },
421
+ :package => lambda { |manager, command, package, version|
422
+ return if manager == package
423
+ command =~ /gem(\d+\.\d+(?:\.\d+)?) install/
424
+ ruby_version = $1
425
+ case manager
426
+
427
+ # `apt` packages use the builtin `package` resource type with the
428
+ # standard added resources to handle upgrading an old RubyGems
429
+ # install.
430
+ when "apt"
431
+ cookbook.apt_package package, :version => version
432
+ if package =~ /^rubygems(\d+\.\d+(?:\.\d+)?)$/
433
+ v = $1
434
+ cookbook.gem_package "rubygems-update",
435
+ :gem_binary => "/usr/bin/gem#{v}"
436
+ cookbook.execute "/bin/sh -c '/usr/bin/ruby#{v
437
+ } $(PATH=\"$PATH:/var/lib/gems/#{v
438
+ }/bin\" which update_rubygems)'"
439
+ end
440
+
441
+ # Gems themselves, no matter what version of Ruby they're for,
442
+ # can be managed by the builtin `package` resource type because
443
+ # Opscode thoughtfully allows specifying the `gem` command
444
+ # to run for each resource individually.
445
+ when /rubygems(\d+\.\d+(?:\.\d+)?)/
446
+ v = $1
447
+ cookbook.gem_package package,
448
+ :gem_binary => "/usr/bin/gem#{v}",
449
+ :version => version
450
+
451
+ # Because dependencies are handled by order, not declaration,
452
+ # Python packages that need to come after `python-setuptools`
453
+ # just do because `apt` is traversed first. Everything else
454
+ # gets an `execute` resource here, too..
455
+ else
456
+ cookbook.execute sprintf(command, package, version)
457
+
458
+ end
459
+ }
460
+ )
461
+
462
+ # Files are handled by the `cookbook_file` resource type and are
463
+ # preceded by the `directory` which contains them. Just like the
464
+ # shell and Puppet generators, this code handles binary files and
465
+ # symbolic links (handled by the `link` resource type), too.
466
+ files.sort.each do |pathname, content|
467
+ cookbook.directory File.dirname(pathname),
468
+ :group => "root",
469
+ :mode => "755",
470
+ :owner => "root",
471
+ :recursive => true
472
+ if Hash == content.class
473
+ if content["_target"]
474
+ cookbook.link pathname,
475
+ :group => "root",
476
+ :owner => "root",
477
+ :to => content["_target"]
478
+ next
479
+ elsif content["_base64"]
480
+ content = content["_base64"]
481
+ end
482
+ end
483
+ cookbook.cookbook_file pathname, content,
484
+ :backup => false,
485
+ :group => "root",
486
+ :mode => "644",
487
+ :owner => "root",
488
+ :source => pathname[1..-1]
489
+ end if files
490
+
491
+ # Source tarballs are lifted directly from the shell code generator
492
+ # and wrapped in `execute` resources.
493
+ sources.sort.each do |dirname, filename|
494
+ cookbook.execute \
495
+ "wget http://s3.amazonaws.com/blueprint-sources/#{filename}"
496
+ cookbook.execute "tar xf #{filename} -C #{dirname}"
497
+ cookbook.execute "rm #{filename}"
498
+ end if sources
499
+
500
+ # With the list of resources specified, generate a tarball. The
501
+ # tarball will contain a minimal `metadata.rb`, a recipe, and any
502
+ # files that should be included. The tarball is written to the
503
+ # `IO` object passed to this method.
504
+ puts cookbook.to_gz(io)
505
+
303
506
  end
304
507
 
305
508
  # Walk a package tree and execute callbacks along the way. Callbacks
306
509
  # are listed in the options hash. These are supported:
307
- # :before => lambda { |manager, command| }
308
- # Executed before a manager's dependencies are enumerated.
309
- # :package => lambda { |manager, command, package, version| }
310
- # Executed when a package is enumerated.
311
- # :after => lambda { |manager, command| }
312
- # Executed after a manager's dependencies are enumerated.
510
+ #
511
+ # * `:before => lambda { |manager, command| }`:
512
+ # Executed before a manager's dependencies are enumerated.
513
+ # * `:package => lambda { |manager, command, package, version| }`:
514
+ # Executed when a package is enumerated.
515
+ # * `:after => lambda { |manager, command| }`:
516
+ # Executed after a manager's dependencies are enumerated.
313
517
  def walk(options={})
314
518
 
315
- # Start with packages installed with apt.
519
+ # Start with packages installed with `apt`.
316
520
  options[:manager] ||= "apt"
317
521
 
318
- # Handle managers.
522
+ # Each manager gets its chance to take action before we loop over all
523
+ # its managed packages.
319
524
  if options[:before].respond_to?(:call)
320
525
  options[:before].call options[:manager],
321
526
  packages[options[:manager]]["_command"]
322
527
  end
323
528
 
324
- # Callback for each package that depends on this manager. Note which
325
- # ones themselves are managers.
529
+ # Each package gets its chance to take action. While we're looping
530
+ # over all the packages, we must note which ones are themselves
531
+ # managers so we can recurse later. We don't start the recursion
532
+ # now because of the possibility that packages managed by this
533
+ # just-encountered manager may also depend indirectly on packages
534
+ # yet to be installed at this level.
326
535
  managers = []
327
536
  packages[options[:manager]]["_packages"].sort.each do |package, version|
328
-
329
- # Handle packages.
330
537
  if options[:package].respond_to?(:call)
331
538
  [version].flatten.each do |v|
332
539
  options[:package].call options[:manager],
333
540
  packages[options[:manager]]["_command"], package, v
334
541
  end
335
542
  end
336
-
337
543
  if options[:manager] != package && packages[package]
338
544
  managers << package
339
545
  end
340
546
  end
341
547
 
342
- # Handle managers.
548
+ # Once more, each manager gets its chance to take action.
343
549
  if options[:after].respond_to?(:call)
344
550
  options[:after].call options[:manager],
345
551
  packages[options[:manager]]["_command"]
346
552
  end
347
553
 
348
- # Recurse into each manager that was just installed. This is done
349
- # after the completed round because there could have been other
350
- # dependencies at that level aside from the manager.
554
+ # Now we can recurse into each manager that was just installed.
351
555
  managers.each do |manager|
352
556
  walk options.merge(:manager => manager)
353
557
  end
@@ -0,0 +1,166 @@
1
+ # The Chef code generator is structured much like the shell code generator
2
+ # because Chef doesn't include any sort of dependency management like
3
+ # Puppet. As expected, we'll start with packages, follow them with files,
4
+ # and finish with source tarballs.
5
+ #
6
+ # The monkeywrench is that the ultimate output is a tarball of a Chef
7
+ # cookbook rather than a single file. For this, we use the lower-level
8
+ # APIs exposed by the `archive-tar-minitar` gem to generate tarballs
9
+ # without writing intermediate files to disk.
10
+ require 'devstructure'
11
+
12
+ # Unfortunately, RubyGems rears its ugly head. The call to `gem` here is
13
+ # just because the gem's name is different than the path being `require`d.
14
+ gem 'archive-tar-minitar', :require => 'archive/tar/minitar'
15
+ require 'archive/tar/minitar'
16
+
17
+ require 'zlib'
18
+
19
+ module Chef
20
+
21
+ # A cookbook is a collection of Chef resources plus the files and other
22
+ # supporting objects needed to run it.
23
+ class Cookbook
24
+
25
+ # The name and options given to a cookbook become the filename and the
26
+ # parameters set in `metadata.rb`. The comment is a bit awkward and
27
+ # exists so the disclaimer supplied with all blueprints can be shown
28
+ # at the top of `metadata.rb`.
29
+ def initialize(name, comment, options={})
30
+ @name, @comment, @options = name, comment, options
31
+ @resources, @files = [], {}
32
+ end
33
+
34
+ # Resources should be added in the order they're required to run. That
35
+ # is how Chef manages dependencies.
36
+ def <<(resource)
37
+ @resources << resource
38
+ end
39
+
40
+ # In Chef, there are lots of top-level helpers for creating resources.
41
+ # This selection of almost-API-alikes facilitates the same thing for
42
+ # a `DevStructure::Chef::Cookbook`.
43
+ def apt_package(name, options={})
44
+ self << Chef::Resource.new(:apt_package, name, options)
45
+ end
46
+ def gem_package(name, options={})
47
+ self << Chef::Resource.new(:gem_package, name, options)
48
+ end
49
+ def execute(name, options={})
50
+ self << Chef::Resource.new(:execute, name, options)
51
+ end
52
+ def directory(name, options={})
53
+ self << Chef::Resource.new(:directory, name, options)
54
+ end
55
+ def link(name, options={})
56
+ self << Chef::Resource.new(:link, name, options)
57
+ end
58
+ def cookbook_file(name, content, options={})
59
+ self << Chef::Resource.new(:cookbook_file, name, options)
60
+ @files[name] = content
61
+ end
62
+
63
+ # This is where the magic happens. The generated Chef cookbook will
64
+ # come as a tarball, written into whatever `IO`-like object is passed
65
+ # here. The DevStructure website will pass `StringIO` while the
66
+ # command-line tools will pass `File`.
67
+ def to_gz(io)
68
+ mtime = Time.now
69
+ gz = Zlib::GzipWriter.new(io)
70
+ tar = Archive::Tar::Minitar::Writer.new(gz)
71
+ tar.mkdir @name, :mode => 0755, :mtime => mtime
72
+
73
+ # `metadata.rb` contains the typical DevStructure comment, a
74
+ # declaration of support for Ubuntu 10.04 or better, and any other
75
+ # metadata passed to the constructor of this `Cookbook`.
76
+ @options[:supports] ||= ["ubuntu", ">= 10.04"]
77
+ metadata = ([@comment] + @options.sort.collect do |key, value|
78
+ "#{key} #{[value].flatten.collect { |v| v.inspect }.join(", ")}\n"
79
+ end).join("")
80
+ tar.add_file_simple("#{@name}/metadata.rb", {
81
+ :mode => 0644,
82
+ :size => metadata.length,
83
+ :mtime => mtime
84
+ }) { |w| w.write metadata }
85
+
86
+ # Build the recipe. Each resource handles its own stringification so
87
+ # the work done here amounts to gathering up a string and placing it
88
+ # in a file in the tarball.
89
+ resources = @resources.collect { |resource| resource.to_s }.join("")
90
+ tar.mkdir "#{@name}/recipes", :mode => 0755, :mtime => mtime
91
+ tar.add_file_simple("#{@name}/recipes/default.rb", {
92
+ :mode => 0644,
93
+ :size => resources.length,
94
+ :mtime => mtime
95
+ }) { |w| w.write resources }
96
+
97
+ # Included any files referenced by `cookbook_file` resources. They
98
+ # all appear in `files/default/` as if that is the root of the
99
+ # filesystem.
100
+ if 0 < @files.length
101
+ tar.mkdir "#{@name}/files", :mode => 0755, :mtime => mtime
102
+ tar.mkdir "#{@name}/files/default", :mode => 0755, :mtime => mtime
103
+ @files.each do |name, content|
104
+ dirnames = File.dirname(name).split("/")
105
+ dirnames.shift
106
+ (0..(dirnames.length - 1)).each do |i|
107
+ tar.mkdir "#{@name}/files/default/#{dirnames[0..i].join("/")}",
108
+ :mode => 0755, :mtime => mtime
109
+ end
110
+ tar.add_file_simple("#{@name}/files/default#{name}", {
111
+ :mode => 0644,
112
+ :size => content.length,
113
+ :mtime => mtime
114
+ }) { |w| w.write content }
115
+ end
116
+ end
117
+
118
+ # Return the finalized tarball.
119
+ tar.close
120
+ gz.close
121
+ io
122
+
123
+ end
124
+
125
+ end
126
+
127
+ # This is a generic Chef resource. One would be wise to use the helpers
128
+ # available in the `Cookbook` class.
129
+ class Resource < Hash
130
+
131
+ # A Chef resource has a type, a name, and some options. The type is
132
+ # simply the name of the method that will be called in the recipe file.
133
+ # the name is the only argument to that method. The options will be
134
+ # converted to method calls and arguments within the block attached to
135
+ # each resource.
136
+ def initialize(type, name, options={})
137
+ super nil
138
+ clear
139
+ options.each { |k, v| self[k.to_s] = v }
140
+ @type, @name = type, name
141
+ end
142
+
143
+ # Stringify differently depending on the number of options so the
144
+ # output always looks like Ruby code should look. Parentheses are
145
+ # always employed here due to grammatical inconsistencies when using
146
+ # braces surrounding a block.
147
+ def to_s
148
+ case length
149
+ when 0
150
+ "#{@type}(#{@name.inspect})\n"
151
+ when 1
152
+ key, value = dup.shift
153
+ "#{@type}(#{@name.inspect}) { #{key} #{value.inspect} }\n"
154
+ else
155
+ out = ["#{@type}(#{@name.inspect}) do\n"]
156
+ out += sort.collect do |key, value|
157
+ "\t#{key} #{value.inspect}\n"
158
+ end
159
+ out << "end\n"
160
+ out.join("")
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ end
@@ -1,6 +1,21 @@
1
+ # The Puppet code generator is used by
2
+ # [`blueprint-create`(1)][blueprint-create] and
3
+ # [`blueprint-show`(1)][blueprint-show]. It operates on the
4
+ # [`blueprint`(5)][blueprint] JSON format generated by
5
+ # [`sandbox-blueprint`(1)][sandbox-blueprint].
6
+ #
7
+ # A manifest contains a collection of zero or more child manifests,
8
+ # which are eventually namespaced beneath their parent, and zero or
9
+ # more child resources, which are listed in no particular order.
10
+ #
11
+ # [blueprint-create]: http://devstructure.github.com/contractor/blueprint-create.1.html
12
+ # [blueprint-show]: http://devstructure.github.com/contractor/blueprint-show.1.html
13
+ # [blueprint]: http://devstructure.github.com/contractor/blueprint.5.html
14
+ # [sandbox-blueprint]: http://devstructure.github.com/contractor/sandbox-blueprint.1.html
1
15
  require 'devstructure'
2
16
 
3
- # Make symbols Comparable.
17
+ # Make `Symbol`s `Comparable` so we can sort each list of resource
18
+ # attributes before converting to a string.
4
19
  class Symbol
5
20
  def <=>(other)
6
21
  to_s <=> other.to_s
@@ -10,15 +25,21 @@ end
10
25
  module DevStructure::Puppet
11
26
 
12
27
  # A Puppet manifest that contains a tree of classes that each contain
13
- # some resources.
28
+ # some resources. Manifests are valid targets of dependencies and we
29
+ # use them heavily in the generated code to keep the inhumane-ness to
30
+ # a minimum. A Manifest object generates a Puppet `class`.
14
31
  class Manifest
15
32
 
33
+ # Each class must have a name and might have a parent. If a manifest
34
+ # has a parent, this signals it to `include` itself in the parent.
16
35
  def initialize(name, parent=nil)
17
36
  @name, @parent = name, parent
18
37
  @manifests, @resources = {}, {}
19
38
  end
20
39
 
21
- # Return a reference to a sub-manifest of the given name.
40
+ # Manifests behave a bit like hashes in that their children can be
41
+ # traversed. Note the children can't be assigned directly because
42
+ # we must maintain parent-child relationships.
22
43
  def [](name)
23
44
  @manifests[name.to_s] ||= self.class.new(name.to_s, @name)
24
45
  end
@@ -26,28 +47,36 @@ module DevStructure::Puppet
26
47
  self[symbol]
27
48
  end
28
49
 
29
- # Add a resource to this manifest.
50
+ # Add a resource to this manifest. Order is never important in Puppet
51
+ # since all dependencies must be declared.
30
52
  def <<(resource)
31
- @resources[resource.type] ||= []
32
- @resources[resource.type] << resource
53
+ @resources[resource.type] ||= {}
54
+ @resources[resource.type][resource.name] = resource
33
55
  end
34
56
 
57
+ # Turn this manifest into a Puppet class. We start with a base level
58
+ # of indentation that we carry through our resources and manifests.
59
+ # Order is again not important so we don't make much effort. The
60
+ # Puppet grammar prohibits dots in class names so we replace `.` with
61
+ # `--`. Resources are grouped by type to reduce the total number of
62
+ # lines required. If this manifest has a parent, the last thing we
63
+ # do is include ourselves in the parent.
35
64
  def to_s(tab="")
36
65
  out = []
37
66
  out << "#{tab}class #{@name.gsub(".", "--")} {"
38
67
  @manifests.each_value do |manifest|
39
68
  out << manifest.to_s("#{tab}\t")
40
69
  end
41
- @resources.each do |type, resources|
70
+ @resources.sort.each do |type, resources|
42
71
  if 1 < resources.length
43
72
  out << "#{tab}\t#{type} {"
44
- resources.each do |resource|
73
+ resources.sort.each do |name, resource|
45
74
  resource.style = :partial
46
75
  out << resource.to_s("#{tab}\t")
47
76
  end
48
77
  out << "#{tab}\t}"
49
78
  else
50
- out << resources.first.to_s("#{tab}\t")
79
+ out << resources.values.first.to_s("#{tab}\t")
51
80
  end
52
81
  end
53
82
  out << "#{tab}}"
@@ -57,13 +86,15 @@ module DevStructure::Puppet
57
86
 
58
87
  end
59
88
 
60
- # A generic Puppet resource to be subclassed. The name of the class
61
- # dictates how the resource will be printed so do not instantiate this
62
- # class directly.
89
+ # A Puppet resource is basically a named hash. The name is unique
90
+ # the Puppet catalog (which may contain any number of manifests in
91
+ # any number of modules). The attributes that are expected vary
92
+ # by the resource's actual type. This implementation uses the class
93
+ # name to determine the type, so do not instantiate `Resource`
94
+ # directly.
63
95
  class Resource < Hash
64
96
  attr_accessor :type, :name, :style
65
97
 
66
- # A resource.
67
98
  def initialize(name, options={})
68
99
  super nil
69
100
  clear
@@ -73,9 +104,10 @@ module DevStructure::Puppet
73
104
  @style = :complete
74
105
  end
75
106
 
76
- # A resource with only a name, typically being listed as a dependency.
77
- # This method enables syntax that looks a lot like the Puppet code that
78
- # will be generated.
107
+ # Resources that are just being listed as dependencies don't need to
108
+ # have all their attributes. This method exists as syntactic sugar
109
+ # for declaring dependencies because it results in something that
110
+ # looks quite a bit like the Puppet language's resource references.
79
111
  def self.[](*args)
80
112
  if 1 == args.length
81
113
  self.new args[0]
@@ -84,7 +116,9 @@ module DevStructure::Puppet
84
116
  end
85
117
  end
86
118
 
87
- # A set of defaults for a resource type.
119
+ # In the Puppet language, a capitalized resource type can be used to
120
+ # provide default values for a type. `DevStructure::Blueprint` sets
121
+ # defaults to provide some inherent order to the Puppet run.
88
122
  def self.defaults(options)
89
123
  resource = self.new(nil, options)
90
124
  resource.style = :defaults
@@ -93,7 +127,7 @@ module DevStructure::Puppet
93
127
 
94
128
  # Return Puppet code for this resource. Whether a complete, partial,
95
129
  # or defaults representation is returned depends on which class method
96
- # instantiated this resource but can be overridden by passing a Symbol.
130
+ # instantiated this resource but can be overridden by passing a `Symbol`.
97
131
  def to_s(tab="")
98
132
  out = []
99
133
 
@@ -113,16 +147,19 @@ module DevStructure::Puppet
113
147
  raise ArgumentError
114
148
  end
115
149
 
116
- # Handle the options Hash.
150
+ # Handle a non-empty set of attributes in alphabetical order and
151
+ # in compliance with the Puppet coding standards. This is a bit
152
+ # conservative and so uses more vertical space than absolutely
153
+ # necessary but it also won't get obnoxiously long.
117
154
  if 0 < length
118
155
 
119
- # Note the longest option name so we can line up => operators as
156
+ # Note the longest option name so we can line up `=>` operators as
120
157
  # per the Puppet coding standards.
121
158
  l = collect { |k, v| k.to_s.length }.max
122
159
 
123
- # Map options to Puppet code strings. Symbols become unquoted
124
- # strings, nils become undefs, and Arrays are flattened. Unless
125
- # the value is a Symbol, #inspect is called on the value.
160
+ # Map options to Puppet code strings. `Symbol`s become unquoted
161
+ # strings, `nil`s become `undef`s, and arrays are flattened.
162
+ # Unless the value is a `Symbol`, #inspect is called on the value.
126
163
  out += sort.collect do |k, v|
127
164
  k = "#{k.to_s}#{" " * (l - k.to_s.length)}"
128
165
  v = if v.respond_to?(:flatten)
@@ -139,7 +176,7 @@ module DevStructure::Puppet
139
176
  "\t#{tab_params}#{k} => #{v},"
140
177
  end
141
178
 
142
- # Close this resource.
179
+ # Close this resource to match the opening.
143
180
  case @style
144
181
  when :complete
145
182
  out << "#{tab}}"
@@ -149,8 +186,8 @@ module DevStructure::Puppet
149
186
  out << "#{tab}}"
150
187
  end
151
188
 
152
- # Don't bother with an empty options Hash. Go ahead and close this
153
- # resource.
189
+ # Don't bother with an empty options hash. Go ahead and close this
190
+ # resource as inconspicuously as possible.
154
191
  else
155
192
  case @style
156
193
  when :complete
@@ -165,8 +202,8 @@ module DevStructure::Puppet
165
202
  out.join "\n"
166
203
  end
167
204
 
168
- # Resources themselves appear in options as dependencies so they are
169
- # represented in Puppet's reference notation.
205
+ # Resources themselves can appear in attributes as dependencies. Their
206
+ # inspected value matches the Puppet resource reference syntax.
170
207
  def inspect
171
208
  "#{@type.capitalize}[\"#{@name}\"]"
172
209
  end
@@ -176,15 +213,16 @@ module DevStructure::Puppet
176
213
 
177
214
  end
178
215
 
179
- # Some actual resource types.
180
- class Package < Resource
181
- end
182
- class Exec < Resource
183
- end
184
- class File < Resource
185
- end
216
+ # As mentioned earlier, we use the class name to determine the resource
217
+ # type. The most common types are `Package`, `Exec`, and `File`.
218
+ class Package < Resource; end
219
+ class Exec < Resource; end
220
+ class File < Resource; end
186
221
 
187
- # A class resource type for dependencies.
222
+ # `Class` is also a useful resource type but needs a little more help
223
+ # because of the stricter rules governing class names in the Puppet
224
+ # grammar. The most common problem is a dot in a package name (like
225
+ # `ruby1.8`, which we replace with `--` in the generated code).
188
226
  class Class < Resource
189
227
  def inspect
190
228
  "#{@type.capitalize}[\"#{@name.gsub(".", "--")}\"]"
data/lib/devstructure.rb CHANGED
@@ -1,2 +1,20 @@
1
+ # The Ruby bindings to the DevStructure API actually include a bit more
2
+ # than that. The following independent modules are available - this
3
+ # serves as nothing more than the root of a namespace.
4
+ #
5
+ # * [`DevStructure::API`](devstructure/api.html):
6
+ # The actual API client as a subclass of `Net::HTTP`.
7
+ # * [`DevStructure::Blueprint`](devstructure/blueprint.html):
8
+ # A Ruby class describing a blueprint.
9
+ # * [`DevStructure::Puppet`](devstructure/puppet.html):
10
+ # A Puppet code generator.
11
+ # * [`DevStructure::Chef`](devstructure/chef.html):
12
+ # A Chef code generator.
13
+ #
14
+ # Further reading:
15
+ #
16
+ # * [DevStructure API documentation](http://docs.devstructure.com/api)
17
+ # * [`ruby-devstructure`(7)](ruby-devstructure.7.html)
18
+ # * [Source code](http://github.com/devstructure/ruby-devstructure)
1
19
  module DevStructure
2
20
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devstructure
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 8
10
- version: 0.1.8
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Richard Crowley
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-06 00:00:00 +00:00
18
+ date: 2010-08-24 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -34,6 +34,34 @@ dependencies:
34
34
  version: 1.4.2
35
35
  type: :runtime
36
36
  version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: ronn
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: rocco
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :development
64
+ version_requirements: *id003
37
65
  description: Ruby bindings to the DevStructure API
38
66
  email: richard@devstructure.com
39
67
  executables: []
@@ -47,6 +75,7 @@ files:
47
75
  - lib/devstructure/api.rb
48
76
  - lib/devstructure/blueprint.rb
49
77
  - lib/devstructure/puppet.rb
78
+ - lib/devstructure/chef.rb
50
79
  has_rdoc: true
51
80
  homepage: http://github.com/devstructure/ruby-devstructure
52
81
  licenses: []