vmc 0.3.15 → 0.3.16.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,6 +12,8 @@ module VMC::Cli
12
12
 
13
13
  attr_reader :no_prompt, :prompt_ok
14
14
 
15
+ MANIFEST = "manifest.yml"
16
+
15
17
  def initialize(options={})
16
18
  @options = options.dup
17
19
  @no_prompt = @options[:noprompts]
@@ -21,6 +23,165 @@ module VMC::Cli
21
23
  if WINDOWS
22
24
  VMC::Cli::Config.colorize = false
23
25
  end
26
+
27
+ @path = @options[:path] || '.'
28
+
29
+ load_manifest manifest_file if manifest_file
30
+ end
31
+
32
+ def manifest_file
33
+ return @options[:manifest] if @options[:manifest]
34
+ return @manifest_file if @manifest_file
35
+
36
+ where = File.expand_path(@path)
37
+ while true
38
+ if File.exists?(File.join(where, MANIFEST))
39
+ @manifest_file = File.join(where, MANIFEST)
40
+ break
41
+ elsif where == "/"
42
+ @manifest_file = nil
43
+ break
44
+ else
45
+ where = File.expand_path("../", where)
46
+ end
47
+ end
48
+
49
+ @manifest_file
50
+ end
51
+
52
+ def load_manifest_structure(file)
53
+ manifest = YAML.load_file file
54
+
55
+ Array(manifest["inherit"]).each do |p|
56
+ manifest = merge_parent(manifest, p)
57
+ end
58
+
59
+ if apps = manifest["applications"]
60
+ apps.each do |k, v|
61
+ abs = File.expand_path(k, file)
62
+ if Dir.pwd.start_with? abs
63
+ manifest = merge_manifest(manifest, v)
64
+ end
65
+ end
66
+ end
67
+
68
+ manifest
69
+ end
70
+
71
+ def resolve_manifest(manifest)
72
+ if apps = manifest["applications"]
73
+ apps.each_value do |v|
74
+ resolve_lexically(v, [manifest])
75
+ end
76
+ end
77
+
78
+ resolve_lexically(manifest, [manifest])
79
+ end
80
+
81
+ def load_manifest(file)
82
+ @manifest = load_manifest_structure(file)
83
+ resolve_manifest(@manifest)
84
+ end
85
+
86
+ def merge_parent(child, path)
87
+ file = File.expand_path("../" + path, manifest_file)
88
+ merge_manifest(child, load_manifest_structure(file))
89
+ end
90
+
91
+ def merge_manifest(child, parent)
92
+ merge = proc do |_, old, new|
93
+ if new.is_a?(Hash) and old.is_a?(Hash)
94
+ old.merge(new, &merge)
95
+ else
96
+ new
97
+ end
98
+ end
99
+
100
+ parent.merge(child, &merge)
101
+ end
102
+
103
+ def resolve_lexically(val, ctx = [@manifest])
104
+ case val
105
+ when Hash
106
+ val.each_value do |v|
107
+ resolve_lexically(v, [val] + ctx)
108
+ end
109
+ when Array
110
+ val.each do |v|
111
+ resolve_lexically(v, ctx)
112
+ end
113
+ when String
114
+ val.gsub!(/\$\{([[:alnum:]\-]+)\}/) do
115
+ resolve_symbol($1, ctx)
116
+ end
117
+ end
118
+
119
+ nil
120
+ end
121
+
122
+ def resolve_symbol(sym, ctx)
123
+ case sym
124
+ when "target-base"
125
+ target_base(ctx)
126
+
127
+ when "target-url"
128
+ target_url(ctx)
129
+
130
+ when "random-word"
131
+ "%04x" % [rand(0x0100000)]
132
+
133
+ else
134
+ found = find_symbol(sym, ctx)
135
+
136
+ if found
137
+ resolve_lexically(found, ctx)
138
+ found
139
+ else
140
+ err(sym, "Unknown symbol in manifest: ")
141
+ end
142
+ end
143
+ end
144
+
145
+ def find_symbol(sym, ctx)
146
+ ctx.each do |h|
147
+ if val = resolve_in(h, sym)
148
+ return val
149
+ end
150
+ end
151
+
152
+ nil
153
+ end
154
+
155
+ def resolve_in(hash, *where)
156
+ find_in_hash(hash, ["properties"] + where) ||
157
+ find_in_hash(hash, ["applications", @application] + where) ||
158
+ find_in_hash(hash, where)
159
+ end
160
+
161
+ def manifest(*where)
162
+ resolve_in(@manifest, *where)
163
+ end
164
+
165
+ def find_in_hash(hash, where)
166
+ what = hash
167
+ where.each do |x|
168
+ return nil unless what.is_a?(Hash)
169
+ what = what[x]
170
+ end
171
+
172
+ what
173
+ end
174
+
175
+ def target_url(ctx = [])
176
+ find_symbol("target", ctx) || VMC::Cli::Config.target_url
177
+ end
178
+
179
+ def target_base(ctx = [])
180
+ if tgt = find_symbol("target", ctx)
181
+ VMC::Cli::Config.base_of(tgt)
182
+ else
183
+ VMC::Cli::Config.suggest_url
184
+ end
24
185
  end
