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 }