artofmission-heroku 1.6.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/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
|