require_relative 'template' require_relative '../http/client' require_dependency 'carto/named_maps/template' module Carto module NamedMaps class Api include ::LoggerHelper HTTP_CONNECT_TIMEOUT_SECONDS = 45 HTTP_REQUEST_TIMEOUT_SECONDS = 60 RETRY_TIME_SECONDS = 2 MAX_RETRY_ATTEMPTS = 3 attr_accessor :user, :visualization def initialize(visualization) @visualization = visualization @user = visualization.user @named_map_template = Carto::NamedMaps::Template.new(visualization) end def index stats_aggregator.timing('carto-named-maps-api.index') do response = stats_aggregator.timing('call') do http_client.get(url, request_params) end if response.code.to_s =~ /^2/ ::JSON.parse(response.response_body).deep_symbolize_keys else log_response(response, 'index') end end end def create stats_aggregator.timing('carto-named-maps-api.create') do params = request_params params[:body] = @named_map_template.to_json response = http_client.post(url, params) response_code = response.code if response_code.to_s =~ /^2/ ::JSON.parse(response.response_body).deep_symbolize_keys elsif response_code != 409 # Ignore when max number of templates is reached log_response(response, 'create') end end end def show stats_aggregator.timing('carto-named-maps-api.show') do response = stats_aggregator.timing('call') do url = url(template_name: @named_map_template.name) response = http_client.get(url, request_params) end response_code = response.code if response_code.to_s =~ /^2/ ::JSON.parse(response.response_body).deep_symbolize_keys else log_response(response, 'show') unless response_code == 404 end end end def update(retries: 0) stats_aggregator.timing('carto-named-maps-api.update') do params = request_params params[:body] = @named_map_template.to_json response = http_client.put(url(template_name: @named_map_template.name), params) response_code_string = response.code.to_s if response_code_string =~ /^2/ ::JSON.parse(response.response_body).deep_symbolize_keys elsif response_code_string =~ /^5/ && retries < MAX_RETRY_ATTEMPTS sleep(RETRY_TIME_SECONDS**retries) update(retries: retries + 1) else log_response(response, 'update') end end end def destroy stats_aggregator.timing('carto-named-maps-api.destroy') do url = url(template_name: @named_map_template.name) response = http_client.delete(url, request_params) response_code = response.code if response_code.to_s =~ /^2/ response.response_body else log_response(response, 'destroy') unless response_code == 404 end end end def upsert show ? update : create end private def stats_aggregator @@stats_aggregator_instance ||= CartoDB::Stats::EditorAPIs.instance end def url(template_name: '') username = user.username user_url = CartoDB.subdomainless_urls? ? "/user/#{username}" : '' "#{protocol}://#{host(username)}:#{port}#{user_url}/api/v1/map/named/#{template_name}?api_key=#{user.api_key}" end def request_params { headers: headers(user.username), ssl_verifypeer: ssl_verifypeer, ssl_verifyhost: ssl_verifyhost, followlocation: true, connecttimeout: HTTP_CONNECT_TIMEOUT_SECONDS, timeout: HTTP_REQUEST_TIMEOUT_SECONDS } end def headers(username) @headers ||= { 'content-type': 'application/json', 'host': domain(username) } end def domain(username) return @domain if @domain config_domain = Cartodb.get_config(:tiler, 'internal', 'domain') CartoDB.subdomainless_urls? ? config_domain : "#{username}.#{config_domain}" end def port @port ||= Cartodb.get_config(:tiler, 'internal', 'port') end def protocol @protocol ||= Cartodb.get_config(:tiler, 'internal', 'protocol') end def ssl_verifypeer @verifycert ||= Cartodb.get_config(:tiler, 'internal', 'verifycert') end def ssl_verifyhost ssl_verifypeer ? 2 : 0 end def host(username) return @host if @host config_host = Cartodb.get_config(:tiler, 'internal', 'host') @host ||= config_host.blank? ? domain(username) : config_host end def http_client @http_client ||= Carto::Http::Client.get('named_maps') end def log_response(response, action) log_error( message: 'Error in named maps API', current_user: user, visualization_id: visualization.id, action: action, request_url: response.request.url, status: response.code, response_body: response.body ) rescue Encoding::UndefinedConversionError => e # Hotfix for preventing https://rollbar.com/carto/CartoDB/items/41457 until we find the root cause # https://cartoteam.slack.com/archives/CEQLWTW9Z/p1599134417001900 # https://app.clubhouse.io/cartoteam/story/101908/fix-encoding-error-while-logging-request Rollbar.error(e) end def log_context super.merge(request_id: Carto::CurrentRequest.request_id, component: 'cartodb.named-maps-client') end end end end