devstructure 0.1.8 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/devstructure/api.rb +37 -2
- data/lib/devstructure/blueprint.rb +283 -79
- data/lib/devstructure/chef.rb +166 -0
- data/lib/devstructure/puppet.rb +74 -36
- data/lib/devstructure.rb +18 -0
- metadata +34 -5
data/lib/devstructure/api.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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,
|
263
|
+
manifest[manager] << Puppet::Package.new(package,
|
264
|
+
:ensure => version
|
265
|
+
)
|
188
266
|
end
|
189
267
|
|
190
|
-
#
|
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
|
-
#
|
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
|
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(
|
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(
|
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
|
-
#
|
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(
|
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 => :
|
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|
|
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
|
-
|
301
|
-
|
302
|
-
|
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
|
-
#
|
308
|
-
#
|
309
|
-
#
|
310
|
-
#
|
311
|
-
#
|
312
|
-
#
|
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
|
-
#
|
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
|
-
#
|
325
|
-
# ones
|
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
|
-
#
|
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
|
-
#
|
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
|
data/lib/devstructure/puppet.rb
CHANGED
@@ -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
|
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
|
-
#
|
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]
|
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
|
61
|
-
#
|
62
|
-
#
|
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
|
-
#
|
77
|
-
#
|
78
|
-
#
|
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
|
-
#
|
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
|
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
|
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.
|
124
|
-
# strings,
|
125
|
-
# the value is a Symbol
|
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
|
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
|
169
|
-
#
|
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
|
-
#
|
180
|
-
|
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
|
-
#
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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-
|
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: []
|