cfoundry-IronFoundry 0.3.34

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ require "cfoundry/baseclient"
2
+
3
+ module CFoundry
4
+ class UAAClient < BaseClient
5
+ attr_accessor :target, :client_id, :redirect_uri, :token, :trace
6
+
7
+ def initialize(
8
+ target = "https://uaa.cloudfoundry.com",
9
+ client_id = "vmc")
10
+ @target = target
11
+ @client_id = client_id
12
+ @redirect_uri = @target + "/redirect/vmc"
13
+ end
14
+
15
+ def prompts
16
+ get("login", nil => :json)[:prompts]
17
+ end
18
+
19
+ def authorize(credentials)
20
+ query = {
21
+ :client_id => @client_id,
22
+ :response_type => "token",
23
+ :redirect_uri => @redirect_uri
24
+ }
25
+
26
+ extract_token(
27
+ post(
28
+ { :credentials => credentials },
29
+ "oauth", "authorize",
30
+ :form => :headers,
31
+ :params => query)[:location])
32
+ end
33
+
34
+ def users
35
+ get("Users", nil => :json)
36
+ end
37
+
38
+ def change_password(guid, new, old)
39
+ put(
40
+ { :schemas => ["urn:scim:schemas:core:1.0"],
41
+ :password => new,
42
+ :oldPassword => old
43
+ },
44
+ "User", guid, "password",
45
+ :json => nil)
46
+ end
47
+
48
+ private
49
+
50
+ def handle_response(response, accept)
51
+ json = accept == :json
52
+
53
+ case response.code
54
+ when 200, 204, 302
55
+ if accept == :headers
56
+ return response.headers
57
+ end
58
+
59
+ if json
60
+ if response.code == 204
61
+ raise "Expected JSON response, got 204 No Content"
62
+ end
63
+
64
+ parse_json(response)
65
+ else
66
+ response
67
+ end
68
+
69
+ when 400, 403
70
+ info = parse_json(response)
71
+ raise Denied.new(403, info[:error_description])
72
+
73
+ when 401
74
+ info = parse_json(response)
75
+ raise Denied.new(401, info[:error_description])
76
+
77
+ when 404
78
+ raise NotFound
79
+
80
+ when 409
81
+ info = parse_json(response)
82
+ raise CFoundry::Denied.new(409, info[:message])
83
+
84
+ when 411, 500, 504
85
+ begin
86
+ raise_error(parse_json(response))
87
+ rescue MultiJson::DecodeError
88
+ raise BadResponse.new(response.code, response)
89
+ end
90
+
91
+ else
92
+ raise BadResponse.new(response.code, response)
93
+ end
94
+ end
95
+
96
+ def extract_token(url)
97
+ _, params = url.split('#')
98
+ return unless params
99
+
100
+ values = {}
101
+ params.split("&").each do |pair|
102
+ key, val = pair.split("=")
103
+ values[key] = val
104
+ end
105
+
106
+ return unless values["access_token"] && values["token_type"]
107
+
108
+ "#{values["token_type"]} #{values["access_token"]}"
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,100 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "digest/sha1"
4
+
5
+ module CFoundry
6
+ module UploadHelpers
7
+ # Default paths to exclude from upload payload.
8
+ UPLOAD_EXCLUDE = %w{.git _darcs .svn}
9
+
10
+ def make_fingerprints(path)
11
+ fingerprints = []
12
+ total_size = 0
13
+
14
+ Dir.glob("#{path}/**/*", File::FNM_DOTMATCH) do |filename|
15
+ next if File.directory?(filename)
16
+
17
+ size = File.size(filename)
18
+
19
+ total_size += size
20
+
21
+ fingerprints << {
22
+ :size => size,
23
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
24
+ :fn => filename
25
+ }
26
+ end
27
+
28
+ [fingerprints, total_size]
29
+ end
30
+
31
+ def prepare_package(path, to)
32
+ if path =~ /\.(jar|war|zip)$/
33
+ CFoundry::Zip.unpack(path, to)
34
+ elsif war_file = Dir.glob("#{path}/*.war").first
35
+ CFoundry::Zip.unpack(war_file, to)
36
+ else
37
+ check_unreachable_links(path)
38
+
39
+ FileUtils.mkdir(to)
40
+
41
+ files = Dir.glob("#{path}/{*,.[^\.]*}")
42
+
43
+ exclude = UPLOAD_EXCLUDE
44
+ if File.exists?("#{path}/.vmcignore")
45
+ exclude += File.read("#{path}/.vmcignore").split(/\n+/)
46
+ end
47
+
48
+ # prevent initial copying if we can, remove sub-files later
49
+ files.reject! do |f|
50
+ exclude.any? do |e|
51
+ File.fnmatch(f.sub(path + "/", ""), e)
52
+ end
53
+ end
54
+
55
+ FileUtils.cp_r(files, to)
56
+
57
+ find_sockets(to).each do |s|
58
+ File.delete s
59
+ end
60
+
61
+ # remove ignored globs more thoroughly
62
+ #
63
+ # note that the above file list only includes toplevel
64
+ # files/directories for cp_r, so this is where sub-files/etc. are
65
+ # removed
66
+ exclude.each do |e|
67
+ Dir.glob("#{to}/#{e}").each do |f|
68
+ FileUtils.rm_rf(f)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def check_unreachable_links(path)
75
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
76
+
77
+ # only used for friendlier error message
78
+ pwd = Pathname.pwd
79
+
80
+ abspath = File.expand_path(path)
81
+ unreachable = []
82
+ files.each do |f|
83
+ file = Pathname.new(f)
84
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
85
+ unreachable << file.relative_path_from(pwd)
86
+ end
87
+ end
88
+
89
+ unless unreachable.empty?
90
+ root = Pathname.new(path).relative_path_from(pwd)
91
+ raise "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'"
92
+ end
93
+ end
94
+
95
+ def find_sockets(path)
96
+ files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
97
+ files && files.select { |f| File.socket? f }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,562 @@
1
+ require "tmpdir"
2
+
3
+ require "cfoundry/zip"
4
+ require "cfoundry/upload_helpers"
5
+ require "cfoundry/chatty_hash"
6
+
7
+ require "cfoundry/v1/framework"
8
+ require "cfoundry/v1/runtime"
9
+
10
+ module CFoundry::V1
11
+ # Class for representing a user's application on a given target (via
12
+ # Client).
13
+ #
14
+ # Does not guarantee that the app exists; used for both app creation and
15
+ # retrieval, as the attributes are all lazily retrieved. Setting attributes
16
+ # does not perform any requests; use #update! to commit your changes.
17
+ class App
18
+ include CFoundry::UploadHelpers
19
+
20
+ # Application name.
21
+ attr_accessor :name
22
+
23
+ # Application instance count.
24
+ attr_accessor :total_instances
25
+
26
+ # Services bound to the application.
27
+ attr_accessor :services
28
+
29
+ # Application environment variables.
30
+ attr_accessor :env
31
+
32
+ # Application memory limit.
33
+ attr_accessor :memory
34
+
35
+ # Application framework.
36
+ attr_accessor :framework
37
+
38
+ # Application runtime.
39
+ attr_accessor :runtime
40
+
41
+ # Application startup command.
42
+ #
43
+ # Used for standalone apps.
44
+ attr_accessor :command
45
+
46
+ # Application debug mode.
47
+ attr_accessor :debug_mode
48
+
49
+ # Application state.
50
+ attr_accessor :state
51
+ alias_method :status, :state
52
+
53
+ # URIs mapped to the application.
54
+ attr_accessor :uris
55
+ alias_method :urls, :uris
56
+
57
+
58
+ # Create an App object.
59
+ #
60
+ # You'll usually call Client#app instead
61
+ def initialize(name, client, manifest = nil)
62
+ @name = name
63
+ @client = client
64
+ @manifest = manifest
65
+ @diff = {}
66
+ end
67
+
68
+ # Show string representing the application.
69
+ def inspect
70
+ "#<App '#@name'>"
71
+ end
72
+
73
+ # Basic equality test by name.
74
+ def eql?(other)
75
+ other.is_a?(self.class) && other.name == @name
76
+ end
77
+ alias :== :eql?
78
+
79
+ # Delete the application from the target.
80
+ #
81
+ # Keeps the metadata, but clears target-specific state from it.
82
+ def delete!
83
+ @client.base.delete_app(@name)
84
+
85
+ if @manifest
86
+ @diff = read_manifest
87
+ @manifest = nil
88
+ end
89
+ end
90
+
91
+ # Create the application on the target.
92
+ #
93
+ # Call this after setting the various attributes.
94
+ def create!
95
+ @client.base.create_app(create_manifest)
96
+ @diff = {}
97
+ end
98
+
99
+ # Check if the application exists on the target.
100
+ def exists?
101
+ @client.base.app(@name)
102
+ true
103
+ rescue CFoundry::NotFound
104
+ false
105
+ end
106
+
107
+ # Retrieve all of the instances of the app, as Instance objects.
108
+ def instances
109
+ @client.base.instances(@name).collect do |m|
110
+ Instance.new(self, m[:index].to_s, @client, m)
111
+ end
112
+ end
113
+
114
+ # Retrieve crashed instances
115
+ def crashes
116
+ @client.base.crashes(@name).collect do |i|
117
+ Instance.new(self, i[:instance].to_s, @client, i)
118
+ end
119
+ end
120
+
121
+ # Retrieve application statistics, e.g. CPU load and memory usage.
122
+ def stats
123
+ @client.base.stats(@name)
124
+ end
125
+
126
+ # Update application attributes. Does not restart the application.
127
+ def update!(what = {})
128
+ what.each do |k, v|
129
+ send(:"#{k}=", v)
130
+ end
131
+
132
+ @client.base.update_app(@name, update_manifest)
133
+
134
+ @manifest = nil
135
+ @diff = {}
136
+
137
+ self
138
+ end
139
+
140
+ # Stop the application.
141
+ def stop!
142
+ update! :state => "STOPPED"
143
+ end
144
+
145
+ # Start the application.
146
+ def start!
147
+ update! :state => "STARTED"
148
+ end
149
+
150
+ # Restart the application.
151
+ def restart!
152
+ stop!
153
+ start!
154
+ end
155
+
156
+ # Determine application health.
157
+ #
158
+ # If all instances are running, returns "RUNNING". If only some are
159
+ # started, returns the precentage of them that are healthy.
160
+ #
161
+ # Otherwise, returns application's status.
162
+ def health
163
+ s = state
164
+ if s == "STARTED"
165
+ healthy_count = running_instances
166
+ expected = total_instances
167
+ if healthy_count && expected > 0
168
+ ratio = healthy_count / expected.to_f
169
+ if ratio == 1.0
170
+ "RUNNING"
171
+ else
172
+ "#{(ratio * 100).to_i}%"
173
+ end
174
+ else
175
+ "N/A"
176
+ end
177
+ else
178
+ s
179
+ end
180
+ end
181
+
182
+ # Check that all application instances are running.
183
+ def healthy?
184
+ # invalidate cache so the check is fresh
185
+ @manifest = nil
186
+ health == "RUNNING"
187
+ end
188
+ alias_method :running?, :healthy?
189
+
190
+ # Is the application stopped?
191
+ def stopped?
192
+ state == "STOPPED"
193
+ end
194
+
195
+ # Is the application started?
196
+ #
197
+ # Note that this does not imply that all instances are running. See
198
+ # #healthy?
199
+ def started?
200
+ state == "STARTED"
201
+ end
202
+
203
+
204
+ { :total_instances => :instances,
205
+ :running_instances => :running_instances,
206
+ :runtime_name => :runtime,
207
+ :framework_name => :framework,
208
+ :service_names => :services,
209
+ :env_array => :env,
210
+ :state => :state,
211
+ :status => :state,
212
+ :uris => :uris,
213
+ :urls => :uris,
214
+ :command => :command,
215
+ :console => :console,
216
+ :memory => :memory,
217
+ :disk => :disk,
218
+ :fds => :fds,
219
+ :debug_mode => :debug,
220
+ :version => :version,
221
+ :meta_version => :meta_version,
222
+ :created => :created
223
+ }.each do |meth, attr|
224
+ define_method(meth) do
225
+ if @diff.key?(attr)
226
+ @diff[attr]
227
+ else
228
+ read_manifest[attr]
229
+ end
230
+ end
231
+
232
+ define_method(:"#{meth}=") do |v|
233
+ @diff[attr] = v
234
+ end
235
+ end
236
+
237
+
238
+ # Shortcut for uris[0]
239
+ def uri
240
+ uris[0]
241
+ end
242
+
243
+ # Shortcut for uris = [x]
244
+ def uri=(x)
245
+ self.uris = [x]
246
+ end
247
+
248
+ alias :url :uri
249
+ alias :url= :uri=
250
+
251
+ # Application framework.
252
+ def framework
253
+ Framework.new(framework_name)
254
+ end
255
+
256
+ def framework=(v) # :nodoc:
257
+ v = v.name if v.is_a?(Framework)
258
+ self.framework_name = v
259
+ end
260
+
261
+ # Application runtime.
262
+ def runtime
263
+ Runtime.new(runtime_name)
264
+ end
265
+
266
+ def runtime=(v) # :nodoc:
267
+ v = v.name if v.is_a?(Runtime)
268
+ self.runtime_name = v
269
+ end
270
+
271
+ def env
272
+ e = env_array || []
273
+
274
+ env = {}
275
+ e.each do |pair|
276
+ name, val = pair.split("=", 2)
277
+ env[name] = val
278
+ end
279
+
280
+ CFoundry::ChattyHash.new(method(:env=), env)
281
+ end
282
+
283
+ def env=(hash)
284
+ unless hash.is_a?(Array)
285
+ hash = hash.collect { |k, v| "#{k}=#{v}" }
286
+ end
287
+
288
+ self.env_array = hash
289
+ end
290
+
291
+ def services
292
+ service_names.collect do |name|
293
+ @client.service_instance(name)
294
+ end
295
+ end
296
+
297
+ def services=(instances)
298
+ self.service_names = instances.collect(&:name)
299
+ end
300
+
301
+
302
+ # Bind services to application.
303
+ def bind(*instances)
304
+ update!(:services => services + instances)
305
+ end
306
+
307
+ # Unbind services from application.
308
+ def unbind(*instances)
309
+ update!(:services =>
310
+ services.reject { |s|
311
+ instances.any? { |i| i.name == s.name }
312
+ })
313
+ end
314
+
315
+ def binds?(instance)
316
+ services.include? instance
317
+ end
318
+
319
+ # Retrieve file listing under path for the first instance of the application.
320
+ #
321
+ # [path]
322
+ # A sequence of strings representing path segments.
323
+ #
324
+ # For example, <code>files("foo", "bar")</code> for +foo/bar+.
325
+ def files(*path)
326
+ Instance.new(self, "0", @client).files(*path)
327
+ end
328
+
329
+ # Retrieve file contents for the first instance of the application.
330
+ #
331
+ # [path]
332
+ # A sequence of strings representing path segments.
333
+ #
334
+ # For example, <code>files("foo", "bar")</code> for +foo/bar+.
335
+ def file(*path)
336
+ Instance.new(self, "0", @client).file(*path)
337
+ end
338
+
339
+ # Upload application's code to target. Do this after #create! and before
340
+ # #start!
341
+ #
342
+ # [path]
343
+ # A path pointing to either a directory, or a .jar, .war, or .zip
344
+ # file.
345
+ #
346
+ # If a .vmcignore file is detected under the given path, it will be used
347
+ # to exclude paths from the payload, similar to a .gitignore.
348
+ #
349
+ # [check_resources]
350
+ # If set to `false`, the entire payload will be uploaded
351
+ # without checking the resource cache.
352
+ #
353
+ # Only do this if you know what you're doing.
354
+ def upload(path, check_resources = true)
355
+ unless File.exist? path
356
+ raise "invalid application path '#{path}'"
357
+ end
358
+
359
+ zipfile = "#{Dir.tmpdir}/#{@name}.zip"
360
+ tmpdir = "#{Dir.tmpdir}/.vmc_#{@name}_files"
361
+
362
+ FileUtils.rm_f(zipfile)
363
+ FileUtils.rm_rf(tmpdir)
364
+
365
+ prepare_package(path, tmpdir)
366
+
367
+ resources = determine_resources(tmpdir) if check_resources
368
+
369
+ packed = CFoundry::Zip.pack(tmpdir, zipfile)
370
+
371
+ @client.base.upload_app(@name, packed && zipfile, resources || [])
372
+ ensure
373
+ FileUtils.rm_f(zipfile) if zipfile
374
+ FileUtils.rm_rf(tmpdir) if tmpdir
375
+ end
376
+
377
+ private
378
+
379
+ ATTR_MAP = {
380
+ :instances => :instances,
381
+ :state => :state,
382
+ :env => :env,
383
+ :uris => :uris,
384
+ :services => :services,
385
+ :debug => :debug,
386
+ :console => :console,
387
+
388
+ :framework => [:staging, :model],
389
+ :runtime => [:staging, :stack],
390
+ :command => [:staging, :command],
391
+
392
+ :meta_version => [:meta, :version],
393
+ :created => [:meta, :created],
394
+
395
+ :memory => [:resources, :memory],
396
+ :disk => [:resources, :disk],
397
+ :fds => [:resources, :fds]
398
+ }
399
+
400
+ def manifest
401
+ @manifest ||= @client.base.app(@name)
402
+ end
403
+
404
+ def write_manifest(body = read_manifest, onto = {})
405
+ onto[:name] = @name
406
+
407
+ ATTR_MAP.each do |what, where|
408
+ if body.key?(what)
409
+ put(body[what], onto, Array(where))
410
+ end
411
+ end
412
+
413
+ onto
414
+ end
415
+
416
+ def put(what, where, path)
417
+ if path.size == 1
418
+ where[path.last] = what
419
+ elsif name = path.first
420
+ where[name] ||= {}
421
+ put(what, where[name], path[1..-1])
422
+ end
423
+
424
+ nil
425
+ end
426
+
427
+ def update_manifest
428
+ write_manifest(@diff, write_manifest)
429
+ end
430
+
431
+ def create_manifest
432
+ write_manifest(@diff)
433
+ end
434
+
435
+ def read_manifest
436
+ { :name => @name,
437
+ :instances => manifest[:instances],
438
+ :running_instances => manifest[:runningInstances],
439
+ :state => manifest[:state],
440
+ :env => manifest[:env],
441
+ :uris => manifest[:uris],
442
+ :version => manifest[:version],
443
+ :services => manifest[:services],
444
+ :framework => manifest[:staging][:model],
445
+ :runtime => manifest[:staging][:stack],
446
+ :console => manifest[:meta][:console],
447
+ :meta_version => manifest[:meta][:version],
448
+ :debug => manifest[:meta][:debug],
449
+ :created => manifest[:meta][:created],
450
+ :memory => manifest[:resources][:memory],
451
+ :disk => manifest[:resources][:disk],
452
+ :fds => manifest[:resources][:fds]
453
+ }
454
+ end
455
+
456
+ # Minimum size for an application payload to bother checking resources.
457
+ RESOURCE_CHECK_LIMIT = 64 * 1024
458
+
459
+ def determine_resources(path)
460
+ fingerprints, total_size = make_fingerprints(path)
461
+
462
+ return if total_size <= RESOURCE_CHECK_LIMIT
463
+
464
+ resources = @client.base.check_resources(fingerprints)
465
+
466
+ resources.each do |resource|
467
+ FileUtils.rm_f resource[:fn]
468
+ resource[:fn].sub!("#{path}/", "")
469
+ end
470
+
471
+ resources
472
+ end
473
+
474
+ # Class represnting a running instance of an application.
475
+ class Instance
476
+ # The application this instance belongs to.
477
+ attr_reader :app
478
+
479
+ # Application instance number.
480
+ attr_reader :id
481
+
482
+ # Create an Instance object.
483
+ #
484
+ # You'll usually call App#instances instead
485
+ def initialize(app, id, client, manifest = {})
486
+ @app = app
487
+ @id = id
488
+ @client = client
489
+ @manifest = manifest
490
+ end
491
+
492
+ # Show string representing the application instance.
493
+ def inspect
494
+ "#<App::Instance '#{@app.name}' \##@id>"
495
+ end
496
+
497
+ # Instance state.
498
+ def state
499
+ @manifest[:state]
500
+ end
501
+ alias_method :status, :state
502
+
503
+ # Instance start time.
504
+ def since
505
+ Time.at(@manifest[:since])
506
+ end
507
+
508
+ # Instance debugger data. If instance is in debug mode, returns a hash
509
+ # containing :ip and :port keys.
510
+ def debugger
511
+ return unless @manifest[:debug_ip] and @manifest[:debug_port]
512
+
513
+ { :ip => @manifest[:debug_ip],
514
+ :port => @manifest[:debug_port]
515
+ }
516
+ end
517
+
518
+ # Instance console data. If instance has a console, returns a hash
519
+ # containing :ip and :port keys.
520
+ def console
521
+ return unless @manifest[:console_ip] and @manifest[:console_port]
522
+
523
+ { :ip => @manifest[:console_ip],
524
+ :port => @manifest[:console_port]
525
+ }
526
+ end
527
+
528
+ # True if instance is starting or running, false if it's down or
529
+ # flapping.
530
+ def healthy?
531
+ case state
532
+ when "STARTING", "RUNNING"
533
+ true
534
+ when "DOWN", "FLAPPING"
535
+ false
536
+ end
537
+ end
538
+
539
+ # Retrieve file listing under path for this instance.
540
+ #
541
+ # [path]
542
+ # A sequence of strings representing path segments.
543
+ #
544
+ # For example, <code>files("foo", "bar")</code> for +foo/bar+.
545
+ def files(*path)
546
+ @client.base.files(@app.name, @id, *path).split("\n").collect do |entry|
547
+ path + [entry.split(/\s+/, 2)[0]]
548
+ end
549
+ end
550
+
551
+ # Retrieve file contents for this instance.
552
+ #
553
+ # [path]
554
+ # A sequence of strings representing path segments.
555
+ #
556
+ # For example, <code>files("foo", "bar")</code> for +foo/bar+.
557
+ def file(*path)
558
+ @client.base.files(@app.name, @id, *path)
559
+ end
560
+ end
561
+ end
562
+ end