class Acmesmith::ChallengeResponders::GoogleCloudDns
Public Class Methods
new(config)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 19 def initialize(config) @config = config @scope = "https://www.googleapis.com/auth/ndev.clouddns.readwrite" @api = Google::Apis::DnsV1::DnsService.new if @config[:compute_engine_service_account] @api.authorization = Google::Auth.get_application_default(@scope) elsif @config[:private_key_json_file] credential = load_json_key(@config[:private_key_json_file]) @api.authorization = Signet::OAuth2::Client.new( token_credential_uri: "https://accounts.google.com/o/oauth2/token", audience: "https://accounts.google.com/o/oauth2/token", scope: @scope, issuer: credential[:email_address], signing_key: credential[:private_key]) else raise "You need to specify authentication options (compute_engine_service_account or private_key_json_file)" end @api.authorization.fetch_access_token! @project_id = @config[:project_id] end
Public Instance Methods
cap_respond_all?()
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 15 def cap_respond_all? true end
cleanup_all(*domain_and_challenges)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 57 def cleanup_all(*domain_and_challenges) challenges_by_zone_names = domain_and_challenges.group_by{ |domain, challenge| domain = canonicalize(domain) find_managed_zone(domain).name } challenges_by_zone_names.each do |zone_name, dcs| change = change_for_challenges(zone_name, dcs, for_cleanup: true) resp = @api.create_change(@project_id, zone_name, change) change_id = resp.id wait_for_sync_by_api(zone_name, change_id) end end
respond_all(*domain_and_challenges)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 40 def respond_all(*domain_and_challenges) challenges_by_zone_names = domain_and_challenges.group_by{ |domain, challenge| domain = canonicalize(domain) find_managed_zone(domain).name } challenges_by_zone_names.each do |zone_name, dcs| change = change_for_challenges(zone_name, dcs) resp = @api.create_change(@project_id, zone_name, change) change_id = resp.id wait_for_sync_by_api(zone_name, change_id) wait_for_sync_by_dns(zone_name, change) end end
support?(type)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 11 def support?(type) type == 'dns-01' end
Private Instance Methods
canonicalize(domain)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 128 def canonicalize(domain) "#{domain}.".gsub(/\.{2,}/, '.') end
change_for_challenges(zone_name, domain_and_challenges, for_cleanup: false)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 142 def change_for_challenges(zone_name, domain_and_challenges, for_cleanup: false) current_rrsets = @api.fetch_all(items: :rrsets) do |token| @api.list_resource_record_sets(@project_id, zone_name, page_token: token) end change = Google::Apis::DnsV1::Change.new change.deletions = domain_and_challenges.map{ |domain, challenge| domain = canonicalize(domain) name = [challenge.record_name, domain].join('.') type = challenge.record_type current_rrsets.find{ |rrset| rrset.type == type && rrset.name == name } }.uniq.compact change.additions = domain_and_challenges.map{ |domain, challenge| domain = canonicalize(domain) name = [challenge.record_name, domain].join('.') type = challenge.record_type data = "\"#{challenge.record_content}\"" { name: name, type: type, rrdatas: [data], } }.group_by{ |rrset_param| [ rrset_param[:name], rrset_param[:type] ] }.map{ |(name, type), rrset_params| current_rrset = current_rrsets.find{ |rrset| rrset.type == type && rrset.name == name } new_rrset = Google::Apis::DnsV1::ResourceRecordSet.new( name: name, type: type, rrdatas: current_rrset ? current_rrset.rrdatas : [], ttl: @config[:ttl] || 5, ) if for_cleanup new_rrset.rrdatas -= rrset_params.map{|rrset| rrset[:rrdatas] }.flatten else new_rrset.rrdatas += rrset_params.map{|rrset| rrset[:rrdatas] }.flatten end new_rrset }.select{ |rrset| rrset.rrdatas != [] } change end
find_managed_zone(domain)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 132 def find_managed_zone(domain) managed_zone = @api.list_managed_zones(@project_id).managed_zones.select do |zone| /(?:\A|\.)#{Regexp.escape(zone.dns_name)}\z/ =~ domain end.max_by{|z| z.dns_name.size } if managed_zone.nil? raise "Domain #{domain} is not managed in Google Cloud DNS [project_id=#{@project_id}]" end managed_zone end
load_json_key(filepath)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 120 def load_json_key(filepath) obj = JSON.parse(File.read(filepath)) { email_address: obj["client_email"], private_key: OpenSSL::PKey.read(obj["private_key"]), } end
wait_for_sync_by_api(zone_name, change_id)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 75 def wait_for_sync_by_api(zone_name, change_id) puts " * requested change: #{change_id}" resp = @api.get_change(@project_id, zone_name, change_id) while resp.status != 'done' puts " * change #{change_id.inspect} is still #{resp.status.inspect}" sleep 5 resp = @api.get_change(@project_id, zone_name, change_id) end puts " * synced!" end
wait_for_sync_by_dns(zone_name, change)
click to toggle source
# File lib/acmesmith/challenge_responders/google_cloud_dns.rb, line 88 def wait_for_sync_by_dns(zone_name, change) puts "=> Checking DNS resource record" nameservers = @api.get_managed_zone(@project_id, zone_name).name_servers puts " * nameservers: #{nameservers.inspect}" nameservers.each do |ns| Resolv::DNS.open(:nameserver => Resolv.getaddresses(ns)) do |dns| dns.timeouts = 5 change.additions.each do |rrset| required_rrdatas = Set.new(rrset.rrdatas.map{|rrdata| rrdata.gsub(/(\A"|"\z)/, '') }) deletion = change.deletions.find{|_deletion| _deletion.name == rrset.name && _deletion.type == rrset.type } if deletion required_rrdatas -= Set.new(deletion.rrdatas) end loop do resources = dns.getresources(rrset.name, Resolv::DNS::Resource::IN::TXT) actual_rrdatas = resources.map(&:data) if required_rrdatas.subset?(Set.new(actual_rrdatas)) puts " * [#{ns} -> #{rrset.name}] success. (actual=#{actual_rrdatas.inspect})" sleep 1 break else puts " * [#{ns} -> #{rrset.name}] failed. (required=#{required_rrdatas.to_a.inspect}, but actual=#{actual_rrdatas.inspect})" sleep 5 end end end end end end