Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'dynrecord' verb. #4

Merged
merged 2 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,32 @@ Congrats! you now have a useless DNS zone with no records! You can add them with

# Tool Usage

if you're using a JSON keyfile as above, you don't need to specify ```--cloud-project``` if the project is named there.

If you don't specify ```--json-keyfile``` then we'l try to use default credentials (i.e. the ones that the ```gcloud``` CLI uses). The examples below do this for clarity.

## ```getzonefile``` and ```putzonefile``` - Zonefile Nonsense

If you want to spit out a mostly valid zonefile from your gcloud-dns zone, this will do it:

`clouddns-sync --json-keyfile=mykeyfile.json --cloud-project=mydnsproject --cloud-dns-zone=myzone getzonefile`
`clouddns-sync --cloud-project=mydnsproject --cloud-dns-zone=myzone getzonefile`

If you have a zonefile, slurp it into gcloud DNS by doing this:

`clouddns-sync --json-keyfile=mykeyfile.json --cloud-project=mydnsproject --cloud-dns-zone=myzone --zonefile=myzonefile putzonefile`
`clouddns-sync --cloud-project=mydnsproject --cloud-dns-zone=myzone --zonefile=myzonefile putzonefile`

You can add `--dry-run` to putzonefile to see what we'd do. You can also add `--prune-missing` to remove RRs that aren't in your zonefile but are in gcloud.

My own use case is to do this once and then do future updates from a data source more reliable than your grandad's text file.

# Update from Nomad cluster (EXPERIMENTAL)
## ```nomad_sync``` Update from Nomad cluster

```clouddns-sync --cloud-project=mydnsproject --cloud-dns-zone=myzone --nomad-server-uri=http://anynomadserver:4646/ nomad_sync```

Right now we build a list of A records by inspecting all allocs and pointing *jobname*.domain to all nodes that hold an alloc in that job. That might not be what you want, but the important thing is that it's what I want. Patches welcome!

## ```dynrecord``` dyndns-style single record updating

This is if you have a DNS name you want to do 'dyndns' style updating for (i.e. we find out what our public IP is and set the specificed A record to that.)

```clouddns-sync --cloud-project=mydnsproject --cloud-dns-zone=myzone --cloud-dns-dyn-record-name=myhomeip.domain.tld. dynrecord```
29 changes: 29 additions & 0 deletions clouddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,32 @@ func buildDnsChange(cloud_rrs, zone_rrs []*dns.ResourceRecordSet, prune_missing
return &ret

}

