github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/mutagen.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"bufio"
     5  	"embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"github.com/drud/ddev/pkg/archive"
     9  	"github.com/drud/ddev/pkg/dockerutil"
    10  	"github.com/drud/ddev/pkg/exec"
    11  	"github.com/drud/ddev/pkg/fileutil"
    12  	"github.com/drud/ddev/pkg/globalconfig"
    13  	"github.com/drud/ddev/pkg/nodeps"
    14  	"github.com/drud/ddev/pkg/output"
    15  	"github.com/drud/ddev/pkg/util"
    16  	"github.com/drud/ddev/pkg/version"
    17  	"github.com/drud/ddev/pkg/versionconstants"
    18  	docker "github.com/fsouza/go-dockerclient"
    19  	"github.com/pkg/errors"
    20  	"os"
    21  	osexec "os/exec"
    22  	"path"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  	"time"
    27  	"unicode"
    28  )
    29  
    30  const mutagenSignatureLabelName = `com.ddev.volume-signature`
    31  
    32  // SetMutagenVolumeOwnership chowns the volume in use to the current user.
    33  // The mutagen volume is mounted both in /var/www (where it gets used) and
    34  // also on /tmp/project_mutagen (where it can be chowned without accidentally hitting
    35  // lots of bind-mounted files).
    36  func SetMutagenVolumeOwnership(app *DdevApp) error {
    37  	// Make sure that if we have a volume mount it's got proper ownership
    38  	uidStr, gidStr, _ := util.GetContainerUIDGid()
    39  	util.Debug("chowning mutagen docker volume for user %s", uidStr)
    40  	_, _, err := app.Exec(
    41  		&ExecOpts{
    42  			Dir: "/tmp",
    43  			Cmd: fmt.Sprintf("sudo chown -R %s:%s /tmp/project_mutagen", uidStr, gidStr),
    44  		})
    45  	if err != nil {
    46  		util.Warning("Failed to chown mutagen volume: %v", err)
    47  	}
    48  	util.Debug("done chowning mutagen docker volume; result=%v", err)
    49  
    50  	return err
    51  }
    52  
    53  // MutagenSyncName transforms a projectname string into
    54  // an acceptable mutagen sync "name"
    55  // See restrictions on sync name at https://mutagen.io/documentation/introduction/names-labels-identifiers
    56  // The input must be a valid DNS name (valid ddev project name)
    57  func MutagenSyncName(name string) string {
    58  	name = strings.ReplaceAll(name, ".", "")
    59  	if len(name) > 0 && unicode.IsNumber(rune(name[0])) {
    60  		name = "a" + name
    61  	}
    62  	return name
    63  }
    64  
    65  // TerminateMutagenSync destroys a mutagen sync session
    66  // It is not an error if the sync session does not exist
    67  func TerminateMutagenSync(app *DdevApp) error {
    68  	syncName := MutagenSyncName(app.Name)
    69  	if MutagenSyncExists(app) {
    70  		_, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "sync", "terminate", syncName)
    71  		if err != nil {
    72  			return err
    73  		}
    74  		util.Debug("Terminated mutagen sync session '%s'", syncName)
    75  	}
    76  	return nil
    77  }
    78  
    79  // PauseMutagenSync pauses a mutagen sync session
    80  func PauseMutagenSync(app *DdevApp) error {
    81  	syncName := MutagenSyncName(app.Name)
    82  	if MutagenSyncExists(app) {
    83  		_, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "sync", "pause", syncName)
    84  		if err != nil {
    85  			return err
    86  		}
    87  		util.Debug("Paused mutagen sync session '%s'", syncName)
    88  	}
    89  	return nil
    90  }
    91  
    92  // SyncAndPauseMutagenSession syncs and pauses a mutagen sync session
    93  func SyncAndPauseMutagenSession(app *DdevApp) error {
    94  	if app.Name == "" {
    95  		return fmt.Errorf("No app.Name provided to SyncAndPauseMutagenSession")
    96  	}
    97  	syncName := MutagenSyncName(app.Name)
    98  
    99  	projStatus, _ := app.SiteStatus()
   100  
   101  	if !MutagenSyncExists(app) {
   102  		return nil
   103  	}
   104  
   105  	mutagenStatus, shortResult, longResult, err := app.MutagenStatus()
   106  	if err != nil {
   107  		return fmt.Errorf("MutagenStatus failed, rv=%v, shortResult=%s, longResult=%s, err=%v", mutagenStatus, shortResult, longResult, err)
   108  	}
   109  
   110  	// We don't want to flush if the web container isn't running
   111  	// because mutagen flush will hang forever - disconnected
   112  	if projStatus == SiteRunning && (mutagenStatus == "ok" || mutagenStatus == "problems") {
   113  		err := app.MutagenSyncFlush()
   114  		if err != nil {
   115  			util.Error("Error on 'mutagen sync flush %s': %v", syncName, err)
   116  		}
   117  	}
   118  	err = PauseMutagenSync(app)
   119  	return err
   120  }
   121  
   122  // GetMutagenConfigFilePath returns the canonical location where the mutagen.yml lives
   123  func GetMutagenConfigFilePath(app *DdevApp) string {
   124  	return filepath.Join(app.GetConfigPath("mutagen"), "mutagen.yml")
   125  }
   126  
   127  // GetMutagenConfigFile looks to see if there's a project .mutagen.yml
   128  // If nothing is found, returns empty
   129  func GetMutagenConfigFile(app *DdevApp) string {
   130  	projectConfig := GetMutagenConfigFilePath(app)
   131  	if fileutil.FileExists(projectConfig) {
   132  		return projectConfig
   133  	}
   134  	return ""
   135  }
   136  
   137  // CreateOrResumeMutagenSync creates or resumes a sync session
   138  // It detects problems with the sync and errors if there are problems
   139  func CreateOrResumeMutagenSync(app *DdevApp) error {
   140  	syncName := MutagenSyncName(app.Name)
   141  	configFile := GetMutagenConfigFile(app)
   142  	if configFile != "" {
   143  		util.Debug("Using mutagen config file %s", configFile)
   144  	}
   145  
   146  	container, err := GetContainer(app, "web")
   147  	if err != nil {
   148  		return err
   149  	}
   150  	if container == nil {
   151  		return fmt.Errorf("web container for %s not found", app.Name)
   152  	}
   153  	if container.State != "running" {
   154  		// TODO: Improve or debug this temporary debug usage
   155  		util.Warning("web container is not running, logs follow")
   156  		logsErr := app.Logs("web", false, false, "100")
   157  		if logsErr != nil {
   158  			util.Warning("error from getting logs: %v", logsErr)
   159  		}
   160  		return fmt.Errorf("Cannot start mutagen sync because web container is not running: %v", container)
   161  	}
   162  
   163  	sessionExists, err := mutagenSyncSessionExists(app)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	if sessionExists {
   168  		util.Debug("Resume mutagen sync if session already exists")
   169  		err := ResumeMutagenSync(app)
   170  		if err != nil {
   171  			return err
   172  		}
   173  	} else {
   174  		vLabel, err := GetMutagenVolumeLabel(app)
   175  		if err != nil {
   176  			return err
   177  		}
   178  		// TODO: Consider using a function to specify the docker beta
   179  		args := []string{"sync", "create", app.AppRoot, fmt.Sprintf("docker:/%s/var/www/html", container.Names[0]), "--no-global-configuration", "--name", syncName, "--label", mutagenSignatureLabelName + "=" + vLabel}
   180  		if configFile != "" {
   181  			args = append(args, fmt.Sprintf(`--configuration-file=%s`, configFile))
   182  		}
   183  		// On Windows, permissions can't be inferred from what is on the host side, so just force 777 for
   184  		// most things
   185  		if runtime.GOOS == "windows" {
   186  			args = append(args, []string{"--permissions-mode=manual", "--default-file-mode-beta=0777", "--default-directory-mode-beta=0777"}...)
   187  		}
   188  		util.Debug("Creating mutagen sync: mutagen %v", args)
   189  		out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), args...)
   190  		if err != nil {
   191  			return fmt.Errorf("Failed to mutagen %v (%v), output=%s", args, err, out)
   192  		}
   193  	}
   194  
   195  	util.Debug("Flushing mutagen sync session '%s'", syncName)
   196  	flushErr := make(chan error, 1)
   197  	stopGoroutine := make(chan bool, 1)
   198  	firstOutputReceived := make(chan bool, 1)
   199  	defer close(flushErr)
   200  	defer close(stopGoroutine)
   201  	defer close(firstOutputReceived)
   202  
   203  	go func() {
   204  		err = app.MutagenSyncFlush()
   205  		flushErr <- err
   206  		return
   207  	}()
   208  
   209  	// In tests or other non-interactive environments we don't need to show the
   210  	// mutagen sync monitor output (and it fills up the test logs)
   211  
   212  	if os.Getenv("DDEV_NONINTERACTIVE") != "true" {
   213  		go func() {
   214  			previousStatus := ""
   215  			curStatus := ""
   216  			sigSent := false
   217  			cmd := osexec.Command(globalconfig.GetMutagenPath(), "sync", "monitor", syncName)
   218  			stdout, _ := cmd.StdoutPipe()
   219  			err = cmd.Start()
   220  			buf := bufio.NewReader(stdout)
   221  			for {
   222  				select {
   223  				case <-stopGoroutine:
   224  					_ = cmd.Process.Kill()
   225  					_, _ = cmd.Process.Wait()
   226  					return
   227  				default:
   228  					line, err := buf.ReadBytes('\r')
   229  					if err != nil {
   230  						return
   231  					}
   232  					l := string(line)
   233  					if strings.HasPrefix(l, "Status:") {
   234  						// If we haven't already notified that output is coming in,
   235  						// then notify.
   236  						if !sigSent {
   237  							firstOutputReceived <- true
   238  							sigSent = true
   239  							_, _ = fmt.Fprintf(os.Stderr, "\n")
   240  						}
   241  
   242  						_, _ = fmt.Fprintf(os.Stderr, "%s", l)
   243  						t := strings.Replace(l, " ", "", 2)
   244  						c := strings.Split(t, " ")
   245  						curStatus = c[0]
   246  						if previousStatus != curStatus {
   247  							_, _ = fmt.Fprintf(os.Stderr, "\n")
   248  						}
   249  						previousStatus = curStatus
   250  					}
   251  				}
   252  			}
   253  		}()
   254  	}
   255  
   256  	outputComing := false
   257  	for {
   258  		select {
   259  		// Complete when the MutagenSyncFlush() completes
   260  		case err = <-flushErr:
   261  			return err
   262  		case outputComing = <-firstOutputReceived:
   263  
   264  		// If we haven't yet received any "Status:" output, do a dot every second
   265  		case <-time.After(1 * time.Second):
   266  			if !outputComing {
   267  				_, _ = fmt.Fprintf(os.Stderr, ".")
   268  			}
   269  		}
   270  	}
   271  }
   272  
   273  func ResumeMutagenSync(app *DdevApp) error {
   274  	args := []string{"sync", "resume", MutagenSyncName(app.Name)}
   275  	util.Debug("Resuming mutagen sync: mutagen %v", args)
   276  	out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), args...)
   277  	if err != nil {
   278  		return fmt.Errorf("Failed to mutagen %v (%v), output=%s", args, err, out)
   279  	}
   280  	return nil
   281  }
   282  
   283  // mutagenSyncSessionExists determines whether an appropriate mutagen sync session already exists
   284  // if it finds one with invalid label, it destroys the existing session.
   285  func mutagenSyncSessionExists(app *DdevApp) (bool, error) {
   286  	syncName := MutagenSyncName(app.Name)
   287  	res, err := exec.RunHostCommandSeparateStreams(globalconfig.GetMutagenPath(), "sync", "list", "--template", "{{ json (index . 0) }}", syncName)
   288  	if err != nil {
   289  		if exitError, ok := err.(*osexec.ExitError); ok {
   290  			// If we got an error, but it's that there were no sessions, return false, no err
   291  			if strings.Contains(string(exitError.Stderr), "did not match any sessions") {
   292  				return false, nil
   293  			}
   294  		}
   295  		return false, err
   296  	}
   297  	session := make(map[string]interface{})
   298  	err = json.Unmarshal([]byte(res), &session)
   299  	if err != nil {
   300  		return false, fmt.Errorf("failed to unmarshall mutagen sync list results '%v': %v", res, err)
   301  	}
   302  
   303  	// Find out if mutagen session labels has label we found in docker volume
   304  	if l, ok := session["labels"].(map[string]interface{}); ok {
   305  		vLabel, vLabelErr := GetMutagenVolumeLabel(app)
   306  		if s, ok := l[mutagenSignatureLabelName]; ok && vLabelErr == nil && vLabel != "" && vLabel == s {
   307  			return true, nil
   308  		}
   309  		// If we happen to find a mutagen session without matching signature, terminate it.
   310  		_ = TerminateMutagenSync(app)
   311  	}
   312  	return false, nil
   313  }
   314  
   315  // MutagenStatus checks to see if there is an error case in mutagen
   316  // We don't want to do a flush yet in that case.
   317  // Note that the available statuses are at https://github.com/mutagen-io/mutagen/blob/master/pkg/synchronization/state.go#L9
   318  // in func (s Status) Description()
   319  // Can return any of those or "nosession" (with more info) if we didn't find a session at all
   320  func (app *DdevApp) MutagenStatus() (status string, shortResult string, mapResult map[string]interface{}, err error) {
   321  	syncName := MutagenSyncName(app.Name)
   322  
   323  	mutagenDataDirectory := os.Getenv("MUTAGEN_DATA_DIRECTORY")
   324  	fullJSONResult, err := exec.RunHostCommandSeparateStreams(globalconfig.GetMutagenPath(), "sync", "list", "--template", `{{ json (index . 0) }}`, syncName)
   325  	if err != nil {
   326  		stderr := ""
   327  		if exitError, ok := err.(*osexec.ExitError); ok {
   328  			stderr = string(exitError.Stderr)
   329  		}
   330  		return fmt.Sprintf("nosession for MUTAGEN_DATA_DIRECTORY=%s", mutagenDataDirectory), fullJSONResult, nil, fmt.Errorf("failed to mutagen sync list %s: stderr='%s', err=%v", syncName, stderr, err)
   331  	}
   332  	session := make(map[string]interface{})
   333  	err = json.Unmarshal([]byte(fullJSONResult), &session)
   334  	if err != nil {
   335  		return fmt.Sprintf("nosession for MUTAGEN_DATA_DIRECTORY=%s; failed to unmarshall mutagen sync list results '%v'", mutagenDataDirectory, fullJSONResult), fullJSONResult, nil, err
   336  	}
   337  
   338  	if paused, ok := session["paused"].(bool); ok && paused == true {
   339  		return "paused", "paused", session, nil
   340  	}
   341  	var ok bool
   342  	if shortResult, ok = session["status"].(string); !ok {
   343  		return "failing", shortResult, session, fmt.Errorf("mutagen sessions may be in invalid state, please `ddev mutagen reset`")
   344  	}
   345  	shortResult = session["status"].(string)
   346  
   347  	// In the odd case where somebody enabled mutagen when it wasn't actually running
   348  	// show a simpler result
   349  	mounted, err := IsMutagenVolumeMounted(app)
   350  	if !mounted {
   351  		return "not enabled", "", session, nil
   352  	}
   353  	if err != nil {
   354  		return "", "", nil, err
   355  	}
   356  
   357  	problems := false
   358  	if alpha, ok := session["alpha"].(map[string]interface{}); ok {
   359  		if _, ok = alpha["scanProblems"]; ok {
   360  			problems = true
   361  		}
   362  	}
   363  	if beta, ok := session["beta"].(map[string]interface{}); ok {
   364  		if _, ok = beta["scanProblems"]; ok {
   365  			problems = true
   366  		}
   367  	}
   368  	if _, ok := session["conflicts"]; ok {
   369  		problems = true
   370  	}
   371  
   372  	// We're going to assume that if it's applying changes things are still OK,
   373  	// even though there may be a whole list of problems.
   374  	// States from json are in https://github.com/mutagen-io/mutagen/blob/bc07f2f0f3f0aba0aff0514bd4739d75444091fe/pkg/synchronization/state.go#L47-L79
   375  	switch shortResult {
   376  	case "paused":
   377  		return "paused", shortResult, session, nil
   378  	case "transitioning":
   379  		fallthrough
   380  	case "staging-alpha":
   381  		fallthrough
   382  	case "connecting-beta":
   383  		fallthrough
   384  	case "staging-beta":
   385  		fallthrough
   386  	case "reconciling":
   387  		fallthrough
   388  	case "scanning":
   389  		fallthrough
   390  	case "saving":
   391  		fallthrough
   392  	case "watching":
   393  		if !problems {
   394  			status = "ok"
   395  		} else {
   396  			status = "problems"
   397  		}
   398  		return status, shortResult, session, nil
   399  	}
   400  	return "failing", shortResult, session, nil
   401  }
   402  
   403  // MutagenSyncFlush performs a mutagen sync flush, waits for result, and checks for errors
   404  func (app *DdevApp) MutagenSyncFlush() error {
   405  	status, _ := app.SiteStatus()
   406  	if status == SiteRunning && app.IsMutagenEnabled() {
   407  		syncName := MutagenSyncName(app.Name)
   408  		if !MutagenSyncExists(app) {
   409  			return errors.Errorf("Mutagen sync session '%s' does not exist", syncName)
   410  		}
   411  		if status, shortResult, session, err := app.MutagenStatus(); err == nil {
   412  			switch status {
   413  			case "paused":
   414  				util.Debug("mutagen sync %s is paused, so not flushing", syncName)
   415  				return nil
   416  			case "failing":
   417  				util.Warning("mutagen sync session %s has status '%s': shortResult='%v', err=%v, session contents='%v'", syncName, status, shortResult, err, session)
   418  			default:
   419  				// This extra sync resume recommended by @xenoscopic to catch situation where
   420  				// not paused but also not connected, in which case the flush will fail.
   421  				out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "sync", "resume", syncName)
   422  				if err != nil {
   423  					return fmt.Errorf("mutagen resume flush %s failed, output=%s, err=%v", syncName, out, err)
   424  				}
   425  				out, err = exec.RunHostCommand(globalconfig.GetMutagenPath(), "sync", "flush", syncName)
   426  				if err != nil {
   427  					return fmt.Errorf("mutagen sync flush %s failed, output=%s, err=%v", syncName, out, err)
   428  				}
   429  			}
   430  		}
   431  
   432  		status, _, _, err := app.MutagenStatus()
   433  		if (status != "ok" && status != "problems" && status != "paused" && status != "failing") || err != nil {
   434  			return err
   435  		}
   436  		util.Debug("Flushed mutagen sync session '%s'", syncName)
   437  	}
   438  	return nil
   439  }
   440  
   441  // MutagenSyncExists detects whether the named sync exists
   442  func MutagenSyncExists(app *DdevApp) bool {
   443  	syncName := MutagenSyncName(app.Name)
   444  
   445  	if !fileutil.FileExists(globalconfig.GetMutagenPath()) {
   446  		return false
   447  	}
   448  	// List syncs with this name that also match appropriate labels
   449  	c := []string{globalconfig.GetMutagenPath(), "sync", "list", syncName}
   450  	out, err := exec.RunHostCommand(c[0], c[1:]...)
   451  	if err != nil && !strings.Contains(out, "Error: unable to locate requested sessions") {
   452  		util.Warning("%v failed: %v output=%v", c, err, out)
   453  	}
   454  	return err == nil
   455  }
   456  
   457  // DownloadMutagen gets the mutagen binary and related and puts it into
   458  // ~/.ddev/.bin
   459  func DownloadMutagen() error {
   460  	StopMutagenDaemon()
   461  	flavor := runtime.GOOS + "_" + runtime.GOARCH
   462  	globalMutagenDir := filepath.Dir(globalconfig.GetMutagenPath())
   463  	destFile := filepath.Join(globalMutagenDir, "mutagen.tgz")
   464  	mutagenURL := fmt.Sprintf("https://github.com/mutagen-io/mutagen/releases/download/v%s/mutagen_%s_v%s.tar.gz", versionconstants.RequiredMutagenVersion, flavor, versionconstants.RequiredMutagenVersion)
   465  	output.UserOut.Printf("Downloading %s ...", mutagenURL)
   466  
   467  	// Remove the existing file. This may help on macOS to prevent the Gatekeeper's
   468  	// caching bug from confusing with a previously downloaded file?
   469  	// Discussion in https://github.com/mutagen-io/mutagen/issues/290#issuecomment-906612749
   470  	_ = os.Remove(globalconfig.GetMutagenPath())
   471  
   472  	_ = os.MkdirAll(globalMutagenDir, 0777)
   473  	err := util.DownloadFile(destFile, mutagenURL, "true" != os.Getenv("DDEV_NONINTERACTIVE"))
   474  	if err != nil {
   475  		return err
   476  	}
   477  	output.UserOut.Printf("Download complete.")
   478  
   479  	err = archive.Untar(destFile, globalMutagenDir, "")
   480  	_ = os.Remove(destFile)
   481  	if err != nil {
   482  		return err
   483  	}
   484  	err = os.Chmod(globalconfig.GetMutagenPath(), 0755)
   485  	if err != nil {
   486  		return err
   487  	}
   488  
   489  	// Stop daemon in case it was already running somewhere else
   490  	StopMutagenDaemon()
   491  	return nil
   492  }
   493  
   494  // StopMutagenDaemon will try to stop a running mutagen daemon
   495  // But no problem if there wasn't one
   496  func StopMutagenDaemon() {
   497  	if fileutil.FileExists(globalconfig.GetMutagenPath()) {
   498  		mutagenDataDirectory := os.Getenv("MUTAGEN_DATA_DIRECTORY")
   499  		out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "daemon", "stop")
   500  		if err != nil && !strings.Contains(out, "unable to connect to daemon") {
   501  			util.Warning("Unable to stop mutagen daemon: %v; MUTAGEN_DATA_DIRECTORY=%s", err, mutagenDataDirectory)
   502  		}
   503  		util.Success("Stopped mutagen daemon")
   504  	}
   505  }
   506  
   507  // StartMutagenDaemon will make sure the daemon is running
   508  func StartMutagenDaemon() {
   509  	if fileutil.FileExists(globalconfig.GetMutagenPath()) {
   510  		out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "daemon", "start")
   511  		if err != nil {
   512  			util.Warning("Failed to run mutagen daemon start: %v, out=%s", err, out)
   513  		}
   514  	}
   515  }
   516  
   517  // DownloadMutagenIfNeeded downloads the proper version of mutagen
   518  // if it's either not yet installed or has the wrong version.
   519  func DownloadMutagenIfNeeded(app *DdevApp) error {
   520  	if !app.IsMutagenEnabled() {
   521  		return nil
   522  	}
   523  	err := os.MkdirAll(globalconfig.GetMutagenDataDirectory(), 0755)
   524  	if err != nil {
   525  		return err
   526  	}
   527  	curVersion, err := version.GetLiveMutagenVersion()
   528  	if err != nil || curVersion != versionconstants.RequiredMutagenVersion {
   529  		err = DownloadMutagen()
   530  		if err != nil {
   531  			return err
   532  		}
   533  	}
   534  	return nil
   535  }
   536  
   537  // MutagenReset stops (with flush), removes the docker volume, starts again (with flush)
   538  func MutagenReset(app *DdevApp) error {
   539  	if app.IsMutagenEnabled() {
   540  		err := app.Stop(false, false)
   541  		if err != nil {
   542  			return errors.Errorf("Failed to stop project %s: %v", app.Name, err)
   543  		}
   544  		err = dockerutil.RemoveVolume(GetMutagenVolumeName(app))
   545  		if err != nil {
   546  			return err
   547  		}
   548  		util.Debug("Removed docker volume %s", GetMutagenVolumeName(app))
   549  	}
   550  	err := TerminateMutagenSync(app)
   551  	if err != nil {
   552  		return err
   553  	}
   554  	util.Debug("Terminated mutagen sync session %s", MutagenSyncName(app.Name))
   555  	return nil
   556  }
   557  
   558  // GetMutagenVolumeName returns the name for the mutagen docker volume
   559  func GetMutagenVolumeName(app *DdevApp) string {
   560  	return app.Name + "_" + "project_mutagen"
   561  }
   562  
   563  // MutagenMonitor shows the output of `mutagen sync monitor <syncName>`
   564  func MutagenMonitor(app *DdevApp) {
   565  	syncName := MutagenSyncName(app.Name)
   566  
   567  	// This doesn't actually return; you have to <ctrl-c> to end it
   568  	c := osexec.Command(globalconfig.GetMutagenPath(), "sync", "monitor", syncName)
   569  	// We only need all three of these because of Windows behavior on git-bash with no pty
   570  	c.Stdout = os.Stdout
   571  	c.Stderr = os.Stderr
   572  	c.Stdin = os.Stdin
   573  	_ = c.Run()
   574  }
   575  
   576  //go:embed mutagen_config_assets
   577  var mutagenConfigAssets embed.FS
   578  
   579  // GenerateMutagenYml generates the .ddev/mutagen.yml
   580  func (app *DdevApp) GenerateMutagenYml() error {
   581  	// Prevent running as root for most cases
   582  	// We really don't want ~/.ddev to have root ownership, breaks things.
   583  	if os.Geteuid() == 0 {
   584  		util.Warning("not generating mutagen config file because running with root privileges")
   585  		return nil
   586  	}
   587  	mutagenYmlPath := GetMutagenConfigFilePath(app)
   588  	if sigExists, err := fileutil.FgrepStringInFile(mutagenYmlPath, nodeps.DdevFileSignature); err == nil && !sigExists {
   589  		// If the signature doesn't exist, they have taken over the file, so return
   590  		return nil
   591  	}
   592  
   593  	c, err := mutagenConfigAssets.ReadFile(path.Join("mutagen_config_assets", "mutagen.yml"))
   594  	if err != nil {
   595  		return err
   596  	}
   597  	content := string(c)
   598  
   599  	// It's impossible to use posix-raw on traditional windows.
   600  	// But this means that there will be errors with rooted symlinks in the container on windows
   601  	symlinkMode := "posix-raw"
   602  	if runtime.GOOS == "windows" {
   603  		symlinkMode = "portable"
   604  	}
   605  	err = os.MkdirAll(filepath.Dir(mutagenYmlPath), 0755)
   606  	if err != nil {
   607  		return err
   608  	}
   609  
   610  	uploadDir := ""
   611  	if app.GetUploadDir() != "" {
   612  		uploadDir = path.Join(app.Docroot, app.GetUploadDir())
   613  	}
   614  
   615  	templateMap := map[string]interface{}{
   616  		"SymlinkMode": symlinkMode,
   617  		"UploadDir":   uploadDir,
   618  	}
   619  	// If no bind mounts, then we can't ignore UploadDir, must sync it
   620  	if globalconfig.DdevGlobalConfig.NoBindMounts {
   621  		templateMap["UploadDir"] = ""
   622  	}
   623  
   624  	err = fileutil.TemplateStringToFile(content, templateMap, mutagenYmlPath)
   625  	return err
   626  }
   627  
   628  // IsMutagenVolumeMounted checks to see if the mutagen volume is mounted
   629  func IsMutagenVolumeMounted(app *DdevApp) (bool, error) {
   630  	client := dockerutil.GetDockerClient()
   631  	container, err := dockerutil.FindContainerByName("ddev-" + app.Name + "-web")
   632  	// If there is no web container found, the volume is not mounted
   633  	if err != nil || container == nil {
   634  		return false, nil
   635  	}
   636  	inspect, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{
   637  		ID: container.ID,
   638  	})
   639  	if err != nil {
   640  		return false, err
   641  	}
   642  	for _, m := range inspect.Mounts {
   643  		if m.Name == app.Name+"_project_mutagen" {
   644  			return true, nil
   645  		}
   646  	}
   647  	return false, nil
   648  }
   649  
   650  // IsMutagenEnabled returns true if mutagen is enabled locally or globally
   651  // It's also required and set if NoBindMounts is set, since we have to have a way
   652  // to get code on there.
   653  func (app *DdevApp) IsMutagenEnabled() bool {
   654  	return app.MutagenEnabled || app.MutagenEnabledGlobal || globalconfig.DdevGlobalConfig.NoBindMounts
   655  }
   656  
   657  // GetMutagenVolumeLabel returns the com.ddev.volume-signature on the project_mutagen docker volume
   658  func GetMutagenVolumeLabel(app *DdevApp) (string, error) {
   659  	labels, err := dockerutil.VolumeLabels(GetMutagenVolumeName(app))
   660  	if err != nil {
   661  		return "", err
   662  	}
   663  	if labels != nil {
   664  		if l, ok := labels[mutagenSignatureLabelName]; ok {
   665  			return l, nil
   666  		}
   667  	}
   668  	return "", nil
   669  }
   670  
   671  // CheckMutagenVolumeSyncCompatibility checks to see if the mutagen label and volume label
   672  // are the same.
   673  // Compatible if:
   674  //   - No volume (or no volume and no mutagen sync session)
   675  //   - Volume and mutagen sync exist and Volume label matches mutagen label
   676  //
   677  // Not compatible if
   678  //   - Volume and mutagen sync exist and have different labels
   679  //   - Volume exists (with label) but there's no mutagen sync session matching it. In this case we'd want
   680  //     to start from scratch with a new volume and sync, so we get authoritative files from alpha (host)
   681  //   - Volume has a label that is not based on this docker context.
   682  //
   683  // Return ok, info, where ok true if compatible, info gives reasoning
   684  func CheckMutagenVolumeSyncCompatibility(app *DdevApp) (ok bool, volumeExists bool, info string) {
   685  	mutagenSyncExists := MutagenSyncExists(app)
   686  	volumeLabel, volumeLabelErr := GetMutagenVolumeLabel(app)
   687  	dockerHostID := dockerutil.GetDockerHostID()
   688  	mutagenLabel := ""
   689  	var mutagenSyncLabelErr error
   690  
   691  	volumeExists = !(volumeLabelErr != nil && errors.Is(docker.ErrNoSuchVolume, volumeLabelErr))
   692  
   693  	if mutagenSyncExists {
   694  		mutagenLabel, mutagenSyncLabelErr = GetMutagenSyncLabel(app)
   695  		if mutagenSyncLabelErr != nil {
   696  			util.Warning("mutagen sync %s exists but unable to get label: %v", app.Name, mutagenSyncLabelErr)
   697  		}
   698  	}
   699  	switch {
   700  	// If there is no volume, everything is fine, proceed.
   701  	case !volumeExists:
   702  		return true, volumeExists, "no docker volume exists, so compatible"
   703  	case mutagenSyncLabelErr != nil:
   704  		return false, volumeExists, "mutagen sync session exists but does not have label"
   705  	// If the labels do not have the current context as first part of label, we have trouble.
   706  	case !strings.HasPrefix(volumeLabel, dockerHostID) || !strings.HasPrefix(mutagenLabel, dockerHostID):
   707  		return false, volumeExists, fmt.Sprintf("volume label '%s' or sync label '%s' does not start with current dockerHostID (%s)", volumeLabel, mutagenLabel, dockerHostID)
   708  	// if we have labels for both and they match, it's all fine.
   709  	case mutagenLabel == volumeLabel:
   710  		return true, volumeExists, fmt.Sprintf("volume and mutagen sync session have the same label: %s", volumeLabel)
   711  	}
   712  
   713  	return false, volumeExists, fmt.Sprintf("CheckMutagenVolumeSyncCompatibility: currentDockerContext=%s mutagenLabel='%s', volumeLabel='%s', mutagenSyncLabelErr='%v', volumeLabelErr='%v'", dockerutil.DockerContext, mutagenLabel, volumeLabel, mutagenSyncLabelErr, volumeLabelErr)
   714  }
   715  
   716  // GetMutagenSyncLabel gets the com.ddev.volume-signature label from an existing sync session
   717  func GetMutagenSyncLabel(app *DdevApp) (string, error) {
   718  	status, _, mapResult, err := app.MutagenStatus()
   719  
   720  	if strings.HasPrefix(status, "nosession") || err != nil {
   721  		return "", fmt.Errorf("no session %s found: %v", MutagenSyncName(app.Name), status)
   722  	}
   723  	if labels, ok := mapResult["labels"].(map[string]interface{}); ok {
   724  		if label, ok := labels[mutagenSignatureLabelName].(string); ok {
   725  			return label, nil
   726  		}
   727  	}
   728  	return "", fmt.Errorf("sync session label not found for sync session %s", MutagenSyncName(app.Name))
   729  }
   730  
   731  // TerminateAllMutagenSync terminates all sync sessions
   732  func TerminateAllMutagenSync() {
   733  	out, err := exec.RunHostCommand(globalconfig.GetMutagenPath(), "sync", "terminate", "-a")
   734  	if err != nil {
   735  		util.Warning("could not terminate all mutagen sessions (mutagen sync terminate -a), output=%s, err=%v", out, err)
   736  	}
   737  }
   738  
   739  // GetDefaultMutagenVolumeSignature gets a new volume signature to be applied to mutagen volume
   740  func GetDefaultMutagenVolumeSignature(app *DdevApp) string {
   741  	return fmt.Sprintf("%s-%v", dockerutil.GetDockerHostID(), time.Now().Unix())
   742  }
   743  
   744  // CheckMutagenUploadDir just tells people if they are using mutagen without upload_dir
   745  func CheckMutagenUploadDir(app *DdevApp) {
   746  	if app.IsMutagenEnabled() && app.GetUploadDir() == "" {
   747  		util.Warning("You have mutagen enabled and your '%s' project type doesn't have an upload_dir set.", app.Type)
   748  		util.Warning("For faster startup and less disk usage,\nset upload_dir to where your user-generated files are stored.")
   749  	}
   750  }