25
186
 
26
187
  # Inject a client to help in testing.
@@ -37,16 +198,8 @@ module VMC::Cli
37
198
  @client_info ||= client.info
38
199
  end
39
200
 
40
- def target_url
41
- @target_url ||= VMC::Cli::Config.target_url
42
- end
43
-
44
- def target_base
45
- @target_base ||= VMC::Cli::Config.suggest_url
46
- end
47
-
48
201
  def auth_token
49
- @auth_token ||= VMC::Cli::Config.auth_token
202
+ @auth_token = VMC::Cli::Config.auth_token(@options[:token_file])
50
203
  end
51
204
 
52
205
  def runtimes_info
@@ -0,0 +1,56 @@
1
+ module VMC::Cli::Command
2
+ class Manifest < Base
3
+ include VMC::Cli::ManifestHelper
4
+
5
+ def initialize(options)
6
+ super
7
+
8
+ # don't resolve any of the manifest template stuff
9
+ if manifest_file
10
+ @manifest = load_manifest_structure manifest_file
11
+ else
12
+ @manifest = {}
13
+ end
14
+ end
15
+
16
+ def edit
17
+ build_manifest
18
+ save_manifest
19
+ end
20
+
21
+ def extend(which)
22
+ parent = load_manifest_structure which
23
+ @manifest = load_manifest_structure which
24
+
25
+ build_manifest
26
+
27
+ simplify(@manifest, parent)
28
+
29
+ @manifest["inherit"] ||= []
30
+ @manifest["inherit"] << which
31
+
32
+ save_manifest(ask("Save where?"))
33
+ end
34
+
35
+ private
36
+
37
+ def simplify(child, parent)
38
+ return unless child.is_a?(Hash) and parent.is_a?(Hash)
39
+
40
+ child.reject! do |k, v|
41
+ if v == parent[k]
42
+ puts "rejecting #{k}"
43
+ true
44
+ else
45
+ simplify(v, parent[k])
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ def build_manifest
52
+ @application = ask("Configure for which application?", :default => ".")
53
+ interact true
54
+ end
55
+ end
56
+ end
@@ -1,3 +1,5 @@
1
+ require "uuidtools"
2
+
1
3
  module VMC::Cli::Command
2
4
 
3
5
  class Services < Base
@@ -112,21 +114,18 @@ module VMC::Cli::Command
112
114
 
113
115
  err "Unknown service '#{service}'" unless info
114
116
 
115
- # TODO: rather than default to a particular port, we should get
116
- # the defaults based on the service name.. i.e. known services should
117
- # have known local default ports for this side of the tunnel.
118
117
  port = pick_tunnel_port(@options[:port] || 10000)
119
118
 
120
119
  raise VMC::Client::AuthError unless client.logged_in?
121
120
 
