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 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