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 }