122
121
  if not tunnel_pushed?
123
122
  display "Deploying tunnel application '#{tunnel_appname}'."
124
- auth = ask("Create a password", :echo => "*")
123
+ auth = UUIDTools::UUID.random_create.to_s
125
124
  push_caldecott(auth)
126
125
  bind_service_banner(service, tunnel_appname, false)
127
126
  start_caldecott
128
127
  else
129
- auth = ask("Password", :echo => "*")
128
+ auth = tunnel_auth
130
129
  end
131
130
 
132
131
  if not tunnel_healthy?(auth)
@@ -166,7 +165,7 @@ module VMC::Cli::Command
166
165
  wait_for_tunnel_end
167
166
  else
168
167
  wait_for_tunnel_start(port)
169
- unless start_local_prog(clients[client_name], conn_info, port)
168
+ unless start_local_prog(clients, client_name, conn_info, port)
170
169
  err "'#{client_name}' executation failed; is it in your $PATH?"
171
170
  end
172
171
  end
@@ -57,7 +57,7 @@ module VMC::Cli::Command
57
57
 
58
58
  def login_and_save_token(email, password)
59
59
  token = client.login(email, password)
60
- VMC::Cli::Config.store_token(token)
60
+ VMC::Cli::Config.store_token(token, @options[:token_file])
61
61
  end
62
62
 
63
63
  end
data/lib/cli/config.rb CHANGED
@@ -37,6 +37,10 @@ module VMC::Cli
37
37
  @target_url
38
38
  end
39
39
 
40
+ def base_of(url)
41
+ url.sub(/^[^\.]+\./, "")
42
+ end
43
+
40
44
  def suggest_url
41
45
  return @suggest_url if @suggest_url
42
46
  ha = target_url.split('.')
@@ -51,8 +55,8 @@ module VMC::Cli
51
55
  lock_and_write(target_file, target_host)
52
56
  end
53
57
 
54
- def all_tokens
55
- token_file = File.expand_path(TOKEN_FILE)
58
+ def all_tokens(token_file_path=nil)
59
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
56
60
  return nil unless File.exists? token_file
57
61
  contents = lock_and_read(token_file).strip
58
62
  JSON.parse(contents)
@@ -60,9 +64,9 @@ module VMC::Cli
60
64
 
61
65
  alias :targets :all_tokens
62
66
 
63
- def auth_token
67
+ def auth_token(token_file_path=nil)
64
68
  return @token if @token
65
- tokens = all_tokens
69
+ tokens = all_tokens(token_file_path)
66
70
  @token = tokens[target_url] if tokens
67
71
  end
68
72
 
@@ -70,10 +74,10 @@ module VMC::Cli
70
74
  FileUtils.rm_f(File.expand_path(TOKEN_FILE))
71
75
  end
72
76
 
73
- def store_token(token)
74
- tokens = all_tokens || {}
77
+ def store_token(token, token_file_path=nil)
78
+ tokens = all_tokens(token_file_path) || {}
75
79
  tokens[target_url] = token
76
- token_file = File.expand_path(TOKEN_FILE)
80
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
77
81
  lock_and_write(token_file, tokens.to_json)
78
82
  end
79
83
 
@@ -129,8 +133,12 @@ module VMC::Cli
129
133
  end
130
134
 
131
135
  def lock_and_read(file)
