github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/hostname_mgt.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/drud/ddev/pkg/ddevhosts"
     6  	"github.com/drud/ddev/pkg/dockerutil"
     7  	"github.com/drud/ddev/pkg/exec"
     8  	"github.com/drud/ddev/pkg/globalconfig"
     9  	"github.com/drud/ddev/pkg/output"
    10  	"github.com/drud/ddev/pkg/util"
    11  	goodhosts "github.com/goodhosts/hostsfile"
    12  	"net"
    13  	"os"
    14  	exec2 "os/exec"
    15  	"runtime"
    16  	"strings"
    17  )
    18  
    19  // windowsDdevExeAvailable says if ddev.exe is available on Windows side
    20  var windowsDdevExeAvailable bool
    21  
    22  // IsWindowsDdevExeAvailable checks to see if we can use ddev.exe on Windows side
    23  func IsWindowsDdevExeAvailable() bool {
    24  	if !globalconfig.DdevGlobalConfig.WSL2NoWindowsHostsMgt && !windowsDdevExeAvailable && dockerutil.IsWSL2() {
    25  		_, err := exec2.LookPath("ddev.exe")
    26  		if err != nil {
    27  			util.Warning("ddev.exe not found in $PATH, please install it on Windows side; err=%v", err)
    28  			windowsDdevExeAvailable = false
    29  			return windowsDdevExeAvailable
    30  		}
    31  		out, err := exec.RunHostCommand("ddev.exe", "--version")
    32  		if err != nil {
    33  			util.Warning("unable to run ddev.exe, please check it on Windows side; err=%v; output=%s", err, out)
    34  			windowsDdevExeAvailable = false
    35  			return windowsDdevExeAvailable
    36  		}
    37  
    38  		_, err = exec2.LookPath("sudo.exe")
    39  		if err != nil {
    40  			util.Warning("sudo.exe not found in $PATH, please install DDEV on Windows side; err=%v", err)
    41  			windowsDdevExeAvailable = false
    42  			return windowsDdevExeAvailable
    43  		}
    44  		windowsDdevExeAvailable = true
    45  	}
    46  	return windowsDdevExeAvailable
    47  }
    48  
    49  // IsHostnameInHostsFile checks to see if the hostname already exists
    50  // On WSL2 it normally assumes that the hosts file is in WSL2WindowsHostsFile
    51  // Otherwise it lets goodhosts decide where the hosts file is.
    52  func IsHostnameInHostsFile(hostname string) (bool, error) {
    53  	dockerIP, err := dockerutil.GetDockerIP()
    54  	if err != nil {
    55  		return false, fmt.Errorf("could not get Docker IP: %v", err)
    56  	}
    57  
    58  	var hosts = &ddevhosts.DdevHosts{}
    59  	if dockerutil.IsWSL2() && !globalconfig.DdevGlobalConfig.WSL2NoWindowsHostsMgt {
    60  		hosts, err = ddevhosts.NewCustomHosts(ddevhosts.WSL2WindowsHostsFile)
    61  	} else {
    62  		hosts, err = ddevhosts.New()
    63  	}
    64  	if err != nil {
    65  		return false, fmt.Errorf("Unable to open hosts file: %v", err)
    66  	}
    67  	return hosts.Has(dockerIP, hostname), nil
    68  }
    69  
    70  // AddHostsEntriesIfNeeded will run sudo ddev hostname to the site URL to the host's /etc/hosts.
    71  // This should be run without admin privs; the ddev hostname command will handle escalation.
    72  func (app *DdevApp) AddHostsEntriesIfNeeded() error {
    73  	var err error
    74  	dockerIP, err := dockerutil.GetDockerIP()
    75  	if err != nil {
    76  		return fmt.Errorf("could not get Docker IP: %v", err)
    77  	}
    78  
    79  	if os.Getenv("DDEV_NONINTERACTIVE") == "true" {
    80  		util.Warning("Not trying to add hostnames because DDEV_NONINTERACTIVE=true")
    81  		return nil
    82  	}
    83  
    84  	for _, name := range app.GetHostnames() {
    85  
    86  		// If we're able to resolve the hostname via DNS or otherwise we
    87  		// don't have to worry about this. This will allow resolution
    88  		// of *.ddev.site for example
    89  		if app.UseDNSWhenPossible && globalconfig.IsInternetActive() {
    90  			// If they have provided "*.<name>" then look up the suffix
    91  			checkName := strings.TrimPrefix(name, "*.")
    92  			hostIPs, err := net.LookupHost(checkName)
    93  
    94  			// If we had successful lookup and dockerIP matches
    95  			// with adding to hosts file.
    96  			if err == nil && len(hostIPs) > 0 && hostIPs[0] == dockerIP {
    97  				continue
    98  			}
    99  		}
   100  
   101  		// We likely won't hit the hosts.Has() as true because
   102  		// we already did a lookup. But check anyway.
   103  		exists, err := IsHostnameInHostsFile(name)
   104  		if exists {
   105  			continue
   106  		}
   107  		if err != nil {
   108  			util.Warning("unable to open hosts file: %v", err)
   109  			continue
   110  		}
   111  		util.Warning("The hostname %s is not currently resolvable, trying to add it to the hosts file", name)
   112  
   113  		out, err := escalateToAddHostEntry(name, dockerIP)
   114  		if err != nil {
   115  			return err
   116  		}
   117  		util.Success(out)
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // AddHostEntry adds an entry to default hosts file
   124  // This is only used by `ddev hostname` and only used with admin privs
   125  func AddHostEntry(name string, ip string) error {
   126  	if os.Getenv("DDEV_NONINTERACTIVE") != "" {
   127  		util.Warning("You must manually add the following entry to your hosts file:\n%s %s\nOr with root/administrative privileges execute 'ddev hostname %s %s'", ip, name, name, ip)
   128  		return nil
   129  	}
   130  
   131  	hosts, err := goodhosts.NewHosts()
   132  	if err != nil {
   133  		return err
   134  	}
   135  	err = hosts.Add(ip, name)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	err = hosts.Flush()
   140  	return err
   141  }
   142  
   143  // RemoveHostsEntriesIfNeeded will remove the site URL from the host's /etc/hosts.
   144  // This should be run without administrative privileges and will escalate
   145  // where needed.
   146  func (app *DdevApp) RemoveHostsEntriesIfNeeded() error {
   147  	if os.Getenv("DDEV_NONINTERACTIVE") == "true" {
   148  		util.Warning("Not trying to remove hostnames because DDEV_NONINTERACTIVE=true")
   149  		return nil
   150  	}
   151  
   152  	dockerIP, err := dockerutil.GetDockerIP()
   153  	if err != nil {
   154  		return fmt.Errorf("could not get Docker IP: %v", err)
   155  	}
   156  
   157  	for _, name := range app.GetHostnames() {
   158  		exists, err := IsHostnameInHostsFile(name)
   159  		if !exists {
   160  			continue
   161  		}
   162  		if err != nil {
   163  			util.Warning("unable to open hosts file: %v", err)
   164  			continue
   165  		}
   166  
   167  		_, err = escalateToRemoveHostEntry(name, dockerIP)
   168  
   169  		if err != nil {
   170  			util.Warning("Failed to remove host entry %s: %v", name, err)
   171  		}
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  // RemoveHostEntry removes named /etc/hosts entry if it exists
   178  // This should be run with administrative privileges only and used by
   179  // ddev hostname only
   180  func RemoveHostEntry(name string, ip string) error {
   181  	if os.Getenv("DDEV_NONINTERACTIVE") != "" {
   182  		util.Warning("You must manually add the following entry to your hosts file:\n%s %s\nOr with root/administrative privileges execute 'ddev hostname %s %s'", ip, name, name, ip)
   183  		return nil
   184  	}
   185  
   186  	hosts, err := goodhosts.NewHosts()
   187  	if err != nil {
   188  		return err
   189  	}
   190  	err = hosts.Remove(ip, name)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	err = hosts.Flush()
   195  	return err
   196  }
   197  
   198  // escalateToAddHostEntry runs the required ddev hostname command to add the entry,
   199  // does it with sudo on the correct platform.
   200  func escalateToAddHostEntry(hostname string, ip string) (string, error) {
   201  	ddevBinary, err := os.Executable()
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  	if dockerutil.IsWSL2() {
   206  		ddevBinary = "ddev.exe"
   207  	}
   208  	out, err := runCommandWithSudo([]string{ddevBinary, "hostname", hostname, ip})
   209  	return out, err
   210  }
   211  
   212  // escalateToRemoveHostEntry runs the required ddev hostname command to remove the entry,
   213  // does it with sudo on the correct platform.
   214  func escalateToRemoveHostEntry(hostname string, ip string) (string, error) {
   215  	ddevBinary, err := os.Executable()
   216  	if err != nil {
   217  		return "", err
   218  	}
   219  	if dockerutil.IsWSL2() {
   220  		ddevBinary = "ddev.exe"
   221  	}
   222  	out, err := runCommandWithSudo([]string{ddevBinary, "hostname", "--remove", hostname, ip})
   223  	return out, err
   224  }
   225  
   226  // runCommandWithSudo adds sudo to command if we aren't already running with root privs
   227  func runCommandWithSudo(args []string) (out string, err error) {
   228  	// We can't escalate in tests, and they know how to deal with it.
   229  	if os.Getenv("DDEV_NONINTERACTIVE") != "" {
   230  		util.Warning("DDEV_NONINTERACTIVE is set. You must manually run '%s'", strings.Join(args, " "))
   231  		return "", nil
   232  	}
   233  	if err != nil {
   234  		return "", fmt.Errorf("could not get home directory for current user. is it set?")
   235  	}
   236  
   237  	if (dockerutil.IsWSL2() && !globalconfig.DdevGlobalConfig.WSL2NoWindowsHostsMgt) && !IsWindowsDdevExeAvailable() {
   238  		return "", fmt.Errorf("ddev.exe is not installed on the Windows side, please install it with 'choco install -y ddev'. It is used to manage the Windows hosts file")
   239  	}
   240  	c := []string{"sudo", "--preserve-env=HOME"}
   241  	if (runtime.GOOS == "windows" || dockerutil.IsWSL2()) && !globalconfig.DdevGlobalConfig.WSL2NoWindowsHostsMgt {
   242  		c = []string{"sudo.exe"}
   243  	}
   244  	c = append(c, args...)
   245  	output.UserOut.Printf("ddev needs to run with administrative privileges.\nYou may be required to enter your password for sudo or allow escalation. ddev is about to issue the command:\n  %s\n", strings.Join(c, ` `))
   246  
   247  	out, err = exec.RunHostCommand(c[0], c[1:]...)
   248  	return out, err
   249  }