X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/43c411ec1441ee1710dc33389d7451f7414a170f..52bf3c0924da239488fff3ed9907c3478ee44e6b:/apps/workbench/app/models/arvados_api_client.rb diff --git a/apps/workbench/app/models/arvados_api_client.rb b/apps/workbench/app/models/arvados_api_client.rb index c7faeb6f03..a7ae8ba3aa 100644 --- a/apps/workbench/app/models/arvados_api_client.rb +++ b/apps/workbench/app/models/arvados_api_client.rb @@ -1,54 +1,142 @@ +require 'httpclient' +require 'thread' + class ArvadosApiClient - class NotLoggedInException < Exception + class NotLoggedInException < StandardError + end + class InvalidApiResponseException < StandardError + end + + @@profiling_enabled = Rails.configuration.profiling_enabled + @@discovery = nil + + # An API client object suitable for handling API requests on behalf + # of the current thread. + def self.new_or_current + # If this thread doesn't have an API client yet, *or* this model + # has been reloaded since the existing client was created, create + # a new client. Otherwise, keep using the latest client created in + # the current thread. + unless Thread.current[:arvados_api_client].andand.class == self + Thread.current[:arvados_api_client] = new + end + Thread.current[:arvados_api_client] end + + def initialize *args + @api_client = nil + @client_mtx = Mutex.new + end + def api(resources_kind, action, data=nil) - arvados_api_token = Thread.current[:arvados_api_token] - arvados_api_token = '' if arvados_api_token.nil? - dataargs = ['--data-urlencode', - "api_token=#{arvados_api_token}", - '--header', - 'Accept:application/json'] + profile_checkpoint + + if not @api_client + @client_mtx.synchronize do + @api_client = HTTPClient.new + if Rails.configuration.arvados_insecure_https + @api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE + else + # Use system CA certificates + @api_client.ssl_config.add_trust_ca('/etc/ssl/certs') + end + end + end + + resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class + url = "#{self.arvados_v1_base}/#{resources_kind}#{action}" + + # Clean up /arvados/v1/../../discovery/v1 to /discovery/v1 + url.sub! '/arvados/v1/../../', '/' + + query = { + 'api_token' => Thread.current[:arvados_api_token] || '', + 'reader_tokens' => (Thread.current[:reader_tokens] || []).to_json, + } if !data.nil? data.each do |k,v| - dataargs << '--data-urlencode' if v.is_a? String or v.nil? - dataargs << "#{k}=#{v}" - elsif v == true or v == false - dataargs << "#{k}=#{v ? 1 : 0}" + query[k] = v + elsif v == true + query[k] = 1 + elsif v == false + query[k] = 0 else - dataargs << "#{k}=#{JSON.dump(v)}" + query[k] = JSON.dump(v) end end else - dataargs << '--data-urlencode' << '_method=GET' + query["_method"] = "GET" end - json = nil - resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class - url = "#{self.arvados_v1_base}/#{resources_kind}#{action}" - IO.popen([ENV, - 'curl', - '-s', - *dataargs, - url], - 'r') do |io| - json = io.read - end - resp = Oj.load(json, :symbol_keys => true) - if resp[:errors] - if resp[:errors][0] == 'Not logged in' - raise NotLoggedInException.new - else - errors = resp[:errors] - errors = errors.join("\n\n") if errors.is_a? Array - raise "API errors:\n\n#{errors}\n" - end + if @@profiling_enabled + query["_profile"] = "true" + end + + header = {"Accept" => "application/json"} + + profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]} #{query[:filters]}" } + msg = @client_mtx.synchronize do + @api_client.post(url, + query, + header: header) + end + profile_checkpoint 'API transaction' + + if msg.status_code == 401 + raise NotLoggedInException.new + end + + json = msg.content + + begin + resp = Oj.load(json, :symbol_keys => true) + rescue Oj::ParseError + raise InvalidApiResponseException.new json + end + if not resp.is_a? Hash + raise InvalidApiResponseException.new json + end + if msg.status_code != 200 + errors = resp[:errors] + errors = errors.join("\n\n") if errors.is_a? Array + raise "#{errors} [API: #{msg.status_code}]" end + if resp[:_profile] + Rails.logger.info "API client: " \ + "#{resp.delete(:_profile)[:request_time]} request_time" + end + profile_checkpoint 'Parse response' resp end + def self.patch_paging_vars(ary, items_available, offset, limit, links=nil) + if items_available + (class << ary; self; end).class_eval { attr_accessor :items_available } + ary.items_available = items_available + end + if offset + (class << ary; self; end).class_eval { attr_accessor :offset } + ary.offset = offset + end + if limit + (class << ary; self; end).class_eval { attr_accessor :limit } + ary.limit = limit + end + if links + (class << ary; self; end).class_eval { attr_accessor :links } + ary.links = links + end + ary + end + def unpack_api_response(j, kind=nil) if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/) - j[:items].collect { |x| unpack_api_response x, j[:kind] } + ary = j[:items].collect { |x| unpack_api_response x, x[:kind] } + links = ArvadosResourceList.new Link + links.results = (j[:links] || []).collect do |x| + unpack_api_response x, x[:kind] + end + self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit], links) elsif j.is_a? Hash and (kind || j[:kind]) oclass = self.kind_class(kind || j[:kind]) if oclass @@ -88,8 +176,8 @@ class ArvadosApiClient Rails.configuration.arvados_v1_base end - def arvados_schema - @arvados_schema ||= api 'schema', '' + def discovery + @@discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', '' end def kind_class(kind) @@ -99,4 +187,15 @@ class ArvadosApiClient def class_kind(resource_class) resource_class.to_s.underscore end + + protected + def profile_checkpoint label=nil + return if !@@profiling_enabled + label = yield if block_given? + t = Time.now + if label and @profile_t0 + Rails.logger.info "API client: #{t - @profile_t0} #{label}" + end + @profile_t0 = t + end end