diff options
| author | Franck Cuny <franck@fcuny.net> | 2023-03-12 18:23:05 -0700 |
|---|---|---|
| committer | Franck Cuny <franck@fcuny.net> | 2023-03-12 18:23:05 -0700 |
| commit | c06d0f4e79edfe0a5ec650b2d2a8c1a441ed3226 (patch) | |
| tree | 906d8eed426a82ae80f9644e4d5857d4b41cd11f /main.go | |
| download | dns-updater-c06d0f4e79edfe0a5ec650b2d2a8c1a441ed3226.tar.gz | |
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.
Diffstat (limited to '')
| -rw-r--r-- | main.go | 179 |
1 files changed, 179 insertions, 0 deletions
@@ -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] <COMMAND> + +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) + } + } +} |
