github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/exoscale/exoscaleProvider.go (about) 1 package exoscale 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/exoscale/egoscale" 10 11 "github.com/StackExchange/dnscontrol/v2/models" 12 "github.com/StackExchange/dnscontrol/v2/providers" 13 "github.com/StackExchange/dnscontrol/v2/providers/diff" 14 ) 15 16 type exoscaleProvider struct { 17 client *egoscale.Client 18 } 19 20 // NewExoscale creates a new Exoscale DNS provider. 21 func NewExoscale(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 22 endpoint, apiKey, secretKey := m["dns-endpoint"], m["apikey"], m["secretkey"] 23 24 return &exoscaleProvider{client: egoscale.NewClient(endpoint, apiKey, secretKey)}, nil 25 } 26 27 var features = providers.DocumentationNotes{ 28 providers.CanUseAlias: providers.Can(), 29 providers.CanUseCAA: providers.Can(), 30 providers.CanUsePTR: providers.Can(), 31 providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"), 32 providers.CanUseTLSA: providers.Cannot(), 33 providers.DocCreateDomains: providers.Cannot(), 34 providers.DocDualHost: providers.Cannot("Exoscale does not allow sufficient control over the apex NS records"), 35 providers.DocOfficiallySupported: providers.Cannot(), 36 providers.CanGetZones: providers.Unimplemented(), 37 } 38 39 func init() { 40 providers.RegisterDomainServiceProviderType("EXOSCALE", NewExoscale, features) 41 } 42 43 // EnsureDomainExists returns an error if domain doesn't exist. 44 func (c *exoscaleProvider) EnsureDomainExists(domain string) error { 45 ctx := context.Background() 46 _, err := c.client.GetDomain(ctx, domain) 47 if err != nil { 48 _, err := c.client.CreateDomain(ctx, domain) 49 if err != nil { 50 return err 51 } 52 } 53 return err 54 } 55 56 // GetNameservers returns the nameservers for domain. 57 func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { 58 return nil, nil 59 } 60 61 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 62 func (c *exoscaleProvider) GetZoneRecords(domain string) (models.Records, error) { 63 return nil, fmt.Errorf("not implemented") 64 // This enables the get-zones subcommand. 65 // Implement this by extracting the code from GetDomainCorrections into 66 // a single function. For most providers this should be relatively easy. 67 } 68 69 // GetDomainCorrections returns a list of corretions for the domain. 70 func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 71 dc.Punycode() 72 73 ctx := context.Background() 74 records, err := c.client.GetRecords(ctx, dc.Name) 75 if err != nil { 76 return nil, err 77 } 78 79 existingRecords := make([]*models.RecordConfig, 0, len(records)) 80 for _, r := range records { 81 if r.RecordType == "SOA" || r.RecordType == "NS" { 82 continue 83 } 84 if r.Name == "" { 85 r.Name = "@" 86 } 87 if r.RecordType == "CNAME" || r.RecordType == "MX" || r.RecordType == "ALIAS" { 88 r.Content += "." 89 } 90 // exoscale adds these odd txt records that mirror the alias records. 91 // they seem to manage them on deletes and things, so we'll just pretend they don't exist 92 if r.RecordType == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") { 93 continue 94 } 95 rec := &models.RecordConfig{ 96 TTL: uint32(r.TTL), 97 Original: r, 98 } 99 rec.SetLabel(r.Name, dc.Name) 100 switch rtype := r.RecordType; rtype { 101 case "ALIAS", "URL": 102 rec.Type = r.RecordType 103 rec.SetTarget(r.Content) 104 case "MX": 105 if err := rec.SetTargetMX(uint16(r.Prio), r.Content); err != nil { 106 panic(fmt.Errorf("unparsable record received from exoscale: %w", err)) 107 } 108 case "SRV": 109 var err error 110 parts := strings.Fields(r.Content) 111 if len(parts) == 3 { 112 err = rec.SetTargetSRVPriorityString(uint16(r.Prio), r.Content) 113 } else { 114 r.Content += "." 115 err = rec.PopulateFromString(r.RecordType, r.Content, dc.Name) 116 } 117 if err != nil { 118 panic(fmt.Errorf("unparsable record received from exoscale: %w", err)) 119 } 120 default: 121 if err := rec.PopulateFromString(r.RecordType, r.Content, dc.Name); err != nil { 122 panic(fmt.Errorf("unparsable record received from exoscale: %w", err)) 123 } 124 } 125 existingRecords = append(existingRecords, rec) 126 } 127 removeOtherNS(dc) 128 129 // Normalize 130 models.PostProcessRecords(existingRecords) 131 132 differ := diff.New(dc) 133 _, create, delete, modify := differ.IncrementalDiff(existingRecords) 134 135 var corrections = []*models.Correction{} 136 137 for _, del := range delete { 138 rec := del.Existing.Original.(egoscale.DNSRecord) 139 corrections = append(corrections, &models.Correction{ 140 Msg: del.String(), 141 F: c.deleteRecordFunc(rec.ID, dc.Name), 142 }) 143 } 144 145 for _, cre := range create { 146 rec := cre.Desired 147 corrections = append(corrections, &models.Correction{ 148 Msg: cre.String(), 149 F: c.createRecordFunc(rec, dc.Name), 150 }) 151 } 152 153 for _, mod := range modify { 154 old := mod.Existing.Original.(egoscale.DNSRecord) 155 new := mod.Desired 156 corrections = append(corrections, &models.Correction{ 157 Msg: mod.String(), 158 F: c.updateRecordFunc(&old, new, dc.Name), 159 }) 160 } 161 162 return corrections, nil 163 } 164 165 // Returns a function that can be invoked to create a record in a zone. 166 func (c *exoscaleProvider) createRecordFunc(rc *models.RecordConfig, domainName string) func() error { 167 return func() error { 168 client := c.client 169 170 target := rc.GetTargetCombined() 171 name := rc.GetLabel() 172 173 if rc.Type == "MX" { 174 target = rc.Target 175 } 176 177 if rc.Type == "NS" && (name == "@" || name == "") { 178 name = "*" 179 } 180 181 record := egoscale.DNSRecord{ 182 Name: name, 183 RecordType: rc.Type, 184 Content: target, 185 TTL: int(rc.TTL), 186 Prio: int(rc.MxPreference), 187 } 188 ctx := context.Background() 189 _, err := client.CreateRecord(ctx, domainName, record) 190 if err != nil { 191 return err 192 } 193 194 return nil 195 } 196 } 197 198 // Returns a function that can be invoked to delete a record in a zone. 199 func (c *exoscaleProvider) deleteRecordFunc(recordID int64, domainName string) func() error { 200 return func() error { 201 client := c.client 202 203 ctx := context.Background() 204 if err := client.DeleteRecord(ctx, domainName, recordID); err != nil { 205 return err 206 } 207 208 return nil 209 210 } 211 } 212 213 // Returns a function that can be invoked to update a record in a zone. 214 func (c *exoscaleProvider) updateRecordFunc(old *egoscale.DNSRecord, rc *models.RecordConfig, domainName string) func() error { 215 return func() error { 216 client := c.client 217 218 target := rc.GetTargetCombined() 219 name := rc.GetLabel() 220 221 if rc.Type == "MX" { 222 target = rc.Target 223 } 224 225 if rc.Type == "NS" && (name == "@" || name == "") { 226 name = "*" 227 } 228 229 record := egoscale.UpdateDNSRecord{ 230 Name: name, 231 RecordType: rc.Type, 232 Content: target, 233 TTL: int(rc.TTL), 234 Prio: int(rc.MxPreference), 235 ID: old.ID, 236 } 237 238 ctx := context.Background() 239 _, err := client.UpdateRecord(ctx, domainName, record) 240 if err != nil { 241 return err 242 } 243 244 return nil 245 } 246 } 247 248 func defaultNSSUffix(defNS string) bool { 249 return (strings.HasSuffix(defNS, ".exoscale.io.") || 250 strings.HasSuffix(defNS, ".exoscale.com.") || 251 strings.HasSuffix(defNS, ".exoscale.ch.") || 252 strings.HasSuffix(defNS, ".exoscale.net.")) 253 } 254 255 // remove all non-exoscale NS records from our desired state. 256 // if any are found, print a warning 257 func removeOtherNS(dc *models.DomainConfig) { 258 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 259 for _, rec := range dc.Records { 260 if rec.Type == "NS" { 261 // apex NS inside exoscale are expected. 262 if rec.GetLabelFQDN() == dc.Name && defaultNSSUffix(rec.GetTargetField()) { 263 continue 264 } 265 fmt.Printf("Warning: exoscale.com(.io, .ch, .net) does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField()) 266 continue 267 } 268 newList = append(newList, rec) 269 } 270 dc.Records = newList 271 }