elasticdot 1.3.3

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.
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: []