diff --git a/client.go b/client.go index ecde037..6d9223e 100644 --- a/client.go +++ b/client.go @@ -12,7 +12,7 @@ import ( "github.com/libdns/libdns" ) -// nameClient extends the namedotcom api and request handler to the provider.. +// nameClient extends the namedotcom api and request handler to the provider. type nameClient struct { client *nameDotCom mutex sync.Mutex @@ -20,11 +20,11 @@ type nameClient struct { // getClient initiates a new nameClient and assigns it to the provider.. func (p *Provider) getClient(ctx context.Context) error { - newNameclient, err := NewNameDotComClient(ctx, p.Token, p.User, p.Server) + newNameClient, err := NewNameDotComClient(ctx, p.Token, p.User, p.Server) if err != nil { return err } - p.client = newNameclient + p.client = newNameClient return nil } @@ -71,12 +71,32 @@ func (p *Provider) listZones(ctx context.Context) ([]libdns.Zone, error) { return zones, nil } -// listAllRecords returns all records for the given zone .. GET /v4/domains/{ domainName }/records -func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Record, error) { +func (p *Provider) getRecordId(ctx context.Context, zone string, record libdns.Record) (int32, error) { + records, err := p.listAllRecords(ctx, zone) + if err != nil { + return 0, err + } + + name := libdns.AbsoluteName(record.RR().Name, zone) + for _, rec := range records { + if rec.Type == record.RR().Type && rec.Fqdn == name && rec.Answer == record.RR().Data { + return rec.ID, nil + } + } + + return 0, fmt.Errorf("could not find record with name %s", record.RR().Name) +} + +func (p *Provider) listAllRecordsLocked(ctx context.Context, zone string) ([]nameDotComRecord, error) { p.mutex.Lock() defer p.mutex.Unlock() + return p.listAllRecords(ctx, zone) +} + +// listAllRecords returns all records for the given zone . GET /v4/domains/{ domainName }/records +func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]nameDotComRecord, error) { var ( - records []libdns.Record + records []nameDotComRecord /*** 'zone' args that are passed in using compliant zone formats have the FQDN '.' suffix qualifier and in order to use the zone arg as a domainName reference to name.com's api we must remove the '.' suffix. @@ -92,24 +112,24 @@ func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Re ) if err = p.getClient(ctx); err != nil { - return []libdns.Record{}, err + return []nameDotComRecord{}, err } - // handle pagination, in case domain has more records then the default of 1000 per page + // handle pagination, in case domain has more records than the default of 1000 per page for reqPage > 0 { if reqPage != 0 { endpoint := fmt.Sprintf("/v4/domains/%s/records?page=%d", unFQDNzone, reqPage) if body, err = p.client.doRequest(ctx, method, endpoint, nil); err != nil { - return []libdns.Record{}, fmt.Errorf("request failed: %w", err) + return []nameDotComRecord{}, fmt.Errorf("request failed: %w", err) } if err = json.NewDecoder(body).Decode(resp); err != nil { - return []libdns.Record{}, fmt.Errorf("could not decode name.com's response: %w", err) + return []nameDotComRecord{}, fmt.Errorf("could not decode name.com's response: %w", err) } for _, record := range resp.Records { - records = append(records, record.toLibDNSRecord()) + records = append(records, record) } reqPage = int(resp.NextPage) @@ -123,37 +143,41 @@ func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Re func (p *Provider) deleteRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) { p.mutex.Lock() defer p.mutex.Unlock() + + recordId, err := p.getRecordId(ctx, zone, record) + if err != nil { + return libdns.RR{}, err + } + var ( shouldDelete nameDotComRecord unFQDNzone = strings.TrimSuffix(zone, ".") method = "DELETE" - endpoint = fmt.Sprintf("/v4/domains/%s/records/%s", unFQDNzone, record.ID) + endpoint = fmt.Sprintf("/v4/domains/%s/records/%d", unFQDNzone, recordId) body io.Reader post = &bytes.Buffer{} - - err error ) - shouldDelete.fromLibDNSRecord(record, unFQDNzone) + shouldDelete.fromLibDNSRecord(recordId, record, unFQDNzone) if err = p.getClient(ctx); err != nil { - return libdns.Record{}, err + return libdns.RR{}, err } if err = json.NewEncoder(post).Encode(shouldDelete); err != nil { - return libdns.Record{}, fmt.Errorf("could not encode form data for request: %w", err) + return libdns.RR{}, fmt.Errorf("could not encode form data for request: %w", err) } if body, err = p.client.doRequest(ctx, method, endpoint, post); err != nil { - return libdns.Record{}, fmt.Errorf("request to delete the record was not successful: %w", err) + return libdns.RR{}, fmt.Errorf("request to delete the record was not successful: %w", err) } if err = json.NewDecoder(body).Decode(&shouldDelete); err != nil { - return libdns.Record{}, fmt.Errorf("could not decode the response from name.com: %w", err) + return libdns.RR{}, fmt.Errorf("could not decode the response from name.com: %w", err) } - return shouldDelete.toLibDNSRecord(), nil + return shouldDelete.toLibDNSRecord(unFQDNzone) } // upsertRecord PUT || POST /v4/domains/{ domainName }/records/{ record.ID } @@ -161,31 +185,31 @@ func (p *Provider) upsertRecord(ctx context.Context, zone string, canidateRecord p.mutex.Lock() defer p.mutex.Unlock() + recordId, err := p.getRecordId(ctx, zone, canidateRecord) + var ( shouldUpsert nameDotComRecord unFQDNzone = strings.TrimSuffix(zone, ".") method = "PUT" - endpoint = fmt.Sprintf("/v4/domains/%s/records/%s", unFQDNzone, canidateRecord.ID) + endpoint = fmt.Sprintf("/v4/domains/%s/records/%d", unFQDNzone, recordId) body io.Reader post = &bytes.Buffer{} - - err error ) - if canidateRecord.ID == "" { + if err != nil { method = "POST" endpoint = fmt.Sprintf("/v4/domains/%s/records", unFQDNzone) } - shouldUpsert.fromLibDNSRecord(canidateRecord, unFQDNzone) + shouldUpsert.fromLibDNSRecord(recordId, canidateRecord, unFQDNzone) if err = p.getClient(ctx); err != nil { - return libdns.Record{}, err + return libdns.RR{}, err } if err = json.NewEncoder(post).Encode(shouldUpsert); err != nil { - return libdns.Record{}, fmt.Errorf("could not encode the form data for the request: %w", err) + return libdns.RR{}, fmt.Errorf("could not encode the form data for the request: %w", err) } if body, err = p.client.doRequest(ctx, method, endpoint, post); err != nil { @@ -193,12 +217,12 @@ func (p *Provider) upsertRecord(ctx context.Context, zone string, canidateRecord err = fmt.Errorf("name.com will not allow an update to a record that has identical values to an existing record: %w", err) } - return libdns.Record{}, fmt.Errorf("request to update the record was not successful: %w", err) + return libdns.RR{}, fmt.Errorf("request to update the record was not successful: %w", err) } if err = json.NewDecoder(body).Decode(&shouldUpsert); err != nil { - return libdns.Record{}, fmt.Errorf("could not decode name.com's response: %w", err) + return libdns.RR{}, fmt.Errorf("could not decode name.com's response: %w", err) } - return shouldUpsert.toLibDNSRecord(), nil + return shouldUpsert.toLibDNSRecord(unFQDNzone) } diff --git a/go.mod b/go.mod index aad2882..34bb2bc 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,7 @@ module github.com/libdns/namedotcom -go 1.16 +go 1.25 require ( - github.com/libdns/libdns v0.2.2 - github.com/pkg/errors v0.9.1 + github.com/libdns/libdns v1.1.1 ) diff --git a/go.sum b/go.sum index 1618298..9e348ff 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/namedotcom.go b/namedotcom.go index 08b2ff5..74d48f8 100644 --- a/namedotcom.go +++ b/namedotcom.go @@ -5,16 +5,17 @@ package namedotcom import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" + "net/netip" "regexp" "strconv" "strings" "time" "github.com/libdns/libdns" - "github.com/pkg/errors" ) // default timeout for the http request handler (seconds) @@ -77,10 +78,10 @@ func (n *nameDotCom) errorResponse(resp *http.Response) error { er := &errorResponse{} err := json.NewDecoder(resp.Body).Decode(er) if err != nil { - return errors.Wrap(err, "api returned unexpected response") + return fmt.Errorf("api returned unexpected response: %w", err) } - return errors.WithStack(er) + return err } // doRequest is the base http request handler including a request context. @@ -92,7 +93,6 @@ func (n *nameDotCom) doRequest(ctx context.Context, method, endpoint string, pos } req.SetBasicAuth(n.User, n.Token) - resp, err := n.client.Do(req) if err != nil { return nil, err @@ -105,33 +105,96 @@ func (n *nameDotCom) doRequest(ctx context.Context, method, endpoint string, pos } // fromLibDNSRecord maps a name.com record from a libdns record -func (n *nameDotComRecord) fromLibDNSRecord(record libdns.Record, zone string) { - var id int64 - if record.ID != "" { - id, _ = strconv.ParseInt(record.ID, 10, 32) - } - n.ID = int32(id) - n.Type = record.Type +func (n *nameDotComRecord) fromLibDNSRecord(id int32, record libdns.Record, zone string) { + n.ID = id + n.Type = record.RR().Type n.Host = n.toSanitized(record, zone) - n.Answer = record.Value - n.TTL = uint32(record.TTL.Seconds()) + n.Answer = record.RR().Data + n.TTL = uint32(record.RR().TTL.Seconds()) } // toLibDNSRecord maps a name.com record to a libdns record -func (n *nameDotComRecord) toLibDNSRecord() libdns.Record { - return libdns.Record{ - ID: fmt.Sprint(n.ID), - Type: n.Type, - Name: n.Host, - Value: n.Answer, - TTL: time.Duration(n.TTL) * time.Second, +func (record *nameDotComRecord) toLibDNSRecord(zone string) (libdns.Record, error) { + name := libdns.RelativeName(record.Fqdn, zone) + ttl := time.Duration(record.TTL) * time.Second + + switch record.Type { + case "A", "AAAA": + ip, err := netip.ParseAddr(record.Answer) + if err != nil { + return libdns.Address{}, err + } + return libdns.Address{ + Name: name, + TTL: ttl, + IP: ip, + }, nil + case "CAA": + contentParts := strings.SplitN(record.Answer, " ", 3) + flags, err := strconv.Atoi(contentParts[0]) + if err != nil { + return libdns.CAA{}, err + } + tag := contentParts[1] + value := contentParts[2] + return libdns.CAA{ + Name: name, + TTL: ttl, + Flags: uint8(flags), + Tag: tag, + Value: value, + }, nil + case "CNAME": + return libdns.CNAME{ + Name: name, + TTL: ttl, + Target: record.Answer, + }, nil + case "SRV": + priority := record.Priority + + nameParts := strings.SplitN(name, ".", 2) + if len(nameParts) < 2 { + return libdns.SRV{}, fmt.Errorf("name %v does not contain enough fields; expected format: '_service._proto'", name) + } + contentParts := strings.SplitN(record.Answer, " ", 3) + if len(contentParts) < 3 { + return libdns.SRV{}, fmt.Errorf("content %v does not contain enough fields; expected format: 'weight port target'", name) + } + weight, err := strconv.Atoi(contentParts[0]) + if err != nil { + return libdns.SRV{}, fmt.Errorf("invalid value for weight %v; expected integer", record.Priority) + } + port, err := strconv.Atoi(contentParts[1]) + if err != nil { + return libdns.SRV{}, fmt.Errorf("invalid value for port %v; expected integer", record.Priority) + } + + return libdns.SRV{ + Service: strings.TrimPrefix(nameParts[0], "_"), + Transport: strings.TrimPrefix(nameParts[1], "_"), + Name: zone, + TTL: ttl, + Priority: uint16(priority), + Weight: uint16(weight), + Port: uint16(port), + Target: contentParts[2], + }, nil + case "TXT": + return libdns.TXT{ + Name: name, + TTL: ttl, + Text: record.Answer, + }, nil + default: + return libdns.RR{}, fmt.Errorf("Unsupported record type: %s", record.Type) } } // name.com's api server expects the sub domain name to be relavtive and have no trailing period // , e.g. "sub.zone." -> "sub" func (n *nameDotComRecord) toSanitized(libdnsRecord libdns.Record, zone string) string { - return strings.TrimSuffix(strings.Replace(libdnsRecord.Name, zone, "", -1), ".") + return strings.TrimSuffix(strings.Replace(libdnsRecord.RR().Name, zone, "", -1), ".") } // NewNameDotComClient returns a new name.com client struct diff --git a/provider.go b/provider.go index 76d13a7..2eb1219 100644 --- a/provider.go +++ b/provider.go @@ -4,6 +4,7 @@ package namedotcom import ( "context" + "fmt" "github.com/libdns/libdns" ) @@ -24,7 +25,17 @@ func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record return nil, err } - return records, nil + var result []libdns.Record + + for _, record := range records { + rec, err := record.toLibDNSRecord(zone) + if err != nil { + return []libdns.Record{}, fmt.Errorf("could not decode name.com's response: %w", err) + } + result = append(result, rec) + } + + return result, nil } // AppendRecords adds records to the zone. It returns the records that were added. @@ -74,7 +85,6 @@ func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []lib } func (p *Provider) ListZones(ctx context.Context) ([]libdns.Zone, error) { - zones, err := p.listZones(ctx) if err != nil { return nil, err diff --git a/provider_test.go b/provider_test.go index 9efb0cd..8a0b9de 100644 --- a/provider_test.go +++ b/provider_test.go @@ -2,6 +2,7 @@ package namedotcom import ( "context" + "net/netip" "os" "strings" "testing" @@ -29,32 +30,28 @@ func init() { } newRecordSet = []libdns.Record{ - { - Type: "txt", - Name: "__test_txt_record.example.com", - Value: "old_value", - TTL: time.Duration(300), - }, { - - Type: "A", - Name: "test2", - Value: "10.10.0.2", - TTL: time.Duration(300), + libdns.TXT{ + Name: "__test_txt_record.example.com", + TTL: time.Duration(300), + Text: "old_value", + }, + libdns.Address{ + Name: "test2", + TTL: time.Duration(300), + IP: netip.MustParseAddr("10.10.0.2"), }, } updateSet = []libdns.Record{ - { - Type: "txt", - Name: "__test_txt_record.example.com", - Value: "new_value", - TTL: time.Duration(300), - }, { - - Type: "A", - Name: "test2", - Value: "10.10.0.2", - TTL: time.Duration(300), + libdns.TXT{ + Name: "__test_txt_record.example.com", + TTL: time.Duration(300), + Text: "new_value", + }, + libdns.Address{ + Name: "test2", + TTL: time.Duration(300), + IP: netip.MustParseAddr("10.10.0.2"), }, } } @@ -128,11 +125,11 @@ func TestProvider_SetRecords(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - testName := strings.ToLower(updateSet[0].Name) + testName := strings.ToLower(updateSet[0].RR().Name) t.Log(rollingRecords) for _, rec := range rollingRecords { - if testName == rec.Name { + if testName == rec.RR().Name { updateSet[0].ID = rec.ID } } @@ -169,10 +166,10 @@ func TestProvider_DeleteRecords(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - testName := strings.ToLower(updateSet[0].Name) + testName := strings.ToLower(updateSet[0].RR().Name) for _, rec := range rollingRecords { - if testName == rec.Name { + if testName == rec.RR().Name { updateSet[0].ID = rec.ID } }