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

Feature/save dns records to hosts #172

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
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
216 changes: 201 additions & 15 deletions commands/dns-records.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package commands

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"

"github.com/bitly/go-simplejson"
Expand All @@ -17,12 +19,61 @@ type DNSRecords struct {
BaseCommand
}

// DNSRecord is the struct for a single DNS entry
type DNSRecord struct {
Id string // nolint
Name string
Image string
IPs []string
TTL int64
Aliases []string
}

// DNSRecordsList is an array of DNSRecords
type DNSRecordsList []*DNSRecord

const (
unixHostsPreamble = "##+++ added by rig"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'll use this regardless of the underlying os as markers for starting/removing items so perhaps just call this hostsPreamble

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Thanks.

unixHostsPostamble = "##--- end rig additions"
)

func (record *DNSRecord) String() string {
result := ""
for _, ip := range record.IPs {
result += fmt.Sprintf("%s\t%s.%s.%s\n", ip, record.Name, record.Image, "vm")
// attach any aliases too
for _, a := range record.Aliases {
result += fmt.Sprintf("%s\t%s\n", ip, a)
}
}
return result
}

// String converts a list of DNSRecords to a formatted string
func (hosts DNSRecordsList) String() string {
result := ""
for _, host := range hosts {
result += host.String()
}
return result
}

