From c06d0f4e79edfe0a5ec650b2d2a8c1a441ed3226 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 12 Mar 2023 18:23:05 -0700 Subject: a CLI to update records for fcuny.xyz I want a simple solution to update the records for fcuny.xyz. The host that serves these records is a tailscale node, so I want to also query tailscale to get the IP for that host instead of hard coding the value. Some other information are hard coded, like the name of the project in GCP, etc. --- main.go | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 main.go (limited to 'main.go') diff --git a/main.go b/main.go new file mode 100644 index 0000000..b607963 --- /dev/null +++ b/main.go @@ -0,0 +1,179 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "strings" + + "google.golang.org/api/dns/v1" +) + +const ( + GCP_PROJECT_NAME = "fcuny-homelab" + GCP_MANAGED_ZONE = "fcuny-xyz" + TS_DEVICE_NAME = "tahoe" + TTL = 300 +) + +var desiredRecords = []string{ + "bt", + "dash", + "music", + "unifi", +} + +const usage = `Usage: + dns-updater [OPTIONS] + +Options: + -ts-api-key API key for tailscale + +Examples: + $ dns-updater -ts-api-key $(passage credentials/tailscale) + $ dns-updater -ts-api-key $(passage credentials/tailscale) update +` + +var l = log.New(os.Stderr, "", 0) + +func main() { + var ( + tsAPIKeyFlag string + command string + ) + + flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } + flag.StringVar(&tsAPIKeyFlag, "ts-api-key", "", "the API key for tailscale") + flag.Parse() + + if flag.NArg() > 1 { + flag.Usage() + os.Exit(1) + } else { + command = flag.Arg(0) + if command != "" && command != "update" { + flag.Usage() + os.Exit(1) + } + } + + ctx := context.Background() + + // we only care about IPv4 for now + tsIpV4Addresses, _, err := getTsIpsDevice(ctx, tsAPIKeyFlag, TS_DEVICE_NAME) + if err != nil { + l.Fatalf("failed to get the IP addresses for %s: %v", TS_DEVICE_NAME, err) + } + + svc, err := dns.NewService(ctx) + if err != nil { + l.Fatalf("failed to create the client for Google Cloud DNS: %v", err) + } + + zone, err := svc.ManagedZones.Get(GCP_PROJECT_NAME, GCP_MANAGED_ZONE).Context(ctx).Do() + if err != nil { + l.Fatalf("failed to get information about the managed zone %s: %+v", GCP_MANAGED_ZONE, err) + } + + recordSets, err := svc.ResourceRecordSets.List(GCP_PROJECT_NAME, GCP_MANAGED_ZONE).Context(ctx).Do() + if err != nil { + l.Fatalf("failed to get the list of records: %+v", err) + } + + switch command { + case "update": + updateRecords(ctx, svc, recordSets, zone, tsIpV4Addresses) + default: + listRecords(recordSets) + } +} + +func listRecords(recordSets *dns.ResourceRecordSetsListResponse) { + for _, record := range recordSets.Rrsets { + switch record.Type { + case "A": + ips := strings.Join(record.Rrdatas[:], ",") + fmt.Printf("%-25s %4d %s\n", record.Name, record.Ttl, ips) + } + } +} + +func updateRecords(ctx context.Context, svc *dns.Service, recordSets *dns.ResourceRecordSetsListResponse, zone *dns.ManagedZone, tsIpV4Addresses []string) { + var ( + existingRecordSets = []*dns.ResourceRecordSet{} + recordSetsToAdd = []*dns.ResourceRecordSet{} + recordSetsToDelete = []*dns.ResourceRecordSet{} + ) + + for _, record := range recordSets.Rrsets { + if record.Type == "A" { + existingRecordSets = append(existingRecordSets, record) + } + } + + // first pass: create what's missing + for _, subdomain := range desiredRecords { + found := false + subdomain = fmt.Sprintf("%s.%s", subdomain, zone.DnsName) + for _, r := range existingRecordSets { + if subdomain == r.Name && r.Type == "A" { + // check that the IP addresses are correct + ipsFound := 0 + for _, rr := range r.Rrdatas { + for _, ip := range tsIpV4Addresses { + if rr == ip { + ipsFound += 1 + continue + } + } + } + // while we found the subdomain with the correct type, + // we also need to make sure the list of IPs is + // correct. If they are not, we delete the record and + // add it again with the correct values. + if ipsFound == len(tsIpV4Addresses) { + found = true + continue + } else { + l.Printf("will delete %s (incorrect IPv4 addresses)\n", subdomain) + recordSetsToDelete = append(recordSetsToDelete, r) + } + } + } + if !found { + l.Printf("will add %s\n", subdomain) + r := &dns.ResourceRecordSet{ + Name: subdomain, + Type: "A", + Ttl: TTL, + Rrdatas: tsIpV4Addresses, + } + recordSetsToAdd = append(recordSetsToAdd, r) + } + } + + // second pass: delete what's not needed + for _, r := range existingRecordSets { + found := false + for _, subdomain := range desiredRecords { + subdomain = fmt.Sprintf("%s.%s", subdomain, zone.DnsName) + if subdomain == r.Name && r.Type == "A" { + found = true + continue + } + } + if !found { + l.Printf("will delete %s\n", r.Name) + recordSetsToDelete = append(recordSetsToDelete, r) + } + } + + if len(recordSetsToAdd) > 0 || len(recordSetsToDelete) > 0 { + change := &dns.Change{Additions: recordSetsToAdd, Deletions: recordSetsToDelete} + if _, err := svc.Changes.Create(GCP_PROJECT_NAME, GCP_MANAGED_ZONE, change).Context(ctx).Do(); err != nil { + l.Fatalf("failed to apply the change: %+v", err) + } + } +} -- cgit v1.2.3