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 }