// Commands returns the operations supported by this command
func (cmd *DNSRecords) Commands() []cli.Command {
return []cli.Command{
{
Name: "dns-records",
Usage: "List all DNS records for running containers",
Name: "dns-records",
Usage: "List all DNS records for running containers",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "save",
Usage: "Save the DNS records to /etc/hosts or {FIX insert Windows desscription}",
},
cli.BoolFlag{
Name: "remove",
Usage: "Remove the DNS records from /etc/hosts or {FIX insert Windows desscription}",
},
},
Before: cmd.Before,
Action: cmd.Run,
},
Expand All @@ -31,27 +82,31 @@ func (cmd *DNSRecords) Commands() []cli.Command {

// Run executes the `rig dns-records` command
func (cmd *DNSRecords) Run(c *cli.Context) error {
// Don't require rig to be started to remove records
if c.Bool("remove") {
if c.Bool("save") {
return cmd.Failure("--remove and --save are mutually exclusive", "COMMAND-ERROR", 13)
}
// TODO The VM might have to be up for Windows
return cmd.removeDNSRecords()
}

records, err := cmd.LoadRecords()
if err != nil {
return cmd.Failure(err.Error(), "COMMAND-ERROR", 13)
}

for _, record := range records {
for _, ip := range record["IPs"].([]interface{}) {
fmt.Printf("%s\t%s.%s.%s\n", ip, record["Name"], record["Image"], "vm")
// attach any aliases too
for _, a := range record["Aliases"].([]interface{}) {
fmt.Printf("%s\t%s\n", ip, a)
}
}
if c.Bool("save") {
return cmd.saveDNSRecords(records)
}

printDNSRecords(records)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you do a save or remove you likely don't want the records printed out too so maybe we only print the records if save or remove have not been specified

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They always return if they do a save or remove. So it just falls through to the default if they don't specify either.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH yeah. ha. You got it.


return cmd.Success("")
}

// LoadRecords retrieves the records from DNSDock and processes/return them
func (cmd *DNSRecords) LoadRecords() ([]map[string]interface{}, error) {
func (cmd *DNSRecords) LoadRecords() ([]*DNSRecord, error) {
ip, err := util.Command("docker", "inspect", "--format", "{{.NetworkSettings.IPAddress}}", "dnsdock").Output()
if err != nil {
return nil, fmt.Errorf("failed to discover dnsdock IP address: %s", err)
Expand All @@ -74,11 +129,142 @@ func (cmd *DNSRecords) LoadRecords() ([]map[string]interface{}, error) {
}

dnsdockMap, _ := js.Map()
records := []map[string]interface{}{}
for id, value := range dnsdockMap {
record := value.(map[string]interface{})
record["Id"] = id
records := make([]*DNSRecord, 0, 20)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interested in your thoughts behind the use of the capacity argument here. As near as I can tell this doesn't set any upper bound on the size of the array though I had to write a quick test to make sure. Is the initial capacity you picked arbitrary (both here and below where you used 10) or is there something behind that particular value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely arbitrary. Yes, when accumulating the array contents via append() more space will be allocated if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that it's a dynamic array perhaps just drop the capacity argument here and anywhere it doesn't have an obvious purpose. This is to prevent confusing anyone in the future about why it is there, how it got picked, does it actually result in enforced bounds, etc? Alternatively, comment something to the effect of what I think may be true about it:

// capacity picked arbitrarily with a size likely to prevent need to reallocate underlying arrays while appending items in most cases

I also presume that has some potential performance benefit though at the scales here I think it's unobservable.

If that comment isn't accurate I'd be interested in what the benefits are of declaring a capacity are in the case where append is going to be used. When using copy it's an obvious useful item to restrict the number of items you want from a potentially larger array.

Thanks for the pointers as I drag myself deeper into go.

for id, rawValue := range dnsdockMap {
// Cast rawValue to its actual type
value := rawValue.(map[string]interface{})
ttl, _ := value["TTL"].(json.Number).Int64()
record := &DNSRecord{
Id: id,
Name: value["Name"].(string),
Image: value["Image"].(string),
TTL: ttl,
}
record.IPs = make([]string, 0, 10)
for _, ip := range value["IPs"].([]interface{}) {
record.IPs = append(record.IPs, ip.(string))
}
record.Aliases = make([]string, 0, 10)
for _, alias := range value["Aliases"].([]interface{}) {
record.Aliases = append(record.Aliases, alias.(string))
}
records = append(records, record)
}
return records, nil
}

func printDNSRecords(records []*DNSRecord) {
for _, record := range records {
fmt.Print(record)
}
}

// Write the records to /etc/hosts or FIX Windows?
func (cmd *DNSRecords) saveDNSRecords(records []*DNSRecord) error {
if util.IsMac() || util.IsLinux() {
return cmd.saveDNSRecordsUnix(records)
} else if util.IsWindows() {
return cmd.saveDNSRecordsWindows(records)
}
return cmd.Success("Not implemented")
}

// ⚠ Administrative privileges needed...

func (cmd *DNSRecords) saveDNSRecordsUnix(records []*DNSRecord) error {
// Both of these are []string
oldHostEntries := util.LoadFile("/etc/hosts")
newHostEntries := stripDNS(oldHostEntries)
// records.String does the formatting, so convert both to a string
oldHosts := strings.Join(oldHostEntries, "\n")
newHosts := strings.Join(newHostEntries, "\n") + "\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also do something like this...

fmt.Sprintf(`%s
%s
%s`, x, y, z)

to get the newlines you want.

unixHostsPreamble + "\n" +
DNSRecordsList(records).String() +
unixHostsPostamble + "\n"
if oldHosts == newHosts {
return cmd.Success("No changes made")
}
return cmd.writeEtcHosts(newHosts)
}

func (cmd *DNSRecords) saveDNSRecordsWindows(records []*DNSRecord) error {
return cmd.Failure("Not Implemented", "COMMAND-ERROR", 13)
}

func (cmd *DNSRecords) removeDNSRecords() error {
if util.IsMac() || util.IsLinux() {
return cmd.removeDNSRecordsUnix()
} else if util.IsWindows() {
return cmd.removeDNSRecordsWindows()
}
return cmd.Success("Not implemented")
}

func (cmd *DNSRecords) removeDNSRecordsUnix() error {
oldHostsEntries := util.LoadFile("/etc/hosts")
newHostsEntries := stripDNS(oldHostsEntries)
oldHosts := strings.Join(oldHostsEntries, "\n")
newHosts := strings.Join(newHostsEntries, "\n")
if oldHosts == newHosts {
return cmd.Success("No changes made")
}
return cmd.writeEtcHosts(newHosts)
}

func (cmd *DNSRecords) removeDNSRecordsWindows() error {
return cmd.Failure("Not Implemented", "COMMAND-ERROR", 13)
}

// Save a new version of /etc/hosts, arg is the full text to save
func (cmd *DNSRecords) writeEtcHosts(hostsText string) error {
// Make sure it ends in a newline
if hostsText[len(hostsText)-1] != '\n' {
hostsText += "\n"
}
// Write new version to a temp file
tmpfile, err := ioutil.TempFile("", "rig-hosts")
if err != nil {
return cmd.Failure("Unable to create hosts tempfile: "+err.Error(), "COMMAND-ERROR", 13)
}
tmpname := tmpfile.Name()
defer os.Remove(tmpname)
if _, err := tmpfile.Write([]byte(hostsText)); err != nil {
return cmd.Failure("Unable to write hosts tempfile: ("+tmpname+") "+err.Error(), "COMMAND-ERROR", 13)
}
if err := tmpfile.Close(); err != nil {
return cmd.Failure("Unable to close hosts tempfile: ("+tmpname+") "+err.Error(), "COMMAND-ERROR", 13)
}
// mv it into place. This is safer than trying to write /etc/hosts on the fly.
if err := util.EscalatePrivilege(); err != nil {
return cmd.Failure("Unable to obtain privileges to replace: "+err.Error(), "COMMAND-ERROR", 13)
}
if err := util.Command("sudo", "mv", "-f", tmpname, "/etc/hosts").Run(); err != nil {
return cmd.Failure("Unable to replace /etc/hosts: "+err.Error(), "COMMAND-ERROR", 13)
}
return cmd.Success("/etc/hosts updated")
}

// Remove a section of the hosts file we previously added
func stripDNS(hosts []string) []string {
const (
looking = iota
found
)
results := make([]string, 0, 1000)
state := looking
for _, host := range hosts {
switch state {
case looking:
if host == unixHostsPreamble {
state = found
} else {
results = append(results, host)
}
case found:
if host == unixHostsPostamble {
state = looking
}
}
}
return results
}
4 changes: 2 additions & 2 deletions commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ func (cmd *Doctor) Run(c *cli.Context) error {
if records, err := dnsRecords.LoadRecords(); err == nil {
resolved := false
for _, record := range records {
if record["Name"] == "dnsdock" {
if record.Name == "dnsdock" {
resolved = true
cmd.out.Info("DNS and routing services are working. DNSDock resolves to %s", record["IPs"])
cmd.out.Info("DNS and routing services are working. DNSDock resolves to %s", record.IPs)
break
}
}
Expand Down
26 changes: 26 additions & 0 deletions util/filesystem.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package util

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/kardianos/osext"
)
Expand Down Expand Up @@ -91,3 +94,26 @@ func TouchFile(pathToFile string, workingDir string) error {
f.Close()
return nil
}

// LoadFile loads a file into an array, without the newlines
func LoadFile(filename string) []string {
lines := make([]string, 0, 1000)
f, err := os.Open(filename)
if err != nil {
return lines
}
defer f.Close()
r := bufio.NewReader(f)
for {
switch ln, err := r.ReadString('\n'); err {
case nil:
ln = strings.Replace(ln, "\r", "", -1)
ln = strings.Replace(ln, "\n", "", -1)
lines = append(lines, ln)
case io.EOF:
return lines
default:
fmt.Println(err)
}
}
}