module Fog module Google module Shared attr_reader :project, :api_version, :api_url ## # Initializes shared attributes # # @param [String] project Google Cloud Project # @param [String] api_version Google API version # @param [String] base_url Google API base url # @return [void] def shared_initialize(project, api_version, base_url) @project = project @api_version = api_version @api_url = base_url + api_version + "/projects/" end ## # Initializes the Google API Client # # @param [Hash] options Google API options # @option options [String] :google_client_email A @developer.gserviceaccount.com email address to use # @option options [String] :google_key_location The location of a pkcs12 key file # @option options [String] :google_key_string The content of the pkcs12 key file # @option options [String] :google_json_key_location The location of a JSON key file # @option options [String] :google_json_key_string The content of the JSON key file # @option options [String] :google_api_scope_url The access scope URLs # @option options [String] :app_name The app name to set in the user agent # @option options [String] :app_version The app version to set in the user agent # @option options [Google::APIClient] :google_client Existing Google API Client # @option options [Hash] :google_client_options A hash to send adition options to Google API Client # @return [Google::APIClient] Google API Client # @raises [ArgumentError] If there is any missing argument def initialize_google_client(options) # NOTE: loaded here to avoid requiring this as a core Fog dependency begin require "google/api_client" rescue LoadError => error Fog::Logger.warning("Please install the google-api-client gem before using this provider") raise error end # User can provide an existing Google API Client client = options[:google_client] return client unless client.nil? # Create a signing key signing_key = create_signing_key(options) # Validate required arguments unless options[:google_client_email] raise ArgumentError.new("Missing required arguments: google_client_email") end unless options[:google_api_scope_url] raise ArgumentError.new("Missing required arguments: google_api_scope_url") end # Create a new Google API Client new_pk12_google_client( options[:google_client_email], signing_key, options[:google_api_scope_url], options[:app_name], options[:app_version], options[:google_client_options] || {} ) end ## # Creates a Google signing key # def create_signing_key(options) if options[:google_json_key_location] || options[:google_json_key_string] if options[:google_json_key_location] json_key_location = File.expand_path(options[:google_json_key_location]) json_key = File.open(json_key_location, "r", &:read) else json_key = options[:google_json_key_string] end json_key_hash = Fog::JSON.decode(json_key) unless json_key_hash.key?("client_email") || json_key_hash.key?("private_key") raise ArgumentError.new("Invalid Google JSON key") end options[:google_client_email] = json_key_hash["client_email"] ::Google::APIClient::KeyUtils.load_from_pem(json_key_hash["private_key"], "notasecret") elsif options[:google_key_location] || options[:google_key_string] google_key = if options[:google_key_location] File.expand_path(options[:google_key_location]) else options[:google_key_string] end ::Google::APIClient::KeyUtils.load_from_pkcs12(google_key, "notasecret") else raise ArgumentError.new("Missing required arguments: google_key_location, google_key_string, " \ "google_json_key_location or google_json_key_string") end end ## # Create a Google API Client with a user email and a pkcs12 key # # @param google_client_email [String] A @developer.gserviceaccount.com. # email address to use # @param signing_key [OpenSSL::PKey] The private key for signing # @param google_api_scope_url [String] Access scope URLs # @param app_name [String] The app name to set in the user agent # @param app_version [String] The app version to set in the user agent # @param google_client_options [Hash] additional options to pass to the # underlying google client # @return [Google::APIClient] a newly-constructed Google API Client def new_pk12_google_client(google_client_email, signing_key, google_api_scope_url, app_name = nil, app_version = nil, google_client_options = {}) application_name = app_name.nil? ? "fog" : "#{app_name}/#{app_version || '0.0.0'} fog" api_client_options = { :application_name => application_name, :application_version => Fog::Google::VERSION }.merge(google_client_options) client = ::Google::APIClient.new(api_client_options) client.authorization = Signet::OAuth2::Client.new( :audience => "https://accounts.google.com/o/oauth2/token", :auth_provider_x509_cert_url => "https://www.googleapis.com/oauth2/v1/certs", :client_x509_cert_url => "https://www.googleapis.com/robot/v1/metadata/x509/#{google_client_email}", :issuer => google_client_email, :scope => google_api_scope_url, :signing_key => signing_key, :token_credential_uri => "https://accounts.google.com/o/oauth2/token" ) client.authorization.fetch_access_token! client end ## # Executes a request and wraps it in a result object # # @param [Google::APIClient::Method] api_method The method object or the RPC name of the method being executed # @param [Hash] parameters The parameters to send to the method # @param [Hash] body_object The body object of the request # @return [Excon::Response] The result from the API def request(api_method, parameters, body_object = nil, media = nil) client_parms = { :api_method => api_method, :parameters => parameters } # The Google API complains when given null values for enums, so just don't pass it any null fields # XXX It may still balk if we have a nested object, e.g.: # {:a_field => "string", :a_nested_field => { :an_empty_nested_field => nil } } client_parms[:body_object] = body_object.reject { |_k, v| v.nil? } if body_object client_parms[:media] = media if media result = @client.execute(client_parms) build_excon_response(result.body.nil? || result.body.empty? ? nil : Fog::JSON.decode(result.body), result.status) end ## # Builds an Excon response # # @param [Hash] Response body # @param [Integer] Response status # @return [Excon::Response] Excon response def build_excon_response(body, status = 200) response = Excon::Response.new(:body => body, :status => status) if body && body.key?("error") msg = "Google Cloud did not return an error message" if body["error"].is_a?(Hash) response.status = body["error"]["code"] if body["error"].key?("errors") msg = body["error"]["errors"].map { |error| error["message"] }.join(", ") elsif body["error"].key?("message") msg = body["error"]["message"] end elsif body["error"].is_a?(Array) msg = body["error"].map { |error| error["code"] }.join(", ") end case response.status when 404 raise Fog::Errors::NotFound.new(msg) else raise Fog::Errors::Error.new(msg) end end response end end end end