aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2023-03-12 18:23:05 -0700
committerFranck Cuny <franck@fcuny.net>2023-03-12 18:23:05 -0700
commitc06d0f4e79edfe0a5ec650b2d2a8c1a441ed3226 (patch)
tree906d8eed426a82ae80f9644e4d5857d4b41cd11f /main.go
downloaddns-updater-main.tar.gz
a CLI to update records for fcuny.xyzHEADmain
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 'main.go')
-rw-r--r--main.go179
1 files changed, 179 insertions, 0 deletions
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] <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)
+ }
+ }
+}