vmc 0.3.15 → 0.3.16.beta.1

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