github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/hostname_mgt.go (about)

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