github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/utils.go (about) 1 package ddevapp 2 3 import ( 4 "fmt" 5 "github.com/drud/ddev/pkg/globalconfig" 6 "github.com/drud/ddev/pkg/nodeps" 7 "github.com/drud/ddev/pkg/output" 8 docker "github.com/fsouza/go-dockerclient" 9 "github.com/jedib0t/go-pretty/v6/table" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "os" 16 "text/template" 17 18 "github.com/Masterminds/sprig/v3" 19 "github.com/drud/ddev/pkg/dockerutil" 20 "github.com/drud/ddev/pkg/fileutil" 21 "github.com/drud/ddev/pkg/util" 22 ) 23 24 // GetActiveProjects returns an array of ddev projects 25 // that are currently live in docker. 26 func GetActiveProjects() []*DdevApp { 27 apps := make([]*DdevApp, 0) 28 labels := map[string]string{ 29 "com.ddev.platform": "ddev", 30 "com.docker.compose.service": "web", 31 } 32 containers, err := dockerutil.FindContainersByLabels(labels) 33 34 if err == nil { 35 for _, siteContainer := range containers { 36 approot, ok := siteContainer.Labels["com.ddev.approot"] 37 if !ok { 38 break 39 } 40 41 app, err := NewApp(approot, true) 42 43 // Artificially populate sitename and apptype based on labels 44 // if NewApp() failed. 45 if err != nil { 46 app.Name = siteContainer.Labels["com.ddev.site-name"] 47 app.Type = siteContainer.Labels["com.ddev.app-type"] 48 app.AppRoot = siteContainer.Labels["com.ddev.approot"] 49 } 50 apps = append(apps, app) 51 } 52 } 53 54 return apps 55 } 56 57 // RenderHomeRootedDir shortens a directory name to replace homedir with ~ 58 func RenderHomeRootedDir(path string) string { 59 userDir, err := os.UserHomeDir() 60 util.CheckErr(err) 61 result := strings.Replace(path, userDir, "~", 1) 62 result = strings.Replace(result, "\\", "/", -1) 63 return result 64 } 65 66 // RenderAppRow will add an application row to an existing table for describe and list output. 67 func RenderAppRow(t table.Writer, row map[string]interface{}) { 68 status := fmt.Sprint(row["status_desc"]) 69 urls := "" 70 mutagenStatus := "" 71 if row["status"] == SiteRunning { 72 urls = row["primary_url"].(string) 73 if row["mutagen_enabled"] == true { 74 if _, ok := row["mutagen_status"]; ok { 75 mutagenStatus = row["mutagen_status"].(string) 76 } else { 77 mutagenStatus = "not enabled" 78 } 79 if mutagenStatus != "ok" { 80 mutagenStatus = util.ColorizeText(mutagenStatus, "red") 81 } 82 status = fmt.Sprintf("%s (%s)", status, mutagenStatus) 83 } 84 } 85 86 status = FormatSiteStatus(status) 87 88 t.AppendRow(table.Row{ 89 row["name"], status, row["shortroot"], urls, row["type"], 90 }) 91 92 } 93 94 // Cleanup will remove ddev containers and volumes even if docker-compose.yml 95 // has been deleted. 96 func Cleanup(app *DdevApp) error { 97 client := dockerutil.GetDockerClient() 98 99 // Find all containers which match the current site name. 100 labels := map[string]string{ 101 "com.ddev.site-name": app.GetName(), 102 } 103 104 // remove project network 105 // "docker-compose down" - removes project network and any left-overs 106 // There can be awkward cases where we're doing an app.Stop() but the rendered 107 // yaml does not exist, all in testing situations. 108 if fileutil.FileExists(app.DockerComposeFullRenderedYAMLPath()) { 109 _, _, err := dockerutil.ComposeCmd([]string{app.DockerComposeFullRenderedYAMLPath()}, "down") 110 if err != nil { 111 util.Warning("Failed to docker-compose down: %v", err) 112 } 113 } 114 115 // If any leftovers or lost souls, find them as well 116 containers, err := dockerutil.FindContainersByLabels(labels) 117 if err != nil { 118 return err 119 } 120 // First, try stopping the listed containers if they are running. 121 for i := range containers { 122 containerName := containers[i].Names[0][1:len(containers[i].Names[0])] 123 removeOpts := docker.RemoveContainerOptions{ 124 ID: containers[i].ID, 125 RemoveVolumes: true, 126 Force: true, 127 } 128 output.UserOut.Printf("Removing container: %s", containerName) 129 if err = client.RemoveContainer(removeOpts); err != nil { 130 return fmt.Errorf("could not remove container %s: %v", containerName, err) 131 } 132 } 133 // Always kill the temporary volumes on ddev remove 134 vols := []string{app.GetNFSMountVolumeName(), "ddev-" + app.Name + "-snapshots", app.Name + "-ddev-config"} 135 136 for _, volName := range vols { 137 _ = dockerutil.RemoveVolume(volName) 138 } 139 140 err = StopRouterIfNoContainers() 141 return err 142 } 143 144 // CheckForConf checks for a config.yaml at the cwd or parent dirs. 145 func CheckForConf(confPath string) (string, error) { 146 if fileutil.FileExists(filepath.Join(confPath, ".ddev", "config.yaml")) { 147 return confPath, nil 148 } 149 150 // Keep going until we can't go any higher 151 for filepath.Dir(confPath) != confPath { 152 confPath = filepath.Dir(confPath) 153 if fileutil.FileExists(filepath.Join(confPath, ".ddev", "config.yaml")) { 154 return confPath, nil 155 } 156 } 157 158 return "", fmt.Errorf("no %s file was found in this directory or any parent", filepath.Join(".ddev", "config.yaml")) 159 } 160 161 // ddevContainersRunning determines if any ddev-controlled containers are currently running. 162 func ddevContainersRunning() (bool, error) { 163 labels := map[string]string{ 164 "com.ddev.platform": "ddev", 165 } 166 containers, err := dockerutil.FindContainersByLabels(labels) 167 if err != nil { 168 return false, err 169 } 170 171 for _, container := range containers { 172 if _, ok := container.Labels["com.ddev.platform"]; ok { 173 return true, nil 174 } 175 } 176 return false, nil 177 } 178 179 // getTemplateFuncMap will return a map of useful template functions. 180 func getTemplateFuncMap() map[string]interface{} { 181 // Use sprig's template function map as a base 182 m := sprig.FuncMap() 183 184 // Add helpful utilities on top of it 185 m["joinPath"] = path.Join 186 187 return m 188 } 189 190 // gitIgnoreTemplate will write a .gitignore file. 191 // This template expects string slice to be provided, with each string corresponding to 192 // a line in the resulting .gitignore. 193 const gitIgnoreTemplate = `{{.Signature}}: Automatically generated ddev .gitignore. 194 # You can remove the above line if you want to edit and maintain this file yourself. 195 /.gitignore 196 {{range .IgnoredItems}} 197 /{{.}}{{end}} 198 ` 199 200 type ignoreTemplateContents struct { 201 Signature string 202 IgnoredItems []string 203 } 204 205 // CreateGitIgnore will create a .gitignore file in the target directory if one does not exist. 206 // Each value in ignores will be added as a new line to the .gitignore. 207 func CreateGitIgnore(targetDir string, ignores ...string) error { 208 gitIgnoreFilePath := filepath.Join(targetDir, ".gitignore") 209 210 if fileutil.FileExists(gitIgnoreFilePath) { 211 sigFound, err := fileutil.FgrepStringInFile(gitIgnoreFilePath, nodeps.DdevFileSignature) 212 if err != nil { 213 return err 214 } 215 216 // If we sigFound the file and did not find the signature in .ddev/.gitignore, warn about it. 217 if !sigFound { 218 util.Warning("User-managed %s will not be managed/overwritten by ddev", gitIgnoreFilePath) 219 return nil 220 } 221 // Otherwise, remove the existing file to prevent surprising template results 222 err = os.Remove(gitIgnoreFilePath) 223 if err != nil { 224 return err 225 } 226 } 227 err := os.MkdirAll(targetDir, 0777) 228 if err != nil { 229 return err 230 } 231 232 generatedIgnores := []string{} 233 for _, p := range ignores { 234 sigFound, err := fileutil.FgrepStringInFile(p, nodeps.DdevFileSignature) 235 if sigFound || err != nil { 236 generatedIgnores = append(generatedIgnores, p) 237 } 238 } 239 240 tmpl, err := template.New("gitignore").Funcs(getTemplateFuncMap()).Parse(gitIgnoreTemplate) 241 if err != nil { 242 return err 243 } 244 245 file, err := os.OpenFile(gitIgnoreFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 246 if err != nil { 247 return err 248 } 249 defer util.CheckClose(file) 250 251 parms := ignoreTemplateContents{ 252 Signature: nodeps.DdevFileSignature, 253 IgnoredItems: generatedIgnores, 254 } 255 256 //nolint: revive 257 if err = tmpl.Execute(file, parms); err != nil { 258 return err 259 } 260 261 return nil 262 } 263 264 // isTar determines whether the object at the filepath is a .tar archive. 265 func isTar(filepath string) bool { 266 tarSuffixes := []string{"tar", "tar.gz", "tar.bz2", "tar.xz", "tgz", "tar.xz", "tar.bz2"} 267 for _, suffix := range tarSuffixes { 268 if strings.HasSuffix(filepath, suffix) { 269 return true 270 } 271 } 272 273 return false 274 } 275 276 // isZip determines if the object at hte filepath is a .zip. 277 func isZip(filepath string) bool { 278 if strings.HasSuffix(filepath, ".zip") { 279 return true 280 } 281 282 return false 283 } 284 285 // GetErrLogsFromApp is used to do app.Logs on an app after an error has 286 // been received, especially on app.Start. This is really for testing only 287 func GetErrLogsFromApp(app *DdevApp, errorReceived error) (string, error) { 288 var serviceName string 289 if errorReceived == nil { 290 return "no error detected", nil 291 } 292 errString := errorReceived.Error() 293 errString = strings.Replace(errString, "Received unexpected error:", "", -1) 294 errString = strings.Replace(errString, "\n", "", -1) 295 errString = strings.Trim(errString, " \t\n\r") 296 if strings.Contains(errString, "container failed") || strings.Contains(errString, "container did not become ready") || strings.Contains(errString, "failed to become ready") { 297 splitError := strings.Split(errString, " ") 298 if len(splitError) > 0 && nodeps.ArrayContainsString([]string{"web", "db", "ddev-router", "ddev-ssh-agent"}, splitError[0]) { 299 serviceName = splitError[0] 300 logs, err := app.CaptureLogs(serviceName, false, "") 301 if err != nil { 302 return "", err 303 } 304 return logs, nil 305 } 306 } 307 return "", fmt.Errorf("no logs found for service %s (Inspected err=%v)", serviceName, errorReceived) 308 } 309 310 // CheckForMissingProjectFiles returns an error if the project's configuration or project root cannot be found 311 func CheckForMissingProjectFiles(project *DdevApp) error { 312 status, _ := project.SiteStatus() 313 if status == SiteConfigMissing || status == SiteDirMissing { 314 return fmt.Errorf("ddev can no longer find your project files at %s. If you would like to continue using ddev to manage this project please restore your files to that directory. If you would like to make ddev forget this project, you may run 'ddev stop --unlist %s'", project.GetAppRoot(), project.GetName()) 315 } 316 317 return nil 318 } 319 320 // GetProjects returns projects that are listed 321 // in globalconfig projectlist (or in docker container labels, or both) 322 // if activeOnly is true, only show projects that aren't stopped 323 // (or broken, missing config, missing files) 324 func GetProjects(activeOnly bool) ([]*DdevApp, error) { 325 apps := make(map[string]*DdevApp) 326 projectList := globalconfig.GetGlobalProjectList() 327 328 // First grab the GetActiveApps (docker labels) version of the projects and make sure it's 329 // included. Hopefully docker label information and global config information will not 330 // be out of sync very often. 331 dockerActiveApps := GetActiveProjects() 332 for _, app := range dockerActiveApps { 333 apps[app.Name] = app 334 } 335 336 // Now get everything we can find in global project list 337 for name, info := range projectList { 338 // Skip apps already found running in docker 339 if _, ok := apps[name]; ok { 340 continue 341 } 342 343 app, err := NewApp(info.AppRoot, true) 344 if err != nil { 345 util.Warning("unable to create project at project root '%s': %v", info.AppRoot, err) 346 continue 347 } 348 349 // If the app we just loaded was already found with a different name, complain 350 if _, ok := apps[app.Name]; ok { 351 util.Warning(`Project '%s' was found in configured directory %s and it is already used by project '%s'. If you have changed the name of the project, please "ddev stop --unlist %s" `, app.Name, app.AppRoot, name, name) 352 continue 353 } 354 355 status, _ := app.SiteStatus() 356 if !activeOnly || (status != SiteStopped && status != SiteConfigMissing && status != SiteDirMissing) { 357 apps[app.Name] = app 358 } 359 } 360 361 appSlice := []*DdevApp{} 362 for _, v := range apps { 363 appSlice = append(appSlice, v) 364 } 365 sort.Slice(appSlice, func(i, j int) bool { return appSlice[i].Name < appSlice[j].Name }) 366 367 return appSlice, nil 368 } 369 370 // GetInactiveProjects returns projects that are currently running 371 func GetInactiveProjects() ([]*DdevApp, error) { 372 var inactiveApps []*DdevApp 373 374 apps, err := GetProjects(false) 375 376 if err != nil { 377 return nil, err 378 } 379 380 for _, app := range apps { 381 status, _ := app.SiteStatus() 382 if status != SiteRunning { 383 inactiveApps = append(inactiveApps, app) 384 } 385 } 386 387 return inactiveApps, nil 388 } 389 390 // ExtractProjectNames returns a list of names by a bunch of projects 391 func ExtractProjectNames(apps []*DdevApp) []string { 392 var names []string 393 for _, app := range apps { 394 names = append(names, app.Name) 395 } 396 397 return names 398 } 399 400 // GetRelativeWorkingDirectory returns the relative working directory relative to project root 401 // Note that the relative dir is returned as unix-style forward-slashes 402 func (app *DdevApp) GetRelativeWorkingDirectory() string { 403 pwd, _ := os.Getwd() 404 405 // Find the relative dir 406 relativeWorkingDir := strings.TrimPrefix(pwd, app.AppRoot) 407 // Convert to slash/linux/macos notation, should work everywhere 408 relativeWorkingDir = filepath.ToSlash(relativeWorkingDir) 409 // remove any leading / 410 relativeWorkingDir = strings.TrimLeft(relativeWorkingDir, "/") 411 412 return relativeWorkingDir 413 }