cfoundry 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +30 -0
- data/Rakefile +20 -0
- data/lib/cfoundry.rb +2 -0
- data/lib/cfoundry/app.rb +369 -0
- data/lib/cfoundry/client.rb +97 -0
- data/lib/cfoundry/errors.rb +77 -0
- data/lib/cfoundry/restclient.rb +259 -0
- data/lib/cfoundry/service.rb +59 -0
- data/lib/cfoundry/user.rb +47 -0
- data/lib/cfoundry/version.rb +3 -0
- data/lib/cfoundry/zip.rb +43 -0
- data/spec/Rakefile +14 -0
- data/spec/client_spec.rb +206 -0
- data/spec/helpers.rb +29 -0
- metadata +117 -0
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Copyright (c)2012, Alex Suraci
|
2
|
+
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
* Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
* Redistributions in binary form must reproduce the above
|
12
|
+
copyright notice, this list of conditions and the following
|
13
|
+
disclaimer in the documentation and/or other materials provided
|
14
|
+
with the distribution.
|
15
|
+
|
16
|
+
* Neither the name of Alex Suraci nor the names of other
|
17
|
+
contributors may be used to endorse or promote products derived
|
18
|
+
from this software without specific prior written permission.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
21
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
22
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
23
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
24
|
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
25
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
26
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
27
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
28
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
29
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
task :default => "spec"
|
5
|
+
|
6
|
+
desc "Run specs"
|
7
|
+
task "spec" => ["bundler:install", "test:spec"]
|
8
|
+
|
9
|
+
namespace "bundler" do
|
10
|
+
desc "Install gems"
|
11
|
+
task "install" do
|
12
|
+
sh("bundle install")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
namespace "test" do
|
17
|
+
task "spec" do |t|
|
18
|
+
sh("cd spec && bundle exec rake spec")
|
19
|
+
end
|
20
|
+
end
|
data/lib/cfoundry.rb
ADDED
data/lib/cfoundry/app.rb
ADDED
@@ -0,0 +1,369 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "digest/sha1"
|
3
|
+
require "pathname"
|
4
|
+
require "tmpdir"
|
5
|
+
|
6
|
+
require "cfoundry/zip"
|
7
|
+
|
8
|
+
module CFoundry
|
9
|
+
class App
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
def initialize(name, client, manifest = nil)
|
13
|
+
@name = name
|
14
|
+
@client = client
|
15
|
+
@manifest = manifest
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"#<App '#@name'>"
|
20
|
+
end
|
21
|
+
|
22
|
+
def manifest
|
23
|
+
@manifest ||= @client.rest.app(@name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete!
|
27
|
+
@client.rest.delete_app(@name)
|
28
|
+
|
29
|
+
if @manifest
|
30
|
+
@manifest.delete "meta"
|
31
|
+
@manifest.delete "version"
|
32
|
+
@manifest.delete "state"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create!
|
37
|
+
@client.rest.create_app(@manifest.merge("name" => @name))
|
38
|
+
@manifest = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def exists?
|
42
|
+
@client.rest.app(@name)
|
43
|
+
true
|
44
|
+
rescue CFoundry::NotFound
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def instances
|
49
|
+
@client.rest.instances(@name).collect do |m|
|
50
|
+
Instance.new(@name, m["index"], @client, m)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def stats
|
55
|
+
@client.rest.stats(@name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def update!(what = {})
|
59
|
+
# TODO: hacky; can we not just set in meta field?
|
60
|
+
# we write to manifest["debug"] but read from manifest["meta"]["debug"]
|
61
|
+
what[:debug] = debug_mode
|
62
|
+
|
63
|
+
@client.rest.update_app(@name, manifest.merge(what))
|
64
|
+
@manifest = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop!
|
68
|
+
update! "state" => "STOPPED"
|
69
|
+
end
|
70
|
+
|
71
|
+
def start!
|
72
|
+
update! "state" => "STARTED"
|
73
|
+
end
|
74
|
+
|
75
|
+
def restart!
|
76
|
+
stop!
|
77
|
+
start!
|
78
|
+
end
|
79
|
+
|
80
|
+
def health
|
81
|
+
s = state
|
82
|
+
if s == "STARTED"
|
83
|
+
healthy_count = manifest["runningInstances"]
|
84
|
+
expected = manifest["instances"]
|
85
|
+
if healthy_count && expected > 0
|
86
|
+
ratio = healthy_count / expected.to_f
|
87
|
+
if ratio == 1.0
|
88
|
+
"RUNNING"
|
89
|
+
else
|
90
|
+
"#{(ratio * 100).to_i}%"
|
91
|
+
end
|
92
|
+
else
|
93
|
+
"N/A"
|
94
|
+
end
|
95
|
+
else
|
96
|
+
s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def healthy?
|
101
|
+
# invalidate cache so the check is fresh
|
102
|
+
@manifest = nil
|
103
|
+
health == "RUNNING"
|
104
|
+
end
|
105
|
+
|
106
|
+
def stopped?
|
107
|
+
state == "STOPPED"
|
108
|
+
end
|
109
|
+
|
110
|
+
def started?
|
111
|
+
state == "STARTED"
|
112
|
+
end
|
113
|
+
alias_method :running?, :started?
|
114
|
+
|
115
|
+
{ :total_instances => "instances",
|
116
|
+
:state => "state",
|
117
|
+
:status => "state",
|
118
|
+
:services => "services",
|
119
|
+
:uris => "uris",
|
120
|
+
:urls => "uris",
|
121
|
+
:env => "env"
|
122
|
+
}.each do |meth, attr|
|
123
|
+
define_method(meth) do
|
124
|
+
manifest[attr]
|
125
|
+
end
|
126
|
+
|
127
|
+
define_method(:"#{meth}=") do |v|
|
128
|
+
@manifest ||= {}
|
129
|
+
@manifest[attr] = v
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def framework
|
134
|
+
manifest["staging"]["framework"] ||
|
135
|
+
manifest["staging"]["model"]
|
136
|
+
end
|
137
|
+
|
138
|
+
def framework=(v)
|
139
|
+
@manifest ||= {}
|
140
|
+
@manifest["staging"] ||= {}
|
141
|
+
|
142
|
+
if @manifest["staging"].key? "model"
|
143
|
+
@manifest["staging"]["model"] = v
|
144
|
+
else
|
145
|
+
@manifest["staging"]["framework"] = v
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def runtime
|
150
|
+
manifest["staging"]["runtime"] ||
|
151
|
+
manifest["staging"]["stack"]
|
152
|
+
end
|
153
|
+
|
154
|
+
def runtime=(v)
|
155
|
+
@manifest ||= {}
|
156
|
+
@manifest["staging"] ||= {}
|
157
|
+
|
158
|
+
if @manifest["staging"].key? "stack"
|
159
|
+
@manifest["staging"]["stack"] = v
|
160
|
+
else
|
161
|
+
@manifest["staging"]["runtime"] = v
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def memory
|
166
|
+
manifest["resources"]["memory"]
|
167
|
+
end
|
168
|
+
|
169
|
+
def memory=(v)
|
170
|
+
@manifest ||= {}
|
171
|
+
@manifest["resources"] ||= {}
|
172
|
+
@manifest["resources"]["memory"] = v
|
173
|
+
end
|
174
|
+
|
175
|
+
def debug_mode
|
176
|
+
manifest.fetch("debug") { manifest["meta"] && manifest["meta"]["debug"] }
|
177
|
+
end
|
178
|
+
|
179
|
+
def debug_mode=(v)
|
180
|
+
@manifest ||= {}
|
181
|
+
@manifest["debug"] = v
|
182
|
+
end
|
183
|
+
|
184
|
+
def bind(*service_names)
|
185
|
+
update!("services" => services + service_names)
|
186
|
+
end
|
187
|
+
|
188
|
+
def unbind(*service_names)
|
189
|
+
update!("services" =>
|
190
|
+
services.reject { |s|
|
191
|
+
service_names.include?(s)
|
192
|
+
})
|
193
|
+
end
|
194
|
+
|
195
|
+
def files(*path)
|
196
|
+
Instance.new(@name, 0, @client).files(*path)
|
197
|
+
end
|
198
|
+
|
199
|
+
def file(*path)
|
200
|
+
Instance.new(@name, 0, @client).file(*path)
|
201
|
+
end
|
202
|
+
|
203
|
+
UPLOAD_EXCLUDE = %w{.git _darcs .svn}
|
204
|
+
|
205
|
+
def upload(path, check_resources = true)
|
206
|
+
unless File.exist? path
|
207
|
+
raise "invalid application path '#{path}'"
|
208
|
+
end
|
209
|
+
|
210
|
+
zipfile = "#{Dir.tmpdir}/#{@name}.zip"
|
211
|
+
tmpdir = "#{Dir.tmpdir}/.vmc_#{@name}_files"
|
212
|
+
|
213
|
+
FileUtils.rm_f(zipfile)
|
214
|
+
FileUtils.rm_rf(tmpdir)
|
215
|
+
|
216
|
+
prepare_package(path, tmpdir)
|
217
|
+
|
218
|
+
resources = determine_resources(tmpdir) if check_resources
|
219
|
+
|
220
|
+
CFoundry::Zip.pack(tmpdir, zipfile)
|
221
|
+
|
222
|
+
@client.rest.upload_app(@name, zipfile, resources || [])
|
223
|
+
ensure
|
224
|
+
FileUtils.rm_f(zipfile) if zipfile
|
225
|
+
FileUtils.rm_rf(tmpdir) if tmpdir
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
def prepare_package(path, to)
|
231
|
+
if path =~ /\.(war|zip)$/
|
232
|
+
CFoundry::Zip.unpack(path, to)
|
233
|
+
elsif war_file = Dir.glob("#{path}/*.war").first
|
234
|
+
CFoundry::Zip.unpack(war_file, to)
|
235
|
+
else
|
236
|
+
check_unreachable_links(path)
|
237
|
+
|
238
|
+
FileUtils.mkdir(to)
|
239
|
+
|
240
|
+
files = Dir.glob("#{path}/{*,.[^\.]*}")
|
241
|
+
|
242
|
+
UPLOAD_EXCLUDE.each do |e|
|
243
|
+
files.delete e
|
244
|
+
end
|
245
|
+
|
246
|
+
FileUtils.cp_r(files, to)
|
247
|
+
|
248
|
+
find_sockets(to).each do |s|
|
249
|
+
File.delete s
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
RESOURCE_CHECK_LIMIT = 64 * 1024
|
255
|
+
|
256
|
+
def determine_resources(path)
|
257
|
+
fingerprints = []
|
258
|
+
total_size = 0
|
259
|
+
|
260
|
+
Dir.glob("#{path}/**/*", File::FNM_DOTMATCH) do |filename|
|
261
|
+
next if File.directory?(filename)
|
262
|
+
|
263
|
+
size = File.size(filename)
|
264
|
+
|
265
|
+
total_size += size
|
266
|
+
|
267
|
+
fingerprints << {
|
268
|
+
:size => size,
|
269
|
+
:sha1 => Digest::SHA1.file(filename).hexdigest,
|
270
|
+
:fn => filename
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
return if total_size <= RESOURCE_CHECK_LIMIT
|
275
|
+
|
276
|
+
resources = @client.rest.check_resources(fingerprints)
|
277
|
+
|
278
|
+
resources.each do |resource|
|
279
|
+
FileUtils.rm_f resource["fn"]
|
280
|
+
resource["fn"].sub!("#{path}/", '')
|
281
|
+
end
|
282
|
+
|
283
|
+
resources
|
284
|
+
end
|
285
|
+
|
286
|
+
def check_unreachable_links(path)
|
287
|
+
files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
|
288
|
+
|
289
|
+
# only used for friendlier error message
|
290
|
+
pwd = Pathname.pwd
|
291
|
+
|
292
|
+
abspath = File.expand_path(path)
|
293
|
+
unreachable = []
|
294
|
+
files.each do |f|
|
295
|
+
file = Pathname.new(f)
|
296
|
+
if file.symlink? && !file.realpath.to_s.start_with?(abspath)
|
297
|
+
unreachable << file.relative_path_from(pwd)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
unless unreachable.empty?
|
302
|
+
root = Pathname.new(path).relative_path_from(pwd)
|
303
|
+
raise "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def find_sockets(path)
|
308
|
+
files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
|
309
|
+
files && files.select { |f| File.socket? f }
|
310
|
+
end
|
311
|
+
|
312
|
+
class Instance
|
313
|
+
attr_reader :app, :index, :manifest
|
314
|
+
|
315
|
+
def initialize(appname, index, client, manifest = {})
|
316
|
+
@app = appname
|
317
|
+
@index = index
|
318
|
+
@client = client
|
319
|
+
@manifest = manifest
|
320
|
+
end
|
321
|
+
|
322
|
+
def inspect
|
323
|
+
"#<App::Instance '#@app' \##@index>"
|
324
|
+
end
|
325
|
+
|
326
|
+
def state
|
327
|
+
@manifest["state"]
|
328
|
+
end
|
329
|
+
alias_method :status, :state
|
330
|
+
|
331
|
+
def since
|
332
|
+
Time.at(@manifest["since"])
|
333
|
+
end
|
334
|
+
|
335
|
+
def debugger
|
336
|
+
return unless @manifest["debug_ip"] and @manifest["debug_port"]
|
337
|
+
{ "ip" => @manifest["debug_ip"],
|
338
|
+
"port" => @manifest["debug_port"]
|
339
|
+
}
|
340
|
+
end
|
341
|
+
|
342
|
+
def console
|
343
|
+
return unless @manifest["console_ip"] and @manifest["console_port"]
|
344
|
+
{ "ip" => @manifest["console_ip"],
|
345
|
+
"port" => @manifest["console_port"]
|
346
|
+
}
|
347
|
+
end
|
348
|
+
|
349
|
+
def healthy?
|
350
|
+
case state
|
351
|
+
when "STARTING", "RUNNING"
|
352
|
+
true
|
353
|
+
when "DOWN", "FLAPPING"
|
354
|
+
false
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def files(*path)
|
359
|
+
@client.rest.files(@app, @index, *path).split("\n").collect do |entry|
|
360
|
+
path + [entry.split(/\s+/, 2)[0]]
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def file(*path)
|
365
|
+
@client.rest.files(@app, @index, *path)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "cfoundry/restclient"
|
2
|
+
require "cfoundry/app"
|
3
|
+
require "cfoundry/service"
|
4
|
+
require "cfoundry/user"
|
5
|
+
|
6
|
+
|
7
|
+
module CFoundry
|
8
|
+
class Client
|
9
|
+
attr_reader :rest
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@rest = RESTClient.new(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def target
|
16
|
+
@rest.target
|
17
|
+
end
|
18
|
+
|
19
|
+
def proxy
|
20
|
+
@rest.proxy
|
21
|
+
end
|
22
|
+
|
23
|
+
def proxy=(x)
|
24
|
+
@rest.proxy = x
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Cloud metadata
|
29
|
+
def info
|
30
|
+
@rest.info
|
31
|
+
end
|
32
|
+
|
33
|
+
def system_services
|
34
|
+
@rest.system_services
|
35
|
+
end
|
36
|
+
|
37
|
+
def system_runtimes
|
38
|
+
@rest.system_runtimes
|
39
|
+
end
|
40
|
+
|
41
|
+
# Users
|
42
|
+
def users
|
43
|
+
@rest.users.collect do |json|
|
44
|
+
CFoundry::User.new(
|
45
|
+
json["email"],
|
46
|
+
self,
|
47
|
+
{ "email" => json["email"],
|
48
|
+
"admin" => json["admin"] })
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def user(email)
|
53
|
+
CFoundry::User.new(email, self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def register(email, password)
|
57
|
+
@rest.create_user(:email => email, :password => password)
|
58
|
+
user(email)
|
59
|
+
end
|
60
|
+
|
61
|
+
def login(email, password)
|
62
|
+
@rest.token =
|
63
|
+
@rest.create_token({ :password => password }, email)["token"]
|
64
|
+
end
|
65
|
+
|
66
|
+
def logout
|
67
|
+
@rest.token = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def logged_in?
|
71
|
+
!!@rest.token
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Applications
|
76
|
+
def apps
|
77
|
+
@rest.apps.collect do |json|
|
78
|
+
CFoundry::App.new(json["name"], self, json)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def app(name)
|
83
|
+
CFoundry::App.new(name, self)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Services
|
87
|
+
def services
|
88
|
+
@rest.services.collect do |json|
|
89
|
+
CFoundry::Service.new(json["name"], self, json)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def service(name)
|
94
|
+
CFoundry::Service.new(name, self)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module CFoundry
|
2
|
+
class APIError < RuntimeError
|
3
|
+
class << self
|
4
|
+
attr_reader :error_code, :description
|
5
|
+
|
6
|
+
def setup(code, description = nil)
|
7
|
+
@error_code = code
|
8
|
+
@description = description
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(error_code = nil, description = nil)
|
13
|
+
@error_code = error_code
|
14
|
+
@description = description
|
15
|
+
end
|
16
|
+
|
17
|
+
def error_code
|
18
|
+
@error_code || self.class.error_code
|
19
|
+
end
|
20
|
+
|
21
|
+
def description
|
22
|
+
@description || self.class.description
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
if error_code
|
27
|
+
"#{error_code}: #{description}"
|
28
|
+
else
|
29
|
+
description
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NotFound < APIError
|
35
|
+
setup(404, "entity not found or inaccessible")
|
36
|
+
end
|
37
|
+
|
38
|
+
class TargetRefused < APIError
|
39
|
+
@description = "target refused connection"
|
40
|
+
|
41
|
+
attr_reader :message
|
42
|
+
|
43
|
+
def initialize(message)
|
44
|
+
@message = message
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{description} (#{@message})"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class UploadFailed < APIError
|
53
|
+
setup(402)
|
54
|
+
end
|
55
|
+
|
56
|
+
class Denied < APIError
|
57
|
+
attr_reader :error_code, :description
|
58
|
+
|
59
|
+
def initialize(
|
60
|
+
error_code = 200,
|
61
|
+
description = "Operation not permitted")
|
62
|
+
@error_code = error_code
|
63
|
+
@description = description
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class BadResponse < StandardError
|
68
|
+
def initialize(code, body = nil)
|
69
|
+
@code = code
|
70
|
+
@body = body
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
"target failed to handle our request due to an internal error (#{@code})"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
require "restclient"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
require "cfoundry/errors"
|
5
|
+
|
6
|
+
|
7
|
+
module CFoundry
|
8
|
+
class RESTClient
|
9
|
+
attr_accessor :target, :token, :proxy
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
target = "http://api.cloudfoundry.com",
|
13
|
+
token = nil)
|
14
|
+
@target = target
|
15
|
+
@token = token
|
16
|
+
end
|
17
|
+
|
18
|
+
# Cloud metadata
|
19
|
+
def info
|
20
|
+
json_get("info")
|
21
|
+
end
|
22
|
+
|
23
|
+
def system_services
|
24
|
+
json_get("info", "services")
|
25
|
+
end
|
26
|
+
|
27
|
+
def system_runtimes
|
28
|
+
json_get("info", "runtimes")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Users
|
32
|
+
def users
|
33
|
+
json_get("users")
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_user(payload)
|
37
|
+
post(payload.to_json, "users")
|
38
|
+
end
|
39
|
+
|
40
|
+
def user(email)
|
41
|
+
json_get("users", email)
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_user(email)
|
45
|
+
delete("users", email)
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_user(email, payload)
|
50
|
+
put(payload.to_json, "users", email)
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_token(payload, email)
|
54
|
+
json_post(payload.to_json, "users", email, "tokens")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Applications
|
58
|
+
def apps
|
59
|
+
json_get("apps")
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_app(payload)
|
63
|
+
json_post(payload.to_json, "apps")
|
64
|
+
end
|
65
|
+
|
66
|
+
def app(name)
|
67
|
+
json_get("apps", name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def instances(name)
|
71
|
+
json_get("apps", name, "instances")["instances"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def files(name, instance, *path)
|
75
|
+
get("apps", name, "instances", instance, "files", *path)
|
76
|
+
end
|
77
|
+
alias :file :files
|
78
|
+
|
79
|
+
def update_app(name, payload)
|
80
|
+
put(payload.to_json, "apps", name)
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete_app(name)
|
84
|
+
delete("apps", name)
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def stats(name)
|
89
|
+
json_get("apps", name, "stats")
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_resources(fingerprints)
|
93
|
+
json_post(fingerprints.to_json, "resources")
|
94
|
+
end
|
95
|
+
|
96
|
+
def upload_app(name, zipfile, resources = [])
|
97
|
+
payload = {
|
98
|
+
:_method => "put",
|
99
|
+
:resources => resources.to_json,
|
100
|
+
:application =>
|
101
|
+
if zipfile.is_a? File
|
102
|
+
zipfile
|
103
|
+
else
|
104
|
+
File.new(zipfile, "rb")
|
105
|
+
end
|
106
|
+
}
|
107
|
+
post(payload, "apps", name, "application")
|
108
|
+
rescue RestClient::ServerBrokeConnection
|
109
|
+
retry
|
110
|
+
end
|
111
|
+
|
112
|
+
# Services
|
113
|
+
def services
|
114
|
+
json_get("services")
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_service(manifest)
|
118
|
+
json_post(manifest.to_json, "services")
|
119
|
+
end
|
120
|
+
|
121
|
+
def service(name)
|
122
|
+
json_get("services", name)
|
123
|
+
end
|
124
|
+
|
125
|
+
def delete_service(name)
|
126
|
+
delete("services", name)
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def request(type, segments, options = {})
|
132
|
+
headers = {}
|
133
|
+
headers["AUTHORIZATION"] = @token if @token
|
134
|
+
headers["PROXY-USER"] = @proxy if @proxy
|
135
|
+
headers["Content-Type"] = "application/json" # TODO: probably not always
|
136
|
+
# and set Accept
|
137
|
+
headers["Content-Length"] =
|
138
|
+
options[:payload] ? options[:payload].size : 0
|
139
|
+
|
140
|
+
req = options.dup
|
141
|
+
req[:method] = type
|
142
|
+
req[:url] = url(segments)
|
143
|
+
req[:headers] = headers.merge(req[:headers] || {})
|
144
|
+
|
145
|
+
json = req.delete :json
|
146
|
+
|
147
|
+
trace = false
|
148
|
+
RestClient::Request.execute(req) do |response, request|
|
149
|
+
if trace
|
150
|
+
puts '>>>'
|
151
|
+
puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
|
152
|
+
puts "REQUEST: #{req[:method]} #{req[:url]}"
|
153
|
+
puts "RESPONSE_HEADERS:"
|
154
|
+
response.headers.each do |key, value|
|
155
|
+
puts " #{key} : #{value}"
|
156
|
+
end
|
157
|
+
puts "REQUEST_HEADERS:"
|
158
|
+
request.headers.each do |key, value|
|
159
|
+
puts " #{key} : #{value}"
|
160
|
+
end
|
161
|
+
puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
|
162
|
+
puts "RESPONSE: [#{response.code}]"
|
163
|
+
begin
|
164
|
+
puts JSON.pretty_generate(JSON.parse(response.body))
|
165
|
+
rescue
|
166
|
+
puts "#{response.body}"
|
167
|
+
end
|
168
|
+
puts '<<<'
|
169
|
+
end
|
170
|
+
|
171
|
+
case response.code
|
172
|
+
when 200, 204, 302
|
173
|
+
if json
|
174
|
+
if response.code == 204
|
175
|
+
raise "Expected JSON response, got 204 No Content"
|
176
|
+
end
|
177
|
+
|
178
|
+
JSON.parse response
|
179
|
+
else
|
180
|
+
response
|
181
|
+
end
|
182
|
+
|
183
|
+
# TODO: figure out how/when the CC distinguishes these
|
184
|
+
when 400, 403
|
185
|
+
info = JSON.parse response
|
186
|
+
raise Denied.new(
|
187
|
+
info["code"],
|
188
|
+
info["description"])
|
189
|
+
|
190
|
+
when 404
|
191
|
+
raise NotFound
|
192
|
+
|
193
|
+
when 411, 500, 504
|
194
|
+
begin
|
195
|
+
raise_error(JSON.parse(response))
|
196
|
+
rescue JSON::ParserError
|
197
|
+
raise BadResponse.new(response.code, response)
|
198
|
+
end
|
199
|
+
|
200
|
+
else
|
201
|
+
raise BadResponse.new(response.code, response)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
205
|
+
raise TargetRefused, e.message
|
206
|
+
end
|
207
|
+
|
208
|
+
def raise_error(info)
|
209
|
+
case info["code"]
|
210
|
+
when 402
|
211
|
+
raise UploadFailed.new(info["description"])
|
212
|
+
else
|
213
|
+
raise APIError.new(info["code"], info["description"])
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def get(*path)
|
218
|
+
request(:get, path)
|
219
|
+
end
|
220
|
+
|
221
|
+
def delete(*path)
|
222
|
+
request(:delete, path)
|
223
|
+
end
|
224
|
+
|
225
|
+
def post(payload, *path)
|
226
|
+
request(:post, path, :payload => payload)
|
227
|
+
end
|
228
|
+
|
229
|
+
def put(payload, *path)
|
230
|
+
request(:put, path, :payload => payload)
|
231
|
+
end
|
232
|
+
|
233
|
+
def json_get(*path)
|
234
|
+
request(:get, path, :json => true)
|
235
|
+
end
|
236
|
+
|
237
|
+
def json_delete(*path)
|
238
|
+
request(:delete, path, :json => true)
|
239
|
+
end
|
240
|
+
|
241
|
+
def json_post(payload, *path)
|
242
|
+
request(:post, path, :payload => payload, :json => true)
|
243
|
+
end
|
244
|
+
|
245
|
+
def json_put(payload, *path)
|
246
|
+
request(:put, path, :payload => payload, :json => true)
|
247
|
+
end
|
248
|
+
|
249
|
+
def url(segments)
|
250
|
+
"#@target/#{safe_path(segments)}"
|
251
|
+
end
|
252
|
+
|
253
|
+
def safe_path(*segments)
|
254
|
+
segments.flatten.collect { |x|
|
255
|
+
URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
256
|
+
}.join("/")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CFoundry
|
2
|
+
class Service
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, client, manifest = nil)
|
6
|
+
@name = name
|
7
|
+
@client = client
|
8
|
+
@manifest = manifest
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
"#<Service '#@name'>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def manifest
|
16
|
+
@manifest ||= @client.rest.service(@name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete!
|
20
|
+
@client.rest.delete_service(@name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!
|
24
|
+
@client.rest.create_service(@manifest.merge("name" => @name))
|
25
|
+
end
|
26
|
+
|
27
|
+
def exists?
|
28
|
+
@client.rest.service(@name)
|
29
|
+
true
|
30
|
+
rescue CFoundry::NotFound
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def created
|
35
|
+
Time.at(meta["created"])
|
36
|
+
end
|
37
|
+
|
38
|
+
def updated
|
39
|
+
Time.at(meta["updated"])
|
40
|
+
end
|
41
|
+
|
42
|
+
{ :type => "type",
|
43
|
+
:vendor => "vendor",
|
44
|
+
:version => "version",
|
45
|
+
:properties => "properties",
|
46
|
+
:tier => "tier",
|
47
|
+
:meta => "meta"
|
48
|
+
}.each do |meth, attr|
|
49
|
+
define_method(meth) do
|
50
|
+
manifest[attr]
|
51
|
+
end
|
52
|
+
|
53
|
+
define_method(:"#{meth}=") do |v|
|
54
|
+
@manifest ||= {}
|
55
|
+
@manifest[attr] = v
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CFoundry
|
2
|
+
class User
|
3
|
+
attr_reader :email
|
4
|
+
|
5
|
+
def initialize(email, client, manifest = nil)
|
6
|
+
@email = email
|
7
|
+
@client = client
|
8
|
+
@manifest = manifest
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
"#<User '#@email'>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def manifest
|
16
|
+
@manifest ||= @client.rest.user(@email)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete!
|
20
|
+
@client.rest.delete_user(@email)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!
|
24
|
+
@client.rest.create_user(@manifest.merge("email" => @email))
|
25
|
+
end
|
26
|
+
|
27
|
+
def update!(what = {})
|
28
|
+
@client.rest.update_user(@email, manifest.merge(what))
|
29
|
+
@manifest = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def exists?
|
33
|
+
@client.rest.user(@email)
|
34
|
+
true
|
35
|
+
rescue CFoundry::Denied
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def admin?
|
40
|
+
manifest["admin"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def password=(str)
|
44
|
+
manifest["password"] = str
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/cfoundry/zip.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'zip/zipfilesystem'
|
2
|
+
|
3
|
+
module CFoundry
|
4
|
+
module Zip
|
5
|
+
PACK_EXCLUSION_GLOBS = ['..', '.', '*~', '#*#', '*.log']
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def entry_lines(file)
|
10
|
+
entries = []
|
11
|
+
::Zip::ZipFile.foreach(file) do |zentry|
|
12
|
+
entries << zentry
|
13
|
+
end
|
14
|
+
entries
|
15
|
+
end
|
16
|
+
|
17
|
+
def unpack(file, dest)
|
18
|
+
::Zip::ZipFile.foreach(file) do |zentry|
|
19
|
+
epath = "#{dest}/#{zentry}"
|
20
|
+
dirname = File.dirname(epath)
|
21
|
+
FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
|
22
|
+
zentry.extract(epath) unless File.exists?(epath)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def files_to_pack(dir)
|
27
|
+
Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).select do |f|
|
28
|
+
File.exists?(f) &&
|
29
|
+
PACK_EXCLUSION_GLOBS.none? do |e|
|
30
|
+
File.fnmatch(e, File.basename(f))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def pack(dir, zipfile)
|
36
|
+
::Zip::ZipFile.open(zipfile, true) do |zf|
|
37
|
+
files_to_pack(dir).each do |f|
|
38
|
+
zf.add(f.sub("#{dir}/",''), f)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/spec/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.require(:default, :development)
|
4
|
+
|
5
|
+
require 'rake/dsl_definition'
|
6
|
+
require 'rake'
|
7
|
+
require 'rspec'
|
8
|
+
require 'rspec/core/rake_task'
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new do |t|
|
11
|
+
t.pattern = "**/*_spec.rb"
|
12
|
+
t.rspec_opts = ["--format", "documentation", "--colour"]
|
13
|
+
end
|
14
|
+
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require "cfoundry"
|
2
|
+
require "./helpers"
|
3
|
+
|
4
|
+
RSpec.configure do |c|
|
5
|
+
c.include CFoundryHelpers
|
6
|
+
end
|
7
|
+
|
8
|
+
describe CFoundry::Client do
|
9
|
+
TARGET = ENV["CFOUNDRY_TEST_TARGET"] || "http://api.vcap.me"
|
10
|
+
USER = ENV["CFOUNDRY_TEST_USER"] || "dev@cloudfoundry.org"
|
11
|
+
PASSWORD = ENV["CFOUNDRY_TEST_PASSWORD"] || "test"
|
12
|
+
|
13
|
+
before(:all) do
|
14
|
+
@client = CFoundry::Client.new(TARGET)
|
15
|
+
@client.login(USER, PASSWORD)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe :target do
|
19
|
+
it "returns the current API target" do
|
20
|
+
@client.target.should == TARGET
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "metadata" do
|
25
|
+
describe :info do
|
26
|
+
it "returns the cloud meta-info" do
|
27
|
+
@client.info.should be_a(Hash)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe :system_services do
|
32
|
+
it "returns the service vendors" do
|
33
|
+
@client.system_services.should be_a(Hash)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "denies if not authenticated" do
|
37
|
+
without_auth do
|
38
|
+
proc {
|
39
|
+
@client.system_services
|
40
|
+
}.should raise_error(CFoundry::Denied)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe :system_runtimes do
|
46
|
+
it "returns the supported runtime information" do
|
47
|
+
@client.system_runtimes.should be_a(Hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "works if not authenticated" do
|
51
|
+
without_auth do
|
52
|
+
proc {
|
53
|
+
@client.system_runtimes
|
54
|
+
}.should_not raise_error(CFoundry::Denied)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe :user do
|
61
|
+
it "creates a lazy User object" do
|
62
|
+
with_new_user do
|
63
|
+
@client.user(@user.email).should be_a(CFoundry::User)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe :register do
|
69
|
+
it "registers an account and returns the User" do
|
70
|
+
email = random_user
|
71
|
+
|
72
|
+
user = @client.register(email, "test")
|
73
|
+
|
74
|
+
begin
|
75
|
+
@client.user(email).should satisfy(&:exists?)
|
76
|
+
ensure
|
77
|
+
user.delete!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "fails if a user by that name already exists" do
|
82
|
+
proc {
|
83
|
+
@client.register(USER, PASSWORD)
|
84
|
+
}.should raise_error(CFoundry::Denied)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe :login do
|
89
|
+
it "authenticates and sets the client token" do
|
90
|
+
client = CFoundry::Client.new(TARGET)
|
91
|
+
email = random_user
|
92
|
+
pass = random_str
|
93
|
+
|
94
|
+
client.register(email, pass)
|
95
|
+
begin
|
96
|
+
client.login(email, pass)
|
97
|
+
client.should satisfy(&:logged_in?)
|
98
|
+
ensure
|
99
|
+
@client.user(email).delete!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it "fails with invalid credentials" do
|
104
|
+
client = CFoundry::Client.new(TARGET)
|
105
|
+
email = random_user
|
106
|
+
|
107
|
+
client.register(email, "right")
|
108
|
+
begin
|
109
|
+
proc {
|
110
|
+
client.login(email, "wrong")
|
111
|
+
}.should raise_error(CFoundry::Denied)
|
112
|
+
ensure
|
113
|
+
@client.user(email).delete!
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe :logout do
|
119
|
+
it "clears the login token" do
|
120
|
+
client = CFoundry::Client.new(TARGET)
|
121
|
+
email = random_user
|
122
|
+
pass = random_str
|
123
|
+
|
124
|
+
client.register(email, pass)
|
125
|
+
|
126
|
+
begin
|
127
|
+
client.login(email, pass)
|
128
|
+
client.should satisfy(&:logged_in?)
|
129
|
+
|
130
|
+
client.logout
|
131
|
+
client.should_not satisfy(&:logged_in?)
|
132
|
+
ensure
|
133
|
+
@client.user(email).delete!
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe :app do
|
139
|
+
it "creates a lazy App object" do
|
140
|
+
@client.app("foo").should be_a(CFoundry::App)
|
141
|
+
@client.app("foo").name.should == "foo"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe :apps do
|
146
|
+
it "returns an empty array if a user has no apps" do
|
147
|
+
with_new_user do
|
148
|
+
@client.apps.should == []
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns an array of App objects if a user has any" do
|
153
|
+
with_new_user do
|
154
|
+
name = random_str
|
155
|
+
|
156
|
+
new = @client.app(name)
|
157
|
+
new.total_instances = 1
|
158
|
+
new.urls = []
|
159
|
+
new.framework = "sinatra"
|
160
|
+
new.runtime = "ruby18"
|
161
|
+
new.memory = 64
|
162
|
+
new.create!
|
163
|
+
|
164
|
+
apps = @client.apps
|
165
|
+
apps.should be_a(Array)
|
166
|
+
apps.size.should == 1
|
167
|
+
apps.first.should be_a(CFoundry::App)
|
168
|
+
apps.first.name.should == name
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe :service do
|
174
|
+
it "creates a lazy Service object" do
|
175
|
+
@client.service("foo").should be_a(CFoundry::Service)
|
176
|
+
@client.service("foo").name.should == "foo"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe :services do
|
181
|
+
it "returns an empty array if a user has no apps" do
|
182
|
+
with_new_user do
|
183
|
+
@client.services.should == []
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
it "returns an array of Service objects if a user has any" do
|
188
|
+
with_new_user do
|
189
|
+
name = random_str
|
190
|
+
|
191
|
+
new = @client.service(name)
|
192
|
+
new.type = "key-value"
|
193
|
+
new.vendor = "redis"
|
194
|
+
new.version = "2.2"
|
195
|
+
new.tier = "free"
|
196
|
+
new.create!
|
197
|
+
|
198
|
+
svcs = @client.services
|
199
|
+
svcs.should be_a(Array)
|
200
|
+
svcs.size.should == 1
|
201
|
+
svcs.first.should be_a(CFoundry::Service)
|
202
|
+
svcs.first.name.should == name
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module CFoundryHelpers
|
2
|
+
def random_str
|
3
|
+
format("%x", rand(1000000))
|
4
|
+
end
|
5
|
+
|
6
|
+
def random_user
|
7
|
+
"#{random_str}@cfoundry-spec.com"
|
8
|
+
end
|
9
|
+
|
10
|
+
def without_auth
|
11
|
+
proxy = @client.proxy
|
12
|
+
@client.logout
|
13
|
+
@client.proxy = nil
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
@client.login(USER, PASSWORD)
|
17
|
+
@client.proxy = proxy
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_new_user
|
21
|
+
@user = @client.register(random_user, "test")
|
22
|
+
@client.proxy = @user.email
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
@client.proxy = nil
|
26
|
+
@user.delete!
|
27
|
+
@user = nil
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cfoundry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Suraci
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-05 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: &70172796555500 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70172796555500
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: json_pure
|
27
|
+
requirement: &70172796555080 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70172796555080
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rubyzip
|
38
|
+
requirement: &70172796554660 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70172796554660
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70172796554240 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70172796554240
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &70172796553820 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70172796553820
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- asuraci@vmware.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- LICENSE
|
77
|
+
- Rakefile
|
78
|
+
- lib/cfoundry/app.rb
|
79
|
+
- lib/cfoundry/client.rb
|
80
|
+
- lib/cfoundry/errors.rb
|
81
|
+
- lib/cfoundry/restclient.rb
|
82
|
+
- lib/cfoundry/service.rb
|
83
|
+
- lib/cfoundry/user.rb
|
84
|
+
- lib/cfoundry/version.rb
|
85
|
+
- lib/cfoundry/zip.rb
|
86
|
+
- lib/cfoundry.rb
|
87
|
+
- spec/client_spec.rb
|
88
|
+
- spec/helpers.rb
|
89
|
+
- spec/Rakefile
|
90
|
+
homepage: http://cloudfoundry.com/
|
91
|
+
licenses: []
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project: cfoundry
|
110
|
+
rubygems_version: 1.8.10
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: High-level library for working with the Cloud Foundry API.
|
114
|
+
test_files:
|
115
|
+
- spec/client_spec.rb
|
116
|
+
- spec/helpers.rb
|
117
|
+
- spec/Rakefile
|