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.
- 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: []
|