func updateOneARecord(dns_spec *CloudDNSSpec, record_name string, old_ip, new_ip string) error {

log.Printf("Updating Cloud DNS: %s : %s -> %s", record_name, old_ip, new_ip)

change := &dns.Change{
Additions: []*dns.ResourceRecordSet{
{
Name: record_name,
Type: "A",
Rrdatas: []string{new_ip},
Ttl: int64(*dns_spec.default_ttl),
},
},
}

// Gcloud DNS shits the bed if you try to delete a record that's not there.
if old_ip != "" {
new_rr := dns.ResourceRecordSet{
Name: record_name,
Type: "A",
Rrdatas: []string{old_ip},
Ttl: int64(*dns_spec.default_ttl),
}
change.Deletions = append(change.Deletions, &new_rr)
}

return processCloudDnsChange(dns_spec, change)
}
100 changes: 85 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package main
import (
"context"
"flag"
"io"
"log"
"net"
"net/http"
"os"
"time"

Expand All @@ -21,20 +24,44 @@ type CloudDNSSpec struct {
dry_run *bool
}

func getMyIP() (string, error) {
res, err := http.Get("http://whatismyip.akamai.com")
if err != nil {
log.Fatal("HTTP Error getting our IP: ", err)
}

resBody, err := io.ReadAll(res.Body)
if err != nil {
log.Fatal("Error reading response body: ", err)
}

ip := net.ParseIP(string(resBody))
if ip == nil {
log.Fatalf("Non-IP returned from request: %s", ip)
}

return string(resBody), nil
}

func main() {
var jsonKeyfile = flag.String("json-keyfile", "key.json", "json credentials file for Cloud DNS")
var jsonKeyfile = flag.String("json-keyfile", "", "json credentials file for Cloud DNS")
var cloudProject = flag.String("cloud-project", "", "Google Cloud Project")
var cloudZone = flag.String("cloud-dns-zone", "", "Cloud DNS zone to operate on")
var defaultCloudTtl = flag.Int("cloud-dns-default-ttl", 300, "Default TTL for Cloud DNS records")

var zoneFilename = flag.String("zonefilename", "", "Local zone file to operate on")
var dryRun = flag.Bool("dry-run", false, "Do not update Cloud DNS, print what would be done")
var pruneMissing = flag.Bool("prune-missing", false, "on putzonefile, prune cloud dns entries not in zone file")

// For [get|put]zonefile
var zoneFilename = flag.String("zonefilename", "", "Local zone file to operate on")

// for nomad_sync
var nomadServerURI = flag.String("nomad-server-uri", "http://localhost:4646", "URI for a nomad server to talk to.")
var nomadTokenFile = flag.String("nomad-token-file", "", "file to read ou rnomad token from")
var nomadSyncInterval = flag.Int("nomad-sync-interval-secs", 300, "seconds between nomad updates. set to -1 to sync once only.")

// for dynrecord
var cloudDnsDynRecordName = flag.String("cloud-dns-dyn-record-name", "", "Cloud DNS record to update with our IP")

flag.Parse()

// Verb and flag verification
Expand All @@ -44,10 +71,7 @@ func main() {

verb := flag.Args()[0]

// These are required in all cases
if *cloudProject == "" {
log.Fatal("--cloud-project is required")
}
// Required in all cases
if *cloudZone == "" {
log.Fatal("--cloud-dns-zone is required")
}
Expand All @@ -58,17 +82,32 @@ func main() {
}
}

jsonData, ioerror := os.ReadFile(*jsonKeyfile)
if ioerror != nil {
log.Fatal(*jsonKeyfile, ioerror)
if verb == "dynrecord" {
if *cloudDnsDynRecordName == "" {
log.Fatal("--cloud-dns-dyn-record-name is required for dynrecord")
}
}

ctx := context.Background()
creds := &google_oauth.Credentials{}

if *jsonKeyfile != "" {
jsonData, ioerror := os.ReadFile(*jsonKeyfile)
if ioerror != nil {
log.Fatal(*jsonKeyfile, ioerror)
}
creds, _ = google_oauth.CredentialsFromJSON(ctx, jsonData, "https://www.googleapis.com/auth/cloud-platform")
} else {
creds, _ = google_oauth.FindDefaultCredentials(ctx)
}

creds, err := google_oauth.CredentialsFromJSON(ctx, jsonData, "https://www.googleapis.com/auth/cloud-platform")
// Get project from json keyfile if present.
if creds.ProjectID != "" {
*cloudProject = creds.ProjectID
}

if err != nil {
log.Fatal("Cloud DNS Error: ", err)
if *cloudProject == "" {
log.Fatal("--cloud-project is required if not defined in json credentials")
}

dnsservice, err := dns.NewService(ctx, option.WithCredentials(creds))
Expand All @@ -89,13 +128,44 @@ func main() {
log.Fatal(err)
}

log.Print("Found zone in Cloud DNS")

switch verb {
case "getzonefile":
dumpZonefile(dns_spec)
case "putzonefile":
uploadZonefile(dns_spec, zoneFilename, dryRun, pruneMissing)
case "dynrecord":
my_ip, err := getMyIP()
if err != nil {
log.Fatalf("Error getting our IP: %s", err)
}

log.Printf("Detected IP: %s", my_ip)

current_dns, err := net.LookupIP(*cloudDnsDynRecordName)

current_ip := ""

if err != nil {
log.Print("Error in DNS resolution: ", err)
log.Print("Continuing...")
} else {
if len(current_dns) > 1 {
log.Fatalf("%s resolves to multiple IPs. Weird.", *cloudDnsDynRecordName)
}

current_ip = current_dns[0].To4().String()

if my_ip == current_ip {
log.Print("My IP matches DNS. Nothing to do.")
return
}
}

err = updateOneARecord(dns_spec, *cloudDnsDynRecordName, current_ip, my_ip)

if err != nil {
log.Fatalf("Error Updating GCloud: %s", err)
}
case "nomad_sync":
nomadSpec := &NomadSpec{
uri: *nomadServerURI,
Expand Down