cfoundry 0.1.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/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
|