artofmission-heroku 1.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +66 -0
- data/Rakefile +107 -0
- data/bin/heroku +15 -0
- data/lib/heroku.rb +5 -0
- data/lib/heroku/client.rb +487 -0
- data/lib/heroku/command.rb +96 -0
- data/lib/heroku/commands/account.rb +13 -0
- data/lib/heroku/commands/addons.rb +109 -0
- data/lib/heroku/commands/app.rb +239 -0
- data/lib/heroku/commands/auth.rb +137 -0
- data/lib/heroku/commands/base.rb +133 -0
- data/lib/heroku/commands/bundles.rb +51 -0
- data/lib/heroku/commands/config.rb +55 -0
- data/lib/heroku/commands/db.rb +129 -0
- data/lib/heroku/commands/domains.rb +31 -0
- data/lib/heroku/commands/help.rb +148 -0
- data/lib/heroku/commands/keys.rb +49 -0
- data/lib/heroku/commands/logs.rb +11 -0
- data/lib/heroku/commands/maintenance.rb +13 -0
- data/lib/heroku/commands/plugins.rb +25 -0
- data/lib/heroku/commands/ps.rb +37 -0
- data/lib/heroku/commands/service.rb +23 -0
- data/lib/heroku/commands/sharing.rb +29 -0
- data/lib/heroku/commands/ssl.rb +33 -0
- data/lib/heroku/commands/version.rb +7 -0
- data/lib/heroku/helpers.rb +23 -0
- data/lib/heroku/plugin.rb +65 -0
- data/spec/base.rb +23 -0
- data/spec/client_spec.rb +366 -0
- data/spec/command_spec.rb +15 -0
- data/spec/commands/addons_spec.rb +47 -0
- data/spec/commands/app_spec.rb +175 -0
- data/spec/commands/auth_spec.rb +104 -0
- data/spec/commands/base_spec.rb +114 -0
- data/spec/commands/bundles_spec.rb +48 -0
- data/spec/commands/config_spec.rb +45 -0
- data/spec/commands/db_spec.rb +53 -0
- data/spec/commands/domains_spec.rb +31 -0
- data/spec/commands/keys_spec.rb +60 -0
- data/spec/commands/logs_spec.rb +21 -0
- data/spec/commands/maintenance_spec.rb +21 -0
- data/spec/commands/plugins_spec.rb +26 -0
- data/spec/commands/ps_spec.rb +16 -0
- data/spec/commands/sharing_spec.rb +32 -0
- data/spec/commands/ssl_spec.rb +25 -0
- data/spec/plugin_spec.rb +64 -0
- metadata +150 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
module Heroku::Command
|
2
|
+
class Auth < Base
|
3
|
+
attr_accessor :credentials
|
4
|
+
|
5
|
+
def client
|
6
|
+
@client ||= init_heroku
|
7
|
+
end
|
8
|
+
|
9
|
+
def init_heroku
|
10
|
+
client = Heroku::Client.new(user, password, host)
|
11
|
+
client.on_warning { |msg| self.display("\n#{msg}\n\n") }
|
12
|
+
client
|
13
|
+
end
|
14
|
+
|
15
|
+
def host
|
16
|
+
ENV['HEROKU_HOST'] || 'heroku.com'
|
17
|
+
end
|
18
|
+
|
19
|
+
def reauthorize
|
20
|
+
@credentials = ask_for_credentials
|
21
|
+
write_credentials
|
22
|
+
end
|
23
|
+
|
24
|
+
def user # :nodoc:
|
25
|
+
get_credentials
|
26
|
+
@credentials[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
def password # :nodoc:
|
30
|
+
get_credentials
|
31
|
+
@credentials[1]
|
32
|
+
end
|
33
|
+
|
34
|
+
def credentials_file
|
35
|
+
"#{home_directory}/.heroku/credentials"
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_credentials # :nodoc:
|
39
|
+
return if @credentials
|
40
|
+
unless @credentials = read_credentials
|
41
|
+
@credentials = ask_for_credentials
|
42
|
+
save_credentials
|
43
|
+
end
|
44
|
+
@credentials
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_credentials
|
48
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
def echo_off
|
52
|
+
system "stty -echo"
|
53
|
+
end
|
54
|
+
|
55
|
+
def echo_on
|
56
|
+
system "stty echo"
|
57
|
+
end
|
58
|
+
|
59
|
+
def ask_for_credentials
|
60
|
+
puts "Enter your Heroku credentials."
|
61
|
+
|
62
|
+
print "Email: "
|
63
|
+
user = ask
|
64
|
+
|
65
|
+
print "Password: "
|
66
|
+
password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
|
67
|
+
|
68
|
+
[ user, password ]
|
69
|
+
end
|
70
|
+
|
71
|
+
def ask_for_password_on_windows
|
72
|
+
require "Win32API"
|
73
|
+
char = nil
|
74
|
+
password = ''
|
75
|
+
|
76
|
+
while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
|
77
|
+
break if char == 10 || char == 13 # received carriage return or newline
|
78
|
+
if char == 127 || char == 8 # backspace and delete
|
79
|
+
password.slice!(-1, 1)
|
80
|
+
else
|
81
|
+
password << char.chr
|
82
|
+
end
|
83
|
+
end
|
84
|
+
puts
|
85
|
+
return password
|
86
|
+
end
|
87
|
+
|
88
|
+
def ask_for_password
|
89
|
+
echo_off
|
90
|
+
password = ask
|
91
|
+
puts
|
92
|
+
echo_on
|
93
|
+
return password
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_credentials
|
97
|
+
begin
|
98
|
+
write_credentials
|
99
|
+
Heroku::Command.run_internal('keys:add', args)
|
100
|
+
rescue RestClient::Unauthorized => e
|
101
|
+
delete_credentials
|
102
|
+
raise e unless retry_login?
|
103
|
+
|
104
|
+
display "\nAuthentication failed"
|
105
|
+
@credentials = ask_for_credentials
|
106
|
+
@client = init_heroku
|
107
|
+
retry
|
108
|
+
rescue Exception => e
|
109
|
+
delete_credentials
|
110
|
+
raise e
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def retry_login?
|
115
|
+
@login_attempts ||= 0
|
116
|
+
@login_attempts += 1
|
117
|
+
@login_attempts < 3
|
118
|
+
end
|
119
|
+
|
120
|
+
def write_credentials
|
121
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
122
|
+
File.open(credentials_file, 'w') do |f|
|
123
|
+
f.puts self.credentials
|
124
|
+
end
|
125
|
+
set_credentials_permissions
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_credentials_permissions
|
129
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
130
|
+
FileUtils.chmod 0600, credentials_file
|
131
|
+
end
|
132
|
+
|
133
|
+
def delete_credentials
|
134
|
+
FileUtils.rm_f(credentials_file)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Heroku::Command
|
4
|
+
class Base
|
5
|
+
include Heroku::Helpers
|
6
|
+
|
7
|
+
attr_accessor :args
|
8
|
+
attr_reader :autodetected_app
|
9
|
+
def initialize(args, heroku=nil)
|
10
|
+
@args = args
|
11
|
+
@heroku = heroku
|
12
|
+
@autodetected_app = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def display(msg, newline=true)
|
16
|
+
if newline
|
17
|
+
puts(msg)
|
18
|
+
else
|
19
|
+
print(msg)
|
20
|
+
STDOUT.flush
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_date(date)
|
25
|
+
date = Time.parse(date) if date.is_a?(String)
|
26
|
+
date.strftime("%Y-%m-%d %H:%M %Z")
|
27
|
+
end
|
28
|
+
|
29
|
+
def error(msg)
|
30
|
+
Heroku::Command.error(msg)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ask
|
34
|
+
gets.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
def shell(cmd)
|
38
|
+
FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
|
39
|
+
end
|
40
|
+
|
41
|
+
def heroku
|
42
|
+
@heroku ||= Heroku::Command.run_internal('auth:client', args)
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_app(force=true)
|
46
|
+
app = extract_option('--app')
|
47
|
+
unless app
|
48
|
+
app = extract_app_in_dir(Dir.pwd) ||
|
49
|
+
raise(CommandFailed, "No app specified.\nRun this command from app folder or set it adding --app <app name>") if force
|
50
|
+
@autodetected_app = true
|
51
|
+
end
|
52
|
+
app
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_app_in_dir(dir)
|
56
|
+
return unless remotes = git_remotes(dir)
|
57
|
+
|
58
|
+
if remote = extract_option('--remote')
|
59
|
+
remotes[remote]
|
60
|
+
else
|
61
|
+
apps = remotes.values.uniq
|
62
|
+
case apps.size
|
63
|
+
when 0; return nil
|
64
|
+
when 1; return apps.first
|
65
|
+
else
|
66
|
+
current_dir_name = dir.split('/').last.downcase
|
67
|
+
apps.select { |a| a.downcase == current_dir_name }.first
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def git_remotes(base_dir)
|
73
|
+
git_config = "#{base_dir}/.git/config"
|
74
|
+
unless File.exists?(git_config)
|
75
|
+
parent = base_dir.split('/')[0..-2].join('/')
|
76
|
+
return git_remotes(parent) unless parent.empty?
|
77
|
+
else
|
78
|
+
remotes = {}
|
79
|
+
current_remote = nil
|
80
|
+
File.read(git_config).split(/\n/).each do |l|
|
81
|
+
current_remote = $1 if l.match(/\[remote \"([\w\d-]+)\"\]/)
|
82
|
+
app = (l.match(/url = git@#{heroku.host}:([\w\d-]+)\.git/) || [])[1]
|
83
|
+
if current_remote && app
|
84
|
+
remotes[current_remote.downcase] = app
|
85
|
+
current_remote = nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
return remotes
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def extract_option(options, default=true)
|
93
|
+
values = options.is_a?(Array) ? options : [options]
|
94
|
+
return unless opt_index = args.select { |a| values.include? a }.first
|
95
|
+
opt_position = args.index(opt_index) + 1
|
96
|
+
if args.size > opt_position && opt_value = args[opt_position]
|
97
|
+
if opt_value.include?('--')
|
98
|
+
opt_value = nil
|
99
|
+
else
|
100
|
+
args.delete_at(opt_position)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
opt_value ||= default
|
104
|
+
args.delete(opt_index)
|
105
|
+
block_given? ? yield(opt_value) : opt_value
|
106
|
+
end
|
107
|
+
|
108
|
+
def web_url(name)
|
109
|
+
"http://#{name}.#{heroku.host}/"
|
110
|
+
end
|
111
|
+
|
112
|
+
def git_url(name)
|
113
|
+
"git@#{heroku.host}:#{name}.git"
|
114
|
+
end
|
115
|
+
|
116
|
+
def app_urls(name)
|
117
|
+
"#{web_url(name)} | #{git_url(name)}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def escape(value)
|
121
|
+
heroku.escape(value)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class BaseWithApp < Base
|
126
|
+
attr_accessor :app
|
127
|
+
|
128
|
+
def initialize(args, heroku=nil)
|
129
|
+
super(args, heroku)
|
130
|
+
@app ||= extract_app
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Heroku::Command
|
2
|
+
class Bundles < BaseWithApp
|
3
|
+
def list
|
4
|
+
list = heroku.bundles(app)
|
5
|
+
if list.size > 0
|
6
|
+
list.each do |bundle|
|
7
|
+
space = ' ' * [(18 - bundle[:name].size),1].max
|
8
|
+
display "#{bundle[:name]}" + space + "#{bundle[:state]} #{bundle[:created_at].strftime("%m/%d/%Y %H:%M")}"
|
9
|
+
end
|
10
|
+
else
|
11
|
+
display "#{app} has no bundles."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
alias :index :list
|
15
|
+
|
16
|
+
def capture
|
17
|
+
bundle = args.shift.strip.downcase rescue nil
|
18
|
+
|
19
|
+
bundle = heroku.bundle_capture(app, bundle)
|
20
|
+
display "Began capturing bundle #{bundle} from #{app}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def destroy
|
24
|
+
bundle = args.first.strip.downcase rescue nil
|
25
|
+
unless bundle
|
26
|
+
display "Usage: heroku bundle:destroy <bundle>"
|
27
|
+
else
|
28
|
+
heroku.bundle_destroy(app, bundle)
|
29
|
+
display "Destroyed bundle #{bundle} from #{app}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def download
|
34
|
+
fname = "#{app}.tar.gz"
|
35
|
+
bundle = args.shift.strip.downcase rescue nil
|
36
|
+
url = heroku.bundle_url(app, bundle)
|
37
|
+
File.open(fname, "wb") { |f| f.write RestClient.get(url) }
|
38
|
+
display "Downloaded #{File.stat(fname).size} byte bundle #{fname}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def animate
|
42
|
+
bundle = args.shift.strip.downcase rescue ""
|
43
|
+
if bundle.length == 0
|
44
|
+
display "Usage: heroku bundle:animate <bundle>"
|
45
|
+
else
|
46
|
+
name = heroku.create(nil, :origin_bundle_app => app, :origin_bundle => bundle)
|
47
|
+
display "Animated #{app} #{bundle} into #{app_urls(name)}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Heroku::Command
|
2
|
+
class Config < BaseWithApp
|
3
|
+
def index
|
4
|
+
long = args.delete('--long')
|
5
|
+
vars = heroku.config_vars(app)
|
6
|
+
display_vars(vars, :long => long)
|
7
|
+
end
|
8
|
+
|
9
|
+
def add
|
10
|
+
unless args.size > 0 and args.all? { |a| a.include?('=') }
|
11
|
+
raise CommandFailed, "Usage: heroku config:add <key>=<value> [<key2>=<value2> ...]"
|
12
|
+
end
|
13
|
+
|
14
|
+
vars = args.inject({}) do |vars, arg|
|
15
|
+
key, value = arg.split('=', 2)
|
16
|
+
vars[key] = value
|
17
|
+
vars
|
18
|
+
end
|
19
|
+
|
20
|
+
display "Adding config vars:"
|
21
|
+
display_vars(vars, :indent => 2)
|
22
|
+
|
23
|
+
display "Restarting app...", false
|
24
|
+
heroku.add_config_vars(app, vars)
|
25
|
+
display "done."
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove
|
29
|
+
display "Removing #{args.first} and restarting app...", false
|
30
|
+
heroku.remove_config_var(app, args.first)
|
31
|
+
display "done."
|
32
|
+
end
|
33
|
+
alias :rm :remove
|
34
|
+
|
35
|
+
def clear
|
36
|
+
display "Clearing all config vars and restarting app...", false
|
37
|
+
heroku.clear_config_vars(app)
|
38
|
+
display "done."
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def display_vars(vars, options={})
|
43
|
+
max_length = vars.map { |v| v[0].size }.max
|
44
|
+
vars.keys.sort.each do |key|
|
45
|
+
spaces = ' ' * (max_length - key.size)
|
46
|
+
display "#{' ' * (options[:indent] || 0)}#{key}#{spaces} => #{format(vars[key], options)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def format(value, options)
|
51
|
+
return value if options[:long] || value.size < 36
|
52
|
+
value[0, 16] + '...' + value[-16, 16]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Heroku::Command
|
2
|
+
class Db < BaseWithApp
|
3
|
+
def pull
|
4
|
+
database_url = args.shift.strip rescue ''
|
5
|
+
if database_url == ''
|
6
|
+
database_url = parse_database_yml
|
7
|
+
display "Auto-detected local database: #{database_url}" if database_url != ''
|
8
|
+
end
|
9
|
+
raise(CommandFailed, "Invalid database url") if database_url == ''
|
10
|
+
|
11
|
+
taps_client(database_url) do |client|
|
12
|
+
client.cmd_receive
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def push
|
17
|
+
display("Warning: All data in the the remote database will be overwritten and will not be recoverable.")
|
18
|
+
display("Are you sure you wish to push to the remote database? (y/n)? ", false)
|
19
|
+
if ask.downcase == 'y'
|
20
|
+
database_url = args.shift.strip rescue ''
|
21
|
+
if database_url == ''
|
22
|
+
database_url = parse_database_yml
|
23
|
+
display "Auto-detected local database: #{database_url}" if database_url != ''
|
24
|
+
end
|
25
|
+
raise(CommandFailed, "Invalid database url") if database_url == ''
|
26
|
+
|
27
|
+
taps_client(database_url) do |client|
|
28
|
+
client.cmd_send
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset
|
34
|
+
if !autodetected_app
|
35
|
+
info = heroku.info(app)
|
36
|
+
url = info[:domain_name] || "http://#{info[:name]}.#{heroku.host}/"
|
37
|
+
|
38
|
+
display("Warning: All data in the '#{app}' database will be erased and will not be recoverable.")
|
39
|
+
display("Are you sure you wish to continue? (y/n)? ", false)
|
40
|
+
if ask.downcase == 'y'
|
41
|
+
heroku.database_reset(app)
|
42
|
+
display "Database reset for '#{app}' (#{url})"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
display "Set the app you want to reset the database for by adding --app <app name> to this command"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def parse_database_yml
|
52
|
+
return "" unless File.exists?(Dir.pwd + '/config/database.yml')
|
53
|
+
|
54
|
+
environment = ENV['RAILS_ENV'] || ENV['MERB_ENV'] || ENV['RACK_ENV']
|
55
|
+
environment = 'development' if environment.nil? or environment.empty?
|
56
|
+
|
57
|
+
conf = YAML.load(File.read(Dir.pwd + '/config/database.yml'))[environment]
|
58
|
+
case conf['adapter']
|
59
|
+
when 'sqlite3'
|
60
|
+
return "sqlite://#{conf['database']}"
|
61
|
+
when 'postgresql'
|
62
|
+
uri_hash = conf_to_uri_hash(conf)
|
63
|
+
uri_hash['scheme'] = 'postgres'
|
64
|
+
return uri_hash_to_url(uri_hash)
|
65
|
+
else
|
66
|
+
return uri_hash_to_url(conf_to_uri_hash(conf))
|
67
|
+
end
|
68
|
+
rescue Object => e
|
69
|
+
""
|
70
|
+
end
|
71
|
+
|
72
|
+
def conf_to_uri_hash(conf)
|
73
|
+
uri = {}
|
74
|
+
uri['scheme'] = conf['adapter']
|
75
|
+
uri['username'] = conf['user'] || conf['username']
|
76
|
+
uri['password'] = conf['password']
|
77
|
+
uri['host'] = conf['host'] || conf['hostname']
|
78
|
+
uri['port'] = conf['port']
|
79
|
+
uri['path'] = conf['database']
|
80
|
+
|
81
|
+
conf['encoding'] = 'utf8' if conf['encoding'] == 'unicode' or conf['encoding'].nil?
|
82
|
+
uri['query'] = "encoding=#{conf['encoding']}"
|
83
|
+
|
84
|
+
uri
|
85
|
+
end
|
86
|
+
|
87
|
+
def uri_hash_to_url(uri)
|
88
|
+
url = "#{uri['scheme']}://"
|
89
|
+
if uri['username'].size
|
90
|
+
url += escape(uri['username'])
|
91
|
+
url += ':' + escape(uri['password']) if uri['password']
|
92
|
+
url += "@"
|
93
|
+
url += uri['host'] || '127.0.0.1'
|
94
|
+
else
|
95
|
+
url += uri['host'] if uri['host']
|
96
|
+
end
|
97
|
+
url += uri['port'] if uri['port']
|
98
|
+
url += "/"
|
99
|
+
url += uri['path']
|
100
|
+
url += "?#{uri['query']}" if uri['query']
|
101
|
+
url
|
102
|
+
end
|
103
|
+
|
104
|
+
def taps_client(database_url, &block)
|
105
|
+
chunk_size = 1000
|
106
|
+
Taps::Config.database_url = database_url
|
107
|
+
Taps::Config.verify_database_url
|
108
|
+
|
109
|
+
Taps::ClientSession.start(database_url, "http://heroku:osui59a24am79x@taps.#{heroku.host}", chunk_size) do |client|
|
110
|
+
uri = heroku.database_session(app)
|
111
|
+
client.set_session(uri)
|
112
|
+
client.verify_server
|
113
|
+
yield client
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize(*args)
|
118
|
+
super(*args)
|
119
|
+
|
120
|
+
gem 'taps', '>= 0.2.23', '< 0.3.0'
|
121
|
+
require 'taps/client_session'
|
122
|
+
rescue LoadError
|
123
|
+
message = "Taps Load Error: #{$!.message}\n"
|
124
|
+
message << "You may need to install or update the taps gem to use db commands.\n"
|
125
|
+
message << "On most systems this will be:\n\nsudo gem install taps"
|
126
|
+
error message
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|