elasticdot 1.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/bin/elasticdot ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'elasticdot/cli'
4
+
5
+ ElasticDot::CLI.run ARGV
data/lib/elasticdot.rb ADDED
@@ -0,0 +1,3 @@
1
+ module ElasticDot
2
+ require 'elasticdot/cli'
3
+ end
@@ -0,0 +1,60 @@
1
+ class ElasticDot::API
2
+ require 'restclient'
3
+ require 'json'
4
+
5
+ attr_reader :email, :host
6
+
7
+ def initialize(opts = {})
8
+ @host = opts[:host] || 'https://api.elasticdot.com'
9
+
10
+ if opts[:email] and opts[:password]
11
+ @email, @pass = opts[:email], opts[:password]
12
+ else
13
+ @email, @pass = ElasticDot::Command::Auth.credentials
14
+ end
15
+ end
16
+
17
+ def login
18
+ return unless (@email && @pass)
19
+
20
+ @pass = RestClient.post(
21
+ "#{@host}/auth",
22
+ email: @email, password: @pass
23
+ )
24
+ rescue => e
25
+ puts e.response
26
+ exit 1
27
+ end
28
+
29
+ def method_missing(m, *args, &block)
30
+ unless ['get', 'post', 'put', 'delete'].include? m.to_s
31
+ raise NoMethodError, "undefined method: #{m}"
32
+ end
33
+
34
+ res = self.send('req', m, args)
35
+ JSON.parse res rescue ""
36
+ end
37
+
38
+ private
39
+ def req(method, *args)
40
+ raise if args.empty?
41
+ args = args.shift
42
+ path = args.shift
43
+
44
+ resource = RestClient::Resource.new(
45
+ "#{@host}#{path}", user: @email, password: @pass
46
+ )
47
+
48
+ begin
49
+ resource.send method, (args.first || {})
50
+ rescue => e
51
+ if e.respond_to? :response
52
+ puts e.response
53
+ else
54
+ puts 'Something went wrong, we have been notified.'
55
+ end
56
+
57
+ exit 1
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ module ElasticDot
2
+ libs = Dir[File.join(File.dirname(__FILE__), "initializers", "*.rb")]
3
+ libs.each { |file| require file }
4
+
5
+ require 'elasticdot/command'
6
+
7
+ class CLI
8
+ require 'optparse'
9
+
10
+ def self.parse(args)
11
+ options = {}
12
+
13
+ opt_parser = OptionParser.new do |opts|
14
+ opts.banner = "Usage: example.rb [options]"
15
+
16
+ opts.on("-a", "--app APP", "Specify the app involved") do |v|
17
+ options[:app] = v
18
+ end
19
+
20
+ opts.on("-d", "--database DATABASE", "Specify the database involved") do |v|
21
+ options[:db] = v
22
+ end
23
+
24
+ opts.on("-f", "--follow", "Continue running and print new events (off)") do |v|
25
+ options[:follow] = v
26
+ end
27
+
28
+ opts.on("-p", "--plan PLAN", "Specify pricing plan") do |v|
29
+ options[:plan] = v
30
+ end
31
+
32
+ opts.on("-c", "--extra-conf CONF", "Specify extra config file") do |v|
33
+ options[:conf] = v
34
+ end
35
+
36
+ opts.on("-h", "--help", "Show this help") do |v|
37
+ options[:help] = true
38
+ end
39
+ end
40
+
41
+ opt_parser.parse! args
42
+
43
+ [ARGV, options]
44
+ end
45
+
46
+ def self.run(args)
47
+ cmd, opts = parse ARGV
48
+ ElasticDot::Command.run cmd, opts
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,67 @@
1
+ module ElasticDot::Command
2
+ require 'elasticdot/api'
3
+ require 'elasticdot/command/base'
4
+
5
+ libs = Dir[File.join(File.dirname(__FILE__), "command", "*.rb")]
6
+ libs.each { |file| require file }
7
+
8
+ def self.run(args, opts)
9
+ cmd = args.shift
10
+
11
+ unless cmd
12
+ ElasticDot::Command::Help.root_help
13
+ exit 0
14
+ end
15
+
16
+ klass, act = cmd.split ':', 2
17
+ klass = klass.downcase
18
+
19
+ if klass == 'help'
20
+ m = args.shift || 'root_help'
21
+ m = m.split(':')[0]
22
+
23
+ ElasticDot::Command::Help.send m
24
+ exit 0
25
+ end
26
+
27
+ unless (klass == 'login' or cmd == 'auth:login')
28
+ ElasticDot::Command::Auth.authenticate!
29
+ end
30
+
31
+ ElasticDot::Command::Base.api = ElasticDot::API.new
32
+
33
+ if ElasticDot::Command::Alias.respond_to? klass
34
+ ElasticDot::Command::Alias.send klass.downcase, args, opts
35
+ exit 0
36
+ end
37
+
38
+ act ||= 'list'
39
+
40
+ begin
41
+ klass = "ElasticDot::Command::#{klass.capitalize}".constantize
42
+ rescue
43
+ unless ElasticDot::Command::Base.respond_to? cmd
44
+ puts 'Invalid command: ' + cmd
45
+ exit 1
46
+ end
47
+
48
+ return ElasticDot::Command::Base.send cmd
49
+ end
50
+
51
+ unless klass.respond_to? act
52
+ puts 'Invalid action ' + act
53
+ exit 1
54
+ end
55
+
56
+ method_args = klass.method(act).arity
57
+
58
+ case method_args
59
+ when 0
60
+ klass.send act
61
+ when 1
62
+ klass.send act, opts
63
+ when 2
64
+ klass.send act, args, opts
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ class ElasticDot::Command::Addons < ElasticDot::Command::Base
2
+ def self.add(addon, opts)
3
+ find_app! opts
4
+
5
+ addon, tier = addon[0].split ':', 2
6
+
7
+ puts "Configuring addon #{addon} for app #{@app}..."
8
+
9
+ api.post "/apps/#{@app}/addons/#{addon}", tier: tier
10
+ end
11
+
12
+ def self.remove(addons, opts)
13
+ find_app! opts
14
+
15
+ addons.each do |addon|
16
+ addon = addon.split(':')[0]
17
+
18
+ puts "Removing addon #{addon} from app #{@app}..."
19
+ api.delete "/apps/#{@app}/addons/#{addon}"
20
+ end
21
+ end
22
+
23
+ def self.list
24
+ addons = api.get '/addons'
25
+
26
+ puts '=== available'
27
+ addons.each { |a, i| puts a['name'] }
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ class ElasticDot::Command::Alias < ElasticDot::Command::Base
2
+ def self.login(*opts)
3
+ ElasticDot::Command::Auth.login
4
+ end
5
+
6
+ def self.logout(*opts)
7
+ ElasticDot::Command::Auth.logout
8
+ end
9
+
10
+ def self.signup(*opts)
11
+ puts 'Please go to: https://s.elasticdot.com/signup'
12
+ end
13
+
14
+ def self.version(*opts)
15
+ puts 'ElasticDot CLI 1.3.3'
16
+ end
17
+
18
+ def self.ls(*opts)
19
+ ElasticDot::Command::Apps.list
20
+ end
21
+
22
+ def self.create(args, opts)
23
+ ElasticDot::Command::Apps.create args, opts
24
+ end
25
+
26
+ def self.info(args, opts)
27
+ ElasticDot::Command::Apps.info opts
28
+ end
29
+
30
+ def self.open(args, opts)
31
+ ElasticDot::Command::Apps.open opts
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ class ElasticDot::Command::Apps < ElasticDot::Command::Base
2
+ def self.create(args, opts)
3
+ info = api.post '/domains', domain: args[0]
4
+
5
+ if info['error']
6
+ puts info['error']
7
+ exit 1
8
+ end
9
+
10
+ puts "Creating app #{info['app_name']}... done"
11
+ puts "http://#{info['app_name']}.elasticdot.io/ | #{info['app_repo']}"
12
+
13
+ create_git_remote 'elasticdot', info['app_repo']
14
+ end
15
+
16
+ def self.open(opts)
17
+ require 'launchy'
18
+
19
+ find_app! opts
20
+
21
+ spinner "Opening #{@app}..." do
22
+ info = api.get "/domains/#{@app}"
23
+ Launchy.open info['live_address']
24
+ end
25
+ end
26
+
27
+ def self.destroy(opts)
28
+ find_app! opts
29
+
30
+ spinner "Destroying app #{@app}..." do
31
+ api.delete "/domains/#{@app}"
32
+ end
33
+ end
34
+
35
+ def self.info(opts)
36
+ find_app! opts
37
+
38
+ h = api.get "/domains/#{@app}"
39
+
40
+ puts "=== #{@app}"
41
+ puts
42
+ puts "Git URL:\t#{h['git_repo']}"
43
+ puts "Owner Email:\t#{h['owner_email']}"
44
+ puts "Region:\t\tEU"
45
+ # puts "Slug Size:\t#{h['slug_size']}"
46
+ puts "Web URL:\thttp://#{h['live_address']}"
47
+
48
+ return unless (db = h['db'])
49
+
50
+ puts
51
+ puts "=== Database #{db['identifier']}"
52
+ puts "Plan: \t#{db['plan']['name']}"
53
+ puts "Nodes: \t#{db['plan']['nodes']}" unless db['plan']['shared']
54
+ puts "Version: \t#{db['version']}"
55
+ puts "Status: \t#{db['status']}"
56
+ puts "Name: \t#{db['name']}"
57
+ puts "User: \t#{db['user']}"
58
+ puts "Password: \t#{db['pass']}"
59
+ puts "URI: \t#{db['uri']}"
60
+ puts "Tables: \t#{db['tables']}"
61
+ puts "Disk Space Used:\t#{db['space_used']}"
62
+ puts "AVG CPU Load: \t#{db['cpu_load']}"
63
+ puts "Created at: \t#{db['created_at']}"
64
+ end
65
+
66
+ def self.list
67
+ apps = api.get "/apps?type=web"
68
+ puts '=== My Apps'
69
+ apps.each { |app| puts app }
70
+ end
71
+ end
@@ -0,0 +1,67 @@
1
+ class ElasticDot::Command::Auth < ElasticDot::Command::Base
2
+ require 'netrc'
3
+
4
+ def self.login
5
+ puts "Enter your ElasticDot credentials."
6
+
7
+ print "email: "
8
+ email = ask
9
+
10
+ print "password: "
11
+
12
+ echo_off
13
+ pass = ask_for_password
14
+ echo_on
15
+
16
+ api = ElasticDot::API.new(email: email, password: pass)
17
+ key = api.login
18
+
19
+ netrc.delete 'j.elasticops.com'
20
+
21
+ netrc['j.elasticops.com'] = email, key
22
+ netrc.save
23
+
24
+ true
25
+ end
26
+
27
+ def self.logout
28
+ netrc.delete 'j.elasticops.com'
29
+ netrc.save
30
+
31
+ true
32
+ end
33
+
34
+ def self.authenticate!
35
+ return true if authenticated?
36
+ login
37
+ end
38
+
39
+ def self.credentials
40
+ netrc['j.elasticops.com']
41
+ end
42
+
43
+ private
44
+ def self.netrc
45
+ @netrc ||= Netrc.read netrc_path
46
+ end
47
+
48
+ def self.netrc_path
49
+ default = Netrc.default_path
50
+ encrypted = default + ".gpg"
51
+
52
+ File.exists?(encrypted) ? encrypted : default
53
+ end
54
+
55
+ def self.ask_for_password
56
+ echo_off
57
+ password = ask
58
+ puts
59
+ echo_on
60
+
61
+ password
62
+ end
63
+
64
+ def self.authenticated?
65
+ netrc['j.elasticops.com'] ? true : false
66
+ end
67
+ end
@@ -0,0 +1,121 @@
1
+ class Module
2
+ def cattr_accessor(attribute_name)
3
+ class_eval <<-CODE
4
+ def self.#{attribute_name}
5
+ @@#{attribute_name} ||= nil
6
+ end
7
+ def self.#{attribute_name}=(value)
8
+ @@#{attribute_name} = value
9
+ end
10
+ CODE
11
+ end
12
+ end
13
+
14
+ class ElasticDot::Command::Base
15
+ cattr_accessor :api
16
+
17
+ protected
18
+ def self.echo_off
19
+ with_tty do
20
+ system "stty -echo"
21
+ end
22
+ end
23
+
24
+ def self.echo_on
25
+ with_tty do
26
+ system "stty echo"
27
+ end
28
+ end
29
+
30
+ def self.with_tty(&block)
31
+ return unless $stdin.isatty
32
+ yield
33
+ end
34
+
35
+ def self.ask
36
+ $stdin.gets.to_s.strip
37
+ end
38
+
39
+ def self.find_app!(opts)
40
+ if opts[:app]
41
+ @app = opts[:app]
42
+ elsif app = extract_app_in_dir
43
+ @app = app
44
+ end
45
+
46
+ return true if @app
47
+
48
+ puts 'No app specified.'
49
+ puts 'Specify which app to use with --app APP.'
50
+ exit 1
51
+ end
52
+
53
+ def self.has_git?
54
+ %x{ git --version }
55
+ $?.success?
56
+ end
57
+
58
+ def self.git(args)
59
+ return "" unless has_git?
60
+ flattened_args = [args].flatten.compact.join(" ")
61
+ %x{ git #{flattened_args} 2>&1 }.strip
62
+ end
63
+
64
+ def self.create_git_remote(remote, url)
65
+ return if git('remote').split("\n").include?(remote)
66
+ return unless File.exists?(".git")
67
+
68
+ git "remote add #{remote} #{url}"
69
+
70
+ puts "Git remote #{remote} added"
71
+ end
72
+
73
+ def self.which(cmd)
74
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
75
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
76
+ exts.each { |ext|
77
+ exe = File.join(path, "#{cmd}#{ext}")
78
+ return exe if File.executable? exe
79
+ }
80
+ end
81
+ return nil
82
+ end
83
+
84
+ def self.extract_app_in_dir
85
+ if app = extract_app_from_git_config
86
+ return app
87
+ elsif app = extract_app_from_git_config('elasticdot')
88
+ return app
89
+ else
90
+ nil
91
+ end
92
+ end
93
+
94
+ def self.extract_app_from_git_config(remote = 'origin')
95
+ url = git "config remote.#{remote}.url"
96
+ return nil if url.empty?
97
+
98
+ if url =~ /^git@git\.elasticdot\.com:([\w\d\-\.]+)\.git$/
99
+ $1
100
+ else
101
+ nil
102
+ end
103
+ end
104
+
105
+ def self.spinner(desc, &block)
106
+ chars = %w{ | / - \\ }
107
+
108
+ t = Thread.new { block.call }
109
+ while t.alive?
110
+ print "#{desc} [#{chars[0]}]"
111
+ sleep 0.1
112
+ print "\r"
113
+
114
+ chars.push chars.shift
115
+ end
116
+
117
+ t.join
118
+
119
+ puts "#{desc} [ok]"
120
+ end
121
+ end
@@ -0,0 +1,70 @@
1
+ class ElasticDot::Command::Config < ElasticDot::Command::Base
2
+ def self.set(vars, opts)
3
+ unless vars.size > 0 and vars.all? { |a| a.include?('=') }
4
+ puts "Usage: elasticdot config:set KEY1=VALUE1 [KEY2=VALUE2 ...]\nMust specify KEY and VALUE to set."
5
+ exit 1
6
+ end
7
+
8
+ vars = parse_vars! vars
9
+
10
+ find_app! opts
11
+
12
+ puts "Setting ENV vars..."
13
+
14
+ api.post "/apps/#{@app}/vars", vars: vars
15
+
16
+ vars.each { |k, v| puts "#{k}:\t#{v}" }
17
+
18
+ puts
19
+ puts 'Please use ps:restart command to restart your app when you\'re ready.'
20
+ end
21
+
22
+ def self.unset(vars, opts)
23
+ if vars.empty?
24
+ puts "Usage: elasticdot config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset."
25
+ exit 1
26
+ end
27
+
28
+ find_app! opts
29
+
30
+ puts "Unsetting ENV vars..."
31
+
32
+ api.post "/apps/#{@app}/vars/unset", vars: vars
33
+
34
+ puts
35
+ puts 'Please use ps:restart command to restart your app when you\'re ready.'
36
+ end
37
+
38
+ def self.get(args, opts)
39
+ var = args.shift
40
+ find_app! opts
41
+
42
+ vars = api.get("/domains/#{@app}")['vars']
43
+
44
+ value = vars.select {|v| v['key_name'] == var }.first['value'] rescue nil
45
+
46
+ puts value if value
47
+ end
48
+
49
+ def self.list(opts)
50
+ find_app! opts
51
+
52
+ puts "=== #{@app} Config Vars"
53
+
54
+ vars = api.get("/domains/#{@app}")['vars']
55
+
56
+ vars.each {|v| puts "#{v['key_name']}:\t#{v['value']}" }
57
+ end
58
+
59
+ private
60
+ def self.parse_vars!(vars)
61
+ parsed_vars = {}
62
+
63
+ vars.each do |var|
64
+ k, v = var.split '=', 2
65
+ parsed_vars[k] = v || ''
66
+ end
67
+
68
+ parsed_vars
69
+ end
70
+ end
@@ -0,0 +1,182 @@
1
+ class ElasticDot::Command::Db < ElasticDot::Command::Base
2
+ require 'uri'
3
+
4
+ def self.promote(opts)
5
+ find_app! opts
6
+ find_db! opts
7
+
8
+ spinner "Promoting database..." do
9
+ info = api.post "/databases/#{@db}/promote", app: @app
10
+ end
11
+
12
+ spinner "Restarting dots..." do
13
+ loop do
14
+ sleep 3
15
+ info = api.get "/domains/#{@app}"
16
+ break if info['status'] == 'active'
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.dump(opts)
22
+ unless which 'mysqldump'
23
+ puts 'MySQL client is not installed.'
24
+ puts 'Please install it to proceed.'
25
+ exit 1
26
+ end
27
+
28
+ find_db! opts
29
+
30
+ info = api.get("/databases/#{@db}")
31
+
32
+ uri = URI.parse info['uri']
33
+
34
+ system "mysqldump --opt -c -u#{info['user']} -p#{info['pass']} -h#{uri.host} -P#{uri.port} #{info['name']}"
35
+ end
36
+
37
+ def self.console(opts)
38
+ unless which 'mysql'
39
+ puts 'MySQL client is not installed.'
40
+ puts 'Please install it to proceed.'
41
+ exit 1
42
+ end
43
+
44
+ find_db! opts
45
+
46
+ info = api.get("/databases/#{@db}")
47
+
48
+ uri = URI.parse info['uri']
49
+
50
+ puts 'Attaching... '
51
+ system "mysql -f -u#{info['user']} -p#{info['pass']} -h#{uri.host} -P#{uri.port} #{info['name']}"
52
+ end
53
+
54
+ def self.import(args, opts)
55
+ unless which 'mysql'
56
+ puts 'MySQL client is not installed.'
57
+ puts 'Please install it to proceed.'
58
+ exit 1
59
+ end
60
+
61
+ find_db! opts
62
+
63
+ dump = args.shift
64
+ unless dump
65
+ puts 'Please specify a dump file.'
66
+ exit 1
67
+ end
68
+
69
+ unless File.exists? dump
70
+ puts "#{dump}: no such file or directory"
71
+ exit 1
72
+ end
73
+
74
+ info = api.get("/databases/#{@db}")
75
+
76
+ uri = URI.parse info['uri']
77
+
78
+ spinner 'Importing...' do
79
+ system "mysql -f -u#{info['user']} -p#{info['pass']} -h#{uri.host} -P#{uri.port} #{info['name']} < #{dump}"
80
+ end
81
+ end
82
+
83
+ def self.create(opts)
84
+ find_plan! opts
85
+
86
+ params = {plan: @plan}
87
+
88
+ if conf = opts[:conf]
89
+ unless File.exists? conf
90
+ puts "#{conf}: no such file or directory"
91
+ exit 1
92
+ end
93
+
94
+ f = File.read conf
95
+
96
+ params.merge!(conf: f)
97
+ end
98
+
99
+ info = api.post "/databases", params
100
+
101
+ spinner "Database #{info['identifier']} is provisioning..." do
102
+ until info['status'] == 'active'
103
+ sleep 3
104
+ info = api.get("/databases/#{info['identifier']}")
105
+ end
106
+ end
107
+ end
108
+
109
+ def self.destroy(opts)
110
+ find_db! opts
111
+
112
+ print "Destroying database #{@db}... "
113
+
114
+ info = api.delete("/databases/#{@db}")
115
+
116
+ puts 'done'
117
+ end
118
+
119
+ def self.info(opts)
120
+ find_db! opts
121
+
122
+ db = api.get("/databases/#{@db}")
123
+
124
+ puts "=== Database #{db['identifier']}"
125
+ puts "Plan: \t#{db['plan']['name']}"
126
+ puts "Nodes: \t#{db['plan']['nodes']}" unless db['plan']['shared']
127
+ puts "Version: \t#{db['version']}"
128
+ puts "Status: \t#{db['status']}"
129
+ puts "Name: \t#{db['name']}"
130
+ puts "User: \t#{db['user']}"
131
+ puts "Password: \t#{db['pass']}"
132
+ puts "URI: \t#{db['uri']}"
133
+ puts "Tables: \t#{db['tables']}"
134
+ puts "Disk Space Used:\t#{db['space_used']}"
135
+ puts "AVG CPU Load: \t#{db['cpu_load']}"
136
+ puts "Created at: \t#{db['created_at']}"
137
+ end
138
+
139
+ def self.list(opts)
140
+ puts "=== database list"
141
+
142
+ list = api.get("/databases")
143
+
144
+ list.each {|db| puts db['identifier'] }
145
+ end
146
+
147
+ private
148
+ def self.find_db!(opts)
149
+ @db = opts[:db]
150
+ return true if @db
151
+
152
+ app = opts[:app]
153
+ app ||= extract_app_in_dir
154
+
155
+ unless app
156
+ puts 'No db specified.'
157
+ puts 'Specify at least option --app or --database.'
158
+ exit 1
159
+ end
160
+
161
+ info = api.get "/domains/#{app}"
162
+
163
+ unless info['db']
164
+ puts 'This app has no database associated.'
165
+ puts 'Specify --database option.'
166
+ exit 1
167
+ end
168
+
169
+ @db = info['db']['identifier']
170
+
171
+ true
172
+ end
173
+
174
+ def self.find_plan!(opts)
175
+ @plan = opts[:plan]
176
+ return true if @plan
177
+
178
+ puts 'No plan specified.'
179
+ puts 'Specify which plan to use with -p, --plan PLAN.'
180
+ exit 1
181
+ end
182
+ end
@@ -0,0 +1,55 @@
1
+ class ElasticDot::Command::Domains < ElasticDot::Command::Base
2
+ def self.add(args, opts)
3
+ domain = args.shift
4
+ validate_domain! 'add', domain
5
+
6
+ app = opts[:app]
7
+ find_app! opts
8
+
9
+ puts "Adding #{domain} to #{@app}..."
10
+
11
+ api.post "/domains/#{@app}/aliases", alias: domain
12
+ end
13
+
14
+ def self.remove(args, opts)
15
+ domain = args.shift
16
+ validate_domain! 'remove', 'domain'
17
+
18
+ app = opts[:app]
19
+ find_app! opts
20
+
21
+ puts "Removing #{domain} from #{@app}..."
22
+
23
+ api.delete "/domains/#{@app}/aliases/#{domain}"
24
+ end
25
+
26
+ def self.clear(opts)
27
+ find_app! opts
28
+
29
+ domains = api.get("/domains/#{@app}")['aliases']
30
+
31
+ puts "Removing all domain names from #{@app}..."
32
+ domains.each do |d|
33
+ next if d['factory']
34
+ api.delete "/domains/#{@app}/aliases/#{d['name']}"
35
+ end
36
+ end
37
+
38
+ def self.list(opts)
39
+ find_app! opts
40
+
41
+ domains = api.get("/domains/#{@app}")['aliases']
42
+
43
+ puts "=== #{@app} Domain Names"
44
+ domains.each {|d| puts d['name'] }
45
+ end
46
+
47
+ private
48
+ def self.validate_domain!(m, d)
49
+ return true if d
50
+
51
+ puts "Usage: elasticdot domains:#{m} DOMAIN"
52
+ puts "Must specify DOMAIN to add."
53
+ exit 1
54
+ end
55
+ end
@@ -0,0 +1,106 @@
1
+ class ElasticDot::Command::Help < ElasticDot::Command::Base
2
+ def self.root_help
3
+ puts <<-HELP
4
+ Usage: elasticdot COMMAND [--app APP] [command-specific-options]
5
+
6
+ Primary help topics, type "elasticdot help TOPIC" for more details:
7
+
8
+ apps # manage apps
9
+ keys # manage authentication keys
10
+ config # manage app config vars
11
+ domains # manage custom domains
12
+ db # manage databases
13
+ addons # manage addon resources
14
+
15
+ Additional topics:
16
+
17
+ help # list commands and display help
18
+ version # display version
19
+ HELP
20
+ end
21
+
22
+ def self.apps
23
+ puts <<-HELP
24
+ Apps commands:
25
+
26
+ apps:create [NAME] # create a new app
27
+ apps:destroy # permanently destroy an app
28
+ apps:info # show detailed app information
29
+ apps:open # open the app in a web browser
30
+ apps:list # show app list
31
+ HELP
32
+ end
33
+
34
+ def self.addons
35
+ puts <<-HELP
36
+ Addons commands:
37
+
38
+ addons:list # list all available addons
39
+ addons:add ADDON # install an addon
40
+ addons:remove ADDON1 [ADDON2 ...] # uninstall one or more addons
41
+ HELP
42
+ end
43
+
44
+ def self.domains
45
+ puts <<-HELP
46
+ Domains commands:
47
+
48
+ domains:add DOMAIN # add a custom domain to an app
49
+ domains:clear # remove all custom domains from an app
50
+ domains:remove DOMAIN # remove a custom domain from an app
51
+ HELP
52
+ end
53
+
54
+ def self.keys
55
+ puts <<-HELP
56
+ Keys commands:
57
+
58
+ keys:add [KEY] # add a key for the current user
59
+ keys:clear # remove all authentication keys from the current user
60
+ keys:remove KEY # remove a key from the current user
61
+ HELP
62
+ end
63
+
64
+ def self.config
65
+ puts <<-HELP
66
+ Config commands:
67
+
68
+ config:get KEY # display a config value for an app
69
+ config:set KEY1=VALUE1 [KEY2=VALUE2 ...] # set one or more config vars
70
+ config:unset KEY1 [KEY2 ...] # unset one or more config vars
71
+ HELP
72
+ end
73
+
74
+ def self.ps
75
+ puts <<-HELP
76
+ PS commands:
77
+
78
+ ps:resize web=TIER # resize dot to the given tier
79
+ ps:scale web=N [mode=scaling|manual] # scale dots by the given amount
80
+ ps:stop # stop all dots
81
+ ps:restart # restart all dots
82
+ HELP
83
+ end
84
+
85
+ def self.db
86
+ puts <<-HELP
87
+ DB commands:
88
+
89
+ db:promote # sets DATABASE as your DATABASE_URL
90
+ db:dump # print DATABASE dump to stdout
91
+ db:console # open mysql console for DATABASE
92
+ db:import # import dump file to DATABASE
93
+ db:create # create new database
94
+ db:destroy # destroy DATABASE
95
+ db:info # show info for DATABASE
96
+ db:list # list all databases
97
+ HELP
98
+ end
99
+
100
+ def self.method_missing(m, *args, &block)
101
+ unless self.respond_to? m
102
+ puts "Invalid command: #{m}"
103
+ exit 1
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,93 @@
1
+ class ElasticDot::Command::Keys < ElasticDot::Command::Base
2
+ def self.add
3
+ associate_or_generate_ssh_key
4
+ end
5
+
6
+ def self.remove(keys, opts)
7
+ rkeys = api.get('/stats')['profile']['keys']
8
+
9
+ keys.each do |k|
10
+ rk = rkeys.select {|rk| rk['content'] =~ /#{k}/ }.first
11
+
12
+ unless rk
13
+ puts "Key not found, skipping #{k}..."
14
+ next
15
+ end
16
+
17
+ api.delete "/account/keys/#{rk['id']}"
18
+ end
19
+ end
20
+
21
+ def self.clear
22
+ api.get('/stats')['profile']['keys'].each do |k|
23
+ api.delete "/account/keys/#{k['id']}"
24
+ end
25
+ end
26
+
27
+ def self.list
28
+ puts "=== #{api.email} Keys"
29
+ keys = api.get('/stats')['profile']['keys'].each do |k|
30
+ puts k['content']
31
+ end
32
+ end
33
+
34
+ private
35
+ def self.associate_or_generate_ssh_key
36
+ public_keys = Dir.glob("#{Dir.home}/.ssh/*.pub").sort
37
+
38
+ case public_keys.length
39
+ when 0 then
40
+ puts "Could not find an existing public key."
41
+ print "Would you like to generate one? [Yn] "
42
+
43
+ if ask.strip.downcase == "y"
44
+ puts "Generating new SSH public key."
45
+ generate_ssh_key("id_rsa")
46
+ associate_key("#{Dir.home}/.ssh/id_rsa.pub")
47
+ end
48
+ when 1 then
49
+ puts "Found existing public key: #{public_keys.first}"
50
+ associate_key(public_keys.first)
51
+ else
52
+ puts "Found the following SSH public keys:"
53
+ public_keys.each_with_index do |key, index|
54
+ puts "#{index+1}) #{File.basename(key)}"
55
+ end
56
+
57
+ print "Which would you like to use with your ElasticDot account? "
58
+ choice = ask.to_i - 1
59
+ chosen = public_keys[choice]
60
+ if choice == -1 || chosen.nil?
61
+ puts "Invalid choice"
62
+ exit 1
63
+ end
64
+
65
+ associate_key(chosen)
66
+ end
67
+ end
68
+
69
+ def self.generate_ssh_key(keyfile)
70
+ ssh_dir = File.join(Dir.home, ".ssh")
71
+ unless File.exists?(ssh_dir)
72
+ FileUtils.mkdir_p ssh_dir
73
+ File.chmod(0700, ssh_dir)
74
+ end
75
+
76
+ output = `ssh-keygen -t rsa -N "" -f \"#{Dir.home}/.ssh/#{keyfile}\" 2>&1`
77
+ if ! $?.success?
78
+ puts "Could not generate key: #{output}"
79
+ exit 1
80
+ end
81
+ end
82
+
83
+ def self.associate_key(key)
84
+ puts "Uploading SSH public key #{key}"
85
+
86
+ if File.exists?(key)
87
+ api.post '/account/keys', ssh_key: File.read(key)
88
+ else
89
+ puts "Could not upload SSH public key: key file '" + key + "' does not exist"
90
+ exit 1
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,22 @@
1
+ class ElasticDot::Command::Logs < ElasticDot::Command::Base
2
+ def self.list(opts)
3
+ find_app! opts
4
+
5
+ max_id = nil
6
+
7
+ begin
8
+ res = api.get "/apps/#{@app}/logs?max_id=#{max_id}"
9
+ max_id, events = res['max_id'], res['events']
10
+ events.each {|e| puts e }
11
+ sleep 2
12
+ end while opts[:follow]
13
+ end
14
+
15
+ private
16
+ def self.apps_info(app)
17
+ info = api.get "/domains/#{app}"
18
+
19
+ app_tier = info['production'] ? 'production' : 'development'
20
+ {app_tier: app_tier, scaling: info['scaling'], tier: info['dot_tier']['name'], dots: info['min_dots'] }
21
+ end
22
+ end
@@ -0,0 +1,139 @@
1
+ class ElasticDot::Command::Ps < ElasticDot::Command::Base
2
+ def self.resize(settings, opts)
3
+ find_app! opts
4
+
5
+ params = apps_info @app
6
+
7
+ web = nil
8
+ settings.each do |s|
9
+ p, v = s.split('=',2)
10
+ web = v and break if p == 'web'
11
+ end
12
+
13
+ unless web
14
+ puts 'At the moment you can only resize web processes'
15
+ exit 1
16
+ end
17
+
18
+ params[:tier] = web
19
+
20
+ api.put "/websites/#{@app}/scaling", params
21
+
22
+ spinner "Resizing dots and restarting specified processes..." do
23
+ loop do
24
+ sleep 3
25
+ info = api.get "/domains/#{@app}"
26
+ break if info['status'] == 'active'
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.scale(settings, opts)
32
+ find_app! opts
33
+
34
+ params = apps_info @app
35
+
36
+ web, mode = nil
37
+ settings.each do |s|
38
+ p, v = s.split('=',2)
39
+ web = v if p == 'web'
40
+ mode = v if p == 'mode'
41
+ end
42
+
43
+ unless web
44
+ puts 'Usage: elasticdot ps:scale web=N [mode=auto|manual]'
45
+ exit 1
46
+ end
47
+
48
+ params[:scaling] = mode if mode
49
+ params[:dots] = web
50
+
51
+ api.put "/websites/#{@app}/scaling", params
52
+ info = apps_info @app
53
+
54
+ spinner "Scaling web processes..." do
55
+ loop do
56
+ sleep 3
57
+ info = api.get "/domains/#{@app}"
58
+ break if info['status'] == 'active'
59
+ end
60
+ end
61
+
62
+ puts "Now running #{info[:dots]} dots"
63
+ end
64
+
65
+ def self.list(opts)
66
+ require 'time'
67
+
68
+ find_app! opts
69
+
70
+ loop do
71
+
72
+ info = api.get("/domains/#{@app}")
73
+
74
+ tier = info['dot_tier']
75
+ dots = info['dots']
76
+
77
+ puts "=== web (#{tier['name']}): #{info['procfile']}"
78
+ dots.each_with_index do |dot, i|
79
+ now = Time.now
80
+ elapsed = now - Time.parse(dot['started_at'])
81
+ since = time_ago(now - elapsed)
82
+
83
+ puts "web.#{i+1}: up #{since} cpu: #{dot['cpu_load']}%"
84
+ end
85
+
86
+ break unless opts[:follow]
87
+ sleep 2
88
+ system 'clear'
89
+ end
90
+ end
91
+
92
+ def self.restart(opts)
93
+ find_app! opts
94
+
95
+ api.post "/apps/#{@app}/restart"
96
+
97
+ spinner 'Restarting dots...' do
98
+ loop do
99
+ sleep 3
100
+ info = api.get "/domains/#{@app}"
101
+ break if info['status'] == 'active'
102
+ end
103
+ end
104
+ end
105
+
106
+ def self.stop(opts)
107
+ find_app! opts
108
+
109
+ spinner "Stopping dots..." do
110
+ api.post "/apps/#{@app}/stop"
111
+ end
112
+ end
113
+
114
+ private
115
+ def self.apps_info(app)
116
+ info = api.get "/domains/#{@app}"
117
+
118
+ app_tier = info['production'] ? 'production' : 'development'
119
+ {app_tier: app_tier, scaling: info['scaling'], tier: info['dot_tier']['name'], dots: info['min_dots'] }
120
+ end
121
+
122
+ def self.time_ago(since)
123
+ if since.is_a?(String)
124
+ since = Time.parse(since)
125
+ end
126
+
127
+ elapsed = Time.now - since
128
+
129
+ message = since.strftime("%Y/%m/%d %H:%M:%S")
130
+ if elapsed <= 60
131
+ message << " (~ #{elapsed.floor}s ago)"
132
+ elsif elapsed <= (60 * 60)
133
+ message << " (~ #{(elapsed / 60).floor}m ago)"
134
+ elsif elapsed <= (60 * 60 * 25)
135
+ message << " (~ #{(elapsed / 60 / 60).floor}h ago)"
136
+ end
137
+ message
138
+ end
139
+ end
@@ -0,0 +1,42 @@
1
+ class ElasticDot::Command::Services < ElasticDot::Command::Base
2
+ def self.create(args, opts)
3
+ info = api.post '/domains', domain: args[0], type: 'service'
4
+
5
+ if info['error']
6
+ puts info['error']
7
+ exit 1
8
+ end
9
+
10
+ puts "Creating service app #{info['app_name']}... done"
11
+ puts info['app_repo']
12
+
13
+ create_git_remote 'elasticdot', info['app_repo']
14
+ end
15
+
16
+ def self.destroy(opts)
17
+ find_app! opts
18
+
19
+ spinner "Destroying app #{@app}..." do
20
+ api.delete "/domains/#{@app}"
21
+ end
22
+ end
23
+
24
+ def self.info(opts)
25
+ find_app! opts
26
+
27
+ h = api.get "/domains/#{@app}"
28
+
29
+ puts "=== #{@app}"
30
+ puts
31
+ puts "Git URL:\t#{h['git_repo']}"
32
+ puts "Owner Email:\t#{h['owner_email']}"
33
+ puts "Region:\t\tEU"
34
+ # puts "Slug Size:\t#{h['slug_size']}"
35
+ end
36
+
37
+ def self.list
38
+ apps = api.get "/apps?type=service"
39
+ puts '=== My Services'
40
+ apps.each { |app| puts app }
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ class String
2
+ def constantize
3
+ names = self.split('::')
4
+ names.shift if names.empty? || names.first.empty?
5
+
6
+ constant = Object
7
+ names.each do |name|
8
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
9
+ end
10
+ constant
11
+ end
12
+ end
13
+
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elasticdot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - ElasticDot
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-05-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: netrc
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.7.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.7.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: rest-client
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.6.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.6.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: launchy
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.4.2
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.4.2
62
+ description: ElasticDot Command Line Interface
63
+ email: info@elasticdot.com
64
+ executables:
65
+ - elasticdot
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - bin/elasticdot
70
+ - lib/elasticdot.rb
71
+ - lib/elasticdot/api.rb
72
+ - lib/elasticdot/cli.rb
73
+ - lib/elasticdot/command.rb
74
+ - lib/elasticdot/command/addons.rb
75
+ - lib/elasticdot/command/alias.rb
76
+ - lib/elasticdot/command/apps.rb
77
+ - lib/elasticdot/command/auth.rb
78
+ - lib/elasticdot/command/base.rb
79
+ - lib/elasticdot/command/config.rb
80
+ - lib/elasticdot/command/db.rb
81
+ - lib/elasticdot/command/domains.rb
82
+ - lib/elasticdot/command/help.rb
83
+ - lib/elasticdot/command/keys.rb
84
+ - lib/elasticdot/command/logs.rb
85
+ - lib/elasticdot/command/ps.rb
86
+ - lib/elasticdot/command/services.rb
87
+ - lib/elasticdot/initializers/string.rb
88
+ homepage: http://elasticdot.com
89
+ licenses:
90
+ - MIT
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 1.8.23
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: ElasticDot CLI
113
+ test_files: []