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.
@@ -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