132
- File.open(file, "r") {|f|
133
- f.flock(File::LOCK_EX)
136
+ File.open(file, File::RDONLY) {|f|
137
+ if defined? JRUBY_VERSION
138
+ f.flock(File::LOCK_SH)
139
+ else
140
+ f.flock(File::LOCK_EX)
141
+ end
134
142
  contents = f.read
135
143
  f.flock(File::LOCK_UN)
136
144
  contents
@@ -0,0 +1,238 @@
1
+ require "set"
2
+
3
+ module VMC::Cli::ManifestHelper
4
+ include VMC::Cli::ServicesHelper
5
+
6
+ DEFAULTS = {
7
+ "url" => "${name}.${target-base}",
8
+ "mem" => "128M",
9
+ "instances" => 1
10
+ }
11
+
12
+ MANIFEST = "manifest.yml"
13
+
14
+ YES_SET = Set.new(["y", "Y", "yes", "YES"])
15
+
16
+ # take a block and call it once for each app to push/update.
17
+ # with @application and @app_info set appropriately
18
+ def each_app(panic=true)
19
+ if @manifest and all_apps = @manifest["applications"]
20
+ where = File.expand_path(@path)
21
+ single = false
22
+
23
+ all_apps.each do |path, info|
24
+ app = File.expand_path("../" + path, manifest_file)
25
+ if where.start_with?(app)
26
+ @application = app
27
+ @app_info = info
28
+ yield info["name"]
29
+ single = true
30
+ break
31
+ end
32
+ end
33
+
34
+ unless single
35
+ if where == File.expand_path("../", manifest_file)
36
+ ordered_by_deps(all_apps).each do |path, info|
37
+ app = File.expand_path("../" + path, manifest_file)
38
+ @application = app
39
+ @app_info = info
40
+ yield info["name"]
41
+ end
42
+ else
43
+ err "Path '#{@path}' is not known to manifest '#{manifest_file}'."
44
+ end
45
+ end
46
+ else
47
+ @application = @path
48
+ @app_info = @manifest
49
+ if @app_info
50
+ yield @app_info["name"]
51
+ elsif panic
52
+ err "No applications."
53
+ end
54
+ end
55
+
56
+ nil
57
+ ensure
58
+ @application = nil
59
+ @app_info = nil
60
+ end
61
+
62
+ def interact(many=false)
63
+ @manifest ||= {}
64
+ configure_app(many)
65
+ end
66
+
67
+ def target_manifest
68
+ @options[:manifest] || MANIFEST
69
+ end
70
+
71
+ def save_manifest(save_to = nil)
72
+ save_to ||= target_manifest
73
+
74
+ File.open(save_to, "w") do |f|
75
+ f.write @manifest.to_yaml
76
+ end
77
+
78
+ say "Manifest written to #{save_to}."
79
+ end
80
+
81
+ def configure_app(many=false)
82
+ name = manifest("name") ||
83
+ set(ask("Application Name", :default => manifest("name")), "name")
84
+
85
+ url_template = manifest("url") || DEFAULTS["url"]
86
+ url_resolved = url_template.dup
87
+ resolve_lexically(url_resolved)
88
+
89
+ url = ask("Application Deployed URL", :default => url_resolved)
90
+
91
+ url = url_template if url == url_resolved
92
+
93
+ # common error case is for prompted users to answer y or Y or yes or
94
+ # YES to this ask() resulting in an unintended URL of y. Special
95
+ # case this common error
96
+ url = DEFAULTS["url"] if YES_SET.member? url
97
+
98
+ set url, "url"
99
+
100
+ unless manifest "framework"
101
+ framework = detect_framework
102
+ set framework.name, "framework", "name"
103
+ set(
104
+ { "mem" => framework.mem,
105
+ "description" => framework.description,
106
+ "exec" => framework.exec
107
+ },
108
+ "framework",
109
+ "info"
110
+ )
111
+ end
112
+
113
+ set ask(
114
+ "Memory reservation",
115
+ :default =>
116
+ manifest("mem") ||
117
+ manifest("framework", "info", "mem") ||
118
+ DEFAULTS["mem"],
119
+ :choices => ["128M", "256M", "512M", "1G", "2G"]
120
+ ), "mem"
121
+
122
+ set ask(
123
+ "How many instances?",
124
+ :default => manifest("instances") || DEFAULTS["instances"]
125
+ ), "instances"
126
+
127
+ unless manifest "services"
128
+ services = client.services_info
129
+ unless services.empty?
130
+ bind = ask "Would you like to bind any services to '#{name}'?", :default => false
131
+ bind_services(services.values.collect(&:keys).flatten) if bind
132
+ end
133
+ end
134
+
135
+ if many and ask("Configure for another application?", :default => false)
136
+ @application = ask "Application path?"
137
+ configure_app
138
+ end
139
+ end
140
+
141
+ def set(what, *where)
142
+ where.unshift "applications", @application
143
+
144
+ which = @manifest
145
+ where.each_with_index do |k, i|
146
+ if i + 1 == where.size
147
+ which[k] = what
148
+ else
149
+ which = (which[k] ||= {})
150
+ end
151
+ end
152
+
153
+ what
154
+ end
155
+
156
+ # Detect the appropriate framework.
157
+ def detect_framework(prompt_ok = true)
158
+ framework = VMC::Cli::Framework.detect(@application)
159
+ framework_correct = ask("Detected a #{framework}, is this correct?", :default => true) if prompt_ok && framework
160
+ if prompt_ok && (framework.nil? || !framework_correct)
161
+ display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
162
+ framework = nil if !framework_correct
163
+ framework = VMC::Cli::Framework.lookup(
164
+ ask(
165
+ "Select Application Type",
166
+ :indexed => true,
167
+ :default => framework,
168
+ :choices => VMC::Cli::Framework.known_frameworks
169
+ )
170
+ )
171
+ display "Selected #{framework}"
172
+ end
173
+
174
+ framework
175
+ end
176
+
177
+ def bind_services(services)
178
+ svcs = services.collect(&:to_s).sort!
179
+
180
+ display "The following system services are available"
181
+ configure_service(
182
+ ask(
183
+ "Please select the one you wish to provision",
184
+ :indexed => true,
185
+ :choices => svcs
186
+ ).to_sym
187
+ )
188
+
189
+ if ask "Would you like to bind another service?", :default => false
190
+ bind_services(services)
191
+ end
192
+ end
193
+
194
+ def configure_service(vendor)
195
+ default_name = random_service_name(vendor)
196
+ name = ask "Specify the name of the service", :default => default_name
197
+
198
+ set vendor, "services", name, "type"
199
+ end
200
+
201
+ private
202
+ def ordered_by_deps(apps, abspaths = nil, processed = Set[])
203
+ unless abspaths
204
+ abspaths = {}
205
+ apps.each do |p, i|
206
+ ep = File.expand_path("../" + p, manifest_file)
207
+ abspaths[ep] = i
208
+ end
209
+ end
210
+
211
+ ordered = []
212
+ apps.each do |path, info|
213
+ epath = File.expand_path("../" + path, manifest_file)
214
+
215
+ if deps = info["depends-on"]
216
+ dep_apps = {}
217
+ deps.each do |dep|
218
+ edep = File.expand_path("../" + dep, manifest_file)
219
+
220
+ err "Circular dependency detected." if processed.include? edep
221
+
222
+ dep_apps[dep] = abspaths[edep]
223
+ end
224
+
225
+ processed.add(epath)
226
+
227
+ ordered += ordered_by_deps(dep_apps, abspaths, processed)
228
+ ordered << [path, info]
229
+ elsif not processed.include? epath
230
+ ordered << [path, info]
231
+ processed.add(epath)
232
+ end
233
+ end
234
+
235
+ ordered
236
+ end
237
+
238
+ end
data/lib/cli/runner.rb CHANGED
@@ -31,6 +31,7 @@ class VMC::Cli::Runner
31
31
  opts.on('--passwd PASS') { |pass| @options[:password] = pass }
32
32
  opts.on('--pass PASS') { |pass| @options[:password] = pass }
33
33
  opts.on('--password PASS') { |pass| @options[:password] = pass }
34
+ opts.on('--token-file TOKEN_FILE') { |token_file| @options[:token_file] = token_file }
34
35
  opts.on('--app NAME') { |name| @options[:name] = name }
35
36
  opts.on('--name NAME') { |name| @options[:name] = name }
36
37
  opts.on('--bind BIND') { |bind| @options[:bind] = bind }
@@ -52,6 +53,10 @@ class VMC::Cli::Runner
52
53
  opts.on('-d [MODE]') { |mode| @options[:debug] = mode || "run" }
53
54
  opts.on('--debug [MODE]') { |mode| @options[:debug] = mode || "run" }
54
55
 
56
+ # override manifest file
57
+ opts.on('-m FILE') { |file| @options[:manifest] = file }
58
+ opts.on('--manifest FILE') { |file| @options[:manifest] = file }
59
+
55
60
  opts.on('-q', '--quiet') { @options[:quiet] = true }
56
61
 
57
62
  # Don't use builtin zip
@@ -223,15 +228,15 @@ class VMC::Cli::Runner
223
228
 
224
229
  when 'start'
225
230
  usage('vmc start <appname>')
226
- set_cmd(:apps, :start, 1)
231
+ set_cmd(:apps, :start, @args.size == 1 ? 1 : 0)
227
232
 
228
233
  when 'stop'
229
234
  usage('vmc stop <appname>')
230
- set_cmd(:apps, :stop, 1)
235
+ set_cmd(:apps, :stop, @args.size == 1 ? 1 : 0)
231
236
 
232
237
  when 'restart'
233
238
  usage('vmc restart <appname>')
234
- set_cmd(:apps, :restart, 1)
239
+ set_cmd(:apps, :restart, @args.size == 1 ? 1 : 0)
235
240
 
236
241
  when 'rename'
237
242
  usage('vmc rename <appname> <newname>')
@@ -247,7 +252,7 @@ class VMC::Cli::Runner
247
252
 
248
253
  when 'stats'
249
254
  usage('vmc stats <appname>')
250
- set_cmd(:apps, :stats, 1)
255
+ set_cmd(:apps, :stats, @args.size == 1 ? 1 : 0)
251
256
 
252
257
  when 'map'
253
258
  usage('vmc map <appname> <url>')
@@ -278,12 +283,12 @@ class VMC::Cli::Runner
278
283
  set_cmd(:apps, :logs, 1)
279
284
 
280
285
  when 'instances', 'scale'
281
- if @args.size == 1
282
- usage('vmc instances <appname>')
283
- set_cmd(:apps, :instances, 1)
284
- else
286
+ if @args.size > 1
285
287
  usage('vmc instances <appname> <num|delta>')
286
288
  set_cmd(:apps, :instances, 2)
289
+ else
290
+ usage('vmc instances <appname>')
291
+ set_cmd(:apps, :instances, @args.size == 1 ? 1 : 0)
287
292
  end
288
293
 
289
294
  when 'crashes'
@@ -304,7 +309,7 @@ class VMC::Cli::Runner
304
309
 
305
310
  when 'update'
306
311
  usage('vmc update <appname> [--path PATH]')
307
- set_cmd(:apps, :update, 1)
312
+ set_cmd(:apps, :update, @args.size == 1 ? 1 : 0)
308
313
 
309
314
  when 'services'
310
315
  usage('vmc services')
@@ -389,6 +394,14 @@ class VMC::Cli::Runner
389
394
  @args = @args.unshift('--options')
390
395
  parse_options!
391
396
 
397
+ when 'manifest'
398
+ usage('vmc manifest')
399
+ set_cmd(:manifest, :edit)
400
+
401
+ when 'extend-manifest'
402
+ usage('vmc extend-manifest')
403
+ set_cmd(:manifest, :extend, 1)
404
+
392
405
  else
393
406
  if verb
394
407
  display "vmc: Unknown command [#{verb}]"
@@ -437,7 +450,8 @@ class VMC::Cli::Runner
437
450
  parse_command!
438
451
 
439
452
  if @namespace && @action
440
- eval("VMC::Cli::Command::#{@namespace.to_s.capitalize}").new(@options).send(@action.to_sym, *@args)
453
+ cmd = VMC::Cli::Command.const_get(@namespace.to_s.capitalize)
454
+ cmd.new(@options).send(@action, *@args.collect(&:dup))
441
455
  elsif @help_only || @usage
442
456
  display_usage
443
457
  else