package main import ( "encoding/json" "fmt" "net/http" "os" "strconv" "strings" "sync" "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/pkg/errors" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "update-dns" app.Usage = "update dnsimple with public ip address" app.Action = action app.Flags = []cli.Flag{ cli.StringFlag{ Name: "url", Value: "https://whatismyipv6.buddy.wtf/json", Usage: "url to use to get public ip address", }, cli.StringFlag{ EnvVar: "DNSIMPLE_TOKEN", Name: "dnsimple-token", Usage: "dnsimple token", }, } app.Run(os.Args) } type runtimeError string func (e runtimeError) Error() string { return string(e) } func action(context *cli.Context) error { var wg sync.WaitGroup if context.NArg() < 1 { return runtimeError("requires host argument") } host := context.Args().Get(0) wg.Add(1) var getIPError error var ip string go func() { defer wg.Done() result, err := getIP(context.String("url")) if err != nil { getIPError = errors.Wrap(err, "could not get IP address") return } ip = result.IP }() var dnSimpleAccountID string var getRecordError error var record *dnsimple.ZoneRecord credentials := dnsimple.NewOauthTokenCredentials(context.String("dnsimple-token")) dnSimpleClient := dnsimple.NewClient(credentials) wg.Add(1) go func() { defer wg.Done() accountID, result, err := getRecord(dnSimpleClient, host, "AAAA") dnSimpleAccountID = accountID if err != nil { getRecordError = errors.Wrap(err, "could not get ip for host") return } record = result }() wg.Wait() if getIPError != nil { return errors.Wrap(getIPError, "could no get ip address") } if getRecordError != nil { switch errors.Cause(getRecordError).(type) { case notFoundError: break default: return errors.Wrap(getRecordError, "could no get record") } } subdomain, domain, _ := splitHost(host) if record == nil { _, err := dnSimpleClient.Zones.CreateRecord(dnSimpleAccountID, domain, dnsimple.ZoneRecord{ Name: subdomain, Type: "AAAA", Content: ip, }) if err != nil { return errors.Wrap(err, "could not create record") } fmt.Printf("%s record created for %s as %s\n", "AAAA", host, ip) } else if ip != record.Content { record.Content = ip _, err := dnSimpleClient.Zones.UpdateRecord(dnSimpleAccountID, domain, record.ID, *record) if err != nil { return errors.Wrap(err, "could not update record") } fmt.Printf("%s record for %s updated to %s\n", "AAAA", host, ip) } else { fmt.Printf("%s record for %s already set as %s\n", "AAAA", host, ip) } return nil } type whatIsMyIPResult struct { IP string `json:ip` Version string `json:version` } type notFoundError error func getRecord(client *dnsimple.Client, host string, kind string) (string, *dnsimple.ZoneRecord, error) { subdomain, domain, err := splitHost(host) if err != nil { return "", nil, errors.Wrap(err, "unable to parse host") } accountID, err := getAccountID(client) if err != nil { return "", nil, errors.Wrap(err, "could not get account id") } records, err := client.Zones.ListRecords(accountID, domain, &dnsimple.ZoneRecordListOptions{}) if err != nil { return "", nil, errors.Wrap(err, "could not get list of records") } for _, record := range records.Data { if record.ZoneID == domain && record.Name == subdomain && record.Type == kind { return accountID, &record, nil } } return accountID, nil, notFoundError(errors.New("could not find record")) } func getAccountID(client *dnsimple.Client) (string, error) { whoamiResponse, err := client.Identity.Whoami() if err != nil { return "", errors.Wrap(err, "could not get account information from dnsimple") } if whoamiResponse.Data.Account == nil { return "", errors.New("could not get account information") } return strconv.Itoa(whoamiResponse.Data.Account.ID), 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 } func getIP(url string) (*whatIsMyIPResult, error) { result, err := http.Get(url) if err != nil { return nil, errors.Wrap(err, "could not get url "+url) } defer result.Body.Close() ipResult := &whatIsMyIPResult{} if err := json.NewDecoder(result.Body).Decode(ipResult); err != nil { return nil, errors.Wrap(err, "could parse json from url "+url) } return ipResult, nil }