devstructure 0.1.8 → 0.2.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.
@@ -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: []