Skip to content

Commit

Permalink
Merge pull request #4 from gerrowadat/merge_dyn
Browse files Browse the repository at this point in the history
Add 'dynrecord' verb.
  • Loading branch information
gerrowadat authored Nov 12, 2023
2 parents 76a4f3f + d9f50fa commit 2c5ca0a
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 18 deletions.
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

0 comments on commit 2c5ca0a

Please sign in to comment.