transcriptic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/transcriptic ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # resolve bin path, ignoring symlinks
4
+ require "pathname"
5
+ bin_file = Pathname.new(__FILE__).realpath
6
+
7
+ # add self to libpath
8
+ $:.unshift File.expand_path("../../lib", bin_file)
9
+
10
+ # start up the CLI
11
+ require "transcriptic/cli"
12
+ Transcriptic::CLI.start(*ARGV)
@@ -0,0 +1,3 @@
1
+ module Transcriptic; end
2
+
3
+ require "transcriptic/client"
@@ -0,0 +1,73 @@
1
+ require "transcriptic/api/errors"
2
+ require "transcriptic/api/version"
3
+
4
+ require "transcriptic/api/sequences"
5
+
6
+ srand
7
+
8
+ module Transcriptic
9
+ class API
10
+
11
+ def initialize(options={})
12
+ @api_key = options.delete(:api_key) || ENV['TRANSCRIPTIC_API_KEY']
13
+ user_pass = ":#{@api_key}"
14
+ options = {
15
+ :headers => {},
16
+ :host => 'www.transcriptic.com',
17
+ :scheme => 'https'
18
+ }.merge(options)
19
+ options[:headers] = {
20
+ 'Accept' => 'application/json',
21
+ 'Accept-Encoding' => 'gzip',
22
+ #'Accept-Language' => 'en-US, en;q=0.8',
23
+ 'Authorization' => "Basic #{Base64.encode64(user_pass).gsub("\n", '')}",
24
+ 'User-Agent' => "transcriptic-rb/#{Transcriptic::API::VERSION}",
25
+ 'X-Transcriptic-API-Version' => '3',
26
+ 'X-Ruby-Version' => RUBY_VERSION,
27
+ 'X-Ruby-Platform' => RUBY_PLATFORM
28
+ }.merge(options[:headers])
29
+ @connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options)
30
+ end
31
+
32
+ def request(params, &block)
33
+ begin
34
+ response = @connection.request(params, &block)
35
+ rescue Excon::Errors::SocketError => error
36
+ raise error
37
+ rescue Excon::Errors::Error => error
38
+ klass = case error.response.status
39
+ when 401 then Transcriptic::API::Errors::Unauthorized
40
+ when 402 then Transcriptic::API::Errors::VerificationRequired
41
+ when 403 then Transcriptic::API::Errors::Forbidden
42
+ when 404 then Transcriptic::API::Errors::NotFound
43
+ when 408 then Transcriptic::API::Errors::Timeout
44
+ when 422 then Transcriptic::API::Errors::RequestFailed
45
+ when 423 then Transcriptic::API::Errors::Locked
46
+ when /50./ then Transcriptic::API::Errors::RequestFailed
47
+ else Transcriptic::API::Errors::ErrorWithResponse
48
+ end
49
+
50
+ reerror = klass.new(error.message, error.response)
51
+ reerror.set_backtrace(error.backtrace)
52
+ raise(reerror)
53
+ end
54
+
55
+ if response.body && !response.body.empty?
56
+ if response.headers['Content-Encoding'] == 'gzip'
57
+ response.body = Zlib::GzipReader.new(StringIO.new(response.body)).read
58
+ end
59
+ begin
60
+ response.body = Transcriptic::API::OkJson.decode(response.body)
61
+ rescue
62
+ # leave non-JSON body as is
63
+ end
64
+ end
65
+
66
+ # reset (non-persistent) connection
67
+ @connection.reset
68
+
69
+ response
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ module Transcriptic
2
+ class API
3
+ module Errors
4
+ class Error < StandardError; end
5
+
6
+ class ErrorWithResponse < Error
7
+ attr_reader :response
8
+
9
+ def initialize(message, response)
10
+ super message
11
+ @response = response
12
+ end
13
+ end
14
+
15
+ class Unauthorized < ErrorWithResponse; end
16
+ class VerificationRequired < ErrorWithResponse; end
17
+ class Forbidden < ErrorWithResponse; end
18
+ class NotFound < ErrorWithResponse; end
19
+ class Timeout < ErrorWithResponse; end
20
+ class Locked < ErrorWithResponse; end
21
+ class RequestFailed < ErrorWithResponse; end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Transcriptic
2
+ class API
3
+
4
+ # DELETE /:organization/sequences/:sequence
5
+ def delete_sequence(organization, sequence)
6
+ request(
7
+ :expects => 200,
8
+ :method => :delete,
9
+ :path => "/#{organization}/sequences/#{sequence}"
10
+ )
11
+ end
12
+
13
+ # GET /:organization/sequences
14
+ def get_sequences(organization)
15
+ request(
16
+ :expects => 200,
17
+ :method => :get,
18
+ :path => "/#{organization}/sequences"
19
+ )
20
+ end
21
+
22
+ # POST /:organization/sequences
23
+ def post_domain(organization, sequence)
24
+ request(
25
+ :expects => 201,
26
+ :method => :post,
27
+ :path => "/#{organization}/sequences",
28
+ :query => {'sequence' => sequence}
29
+ )
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Transcriptic
2
+ class API
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,223 @@
1
+ require "transcriptic"
2
+ require "transcriptic/client"
3
+ require "transcriptic/helpers"
4
+
5
+ class Transcriptic::Auth
6
+ class << self
7
+ attr_accessor :credentials
8
+
9
+ def client
10
+ @client ||= begin
11
+ client = Transcriptic::Client.new(user, password, host)
12
+ client.on_warning { |msg| self.display("\n#{msg}\n\n") }
13
+ client
14
+ end
15
+ end
16
+
17
+ def login
18
+ delete_credentials
19
+ get_credentials
20
+ end
21
+
22
+ def logout
23
+ delete_credentials
24
+ end
25
+
26
+ def clear
27
+ @credentials = nil
28
+ @client = nil
29
+ end
30
+
31
+ include Transcriptic::Helpers
32
+
33
+ # just a stub; will raise if not authenticated
34
+ def check
35
+ client.list
36
+ end
37
+
38
+ def default_host
39
+ "transcriptic.com"
40
+ end
41
+
42
+ def host
43
+ ENV['TRANSCRIPTIC_HOST'] || default_host
44
+ end
45
+
46
+ def reauthorize
47
+ @credentials = ask_for_and_save_credentials
48
+ end
49
+
50
+ def user # :nodoc:
51
+ get_credentials
52
+ @credentials[0]
53
+ end
54
+
55
+ def password # :nodoc:
56
+ get_credentials
57
+ @credentials[1]
58
+ end
59
+
60
+ def credentials_file
61
+ if host == default_host
62
+ "#{home_directory}/.transcriptic/credentials"
63
+ else
64
+ "#{home_directory}/.transcriptic/credentials.#{host}"
65
+ end
66
+ end
67
+
68
+ def get_credentials # :nodoc:
69
+ return if @credentials
70
+ unless @credentials = read_credentials
71
+ ask_for_and_save_credentials
72
+ end
73
+ @credentials
74
+ end
75
+
76
+ def read_credentials
77
+ File.exists?(credentials_file) and File.read(credentials_file).split("\n")
78
+ end
79
+
80
+ def echo_off
81
+ system "stty -echo"
82
+ end
83
+
84
+ def echo_on
85
+ system "stty echo"
86
+ end
87
+
88
+ def ask_for_credentials
89
+ puts "Enter your Transcriptic credentials."
90
+
91
+ print "Email: "
92
+ user = ask
93
+
94
+ print "Password: "
95
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
96
+ api_key = Transcriptic::Client.auth(user, password, host)['api_key']
97
+
98
+ [user, api_key]
99
+ end
100
+
101
+ def ask_for_password_on_windows
102
+ require "Win32API"
103
+ char = nil
104
+ password = ''
105
+
106
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
107
+ break if char == 10 || char == 13 # received carriage return or newline
108
+ if char == 127 || char == 8 # backspace and delete
109
+ password.slice!(-1, 1)
110
+ else
111
+ # windows might throw a -1 at us so make sure to handle RangeError
112
+ (password << char.chr) rescue RangeError
113
+ end
114
+ end
115
+ puts
116
+ return password
117
+ end
118
+
119
+ def ask_for_password
120
+ echo_off
121
+ password = ask
122
+ puts
123
+ echo_on
124
+ return password
125
+ end
126
+
127
+ def ask_for_and_save_credentials
128
+ begin
129
+ @credentials = ask_for_credentials
130
+ write_credentials
131
+ check
132
+ rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
133
+ delete_credentials
134
+ clear
135
+ display "Authentication failed."
136
+ retry if retry_login?
137
+ exit 1
138
+ rescue Exception => e
139
+ delete_credentials
140
+ raise e
141
+ end
142
+ check_for_associated_ssh_key unless Transcriptic::Command.current_command == "keys:add"
143
+ end
144
+
145
+ def check_for_associated_ssh_key
146
+ return unless client.keys.length.zero?
147
+ associate_or_generate_ssh_key
148
+ end
149
+
150
+ def associate_or_generate_ssh_key
151
+ public_keys = available_ssh_public_keys.sort
152
+
153
+ case public_keys.length
154
+ when 0 then
155
+ display "Could not find an existing public key."
156
+ display "Would you like to generate one? [Yn] ", false
157
+ unless ask.strip.downcase == "n"
158
+ display "Generating new SSH public key."
159
+ generate_ssh_key("id_rsa")
160
+ associate_key("#{home_directory}/.ssh/id_rsa.pub")
161
+ end
162
+ when 1 then
163
+ display "Found existing public key: #{public_keys.first}"
164
+ associate_key(public_keys.first)
165
+ else
166
+ display "Found the following SSH public keys:"
167
+ public_keys.each_with_index do |key, index|
168
+ display "#{index+1}) #{File.basename(key)}"
169
+ end
170
+ display "Which would you like to use with your Transcriptic account? ", false
171
+ chosen = public_keys[ask.to_i-1] rescue error("Invalid choice")
172
+ associate_key(chosen)
173
+ end
174
+ end
175
+
176
+ def generate_ssh_key(keyfile)
177
+ ssh_dir = File.join(home_directory, ".ssh")
178
+ unless File.exists?(ssh_dir)
179
+ FileUtils.mkdir_p ssh_dir
180
+ File.chmod(0700, ssh_dir)
181
+ end
182
+ `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
183
+ end
184
+
185
+ def associate_key(key)
186
+ display "Uploading ssh public key #{key}"
187
+ client.add_key(File.read(key))
188
+ end
189
+
190
+ def available_ssh_public_keys
191
+ keys = [
192
+ "#{home_directory}/.ssh/id_rsa.pub",
193
+ "#{home_directory}/.ssh/id_dsa.pub"
194
+ ]
195
+ keys.concat(Dir["#{home_directory}/.ssh/*.pub"])
196
+ keys.select { |d| File.exists?(d) }.uniq
197
+ end
198
+
199
+ def retry_login?
200
+ @login_attempts ||= 0
201
+ @login_attempts += 1
202
+ @login_attempts < 3
203
+ end
204
+
205
+ def write_credentials
206
+ FileUtils.mkdir_p(File.dirname(credentials_file))
207
+ f = File.open(credentials_file, 'w')
208
+ f.puts self.credentials
209
+ f.close
210
+ set_credentials_permissions
211
+ end
212
+
213
+ def set_credentials_permissions
214
+ FileUtils.chmod 0700, File.dirname(credentials_file)
215
+ FileUtils.chmod 0600, credentials_file
216
+ end
217
+
218
+ def delete_credentials
219
+ FileUtils.rm_f(credentials_file)
220
+ clear
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,12 @@
1
+ require "transcriptic"
2
+ require "transcriptic/command"
3
+
4
+ class Transcriptic::CLI
5
+
6
+ def self.start(*args)
7
+ command = args.shift.strip rescue "help"
8
+ Transcriptic::Command.load
9
+ Transcriptic::Command.run(command, args)
10
+ end
11
+
12
+ end
@@ -0,0 +1,332 @@
1
+ require 'rexml/document'
2
+ require 'rest-client'
3
+ require 'uri'
4
+ require 'time'
5
+ require 'transcriptic/auth'
6
+ require 'transcriptic/helpers'
7
+ require 'transcriptic/version'
8
+
9
+ # A Ruby class to call the Transcriptic REST API. You might use this if you want to
10
+ # manage your Transcriptic apps from within a Ruby program, such as Capistrano.
11
+ #
12
+ # Example:
13
+ #
14
+ # require 'transcriptic'
15
+ # transcriptic = Transcriptic::Client.new('me@example.com', 'mypass')
16
+ # transcriptic.create('myapp')
17
+ #
18
+ class Transcriptic::Client
19
+
20
+ include Transcriptic::Helpers
21
+ extend Transcriptic::Helpers
22
+
23
+ def self.version
24
+ Transcriptic::VERSION
25
+ end
26
+
27
+ def self.gem_version_string
28
+ "transcriptic-gem/#{version}"
29
+ end
30
+
31
+ attr_accessor :host, :user, :password
32
+
33
+ def self.auth(user, password, host = Transcriptic::Auth.default_host)
34
+ client = new(user, password, host)
35
+ json_decode client.post('/users/sign_in', { 'user[email]' => user, 'user[password]' => password }, :accept => 'json').to_s
36
+ end
37
+
38
+ def initialize(user, password, host=Transcriptic::Auth.default_host)
39
+ @user = user
40
+ @password = password
41
+ @host = host
42
+ end
43
+
44
+ # Show info such as mode, custom domain, and collaborators on an app.
45
+ def status(run_id)
46
+ doc = xml(get("/apps/#{name_or_domain}").to_s)
47
+ attrs = hash_from_xml_doc(doc)[:app]
48
+ attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
49
+ attrs.merge!(:addons => installed_addons(attrs[:name]))
50
+ end
51
+
52
+ # Add an ssh public key to the current user.
53
+ def add_key(key)
54
+ post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' }).to_s
55
+ end
56
+
57
+ # Remove an existing ssh public key from the current user.
58
+ def remove_key(key)
59
+ delete("/user/keys/#{escape(key)}").to_s
60
+ end
61
+
62
+ # Clear all keys on the current user.
63
+ def remove_all_keys
64
+ delete("/user/keys").to_s
65
+ end
66
+
67
+ # Get a list of stacks available to the app, with the current one marked.
68
+ def list_stacks(app_name, options={})
69
+ include_deprecated = options.delete(:include_deprecated) || false
70
+
71
+ json_decode resource("/apps/#{app_name}/stack").get(
72
+ :params => { :include_deprecated => include_deprecated },
73
+ :accept => 'application/json'
74
+ ).to_s
75
+ end
76
+
77
+ class AppCrashed < RuntimeError; end
78
+
79
+ # Show a list of projects which you are a collaborator on.
80
+ def list
81
+ doc = xml(get('/apps').to_s)
82
+ doc.elements.to_a("//apps/app").map do |a|
83
+ name = a.elements.to_a("name").first
84
+ owner = a.elements.to_a("owner").first
85
+ [name.text, owner.text]
86
+ end
87
+ end
88
+
89
+ # Show info such as mode, custom domain, and collaborators on an app.
90
+ def info(name_or_domain)
91
+ name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
92
+ end
93
+
94
+ def organizations
95
+ json_decode resource("/organizations").get(:accept => 'application/json').to_s
96
+ end
97
+
98
+ # Add an ssh public key to the current user.
99
+ def add_sequence(organization, sequence)
100
+ post("/#{organization}/sequences", sequence, { 'Content-Type' => 'text/ssh-authkey' }).to_s
101
+ end
102
+
103
+ # Remove an existing ssh public key from the current user.
104
+ def remove_sequence(organization, sequence)
105
+ delete("/#{organization}/sequences/#{escape(key)}").to_s
106
+ end
107
+
108
+ class Protocol
109
+ attr_accessor :attached, :upid
110
+
111
+ def initialize(client, protocol, upid=nil)
112
+ @client = client
113
+ @protocol = protocol
114
+ @upid = upid
115
+ end
116
+
117
+ # launch the protocol
118
+ def launch(command, attached=false)
119
+ @attached = attached
120
+ @response = @client.post(
121
+ "/runs/#{@app}/services",
122
+ command,
123
+ :content_type => 'text/plain'
124
+ )
125
+ @next_chunk = @response.to_s
126
+ @interval = 0
127
+ self
128
+ rescue RestClient::RequestFailed => e
129
+ raise AppCrashed, e.http_body if e.http_code == 502
130
+ raise
131
+ end
132
+
133
+ # Does the service have any remaining output?
134
+ def end_of_stream?
135
+ @next_chunk.nil?
136
+ end
137
+
138
+ # Read the next chunk of output.
139
+ def read
140
+ chunk = @client.get(@next_chunk)
141
+ if chunk.headers[:location].nil? && chunk.code != 204
142
+ # no more chunks
143
+ @next_chunk = nil
144
+ chunk.to_s
145
+ elsif chunk.to_s == ''
146
+ # assume no content and back off
147
+ @interval = 2
148
+ ''
149
+ elsif location = chunk.headers[:location]
150
+ # some data read and next chunk available
151
+ @next_chunk = location
152
+ @interval = 0
153
+ chunk.to_s
154
+ end
155
+ end
156
+
157
+ # Iterate over all output chunks until EOF is reached.
158
+ def each
159
+ until end_of_stream?
160
+ sleep(@interval)
161
+ output = read
162
+ yield output unless output.empty?
163
+ end
164
+ end
165
+
166
+ # All output as a string
167
+ def to_s
168
+ buf = []
169
+ each { |part| buf << part }
170
+ buf.join
171
+ end
172
+ end
173
+
174
+ def status(run_id)
175
+ json_decode resource("/runs/#{run_id}/status").get(:accept => 'application/json').to_s
176
+ end
177
+
178
+ # Get a Protocol instance to execute commands against.
179
+ def protocol(run_id, upid)
180
+ Protocol.new(self, run_id, upid)
181
+ end
182
+
183
+ def read_logs(run_id, options=[])
184
+ query = "&" + options.join("&") unless options.empty?
185
+ url = get("/runs/#{run_id}/logs?#{query}").to_s
186
+ uri = URI.parse(url);
187
+ http = Net::HTTP.new(uri.host, uri.port)
188
+
189
+ if uri.scheme == 'https'
190
+ http.use_ssl = true
191
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
192
+ end
193
+
194
+ http.read_timeout = 60 * 60 * 24
195
+
196
+ begin
197
+ http.start do
198
+ http.request_get(uri.path + (uri.query ? "?" + uri.query : "")) do |request|
199
+ request.read_body do |chunk|
200
+ yield chunk
201
+ end
202
+ end
203
+ end
204
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
205
+ abort(" ! Could not connect to logging service")
206
+ rescue Timeout::Error, EOFError
207
+ abort("\n ! Request timed out")
208
+ end
209
+ end
210
+
211
+ def on_warning(&blk)
212
+ @warning_callback = blk
213
+ end
214
+
215
+ ##################
216
+
217
+ def resource(uri, options={})
218
+ RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
219
+ resource = RestClient::Resource.new(realize_full_uri(uri), options.merge(:user => user, :password => password))
220
+ resource
221
+ end
222
+
223
+ def get(uri, extra_headers={}) # :nodoc:
224
+ process(:get, uri, extra_headers)
225
+ end
226
+
227
+ def post(uri, payload="", extra_headers={}) # :nodoc:
228
+ process(:post, uri, extra_headers, payload)
229
+ end
230
+
231
+ def put(uri, payload, extra_headers={}) # :nodoc:
232
+ process(:put, uri, extra_headers, payload)
233
+ end
234
+
235
+ def delete(uri, extra_headers={}) # :nodoc:
236
+ process(:delete, uri, extra_headers)
237
+ end
238
+
239
+ def process(method, uri, extra_headers={}, payload=nil)
240
+ headers = transcriptic_headers.merge(extra_headers)
241
+ args = [method, payload, headers].compact
242
+
243
+ resource_options = default_resource_options_for_uri(uri)
244
+
245
+ begin
246
+ response = resource(uri, resource_options).send(*args)
247
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
248
+ host = URI.parse(realize_full_uri(uri)).host
249
+ error " ! Unable to connect to #{host}"
250
+ rescue RestClient::SSLCertificateNotVerified => ex
251
+ host = URI.parse(realize_full_uri(uri)).host
252
+ #error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with TRANSCRIPTIC_SSL_VERIFY=disable"
253
+ end
254
+
255
+ extract_warning(response)
256
+ response
257
+ end
258
+
259
+ def extract_warning(response)
260
+ return unless response
261
+ if response.headers[:x_transcriptic_warning] && @warning_callback
262
+ warning = response.headers[:x_transcriptic_warning]
263
+ @displayed_warnings ||= {}
264
+ unless @displayed_warnings[warning]
265
+ @warning_callback.call(warning)
266
+ @displayed_warnings[warning] = true
267
+ end
268
+ end
269
+ end
270
+
271
+ def transcriptic_headers # :nodoc:
272
+ {
273
+ 'X-Transcriptic-API-Version' => '1',
274
+ 'User-Agent' => self.class.gem_version_string,
275
+ 'X-Ruby-Version' => RUBY_VERSION,
276
+ 'X-Ruby-Platform' => RUBY_PLATFORM
277
+ }
278
+ end
279
+
280
+ def xml(raw) # :nodoc:
281
+ REXML::Document.new(raw)
282
+ end
283
+
284
+ def escape(value) # :nodoc:
285
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
286
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
287
+ end
288
+
289
+ module JSON
290
+ def self.parse(json)
291
+ json_decode(json)
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def realize_full_uri(given)
298
+ full_host = (host =~ /^http/) ? host : "https://www.#{host}"
299
+ host = URI.parse(full_host)
300
+ uri = URI.parse(given)
301
+ uri.host ||= host.host
302
+ uri.scheme ||= host.scheme || "https"
303
+ uri.path = (uri.path[0..0] == "/") ? uri.path : "/#{uri.path}"
304
+ uri.port = host.port if full_host =~ /\:\d+/
305
+ uri.to_s
306
+ end
307
+
308
+ def default_resource_options_for_uri(uri)
309
+ if ENV["TRANSCRIPTIC_SSL_VERIFY"] == "disable"
310
+ {}
311
+ elsif realize_full_uri(uri) =~ %r|^https://www.transcriptic.com|
312
+ { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file }
313
+ else
314
+ {}
315
+ end
316
+ end
317
+
318
+ def local_ca_file
319
+ File.expand_path("../../data/cacert.pem", __FILE__)
320
+ end
321
+
322
+ def hash_from_xml_doc(elements)
323
+ elements.inject({}) do |hash, e|
324
+ next(hash) unless e.respond_to?(:children)
325
+ hash.update(e.name.gsub("-","_").to_sym => case e.children.length
326
+ when 0 then nil
327
+ when 1 then e.text
328
+ else hash_from_xml_doc(e.children)
329
+ end)
330
+ end
331
+ end
332
+ end