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  }