2016-08-23 08:31:50 +08:00
package route53
import (
"encoding/json"
"fmt"
2017-08-05 22:56:42 +08:00
"sort"
2016-08-23 08:31:50 +08:00
"strings"
"time"
2016-12-17 04:10:27 +08:00
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
2016-08-23 08:31:50 +08:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
r53 "github.com/aws/aws-sdk-go/service/route53"
2017-08-05 22:56:42 +08:00
r53d "github.com/aws/aws-sdk-go/service/route53domains"
2017-05-04 01:30:05 +08:00
"github.com/pkg/errors"
2016-08-23 08:31:50 +08:00
)
type route53Provider struct {
2017-08-05 22:56:42 +08:00
client * r53 . Route53
registrar * r53d . Route53Domains
zones map [ string ] * r53 . HostedZone
2016-08-23 08:31:50 +08:00
}
2017-08-05 22:56:42 +08:00
func newRoute53Reg ( conf map [ string ] string ) ( providers . Registrar , error ) {
return newRoute53 ( conf , nil )
}
func newRoute53Dsp ( conf map [ string ] string , metadata json . RawMessage ) ( providers . DNSServiceProvider , error ) {
return newRoute53 ( conf , metadata )
}
func newRoute53 ( m map [ string ] string , metadata json . RawMessage ) ( * route53Provider , error ) {
2016-08-23 08:31:50 +08:00
keyId , secretKey := m [ "KeyId" ] , m [ "SecretKey" ]
2017-05-04 01:30:05 +08:00
2017-08-05 22:56:42 +08:00
// Route53 uses a global endpoint and route53domains
// currently only has a single regional endpoint in us-east-1
// http://docs.aws.amazon.com/general/latest/gr/rande.html#r53_region
2017-05-04 01:30:05 +08:00
config := & aws . Config {
2017-08-05 22:56:42 +08:00
Region : aws . String ( "us-east-1" ) ,
2017-05-04 01:30:05 +08:00
}
if keyId != "" || secretKey != "" {
config . Credentials = credentials . NewStaticCredentials ( keyId , secretKey , "" )
2016-08-23 08:31:50 +08:00
}
2017-05-04 01:30:05 +08:00
sess := session . New ( config )
2016-08-23 08:31:50 +08:00
2017-08-05 22:56:42 +08:00
api := & route53Provider { client : r53 . New ( sess ) , registrar : r53d . New ( sess ) }
2017-05-04 01:30:05 +08:00
err := api . getZones ( )
if err != nil {
return nil , err
}
2016-08-23 08:31:50 +08:00
return api , nil
}
2017-09-15 04:13:17 +08:00
var docNotes = providers . DocumentationNotes {
providers . DocDualHost : providers . Can ( ) ,
providers . DocCreateDomains : providers . Can ( ) ,
providers . DocOfficiallySupported : providers . Can ( ) ,
2017-09-16 01:52:10 +08:00
providers . CanUseAlias : providers . Cannot ( "R53 does not provide a generic ALIAS functionality. They do have 'ALIAS' CNAME types to point at various AWS infrastructure, but dnscontrol has not implemented those." ) ,
2017-09-15 04:13:17 +08:00
}
2016-08-23 08:31:50 +08:00
func init ( ) {
2017-09-15 04:13:17 +08:00
providers . RegisterDomainServiceProviderType ( "ROUTE53" , newRoute53Dsp , providers . CanUsePTR , providers . CanUseSRV , providers . CanUseCAA , docNotes )
2017-08-05 22:56:42 +08:00
providers . RegisterRegistrarType ( "ROUTE53" , newRoute53Reg )
2016-08-23 08:31:50 +08:00
}
2017-08-05 22:56:42 +08:00
2016-08-23 08:31:50 +08:00
func sPtr ( s string ) * string {
return & s
}
2017-01-12 03:38:07 +08:00
2016-08-23 08:31:50 +08:00
func ( r * route53Provider ) getZones ( ) error {
var nextMarker * string
r . zones = make ( map [ string ] * r53 . HostedZone )
for {
2016-12-17 04:10:27 +08:00
if nextMarker != nil {
fmt . Println ( * nextMarker )
}
inp := & r53 . ListHostedZonesInput { Marker : nextMarker }
2016-08-23 08:31:50 +08:00
out , err := r . client . ListHostedZones ( inp )
2017-05-04 01:30:05 +08:00
if err != nil && strings . Contains ( err . Error ( ) , "is not authorized" ) {
return errors . New ( "Check your credentials, your not authorized to perform actions on Route 53 AWS Service" )
} else if err != nil {
2016-08-23 08:31:50 +08:00
return err
}
for _ , z := range out . HostedZones {
domain := strings . TrimSuffix ( * z . Name , "." )
r . zones [ domain ] = z
}
if out . NextMarker != nil {
nextMarker = out . NextMarker
} else {
break
}
}
return nil
}
//map key for grouping records
type key struct {
Name , Type string
}
2017-01-12 03:38:07 +08:00
func getKey ( r * models . RecordConfig ) key {
return key { r . NameFQDN , r . Type }
2016-08-23 08:31:50 +08:00
}
2017-01-04 04:26:08 +08:00
type errNoExist struct {
domain string
}
func ( e errNoExist ) Error ( ) string {
return fmt . Sprintf ( "Domain %s not found in your route 53 account" , e . domain )
}
2016-12-17 04:10:27 +08:00
func ( r * route53Provider ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2017-05-04 01:30:05 +08:00
2016-12-17 04:10:27 +08:00
zone , ok := r . zones [ domain ]
if ! ok {
2017-01-04 04:26:08 +08:00
return nil , errNoExist { domain }
2016-12-17 04:10:27 +08:00
}
z , err := r . client . GetHostedZone ( & r53 . GetHostedZoneInput { Id : zone . Id } )
if err != nil {
return nil , err
}
ns := [ ] * models . Nameserver { }
2017-05-04 01:30:05 +08:00
if z . DelegationSet != nil {
for _ , nsPtr := range z . DelegationSet . NameServers {
ns = append ( ns , & models . Nameserver { Name : * nsPtr } )
}
2016-12-17 04:10:27 +08:00
}
return ns , nil
}
2016-08-23 08:31:50 +08:00
func ( r * route53Provider ) GetDomainCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
2017-03-23 03:08:23 +08:00
dc . Punycode ( )
2016-12-17 04:10:27 +08:00
2016-08-23 08:31:50 +08:00
var corrections = [ ] * models . Correction { }
zone , ok := r . zones [ dc . Name ]
// add zone if it doesn't exist
if ! ok {
2017-01-04 04:26:08 +08:00
return nil , errNoExist { dc . Name }
2016-08-23 08:31:50 +08:00
}
records , err := r . fetchRecordSets ( zone . Id )
if err != nil {
return nil , err
}
2017-01-12 03:38:07 +08:00
var existingRecords = [ ] * models . RecordConfig { }
2016-08-23 08:31:50 +08:00
for _ , set := range records {
for _ , rec := range set . ResourceRecords {
if * set . Type == "SOA" {
continue
}
r := & models . RecordConfig {
2017-07-21 06:59:09 +08:00
NameFQDN : unescape ( set . Name ) ,
Type : * set . Type ,
Target : * rec . Value ,
TTL : uint32 ( * set . TTL ) ,
CombinedTarget : true ,
2016-08-23 08:31:50 +08:00
}
existingRecords = append ( existingRecords , r )
}
}
for _ , want := range dc . Records {
2017-07-20 03:53:40 +08:00
want . MergeToTarget ( )
2016-08-23 08:31:50 +08:00
}
2017-11-08 06:12:17 +08:00
// Normalize
models . Downcase ( existingRecords )
2016-08-23 08:31:50 +08:00
//diff
2017-01-12 03:38:07 +08:00
differ := diff . New ( dc )
_ , create , delete , modify := differ . IncrementalDiff ( existingRecords )
2016-08-23 08:31:50 +08:00
2017-03-23 03:08:23 +08:00
namesToUpdate := map [ key ] [ ] string { }
2016-08-23 08:31:50 +08:00
for _ , c := range create {
2017-03-23 03:08:23 +08:00
namesToUpdate [ getKey ( c . Desired ) ] = append ( namesToUpdate [ getKey ( c . Desired ) ] , c . String ( ) )
2016-08-23 08:31:50 +08:00
}
for _ , d := range delete {
2017-03-23 03:08:23 +08:00
namesToUpdate [ getKey ( d . Existing ) ] = append ( namesToUpdate [ getKey ( d . Existing ) ] , d . String ( ) )
2016-08-23 08:31:50 +08:00
}
for _ , m := range modify {
2017-03-23 03:08:23 +08:00
namesToUpdate [ getKey ( m . Desired ) ] = append ( namesToUpdate [ getKey ( m . Desired ) ] , m . String ( ) )
2016-08-23 08:31:50 +08:00
}
if len ( namesToUpdate ) == 0 {
return nil , nil
}
updates := map [ key ] [ ] * models . RecordConfig { }
//for each name we need to update, collect relevant records from dc
for k := range namesToUpdate {
updates [ k ] = nil
for _ , rc := range dc . Records {
if getKey ( rc ) == k {
updates [ k ] = append ( updates [ k ] , rc )
}
}
}
2017-03-23 03:08:23 +08:00
dels := [ ] * r53 . Change { }
2016-08-23 08:31:50 +08:00
changes := [ ] * r53 . Change { }
2017-03-23 03:08:23 +08:00
changeDesc := ""
delDesc := ""
2016-08-23 08:31:50 +08:00
for k , recs := range updates {
chg := & r53 . Change { }
var rrset * r53 . ResourceRecordSet
if len ( recs ) == 0 {
2017-03-23 03:08:23 +08:00
dels = append ( dels , chg )
2016-08-23 08:31:50 +08:00
chg . Action = sPtr ( "DELETE" )
2017-03-23 03:08:23 +08:00
delDesc += strings . Join ( namesToUpdate [ k ] , "\n" ) + "\n"
2016-08-23 08:31:50 +08:00
// on delete just submit the original resource set we got from r53.
for _ , r := range records {
if * r . Name == k . Name + "." && * r . Type == k . Type {
rrset = r
break
}
}
} else {
2017-03-23 03:08:23 +08:00
changes = append ( changes , chg )
changeDesc += strings . Join ( namesToUpdate [ k ] , "\n" ) + "\n"
2016-08-23 08:31:50 +08:00
//on change or create, just build a new record set from our desired state
chg . Action = sPtr ( "UPSERT" )
rrset = & r53 . ResourceRecordSet {
Name : sPtr ( k . Name ) ,
Type : sPtr ( k . Type ) ,
ResourceRecords : [ ] * r53 . ResourceRecord { } ,
}
for _ , r := range recs {
val := r . Target
rr := & r53 . ResourceRecord {
Value : & val ,
}
rrset . ResourceRecords = append ( rrset . ResourceRecords , rr )
i := int64 ( r . TTL )
rrset . TTL = & i //TODO: make sure that ttls are consistent within a set
}
}
chg . ResourceRecordSet = rrset
}
changeReq := & r53 . ChangeResourceRecordSetsInput {
ChangeBatch : & r53 . ChangeBatch { Changes : changes } ,
}
2017-07-15 04:03:04 +08:00
2017-03-23 03:08:23 +08:00
delReq := & r53 . ChangeResourceRecordSetsInput {
ChangeBatch : & r53 . ChangeBatch { Changes : dels } ,
}
2017-07-15 04:03:04 +08:00
addCorrection := func ( msg string , req * r53 . ChangeResourceRecordSetsInput ) {
2017-03-23 03:08:23 +08:00
corrections = append ( corrections ,
& models . Correction {
2017-07-15 04:03:04 +08:00
Msg : msg ,
2017-03-23 03:08:23 +08:00
F : func ( ) error {
req . HostedZoneId = zone . Id
_ , err := r . client . ChangeResourceRecordSets ( req )
return err
} ,
} )
}
2017-07-15 04:03:04 +08:00
2017-03-23 03:08:23 +08:00
if len ( dels ) > 0 {
2017-07-15 04:03:04 +08:00
addCorrection ( delDesc , delReq )
2017-03-23 03:08:23 +08:00
}
2017-07-15 04:03:04 +08:00
2017-03-23 03:08:23 +08:00
if len ( changes ) > 0 {
2017-07-15 04:03:04 +08:00
addCorrection ( changeDesc , changeReq )
2017-03-23 03:08:23 +08:00
}
2016-08-23 08:31:50 +08:00
return corrections , nil
}
2017-08-05 22:56:42 +08:00
func ( r * route53Provider ) GetRegistrarCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
corrections := [ ] * models . Correction { }
actualSet , err := r . getRegistrarNameservers ( & dc . Name )
if err != nil {
return nil , err
}
sort . Strings ( actualSet )
actual := strings . Join ( actualSet , "," )
expectedSet := [ ] string { }
for _ , ns := range dc . Nameservers {
expectedSet = append ( expectedSet , ns . Name )
}
sort . Strings ( expectedSet )
expected := strings . Join ( expectedSet , "," )
if actual != expected {
return [ ] * models . Correction {
{
Msg : fmt . Sprintf ( "Update nameservers %s -> %s" , actual , expected ) ,
F : func ( ) error {
2017-11-27 23:22:08 +08:00
_ , err := r . updateRegistrarNameservers ( dc . Name , expectedSet )
return err
2017-08-05 22:56:42 +08:00
} ,
} ,
} , nil
}
return corrections , nil
}
func ( r * route53Provider ) getRegistrarNameservers ( domainName * string ) ( [ ] string , error ) {
domainDetail , err := r . registrar . GetDomainDetail ( & r53d . GetDomainDetailInput { DomainName : domainName } )
if err != nil {
return nil , err
}
nameservers := [ ] string { }
for _ , ns := range domainDetail . Nameservers {
nameservers = append ( nameservers , * ns . Name )
}
return nameservers , nil
}
func ( r * route53Provider ) updateRegistrarNameservers ( domainName string , nameservers [ ] string ) ( * string , error ) {
servers := [ ] * r53d . Nameserver { }
for i := range nameservers {
servers = append ( servers , & r53d . Nameserver { Name : & nameservers [ i ] } )
}
domainUpdate , err := r . registrar . UpdateDomainNameservers ( & r53d . UpdateDomainNameserversInput { DomainName : & domainName , Nameservers : servers } )
if err != nil {
return nil , err
}
return domainUpdate . OperationId , nil
}
2016-08-23 08:31:50 +08:00
func ( r * route53Provider ) fetchRecordSets ( zoneID * string ) ( [ ] * r53 . ResourceRecordSet , error ) {
if zoneID == nil || * zoneID == "" {
return nil , nil
}
var next * string
var nextType * string
var records [ ] * r53 . ResourceRecordSet
for {
listInput := & r53 . ListResourceRecordSetsInput {
HostedZoneId : zoneID ,
StartRecordName : next ,
StartRecordType : nextType ,
MaxItems : sPtr ( "100" ) ,
}
list , err := r . client . ListResourceRecordSets ( listInput )
if err != nil {
return nil , err
}
records = append ( records , list . ResourceRecordSets ... )
if list . NextRecordName != nil {
next = list . NextRecordName
nextType = list . NextRecordType
} else {
break
}
}
return records , nil
}
//we have to process names from route53 to match what we expect and to remove their odd octal encoding
func unescape ( s * string ) string {
if s == nil {
return ""
}
name := strings . TrimSuffix ( * s , "." )
name = strings . Replace ( name , ` \052 ` , "*" , - 1 ) //TODO: escape all octal sequences
return name
}
2017-01-04 04:26:08 +08:00
func ( r * route53Provider ) EnsureDomainExists ( domain string ) error {
if _ , ok := r . zones [ domain ] ; ok {
return nil
}
fmt . Printf ( "Adding zone for %s to route 53 account\n" , domain )
in := & r53 . CreateHostedZoneInput {
Name : & domain ,
CallerReference : sPtr ( fmt . Sprint ( time . Now ( ) . UnixNano ( ) ) ) ,
}
2017-05-04 01:30:05 +08:00
_ , err := r . client . CreateHostedZone ( in )
2017-01-04 04:26:08 +08:00
return err
}