cfoundry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ require "cfoundry/version"
2
+ require "cfoundry/client"
@@ -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
@@ -0,0 +1,3 @@
1
+ module CFoundry
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
+
@@ -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