package dns import ( "context" "fmt" "strings" "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/pkg/errors" "golang.org/x/oauth2" ) // Client holds metadata for the dns api client type Client struct { client *dnsimple.Client accountID int64 } // Record is the type used for a Zone record type Record dnsimple.ZoneRecord // Option type type Option func(*Client) // WithOAuth returns Option for oauth func WithOAuth(oauthToken string) Option { return func(c *Client) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken}) oauthClient := oauth2.NewClient(context.Background(), ts) client := dnsimple.NewClient(oauthClient) c.client = client } } // New returns an implementation of a DNS interface func New(options ...Option) *Client { c := &Client{} for _, option := range options { option(c) } return c } // GetRecords returns all record for a host func (d *Client) GetRecords(host, kind string) ([]Record, error) { accountID, err := d.getAccountID() if err != nil { return nil, errors.Wrap(err, "unable to get account id") } subdomain, domain, err := splitHost(host) if err != nil { return nil, errors.Wrap(err, "unable to parse host") } records, err := d.client.Zones.ListRecords( fmt.Sprintf("%d", accountID), domain, &dnsimple.ZoneRecordListOptions{}, ) if err != nil { return nil, errors.Wrap(err, "could not get list of records") } ret := []Record{} for _, record := range records.Data { if record.ZoneID == domain && record.Name == subdomain && record.Type == kind { ret = append(ret, Record(record)) } } return ret, nil } // CreateRecord adds a record to DNS func (d *Client) CreateRecord(host string, record Record) error { accountID, err := d.getAccountID() if err != nil { return errors.Wrap(err, "unable to get account id") } subdomain, domain, err := splitHost(host) if err != nil { return errors.Wrap(err, "unable to parse host") } newRecord := dnsimple.ZoneRecord(record) newRecord.Name = subdomain aID := fmt.Sprintf("%d", accountID) if _, err := d.client.Zones.CreateRecord(aID, domain, newRecord); err != nil { return errors.Wrap(err, "could not create record") } return nil } // UpdateRecord returns all record for a host func (d *Client) UpdateRecord(record Record) error { accountID, err := d.getAccountID() if err != nil { return errors.Wrap(err, "unable to get account id") } aID := fmt.Sprintf("%d", accountID) if _, err := d.client.Zones.UpdateRecord(aID, record.ZoneID, record.ID, dnsimple.ZoneRecord(record)); err != nil { return errors.Wrap(err, "could not update record") } return nil } // DeleteRecord returns all record for a host func (d *Client) DeleteRecord(record Record) error { accountID, err := d.getAccountID() if err != nil { return errors.Wrap(err, "unable to get account id") } aID := fmt.Sprintf("%d", accountID) if _, err := d.client.Zones.DeleteRecord(aID, record.ZoneID, record.ID); err != nil { return errors.Wrap(err, "could not delete record") } return nil } func (d *Client) getAccountID() (int64, error) { if d.accountID != 0 { return d.accountID, nil } whoamiResponse, err := d.client.Identity.Whoami() if err != nil { return 0, errors.Wrap(err, "could not get account information from dnsimple") } if whoamiResponse.Data.Account == nil { return 0, errors.New("could not get account information") } d.accountID = whoamiResponse.Data.Account.ID return d.accountID, nil } // splitHost returns hostname into domain and subdomain strings. // For example: // "some.subdomain.example.com" → ("some.subdomain", "example.com", nil) // "example.com" → ("", "example.com", nil) func splitHost(host string) (subdomain string, domain string, err error) { subdomains := strings.Split(host, ".") if len(subdomains) >= 3 { domain = strings.Join(subdomains[len(subdomains)-2:], ".") subdomain = strings.Join(subdomains[:len(subdomains)-2], ".") } else if len(subdomains) == 2 { domain = strings.Join(subdomains, ".") } else { err = errors.New("invalid domain") } return subdomain, domain, err }