package huaweicloud import ( "fmt" "net/url" "slices" "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" ) // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. func (c *huaweicloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { if err := c.getZones(); err != nil { return nil, err } zoneID, ok := c.zoneIDByDomain[domain] if !ok { return nil, fmt.Errorf("zone %s not found", domain) } records, err := c.fetchZoneRecordsFromRemote(zoneID) if err != nil { return nil, err } // Convert rrsets to DNSControl's RecordConfig existingRecords := []*models.RecordConfig{} for _, rec := range *records { if *rec.Type == "SOA" { continue } nativeRecords, err := nativeToRecords(&rec, domain) if err != nil { return nil, err } existingRecords = append(existingRecords, nativeRecords...) } return existingRecords, nil } // GenerateDomainCorrections takes the desired and existing records // and produces a Correction list. The correction list is simply // a list of functions to call to actually make the desired // correction, and a message to output to the user when the change is // made. func (c *huaweicloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) { if err := c.getZones(); err != nil { return nil, 0, err } zoneID, ok := c.zoneIDByDomain[dc.Name] if !ok { return nil, 0, fmt.Errorf("zone %s not found", dc.Name) } addDefaultMeta(dc.Records) // Make delete happen earlier than creates & updates. var corrections []*models.Correction var deletions []*models.Correction var reports []*models.Correction changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, genComparable) if err != nil { return nil, 0, err } for _, change := range changes { switch change.Type { case diff2.REPORT: reports = append(reports, &models.Correction{Msg: change.MsgsJoined}) case diff2.CREATE: fallthrough case diff2.CHANGE: newRecordsColl := collectRecordsByLineAndWeightAndKey(change.New) oldRecordsColl := collectRecordsByLineAndWeightAndKey(change.Old) corrections = append(corrections, &models.Correction{ Msg: change.MsgsJoined, F: func() error { // delete old records if not exist in new records for key, oldRecords := range oldRecordsColl { if _, ok := newRecordsColl[key]; !ok { rrsetIDOld := getRRSetIDFromRecords(oldRecords) err := c.deleteRRSets(zoneID, rrsetIDOld) if err != nil { return err } } } // modify or create new records for key, newRecords := range newRecordsColl { records, err := recordsToNative(newRecords, change.Key) if err != nil { return err } oldRecords := oldRecordsColl[key] rrsetIDOld := getRRSetIDFromRecords(oldRecords) if len(rrsetIDOld) == 1 { // update existing rrset err = c.updateRRSet(zoneID, rrsetIDOld[0], records) if err != nil { return err } } else { // create new rrset or combine multiple rrsets into one err := c.deleteRRSets(zoneID, rrsetIDOld) if err != nil { return err } err = c.createRRSet(zoneID, records) if err != nil { return err } } } return nil }, }) case diff2.DELETE: rrsetsID := getRRSetIDFromRecords(change.Old) deletions = append(deletions, &models.Correction{ Msg: change.MsgsJoined, F: func() error { return c.deleteRRSets(zoneID, rrsetsID) }, }) default: panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) } } result := append(reports, deletions...) result = append(result, corrections...) return result, actualChangeCount, nil } func collectRecordsByLineAndWeightAndKey(records models.Records) map[string]models.Records { recordsByLineAndWeight := make(map[string]models.Records) for _, rec := range records { line := rec.Metadata[metaLine] weight := rec.Metadata[metaWeight] rrsetKey := rec.Metadata[metaKey] key := weight + "," + line + "," + rrsetKey recordsByLineAndWeight[key] = append(recordsByLineAndWeight[key], rec) } return recordsByLineAndWeight } func addDefaultMeta(recs models.Records) { for _, r := range recs { if r.Metadata == nil { r.Metadata = make(map[string]string) } if r.Metadata[metaLine] == "" { r.Metadata[metaLine] = defaultLine } // apex ns should not have weight isApexNS := r.Type == "NS" && r.Name == "@" if !isApexNS && r.Metadata[metaWeight] == "" { r.Metadata[metaWeight] = defaultWeight } } } func genComparable(rec *models.RecordConfig) string { // apex ns if rec.Type == "NS" && rec.Name == "@" { return "" } weight := rec.Metadata[metaWeight] line := rec.Metadata[metaLine] key := rec.Metadata[metaKey] if weight == "" { weight = defaultWeight } if line == "" { line = defaultLine } return "weight=" + weight + " line=" + line + " key=" + key } func (c *huaweicloudProvider) deleteRRSets(zoneID string, rrsets []string) error { for _, rrset := range rrsets { deletePayload := &model.DeleteRecordSetsRequest{ ZoneId: zoneID, RecordsetId: rrset, } var err error withRetry(func() error { _, err = c.client.DeleteRecordSets(deletePayload) return err }) if err != nil { return err } } return nil } func (c *huaweicloudProvider) createRRSet(zoneID string, rc *model.ShowRecordSetByZoneResp) error { createPayload := &model.CreateRecordSetWithLineRequest{ ZoneId: zoneID, Body: &model.CreateRecordSetWithLineRequestBody{ Name: *rc.Name, Type: *rc.Type, Ttl: rc.Ttl, Records: rc.Records, Weight: rc.Weight, Line: rc.Line, Description: rc.Description, }, } var err error withRetry(func() error { _, err = c.client.CreateRecordSetWithLine(createPayload) return err }) if err != nil { return err } return nil } func (c *huaweicloudProvider) updateRRSet(zoneID, rrsetID string, rc *model.ShowRecordSetByZoneResp) error { updatePayload := &model.UpdateRecordSetsRequest{ ZoneId: zoneID, RecordsetId: rrsetID, Body: &model.UpdateRecordSetsReq{ Name: *rc.Name, Type: *rc.Type, Ttl: rc.Ttl, Records: rc.Records, Weight: rc.Weight, Description: rc.Description, }, } var err error withRetry(func() error { _, err = c.client.UpdateRecordSets(updatePayload) return err }) if err != nil { return err } return nil } func parseMarkerFromURL(link string) (string, error) { // Parse the marker params from the URL // Example: https://dns.myhuaweicloud.com/v2/zones?marker=abcdefg url, err := url.Parse(link) if err != nil { return "", err } marker := url.Query().Get("marker") if marker == "" { return "", fmt.Errorf("marker not found in URL %s", link) } return marker, nil } func (c *huaweicloudProvider) fetchZoneRecordsFromRemote(zoneID string) (*[]model.ShowRecordSetByZoneResp, error) { var nextMarker *string existingRecords := []model.ShowRecordSetByZoneResp{} availableStatus := []string{"ACTIVE", "PENDING_CREATE", "PENDING_UPDATE"} for { payload := model.ShowRecordSetByZoneRequest{ ZoneId: zoneID, Marker: nextMarker, } var res *model.ShowRecordSetByZoneResponse var err error withRetry(func() error { res, err = c.client.ShowRecordSetByZone(&payload) return err }) if err != nil { return nil, err } if res.Recordsets == nil { return &existingRecords, nil } for _, record := range *res.Recordsets { if record.Records == nil { continue } if !slices.Contains(availableStatus, *record.Status) { continue } existingRecords = append(existingRecords, record) } // if has next page, continue to get next page if res.Links.Next != nil { marker, err := parseMarkerFromURL(*res.Links.Next) if err != nil { return nil, err } nextMarker = &marker } else { return &existingRecords, nil } } }