Skip to content

Commit

Permalink
Clean up hostnames when removing project data, closes #831 (#1017)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewfrench committed Jul 30, 2018
1 parent 9f5eca4 commit 165ba99
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 30 deletions.
231 changes: 203 additions & 28 deletions cmd/ddev/cmd/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,233 @@ import (

"github.com/drud/ddev/pkg/output"

"strings"

"github.com/drud/ddev/pkg/ddevapp"
"github.com/drud/ddev/pkg/dockerutil"
"github.com/drud/ddev/pkg/version"
"github.com/lextoumbourou/goodhosts"
"github.com/spf13/cobra"
)

var removeHostName bool
var removeInactive bool

// HostNameCmd represents the hostname command
var HostNameCmd = &cobra.Command{
Use: "hostname [hostname] [ip]",
Short: "Manage your hostfile entries.",
Long: `Manage your hostfile entries.`,
Long: `Manage your hostfile entries. Managing host names has security and usability
implications and requires elevated privileges. You may be asked for a password
to allow ddev to modify your hosts file.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
output.UserOut.Fatal("Invalid arguments supplied. Please use 'ddev hostname [hostname] [ip]'")
}

rawResult := make(map[string]interface{})

hostname, ip := args[0], args[1]
hosts, err := goodhosts.NewHosts()
if err != nil {
rawResult := make(map[string]interface{})
detail := fmt.Sprintf("Could not open hosts file for reading: %v", err)
rawResult["error"] = "READERROR"
rawResult["full_error"] = fmt.Sprintf("%v", err)
output.UserOut.WithField("raw", rawResult).Fatal(fmt.Sprintf("could not open hosts file for read: %v", err))
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}

// Attempt to write the hosts file first to catch any permissions issues early
if err := hosts.Flush(); err != nil {
rawResult := make(map[string]interface{})
detail := fmt.Sprintf("Please use sudo or execute with administrative privileges: %v", err)
rawResult["error"] = "WRITEERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}
if hosts.Has(ip, hostname) {
if output.JSONOutput {
rawResult["error"] = "SUCCESS"
rawResult["detail"] = "hostname already exists in hosts file"
output.UserOut.WithField("raw", rawResult).Info("")

// If requested, remove all inactive host names and exit
if removeInactive {
if len(args) > 0 {
output.UserOut.Fatal("Invalid arguments supplied. 'ddev hostname --remove-all' accepts no arguments.")
}

removeInactiveHostnames(hosts)

return
}

err = hosts.Add(ip, hostname)
if err != nil {
rawResult["error"] = "ADDERROR"
rawResult["full_error"] = fmt.Sprintf("%v", err)
output.UserOut.WithField("raw", rawResult).Fatal(fmt.Sprintf("could not add hostname %s at %s: %v", hostname, ip, err))
// If operating on one host name, two arguments are required
if len(args) != 2 {
output.UserOut.Fatal("Invalid arguments supplied. Please use 'ddev hostname [hostname] [ip]'")
}

if err := hosts.Flush(); err != nil {
rawResult["error"] = "WRITEERROR"
rawResult["full_error"] = fmt.Sprintf("%v", err)
output.UserOut.WithField("raw", rawResult).Fatal(fmt.Sprintf("Could not write hosts file: %v", err))
} else {
rawResult["error"] = "SUCCESS"
rawResult["detail"] = "hostname added to hosts file"
output.UserOut.WithField("raw", rawResult).Info("")
hostname, ip := args[0], args[1]

// If requested, remove the provided host name and exit
if removeHostName {
removeHostname(hosts, ip, hostname)

return
}

// By default, add a host name
addHostname(hosts, ip, hostname)
},
}

// addHostname encapsulates the logic of adding a hostname to the system's hosts file.
func addHostname(hosts goodhosts.Hosts, ip, hostname string) {
var detail string
rawResult := make(map[string]interface{})

if hosts.Has(ip, hostname) {
detail = "Hostname already exists in hosts file"
rawResult["error"] = "SUCCESS"
rawResult["detail"] = detail
output.UserOut.WithField("raw", rawResult).Info(detail)

return
}

if err := hosts.Add(ip, hostname); err != nil {
detail = fmt.Sprintf("Could not add hostname %s at %s: %v", hostname, ip, err)
rawResult["error"] = "ADDERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}

if err := hosts.Flush(); err != nil {
detail = fmt.Sprintf("Could not write hosts file: %v", err)
rawResult["error"] = "WRITEERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}

detail = "Hostname added to hosts file"
rawResult["error"] = "SUCCESS"
rawResult["detail"] = detail
output.UserOut.WithField("raw", rawResult).Info(detail)

return
}

// removeHostname encapsulates the logic of removing a hostname from the system's hosts file.
func removeHostname(hosts goodhosts.Hosts, ip, hostname string) {
var detail string
rawResult := make(map[string]interface{})

if !hosts.Has(ip, hostname) {
detail = "Hostname does not exist in hosts file"
rawResult["error"] = "SUCCESS"
rawResult["detail"] = detail
output.UserOut.WithField("raw", rawResult).Info(detail)

return
}

if err := hosts.Remove(ip, hostname); err != nil {
detail = fmt.Sprintf("Could not remove hostname %s at %s: %v", hostname, ip, err)
rawResult["error"] = "REMOVEERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}

if err := hosts.Flush(); err != nil {
detail = fmt.Sprintf("Could not write hosts file: %v", err)
rawResult["error"] = "WRITEERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)

return
}

detail = "Hostname removed from hosts file"
rawResult["error"] = "SUCCESS"
rawResult["detail"] = detail
output.UserOut.WithField("raw", rawResult).Info(detail)

return
}

// removeInactiveHostnames will remove all host names except those current in use by active projects.
func removeInactiveHostnames(hosts goodhosts.Hosts) {
var detail string
rawResult := make(map[string]interface{})

// Get the list active hosts names to preserve
activeHostNames := make(map[string]bool)
for _, app := range ddevapp.GetApps() {
for _, h := range app.GetHostnames() {
activeHostNames[h] = true
}
}

// Find all current host names for the local IP address
dockerIP, err := dockerutil.GetDockerIP()
if err != nil {
detail = fmt.Sprintf("Failed to get Docker IP: %v", err)
rawResult["error"] = "DOCKERERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)
}

// Iterate through each host line
for _, line := range hosts.Lines {
// Checking if it concerns the local IP address
if line.IP == dockerIP {
// Iterate through each registered host
for _, h := range line.Hosts {
internalResult := make(map[string]interface{})

// Ignore those we want to preserve
if isActiveHost := activeHostNames[h]; isActiveHost {
detail = fmt.Sprintf("Hostname %s at %s is active, preserving", h, line.IP)
internalResult["error"] = "SUCCESS"
internalResult["detail"] = detail
output.UserOut.WithField("raw", internalResult).Info(detail)
continue
}

// Silently ignore those that may not be ddev-managed
if !strings.HasSuffix(h, version.DDevTLD) {
continue
}

// Remaining host names are fair game to be removed
if err := hosts.Remove(line.IP, h); err != nil {
detail = fmt.Sprintf("Could not remove hostname %s at %s: %v", h, line.IP, err)
internalResult["error"] = "REMOVEERROR"
internalResult["full_error"] = detail
output.UserOut.WithField("raw", internalResult).Fatal(detail)
}

detail = fmt.Sprintf("Removed hostname %s at %s", h, line.IP)
internalResult["error"] = "SUCCESS"
internalResult["detail"] = detail
output.UserOut.WithField("raw", internalResult).Info(detail)
}
}
}

if err := hosts.Flush(); err != nil {
detail = fmt.Sprintf("Could not write hosts file: %v", err)
rawResult["error"] = "WRITEERROR"
rawResult["full_error"] = detail
output.UserOut.WithField("raw", rawResult).Fatal(detail)
}

return
}

func init() {
HostNameCmd.Flags().BoolVarP(&removeHostName, "remove", "r", false, "Remove the provided host name - ip correlation")
HostNameCmd.Flags().BoolVarP(&removeInactive, "remove-inactive", "R", false, "Remove host names of inactive projects")
HostNameCmd.Flags().BoolVar(&removeInactive, "fire-bazooka", false, "Alias of --remove-inactive")
_ = HostNameCmd.Flags().MarkHidden("fire-bazooka")

RootCmd.AddCommand(HostNameCmd)
}
51 changes: 49 additions & 2 deletions pkg/ddevapp/ddevapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -887,13 +887,18 @@ func (app *DdevApp) Down(removeData bool) error {
return fmt.Errorf("failed to remove %s: %v", app.GetName(), err)
}

// Remove data/database if we need to.
// Remove data/database/hostname if we need to.
if removeData {
if err = app.RemoveHostsEntries(); err != nil {
return fmt.Errorf("failed to remove hosts entries: %v", err)
}

// Check that app.DataDir is a directory that is safe to remove.
err = validateDataDirRemoval(app)
if err != nil {
return fmt.Errorf("failed to remove data/database directories: %v", err)
}

// mysql data can be set to read-only on linux hosts. PurgeDirectory ensures files
// are writable before we attempt to remove them.
if !fileutil.FileExists(app.DataDir) {
Expand Down Expand Up @@ -989,8 +994,9 @@ func (app *DdevApp) AddHostsEntries() error {

hosts, err := goodhosts.NewHosts()
if err != nil {
util.Failed("could not open hostfile. %s", err)
util.Failed("could not open hostfile: %v", err)
}

for _, name := range app.GetHostnames() {

if hosts.Has(dockerIP, name) {
Expand Down Expand Up @@ -1020,6 +1026,47 @@ func (app *DdevApp) AddHostsEntries() error {
return nil
}

// RemoveHostsEntries will remote the site URL from the host's /etc/hosts.
func (app *DdevApp) RemoveHostsEntries() error {
dockerIP, err := dockerutil.GetDockerIP()
if err != nil {
return fmt.Errorf("could not get Docker IP: %v", err)
}

hosts, err := goodhosts.NewHosts()
if err != nil {
util.Failed("could not open hostfile: %v", err)
}

for _, name := range app.GetHostnames() {
if !hosts.Has(dockerIP, name) {
continue
}

_, err = osexec.LookPath("sudo")
if os.Getenv("DRUD_NONINTERACTIVE") != "" || err != nil {
util.Warning("You must manually remove the following entry from your hosts file:\n%s %s\nOr with root/administrative privileges execute 'ddev hostname --remove %s %s", dockerIP, name, name, dockerIP)
return nil
}

ddevFullPath, err := os.Executable()
util.CheckErr(err)

output.UserOut.Printf("ddev needs to remove an entry from your hosts file.\nIt will require administrative privileges via the sudo command, so you may be required\nto enter your password for sudo. ddev is about to issue the command:")

hostnameArgs := []string{ddevFullPath, "hostname", "--remove", name, dockerIP}
command := strings.Join(hostnameArgs, " ")
util.Warning(fmt.Sprintf(" sudo %s", command))
output.UserOut.Println("Please enter your password if prompted.")

if _, err = exec.RunCommandPipe("sudo", hostnameArgs); err != nil {
util.Warning("Failed to execute sudo command, you will need to manually execute '%s' with administrative privileges", command)
}
}

return nil
}

// prepSiteDirs creates a site's directories for db container mounts
func (app *DdevApp) prepSiteDirs() error {

Expand Down

0 comments on commit 165ba99

Please sign in to comment.