cf 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.
@@ -0,0 +1,91 @@
1
+ require "cf/cli/command"
2
+
3
+ module CF
4
+ class Service < Command
5
+ desc "delete", "Delete a service"
6
+ flag(:name) { |choices|
7
+ ask "Delete which service?", :choices => choices
8
+ }
9
+ def delete(name = nil)
10
+ name ||= input(:name, client.services.collect(&:name))
11
+
12
+ with_progress("Deleting #{c(name, :blue)}") do
13
+ client.service(name).delete!
14
+ end
15
+ end
16
+
17
+ desc "bind", "Bind a service to an application"
18
+ flag(:name) { |choices|
19
+ ask "Which service?", :choices => choices
20
+ }
21
+ flag(:app) { |choices|
22
+ ask "Which application?", :choices => choices
23
+ }
24
+ def bind(name = nil, appname = nil)
25
+ name ||= input(:name, client.services.collect(&:name))
26
+ appname ||= input(:app, client.apps.collect(&:name))
27
+
28
+ with_progress("Binding #{c(name, :blue)} to #{c(appname, :blue)}") do
29
+ client.app(appname).bind(name)
30
+ end
31
+ end
32
+
33
+ desc "unbind", "Unbind a service from an application"
34
+ flag(:name) { |choices|
35
+ ask "Which service?", :choices => choices
36
+ }
37
+ flag(:app) { |choices|
38
+ ask "Which application?", :choices => choices
39
+ }
40
+ def unbind(name = nil, appname = nil)
41
+ appname ||= input(:app, client.apps.collect(&:name))
42
+
43
+ app = client.app(appname)
44
+ name ||= input(:name, app.services)
45
+
46
+ with_progress("Unbinding #{c(name, :blue)} from #{c(appname, :blue)}") do
47
+ app.unbind(name)
48
+ end
49
+ end
50
+
51
+ desc "create", "Create a service"
52
+ flag(:type) { |choices|
53
+ ask "What kind?", :choices => choices
54
+ }
55
+ flag(:name) { |vendor|
56
+ random = sprintf("%x", rand(1000000))
57
+ ask "Name?", :default => "#{vendor}-#{random}"
58
+ }
59
+ def create
60
+ choices = []
61
+ manifests = {}
62
+ client.system_services.each do |type, vendors|
63
+ vendors.each do |vendor, versions|
64
+ versions.each do |version, _|
65
+ choice = "#{vendor} #{version}"
66
+ manifests[choice] = {
67
+ :type => type,
68
+ :vendor => vendor,
69
+ :version => version
70
+ }
71
+
72
+ choices << choice
73
+ end
74
+ end
75
+ end
76
+
77
+ type = input(:type, choices)
78
+ meta = manifests[type]
79
+
80
+ service = client.service(input(:name, meta[:vendor]))
81
+ service.type = meta[:type]
82
+ service.vendor = meta[:vendor]
83
+ service.version = meta[:version]
84
+ service.tier = "free"
85
+
86
+ with_progress("Creating service") do
87
+ service.create!
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,71 @@
1
+ require "cf/cli/command"
2
+
3
+ module CF
4
+ class User < Command
5
+ desc "create [EMAIL]", "Create a user"
6
+ flag(:email) {
7
+ ask("Email")
8
+ }
9
+ flag(:password) {
10
+ ask("Password", :echo => "*", :forget => true)
11
+ }
12
+ flag(:verify) {
13
+ ask("Verify Password", :echo => "*", :forget => true)
14
+ }
15
+ def create(email = nil)
16
+ email ||= input(:email)
17
+ password = input(:password)
18
+ verify = input(:verify)
19
+
20
+ if password != verify
21
+ err "Passwords don't match."
22
+ return
23
+ end
24
+
25
+ with_progress("Creating user") do
26
+ client.register(email, password)
27
+ end
28
+ end
29
+
30
+ desc "delete [EMAIL]", "Delete a user"
31
+ flag(:really) { |email|
32
+ force? || ask("Really delete user #{c(email, :blue)}?", :default => false)
33
+ }
34
+ def delete(email)
35
+ return unless input(:really, email)
36
+
37
+ with_progress("Deleting #{c(email, :blue)}") do
38
+ client.user(email).delete!
39
+ end
40
+ ensure
41
+ forget(:really)
42
+ end
43
+
44
+ desc "passwd [EMAIL]", "Update a user's password"
45
+ flag(:email) {
46
+ ask("Email")
47
+ }
48
+ flag(:password) {
49
+ ask("Password", :echo => "*", :forget => true)
50
+ }
51
+ flag(:verify) {
52
+ ask("Verify Password", :echo => "*", :forget => true)
53
+ }
54
+ def passwd(email = nil)
55
+ email ||= input(:email)
56
+ password = input(:password)
57
+ verify = input(:verify)
58
+
59
+ if password != verify
60
+ err "Passwords don't match."
61
+ return
62
+ end
63
+
64
+ with_progress("Changing password") do
65
+ user = client.user(email)
66
+ user.password = password
67
+ user.update!
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,10 @@
1
+ module CF
2
+ OLD_TARGET_FILE = "~/.vmc_target"
3
+ OLD_TOKENS_FILE = "~/.vmc_token"
4
+
5
+ CONFIG_DIR = "~/.cf"
6
+ PLUGINS_DIR = "#{CONFIG_DIR}/plugins"
7
+ TARGET_FILE = "#{CONFIG_DIR}/target"
8
+ TOKENS_FILE = "#{CONFIG_DIR}/tokens"
9
+ CRASH_FILE = "#{CONFIG_DIR}/crash"
10
+ end
@@ -0,0 +1,60 @@
1
+ module CF
2
+ class Detector
3
+ def initialize(client, path)
4
+ @client = client
5
+ @path = path
6
+ end
7
+
8
+ def all_frameworks
9
+ info = @client.info
10
+ info["frameworks"] || {}
11
+ end
12
+
13
+ def frameworks
14
+ info = @client.info
15
+
16
+ matches = {}
17
+ all_frameworks.each do |name, meta|
18
+ matched = false
19
+ meta["detection"].first.each do |file, match|
20
+ files =
21
+ if File.file? @path
22
+ if File.fnmatch(file, @path)
23
+ [@path]
24
+ else
25
+ []
26
+ end
27
+ else
28
+ Dir.glob("#@path/#{file}")
29
+ end
30
+
31
+ unless files.empty?
32
+ if match == true
33
+ matched = true
34
+ elsif match == false
35
+ matched = false
36
+ break
37
+ else
38
+ files.each do |f|
39
+ contents = File.open(f, &:read)
40
+ if contents =~ Regexp.new(match)
41
+ matched = true
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ if matched
49
+ matches[name] = meta
50
+ end
51
+ end
52
+
53
+ if matches.size == 1
54
+ default = matches.keys.first
55
+ end
56
+
57
+ [matches, default]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ require "cf/constants"
2
+ require "cf/cli"
3
+
4
+ module CF
5
+ module Plugin
6
+ @@plugins = []
7
+
8
+ def self.load_all
9
+ bundled = File.expand_path("../../../plugins/*/main.rb", __FILE__)
10
+ Dir.glob(bundled).each do |main|
11
+ require main
12
+ end
13
+
14
+ Dir.glob(File.expand_path("#{CF::PLUGINS_DIR}/*/main.rb")).each do |main|
15
+ require main
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.Plugin(target = CLI, &blk)
21
+ # SUPER FANCY PLUGIN SYSTEM
22
+ target.class_eval &blk
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module CF
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,508 @@
1
+ require "yaml"
2
+ require "set"
3
+ require "cf/plugin"
4
+
5
+ CF.Plugin do
6
+ class_option :manifest,
7
+ :aliases => "-m", :desc => "Manifest file"
8
+
9
+ class_option :path,
10
+ :aliases => "-p", :desc => "Application path"
11
+ end
12
+
13
+ module CF::Plugins
14
+ module Manifests
15
+ MANIFEST_FILE = "manifest.yml"
16
+
17
+ def manifest
18
+ return @manifest if @manifest
19
+
20
+ if manifest_file && File.exists?(manifest_file)
21
+ @manifest = load_manifest(manifest_file)
22
+ end
23
+ end
24
+
25
+ def save_manifest(save_to = manifest_file)
26
+ err "No manifest to save!" unless @manifest
27
+
28
+ File.open(save_to, "w") do |io|
29
+ YAML.dump(@manifest, io)
30
+ end
31
+ end
32
+
33
+ # find the manifest file to work with
34
+ def manifest_file
35
+ return options[:manifest] if options[:manifest]
36
+ return @manifest_file if @manifest_file
37
+
38
+ where = Dir.pwd
39
+ while true
40
+ if File.exists?(File.join(where, MANIFEST_FILE))
41
+ @manifest_file = File.join(where, MANIFEST_FILE)
42
+ break
43
+ elsif File.basename(where) == "/"
44
+ @manifest_file = nil
45
+ break
46
+ else
47
+ where = File.expand_path("../", where)
48
+ end
49
+ end
50
+
51
+ @manifest_file
52
+ end
53
+
54
+ # load and resolve a given manifest file
55
+ def load_manifest(file)
56
+ manifest = build_manifest(file)
57
+ resolve_manifest(manifest)
58
+ manifest
59
+ end
60
+
61
+ # parse a manifest and merge with its inherited manifests
62
+ def build_manifest(file)
63
+ manifest = YAML.load_file file
64
+
65
+ Array(manifest["inherit"]).each do |p|
66
+ manifest = merge_parent(manifest, p)
67
+ end
68
+
69
+ manifest
70
+ end
71
+
72
+ # merge the manifest at `path' into the `child'
73
+ def merge_parent(child, path)
74
+ file = File.expand_path("../" + path, manifest_file)
75
+ merge_manifest(child, build_manifest(file))
76
+ end
77
+
78
+ # deep hash merge
79
+ def merge_manifest(child, parent)
80
+ merge = proc do |_, old, new|
81
+ if new.is_a?(Hash) and old.is_a?(Hash)
82
+ old.merge(new, &merge)
83
+ else
84
+ new
85
+ end
86
+ end
87
+
88
+ parent.merge(child, &merge)
89
+ end
90
+
91
+ # resolve symbols in a manifest
92
+ def resolve_manifest(manifest)
93
+ if apps = manifest["applications"]
94
+ apps.each_value do |v|
95
+ resolve_lexically(v, [manifest])
96
+ end
97
+ end
98
+
99
+ resolve_lexically(manifest, [manifest])
100
+
101
+ nil
102
+ end
103
+
104
+ # resolve symbols, with hashes introducing new lexical symbols
105
+ def resolve_lexically(val, ctx)
106
+ case val
107
+ when Hash
108
+ val.each_value do |v|
109
+ resolve_lexically(v, [val] + ctx)
110
+ end
111
+ when Array
112
+ val.each do |v|
113
+ resolve_lexically(v, ctx)
114
+ end
115
+ when String
116
+ val.gsub!(/\$\{([[:alnum:]\-]+)\}/) do
117
+ resolve_symbol($1, ctx)
118
+ end
119
+ end
120
+
121
+ nil
122
+ end
123
+
124
+ # resolve a symbol to its value, and then resolve that value
125
+ def resolve_symbol(sym, ctx)
126
+ case sym
127
+ when "target-url"
128
+ target_url(ctx)
129
+
130
+ when "target-base"
131
+ target_url(ctx).sub(/^[^\.]+\./, "")
132
+
133
+ when "random-word"
134
+ "%04x" % [rand(0x0100000)]
135
+
136
+ else
137
+ found = find_symbol(sym, ctx)
138
+
139
+ if found
140
+ resolve_lexically(found, ctx)
141
+ found
142
+ else
143
+ err("Unknown symbol in manifest: #{sym}")
144
+ end
145
+ end
146
+ end
147
+
148
+ # get the target url from either the manifest or the current client
149
+ def target_url(ctx = [])
150
+ find_symbol("target", ctx) || client_target
151
+ end
152
+
153
+ # search for a symbol introduced in the lexical context
154
+ def find_symbol(sym, ctx)
155
+ ctx.each do |h|
156
+ if val = resolve_in(h, sym)
157
+ return val
158
+ end
159
+ end
160
+
161
+ nil
162
+ end
163
+
164
+ # find a value, searching in explicit properties first
165
+ def resolve_in(hash, *where)
166
+ find_in_hash(hash, ["properties"] + where) ||
167
+ find_in_hash(hash, where)
168
+ end
169
+
170
+ # helper for following a path of values in a hash
171
+ def find_in_hash(hash, where)
172
+ what = hash
173
+ where.each do |x|
174
+ return nil unless what.is_a?(Hash)
175
+ what = what[x]
176
+ end
177
+
178
+ what
179
+ end
180
+
181
+ MANIFEST_META = ["applications", "properties"]
182
+
183
+ def toplevel_attributes
184
+ if m = manifest
185
+ m.reject do |k, _|
186
+ MANIFEST_META.include? k
187
+ end
188
+ end
189
+ end
190
+
191
+ def app_info(find_path)
192
+ return unless manifest and manifest["applications"]
193
+
194
+ manifest["applications"].each do |path, info|
195
+ app = File.expand_path("../" + path, manifest_file)
196
+ if find_path == app
197
+ return toplevel_attributes.merge info
198
+ end
199
+ end
200
+
201
+ nil
202
+ end
203
+
204
+ # call a block for each app in a manifest (in dependency order), setting
205
+ # inputs for each app
206
+ def each_app
207
+ given_path = passed_value(:path)
208
+
209
+ if manifest and all_apps = manifest["applications"]
210
+ # given a specific application
211
+ if given_path
212
+ full_path = File.expand_path(given_path)
213
+
214
+ if info = app_info(full_path)
215
+ with_app(full_path, info) do
216
+ yield info
217
+ end
218
+ else
219
+ raise "Path #{given_path} is not described by the manifest."
220
+ end
221
+ else
222
+ # all apps in the manifest
223
+ ordered_by_deps(all_apps).each do |path|
224
+ app = File.expand_path("../" + path, manifest_file)
225
+ info = app_info(app)
226
+
227
+ with_app(app, info) do
228
+ yield info
229
+ end
230
+ end
231
+ end
232
+
233
+ true
234
+
235
+ # manually created or legacy single-app manifest
236
+ elsif single = toplevel_attributes
237
+ with_app(full_path || ".", single) do
238
+ yield single
239
+ end
240
+
241
+ true
242
+
243
+ else
244
+ false
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ def inputs
251
+ @inputs ||= {}
252
+ end
253
+
254
+ # call the block as if the app info and path were given as flags
255
+ def with_app(path, info)
256
+ before_path = inputs[:path]
257
+ before_info = {}
258
+
259
+ inputs[:path] = path
260
+
261
+ info.each do |k, v|
262
+ before_info[k.to_sym] = inputs[k.to_sym]
263
+ inputs[k.to_sym] = v
264
+ end
265
+
266
+ yield
267
+ ensure
268
+ if before_path.nil?
269
+ inputs.delete :path
270
+ else
271
+ inputs[:path] = before_path
272
+ end
273
+
274
+ before_info.each do |k, v|
275
+ if v.nil?
276
+ inputs.delete k
277
+ else
278
+ inputs[k] = v
279
+ end
280
+ end
281
+ end
282
+
283
+ # sort applications in dependency order
284
+ # e.g. if A depends on B, B will be listed before A
285
+ def ordered_by_deps(apps, abspaths = nil, processed = Set[])
286
+ unless abspaths
287
+ abspaths = {}
288
+ apps.each do |p, i|
289
+ ep = File.expand_path("../" + p, manifest_file)
290
+ abspaths[ep] = i
291
+ end
292
+ end
293
+
294
+ ordered = []
295
+ apps.each do |path, info|
296
+ epath = File.expand_path("../" + path, manifest_file)
297
+
298
+ if deps = info["depends-on"]
299
+ dep_apps = {}
300
+ deps.each do |dep|
301
+ edep = File.expand_path("../" + dep, manifest_file)
302
+
303
+ err "Circular dependency detected." if processed.include? edep
304
+
305
+ dep_apps[dep] = abspaths[edep]
306
+ end
307
+
308
+ processed.add(epath)
309
+
310
+ ordered += ordered_by_deps(dep_apps, abspaths, processed)
311
+ ordered << path
312
+ elsif not processed.include? epath
313
+ ordered << path
314
+ processed.add(epath)
315
+ end
316
+ end
317
+
318
+ ordered
319
+ end
320
+
321
+ # detect changes in app info, and update the app if necessary.
322
+ #
323
+ # redeploys the app if necessary (after prompting the user), e.g. for
324
+ # runtime/framework change
325
+ def sync_changes(info)
326
+ app = client.app(info["name"])
327
+ return unless app.exists?
328
+
329
+ diff = {}
330
+ need_restage = []
331
+ info.each do |k, v|
332
+ case k
333
+ when /ur[li]s?/
334
+ old = app.urls
335
+ if old != Array(v)
336
+ diff[k] = [old, v]
337
+ app.urls = Array(v)
338
+ end
339
+ when "env"
340
+ old = app.env
341
+ if old != v
342
+ diff[k] = [old, v]
343
+ app.env = v
344
+ end
345
+ when "framework", "runtime"
346
+ old = app.send(k)
347
+ if old != v
348
+ diff[k] = [old, v]
349
+ app.send(:"#{k}=", v)
350
+ need_restage << k
351
+ end
352
+ when "instances"
353
+ old = app.total_instances
354
+ if old != v
355
+ diff[k] = [old, v]
356
+ app.total_instances = v
357
+ end
358
+ when "mem", "memory"
359
+ old = app.memory
360
+ new = megabytes(v)
361
+ if old != new
362
+ diff[k] = [old, new]
363
+ app.memory = new
364
+ end
365
+ end
366
+ end
367
+
368
+ return if diff.empty?
369
+
370
+ unless simple_output?
371
+ puts "Detected the following changes to #{c(app.name, :blue)}:"
372
+ diff.each do |k, d|
373
+ old, new = d
374
+ label = c(k, need_restage.include?(k) ? :red : :green)
375
+ puts " #{label}: #{old.inspect} #{c("->", :black)} #{new.inspect}"
376
+ end
377
+
378
+ puts ""
379
+ end
380
+
381
+ if need_restage.empty?
382
+ with_progress("Updating #{c(app.name, :blue)}") do
383
+ app.update!
384
+ end
385
+ else
386
+ unless simple_output?
387
+ puts "The following changes require the app to be recreated:"
388
+ need_restage.each do |n|
389
+ puts " #{c(n, :magenta)}"
390
+ end
391
+ puts ""
392
+ end
393
+
394
+ if force? || ask("Redeploy?", :default => false)
395
+ with_progress("Deleting #{c(app.name, :blue)}") do
396
+ app.delete!
397
+ end
398
+
399
+ with_progress("Recreating #{c(app.name, :blue)}") do
400
+ app.create!
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ CF.Plugin(CF::App) do
409
+ include CF::Plugins::Manifests
410
+
411
+ # basic commands that, when given no args, act on the
412
+ # app(s) described by the manifest, in dependency-order
413
+ [:start, :instances, :logs].each do |wrap|
414
+ around(wrap) do |cmd, args|
415
+ if args.empty?
416
+ each_app do |a|
417
+ cmd.call(a["name"])
418
+ puts "" unless simple_output?
419
+ end || err("No applications to act on.")
420
+ else
421
+ cmd.call(args)
422
+ end
423
+ end
424
+ end
425
+
426
+ # same as above but in reverse dependency-order
427
+ [:stop, :delete].each do |wrap|
428
+ around(wrap) do |cmd, args|
429
+ if args.empty?
430
+ reversed = []
431
+ each_app do |a|
432
+ reversed.unshift a["name"]
433
+ end || err("No applications to act on.")
434
+
435
+ reversed.each do |name|
436
+ cmd.call(name)
437
+ puts "" unless simple_output?
438
+ end
439
+ else
440
+ cmd.call(args)
441
+ end
442
+ end
443
+ end
444
+
445
+ # stop apps in reverse dependency order,
446
+ # and then start in dependency order
447
+ around(:restart) do |cmd, args|
448
+ if args.empty?
449
+ reversed = []
450
+ forwards = []
451
+ each_app do |a|
452
+ reversed.unshift a["name"]
453
+ forwards << a["name"]
454
+ end || err("No applications to act on.")
455
+
456
+ reversed.each do |name|
457
+ stop(name)
458
+ end
459
+
460
+ puts "" unless simple_output?
461
+
462
+ forwards.each do |name|
463
+ start(name)
464
+ end
465
+ else
466
+ cmd.call(args)
467
+ end
468
+ end
469
+
470
+ # push and sync meta changes in the manifest
471
+ # also sets env data on creation if present in manifest
472
+ around(:push) do |push, args|
473
+ if args.empty?
474
+ all_pushed =
475
+ each_app do |a|
476
+ app = client.app(a["name"])
477
+ updating = app.exists?
478
+
479
+ start = input(:start)
480
+
481
+ begin
482
+ inputs[:start] = false
483
+
484
+ sync_changes(a)
485
+ push.call(a["name"])
486
+
487
+ unless updating
488
+ app.env = a["env"]
489
+
490
+ if start
491
+ start(a["name"])
492
+ else
493
+ app.update!
494
+ end
495
+ end
496
+ ensure
497
+ inputs[:start] = start
498
+ end
499
+
500
+ puts "" unless simple_output?
501
+ end
502
+
503
+ push.call unless all_pushed
504
+ else
505
+ push.call(args)
506
+ end
507
+ end
508
+ end