nimbu 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/nimbu +12 -0
- data/lib/nimbu.rb +6 -0
- data/lib/nimbu/auth.rb +268 -0
- data/lib/nimbu/cli.rb +12 -0
- data/lib/nimbu/client.rb +269 -0
- data/lib/nimbu/client/rendezvous.rb +76 -0
- data/lib/nimbu/command.rb +189 -0
- data/lib/nimbu/command/auth.rb +38 -0
- data/lib/nimbu/command/base.rb +212 -0
- data/lib/nimbu/command/help.rb +139 -0
- data/lib/nimbu/command/helpers.rb +382 -0
- data/lib/nimbu/command/init.rb +38 -0
- data/lib/nimbu/command/server.rb +22 -0
- data/lib/nimbu/command/themes.rb +86 -0
- data/lib/nimbu/helpers.rb +382 -0
- data/lib/nimbu/server/base.rb +82 -0
- data/lib/nimbu/server/views/index.haml +1 -0
- data/lib/nimbu/version.rb +3 -0
- data/lib/vendor/nimbu/okjson.rb +557 -0
- metadata +121 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require "timeout"
|
2
|
+
require "socket"
|
3
|
+
require "uri"
|
4
|
+
require "nimbu/client"
|
5
|
+
require "nimbu/helpers"
|
6
|
+
|
7
|
+
class Nimbu::Client::Rendezvous
|
8
|
+
attr_reader :rendezvous_url, :connect_timeout, :activity_timeout, :input, :output, :on_connect
|
9
|
+
|
10
|
+
def initialize(opts)
|
11
|
+
@rendezvous_url = opts[:rendezvous_url]
|
12
|
+
@connect_timeout = opts[:connect_timeout]
|
13
|
+
@activity_timeout = opts[:activity_timeout]
|
14
|
+
@input = opts[:input]
|
15
|
+
@output = opts[:output]
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_connect(&blk)
|
19
|
+
@on_connect = blk if block_given?
|
20
|
+
@on_connect
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
uri = URI.parse(rendezvous_url)
|
25
|
+
host, port, secret = uri.host, uri.port, uri.path[1..-1]
|
26
|
+
|
27
|
+
ssl_socket = Timeout.timeout(connect_timeout) do
|
28
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
29
|
+
if ((host =~ /nimbu\.com$/) && !(ENV["HEROKU_SSL_VERIFY"] == "disable"))
|
30
|
+
ssl_context.ca_file = File.expand_path("../../../../data/cacert.pem", __FILE__)
|
31
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
32
|
+
end
|
33
|
+
tcp_socket = TCPSocket.open(host, port)
|
34
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
35
|
+
ssl_socket.connect
|
36
|
+
ssl_socket.puts(secret)
|
37
|
+
ssl_socket.readline
|
38
|
+
ssl_socket
|
39
|
+
end
|
40
|
+
|
41
|
+
on_connect.call if on_connect
|
42
|
+
|
43
|
+
readables = [input, ssl_socket].compact
|
44
|
+
|
45
|
+
begin
|
46
|
+
loop do
|
47
|
+
if o = IO.select(readables, nil, nil, activity_timeout)
|
48
|
+
if (input && (o.first.first == input))
|
49
|
+
begin
|
50
|
+
data = input.readpartial(10000)
|
51
|
+
rescue EOFError
|
52
|
+
readables.delete(input)
|
53
|
+
next
|
54
|
+
end
|
55
|
+
ssl_socket.write(data)
|
56
|
+
ssl_socket.flush
|
57
|
+
elsif (o.first.first == ssl_socket)
|
58
|
+
begin
|
59
|
+
data = ssl_socket.readpartial(10000)
|
60
|
+
rescue EOFError
|
61
|
+
break
|
62
|
+
end
|
63
|
+
output.write(data)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
raise(Timeout::Error.new)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
rescue Interrupt
|
70
|
+
ssl_socket.write("\003")
|
71
|
+
ssl_socket.flush
|
72
|
+
retry
|
73
|
+
rescue Errno::EIO
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'nimbu/helpers'
|
2
|
+
require 'nimbu/version'
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
module Nimbu
|
6
|
+
module Command
|
7
|
+
class CommandFailed < RuntimeError; end
|
8
|
+
|
9
|
+
extend Nimbu::Helpers
|
10
|
+
|
11
|
+
def self.load
|
12
|
+
Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
|
13
|
+
require file
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.commands
|
18
|
+
@@commands ||= {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.command_aliases
|
22
|
+
@@command_aliases ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.namespaces
|
26
|
+
@@namespaces ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.register_command(command)
|
30
|
+
commands[command[:command]] = command
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.register_namespace(namespace)
|
34
|
+
namespaces[namespace[:name]] = namespace
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.current_command
|
38
|
+
@current_command
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.current_args
|
42
|
+
@current_args
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.current_options
|
46
|
+
@current_options
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.global_options
|
50
|
+
@global_options ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.global_option(name, *args)
|
54
|
+
global_options << { :name => name, :args => args }
|
55
|
+
end
|
56
|
+
|
57
|
+
global_option :app, "--app APP", "-a"
|
58
|
+
global_option :confirm, "--confirm APP"
|
59
|
+
global_option :help, "--help", "-h"
|
60
|
+
global_option :remote, "--remote REMOTE"
|
61
|
+
|
62
|
+
def self.prepare_run(cmd, args=[])
|
63
|
+
command = parse(cmd)
|
64
|
+
|
65
|
+
unless command
|
66
|
+
if %w( -v --version ).include?(cmd)
|
67
|
+
display Nimbu::VERSION
|
68
|
+
exit
|
69
|
+
end
|
70
|
+
|
71
|
+
output_with_bang("`#{cmd}` is not a nimbu command.")
|
72
|
+
|
73
|
+
distances = {}
|
74
|
+
(commands.keys + command_aliases.keys).each do |suggestion|
|
75
|
+
distance = string_distance(cmd, suggestion)
|
76
|
+
distances[distance] ||= []
|
77
|
+
distances[distance] << suggestion
|
78
|
+
end
|
79
|
+
|
80
|
+
if distances.keys.min < 4
|
81
|
+
suggestions = distances[distances.keys.min].sort
|
82
|
+
if suggestions.length == 1
|
83
|
+
output_with_bang("Perhaps you meant `#{suggestions.first}`.")
|
84
|
+
else
|
85
|
+
output_with_bang("Perhaps you meant #{suggestions[0...-1].map {|suggestion| "`#{suggestion}`"}.join(', ')} or `#{suggestions.last}`.")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
output_with_bang("See `nimbu help` for additional details.")
|
90
|
+
exit(1)
|
91
|
+
end
|
92
|
+
|
93
|
+
@current_command = cmd
|
94
|
+
|
95
|
+
opts = {}
|
96
|
+
invalid_options = []
|
97
|
+
|
98
|
+
parser = OptionParser.new do |parser|
|
99
|
+
global_options.each do |global_option|
|
100
|
+
parser.on(*global_option[:args]) do |value|
|
101
|
+
opts[global_option[:name]] = value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
command[:options].each do |name, option|
|
105
|
+
parser.on("-#{option[:short]}", "--#{option[:long]}", option[:desc]) do |value|
|
106
|
+
opts[name.gsub("-", "_").to_sym] = value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
parser.order!(args) do |nonopt|
|
113
|
+
invalid_options << nonopt
|
114
|
+
end
|
115
|
+
rescue OptionParser::InvalidOption => ex
|
116
|
+
invalid_options << ex.args.first
|
117
|
+
retry
|
118
|
+
end
|
119
|
+
|
120
|
+
raise OptionParser::ParseError if opts[:help]
|
121
|
+
|
122
|
+
args.concat(invalid_options)
|
123
|
+
|
124
|
+
@current_args = args
|
125
|
+
@current_options = opts
|
126
|
+
|
127
|
+
[ command[:klass].new(args.dup, opts.dup), command[:method] ]
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.run(cmd, arguments=[])
|
131
|
+
object, method = prepare_run(cmd, arguments.dup)
|
132
|
+
object.send(method)
|
133
|
+
rescue RestClient::Unauthorized
|
134
|
+
puts "Authentication failure"
|
135
|
+
unless ENV['HEROKU_API_KEY']
|
136
|
+
run "login"
|
137
|
+
retry
|
138
|
+
end
|
139
|
+
rescue RestClient::PaymentRequired => e
|
140
|
+
retry if run('account:confirm_billing', arguments.dup)
|
141
|
+
rescue RestClient::ResourceNotFound => e
|
142
|
+
error extract_error(e.http_body) {
|
143
|
+
e.http_body =~ /^[\w\s]+ not found$/ ? e.http_body : "Resource not found"
|
144
|
+
}
|
145
|
+
rescue RestClient::Locked => e
|
146
|
+
app = e.response.headers[:x_confirmation_required]
|
147
|
+
if confirm_command(app, extract_error(e.response.body))
|
148
|
+
arguments << '--confirm' << app
|
149
|
+
retry
|
150
|
+
end
|
151
|
+
rescue RestClient::RequestFailed => e
|
152
|
+
error extract_error(e.http_body)
|
153
|
+
rescue RestClient::RequestTimeout
|
154
|
+
error "API request timed out. Please try again, or contact support@nimbu.com if this issue persists."
|
155
|
+
rescue CommandFailed => e
|
156
|
+
error e.message
|
157
|
+
rescue OptionParser::ParseError => ex
|
158
|
+
commands[cmd] ? run("help", [cmd]) : run("help")
|
159
|
+
rescue Interrupt => e
|
160
|
+
error "\n[canceled]"
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.parse(cmd)
|
164
|
+
commands[cmd] || commands[command_aliases[cmd]]
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.extract_error(body, options={})
|
168
|
+
default_error = block_given? ? yield : "Internal server error.\nRun 'nimbu status' to check for known platform issues."
|
169
|
+
parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.parse_error_xml(body)
|
173
|
+
xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
|
174
|
+
msg = xml_errors.map { |a| a.text }.join(" / ")
|
175
|
+
return msg unless msg.empty?
|
176
|
+
rescue Exception
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.parse_error_json(body)
|
180
|
+
json = json_decode(body.to_s) rescue false
|
181
|
+
json ? json['error'] : nil
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.parse_error_plain(body)
|
185
|
+
return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
|
186
|
+
body.to_s
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "nimbu/command/base"
|
2
|
+
|
3
|
+
# authentication (login, logout)
|
4
|
+
#
|
5
|
+
class Nimbu::Command::Auth < Nimbu::Command::Base
|
6
|
+
|
7
|
+
# auth:login
|
8
|
+
#
|
9
|
+
# log in with your nimbu credentials
|
10
|
+
#
|
11
|
+
def login
|
12
|
+
Nimbu::Auth.login
|
13
|
+
display "Authentication successful."
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_command "login", "auth:login"
|
17
|
+
|
18
|
+
# auth:logout
|
19
|
+
#
|
20
|
+
# clear local authentication credentials
|
21
|
+
#
|
22
|
+
def logout
|
23
|
+
Nimbu::Auth.logout
|
24
|
+
display "Local credentials cleared."
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_command "logout", "auth:logout"
|
28
|
+
|
29
|
+
# auth:token
|
30
|
+
#
|
31
|
+
# display your api token
|
32
|
+
#
|
33
|
+
def token
|
34
|
+
display Nimbu::Auth.api_key
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "nimbu/auth"
|
3
|
+
require "nimbu/client/rendezvous"
|
4
|
+
require "nimbu/command"
|
5
|
+
|
6
|
+
class Nimbu::Command::Base
|
7
|
+
include Nimbu::Helpers
|
8
|
+
|
9
|
+
def self.namespace
|
10
|
+
self.to_s.split("::").last.downcase
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :args
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
def initialize(args=[], options={})
|
17
|
+
@args = args
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def app
|
22
|
+
@app ||= if options[:app].is_a?(String)
|
23
|
+
if confirm_mismatch?
|
24
|
+
raise Nimbu::Command::CommandFailed, "Mismatch between --app and --confirm"
|
25
|
+
end
|
26
|
+
options[:app]
|
27
|
+
elsif options[:confirm].is_a?(String)
|
28
|
+
options[:confirm]
|
29
|
+
elsif app_from_dir = extract_app_in_dir(Dir.pwd)
|
30
|
+
app_from_dir
|
31
|
+
else
|
32
|
+
raise Nimbu::Command::CommandFailed, "No app specified.\nRun this command from an app folder or specify which app to use with --app <app name>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def nimbu
|
38
|
+
Nimbu::Auth.client
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def self.inherited(klass)
|
44
|
+
return if klass == Nimbu::Command::Base
|
45
|
+
|
46
|
+
help = extract_help_from_caller(caller.first)
|
47
|
+
|
48
|
+
Nimbu::Command.register_namespace(
|
49
|
+
:name => klass.namespace,
|
50
|
+
:description => help.split("\n").first
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.method_added(method)
|
55
|
+
return if self == Nimbu::Command::Base
|
56
|
+
return if private_method_defined?(method)
|
57
|
+
return if protected_method_defined?(method)
|
58
|
+
|
59
|
+
help = extract_help_from_caller(caller.first)
|
60
|
+
resolved_method = (method.to_s == "index") ? nil : method.to_s
|
61
|
+
command = [ self.namespace, resolved_method ].compact.join(":")
|
62
|
+
banner = extract_banner(help) || command
|
63
|
+
permute = !banner.index("*")
|
64
|
+
banner.gsub!("*", "")
|
65
|
+
|
66
|
+
Nimbu::Command.register_command(
|
67
|
+
:klass => self,
|
68
|
+
:method => method,
|
69
|
+
:namespace => self.namespace,
|
70
|
+
:command => command,
|
71
|
+
:banner => banner,
|
72
|
+
:help => help,
|
73
|
+
:summary => extract_summary(help),
|
74
|
+
:description => extract_description(help),
|
75
|
+
:options => extract_options(help),
|
76
|
+
:permute => permute
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.alias_command(new, old)
|
81
|
+
raise "no such command: #{old}" unless Nimbu::Command.commands[old]
|
82
|
+
Nimbu::Command.command_aliases[new] = old
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_app
|
86
|
+
output_with_bang "Command::Base#extract_app has been deprecated. Please use Command::Base#app instead. #{caller.first}"
|
87
|
+
app
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Parse the caller format and identify the file and line number as identified
|
92
|
+
# in : http://www.ruby-doc.org/core/classes/Kernel.html#M001397. This will
|
93
|
+
# look for a colon followed by a digit as the delimiter. The biggest
|
94
|
+
# complication is windows paths, which have a color after the drive letter.
|
95
|
+
# This regex will match paths as anything from the beginning to a colon
|
96
|
+
# directly followed by a number (the line number).
|
97
|
+
#
|
98
|
+
# Examples of the caller format :
|
99
|
+
# * c:/Ruby192/lib/.../lib/nimbu/command/addons.rb:8:in `<module:Command>'
|
100
|
+
# * c:/Ruby192/lib/.../nimbu-2.0.1/lib/nimbu/command/pg.rb:96:in `<class:Pg>'
|
101
|
+
# * /Users/ph7/...../xray-1.1/lib/xray/thread_dump_signal_handler.rb:9
|
102
|
+
#
|
103
|
+
def self.extract_help_from_caller(line)
|
104
|
+
# pull out of the caller the information for the file path and line number
|
105
|
+
if line =~ /^(.+?):(\d+)/
|
106
|
+
return extract_help($1, $2)
|
107
|
+
end
|
108
|
+
raise "unable to extract help from caller: #{line}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.extract_help(file, line)
|
112
|
+
buffer = []
|
113
|
+
lines = File.read(file).split("\n")
|
114
|
+
|
115
|
+
catch(:done) do
|
116
|
+
(line.to_i-2).downto(0) do |i|
|
117
|
+
case lines[i].strip[0..0]
|
118
|
+
when "", "#" then buffer << lines[i]
|
119
|
+
else throw(:done)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
buffer.map! do |line|
|
125
|
+
line.strip.gsub(/^#/, "")
|
126
|
+
end
|
127
|
+
|
128
|
+
buffer.reverse.join("\n").strip
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.extract_banner(help)
|
132
|
+
help.split("\n").first
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.extract_summary(help)
|
136
|
+
extract_description(help).split("\n").first
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.extract_description(help)
|
140
|
+
lines = help.split("\n").map { |l| l.strip }
|
141
|
+
lines.shift
|
142
|
+
lines.reject do |line|
|
143
|
+
line =~ /^-(.+)#(.+)/
|
144
|
+
end.join("\n").strip
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.extract_options(help)
|
148
|
+
help.split("\n").map { |l| l.strip }.select do |line|
|
149
|
+
line =~ /^-(.+)#(.+)/
|
150
|
+
end.inject({}) do |hash, line|
|
151
|
+
description = line.split("#", 2).last.strip
|
152
|
+
long = line.match(/--([A-Za-z\- ]+)/)[1].strip
|
153
|
+
short = line.match(/-([A-Za-z ])/)[1].strip
|
154
|
+
hash.update(long.split(" ").first => { :desc => description, :short => short, :long => long })
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def extract_option(name, default=true)
|
159
|
+
key = name.gsub("--", "").to_sym
|
160
|
+
return unless options[key]
|
161
|
+
value = options[key] || default
|
162
|
+
block_given? ? yield(value) : value
|
163
|
+
end
|
164
|
+
|
165
|
+
def confirm_mismatch?
|
166
|
+
options[:confirm] && (options[:confirm] != options[:app])
|
167
|
+
end
|
168
|
+
|
169
|
+
def extract_app_in_dir(dir)
|
170
|
+
return unless remotes = git_remotes(dir)
|
171
|
+
|
172
|
+
if remote = options[:remote]
|
173
|
+
remotes[remote]
|
174
|
+
elsif remote = extract_app_from_git_config
|
175
|
+
remotes[remote]
|
176
|
+
else
|
177
|
+
apps = remotes.values.uniq
|
178
|
+
return apps.first if apps.size == 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def extract_app_from_git_config
|
183
|
+
remote = git("config nimbu.remote")
|
184
|
+
remote == "" ? nil : remote
|
185
|
+
end
|
186
|
+
|
187
|
+
def git_remotes(base_dir=Dir.pwd)
|
188
|
+
remotes = {}
|
189
|
+
original_dir = Dir.pwd
|
190
|
+
Dir.chdir(base_dir)
|
191
|
+
|
192
|
+
git("remote -v").split("\n").each do |remote|
|
193
|
+
name, url, method = remote.split(/\s/)
|
194
|
+
if url =~ /^git@#{nimbu.host}:([\w\d-]+)\.git$/
|
195
|
+
remotes[name] = $1
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
Dir.chdir(original_dir)
|
200
|
+
remotes
|
201
|
+
end
|
202
|
+
|
203
|
+
def escape(value)
|
204
|
+
nimbu.escape(value)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
module Nimbu::Command
|
209
|
+
unless const_defined?(:BaseWithApp)
|
210
|
+
BaseWithApp = Base
|
211
|
+
end
|
212
|
+
end
|