Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: clean up missing project if it's gone, fixes #5322 #6029

Merged
merged 3 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 0 additions & 50 deletions cmd/ddev/cmd/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import (
"fmt"
"github.com/ddev/ddev/pkg/globalconfig"
"github.com/stretchr/testify/require"
"runtime"
"strings"
"testing"

"encoding/json"

"os"

"path/filepath"

"github.com/ddev/ddev/pkg/ddevapp"
"github.com/ddev/ddev/pkg/exec"
"github.com/ddev/ddev/pkg/testcommon"
Expand Down Expand Up @@ -227,50 +224,3 @@ func unmarshalJSONLogs(in string) ([]log.Fields, error) {
}
return logData, nil
}

// TestCmdDescribeMissingProjectDirectory ensures the `ddev describe` command returns the expected help text when
// a project's directory no longer exists.
func TestCmdDescribeMissingProjectDirectory(t *testing.T) {
var err error
var out string

if runtime.GOOS == "windows" {
t.Skip("Skipping because unreliable on Windows")
}

assert := asrt.New(t)

projDir, _ := os.Getwd()

projectName := util.RandString(6)

tmpDir := testcommon.CreateTmpDir(t.Name())
defer testcommon.CleanupDir(tmpDir)
defer testcommon.Chdir(tmpDir)()

_, err = exec.RunCommand(DdevBin, []string{"config", "--project-type", "php", "--project-name", projectName})
assert.NoError(err)

_, err = exec.RunCommand(DdevBin, []string{"start", "-y"})
assert.NoError(err)

t.Cleanup(func() {
_, err = exec.RunCommand(DdevBin, []string{"delete", "-Oy", projectName})
assert.NoError(err)
})

_, err = exec.RunCommand(DdevBin, []string{"stop"})
assert.NoError(err)

err = os.Chdir(projDir)
assert.NoError(err)
copyDir := filepath.Join(testcommon.CreateTmpDir(t.Name()), util.RandString(4))
err = os.Rename(tmpDir, copyDir)
assert.NoError(err)

out, err = exec.RunCommand(DdevBin, []string{"describe", projectName})
assert.Error(err, "Expected an error when describing project with no project directory")
assert.Contains(out, "ddev can no longer find your project files")
err = os.Rename(copyDir, tmpDir)
assert.NoError(err)
}
48 changes: 0 additions & 48 deletions cmd/ddev/cmd/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package cmd

import (
"github.com/stretchr/testify/require"
"path/filepath"
"runtime"
"testing"

"os"

"github.com/ddev/ddev/pkg/ddevapp"
"github.com/ddev/ddev/pkg/exec"
"github.com/ddev/ddev/pkg/testcommon"
"github.com/ddev/ddev/pkg/util"
asrt "github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -65,48 +62,3 @@ func TestCmdStart(t *testing.T) {
assert.Equal(ddevapp.SiteRunning, statusDesc, "The status description should be \"running\", but %s status description is: %s", app.GetName(), statusDesc)
}
}

// TestCmdStartMissingProjectDirectory ensures the `ddev start` command returns the expected help text when
// a project's directory no longer exists.
func TestCmdStartMissingProjectDirectory(t *testing.T) {
var err error
var out string

if runtime.GOOS == "windows" {
t.Skip("Skipping because unreliable on Windows")
}

assert := asrt.New(t)

projDir, _ := os.Getwd()

projectName := util.RandString(6)

tmpDir := testcommon.CreateTmpDir(t.Name())
defer testcommon.CleanupDir(tmpDir)
defer testcommon.Chdir(tmpDir)()

_, err = exec.RunCommand(DdevBin, []string{"config", "--project-type", "php", "--project-name", projectName})
assert.NoError(err)

_, err = exec.RunCommand(DdevBin, []string{"start", "-y"})

//nolint: errcheck
defer exec.RunCommand(DdevBin, []string{"stop", "-RO", projectName})
assert.NoError(err)

_, err = exec.RunCommand(DdevBin, []string{"stop"})
assert.NoError(err)

err = os.Chdir(projDir)
assert.NoError(err)

copyDir := filepath.Join(testcommon.CreateTmpDir(t.Name()), util.RandString(4))
err = os.Rename(tmpDir, copyDir)
assert.NoError(err)

out, err = exec.RunCommand(DdevBin, []string{"start", "-y", projectName})

assert.Error(err, "Expected an error when starting project with no project directory")
assert.Contains(out, "ddev can no longer find your project files")
}
58 changes: 0 additions & 58 deletions cmd/ddev/cmd/stop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@ package cmd
import (
"github.com/ddev/ddev/pkg/dockerutil"
"github.com/stretchr/testify/require"
"runtime"
"testing"

"os"

"path/filepath"

"github.com/ddev/ddev/pkg/ddevapp"
"github.com/ddev/ddev/pkg/exec"
"github.com/ddev/ddev/pkg/testcommon"
"github.com/ddev/ddev/pkg/util"
asrt "github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -63,54 +56,3 @@ func TestCmdStop(t *testing.T) {
// ssh-agent should be gone
assert.Nil(sshAgent)
}

// TestCmdStopMissingProjectDirectory ensures the `ddev stop` command can operate on a project when the
// project's directory has been removed.
func TestCmdStopMissingProjectDirectory(t *testing.T) {
var err error
var out string

if runtime.GOOS == "windows" {
t.Skip("Skipping because unreliable on Windows")
}

assert := asrt.New(t)
origDir, _ := os.Getwd()

projectName := util.RandString(6)
tmpDir := testcommon.CreateTmpDir(t.Name())
err = os.Chdir(tmpDir)
require.NoError(t, err)

t.Cleanup(func() {
_, err = exec.RunHostCommand(DdevBin, "stop", "-RO", projectName)
assert.NoError(err)
err = os.Chdir(origDir)
assert.NoError(err)

_ = os.RemoveAll(tmpDir)
})

_, err = exec.RunHostCommand(DdevBin, "config", "--project-type", "php", "--project-name", projectName)
assert.NoError(err)

_, err = exec.RunHostCommand(DdevBin, "start", "-y")
assert.NoError(err)

_, err = exec.RunHostCommand(DdevBin, "stop", projectName)
assert.NoError(err)

err = os.Chdir(origDir)
assert.NoError(err)

copyDir := filepath.Join(testcommon.CreateTmpDir(t.Name()), util.RandString(4))
err = os.Rename(tmpDir, copyDir)
assert.NoError(err)
//nolint: errcheck
defer os.Rename(copyDir, tmpDir)

out, err = exec.RunHostCommand(DdevBin, "stop", projectName)
assert.NoError(err)
assert.Contains(out, "has been stopped")

}
4 changes: 2 additions & 2 deletions pkg/ddevapp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ func NewApp(appRoot string, includeOverrides bool) (*DdevApp, error) {
return nil, fmt.Errorf("ddev config is not useful in your home directory (%s)", homeDir)
}

if !fileutil.FileExists(app.AppRoot) {
return app, fmt.Errorf("project root %s does not exist", app.AppRoot)
if _, err := os.Stat(app.AppRoot); err != nil {
return app, err
}

app.ConfigPath = app.GetConfigPath("config.yaml")
Expand Down
115 changes: 30 additions & 85 deletions pkg/ddevapp/ddevapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2869,51 +2869,6 @@ func TestDdevLogs(t *testing.T) {
switchDir()
}

// TestDdevStopMissingDirectory tests that the 'ddev stop' command works properly on sites with missing directories or DDEV configs.
func TestDdevStopMissingDirectory(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping because unreliable on Windows")
}

assert := asrt.New(t)

site := TestSites[0]
testcommon.ClearDockerEnv()
app := &ddevapp.DdevApp{}
err := app.Init(site.Dir)
assert.NoError(err)

startErr := app.StartAndWait(0)
//nolint: errcheck
defer app.Stop(true, false)
if startErr != nil {
logs, health, err := ddevapp.GetErrLogsFromApp(app, startErr)
assert.NoError(err)
t.Fatalf("app.StartAndWait failed err=%v health=\n%s\n\nlogs from broken container: \n=======\n%s\n========\n", startErr, health, logs)
}

tempPath := testcommon.CreateTmpDir("site-copy")
siteCopyDest := filepath.Join(tempPath, "site")
defer removeAllErrCheck(tempPath, assert)

_ = app.Stop(false, false)

// Move the site directory to a temp location to mimic a missing directory.
err = os.Rename(site.Dir, siteCopyDest)
assert.NoError(err)

//nolint: errcheck
defer os.Rename(siteCopyDest, site.Dir)

// ddev stop (in cmd) actually does the check for missing project files,
// so we imitate that here.
err = ddevapp.CheckForMissingProjectFiles(app)
assert.Error(err)
if err != nil {
assert.Contains(err.Error(), "If you would like to continue using DDEV to manage this project please restore your files to that directory.")
}
}

// TestDdevDescribe tests that the describe command works properly on a running
// and also a stopped project.
func TestDdevDescribe(t *testing.T) {
Expand Down Expand Up @@ -2971,44 +2926,6 @@ func TestDdevDescribe(t *testing.T) {
assert.NoError(err)
}

// TestDdevDescribeMissingDirectory tests that the describe command works properly on sites with missing directories or DDEV configs.
func TestDdevDescribeMissingDirectory(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping because unreliable on Windows")
}

assert := asrt.New(t)
site := TestSites[0]
tempPath := testcommon.CreateTmpDir("site-copy")
siteCopyDest := filepath.Join(tempPath, "site")
defer removeAllErrCheck(tempPath, assert)

app := &ddevapp.DdevApp{}
err := app.Init(site.Dir)
assert.NoError(err)
startErr := app.StartAndWait(0)
//nolint: errcheck
defer app.Stop(true, false)
if startErr != nil {
logs, health, err := ddevapp.GetErrLogsFromApp(app, startErr)
assert.NoError(err)
t.Fatalf("app.StartAndWait failed err=%v health:\n%s\nlogs from broken container: \n=======\n%s\n========\n", startErr, health, logs)
}
// Move the site directory to a temp location to mimic a missing directory.
err = app.Stop(false, false)
assert.NoError(err)
err = os.Rename(site.Dir, siteCopyDest)
assert.NoError(err)

desc, err := app.Describe(false)
assert.NoError(err)
assert.Equal(ddevapp.SiteDirMissing, desc["status"])
assert.Contains(desc["status_desc"], ddevapp.SiteDirMissing, "Status did not include the phrase '%s' when describing a site with missing directories.", ddevapp.SiteDirMissing)
// Move the site directory back to its original location.
err = os.Rename(siteCopyDest, site.Dir)
assert.NoError(err)
}

// TestRouterPortsCheck makes sure that we can detect if the ports are available before starting the router.
func TestRouterPortsCheck(t *testing.T) {
assert := asrt.New(t)
Expand Down Expand Up @@ -4033,16 +3950,44 @@ func TestPortSpecifications(t *testing.T) {
require.NotEmpty(t, globalconfig.DdevProjectList[conflictApp.Name].UsedHostPorts)
}

// TestDdevGetProjects exercises GetProjects()
// TestGetProjects exercises GetProjects()
// It's only here for profiling at this point
func TestDdevGetProjects(t *testing.T) {
func TestGetProjects(t *testing.T) {
assert := asrt.New(t)
defer util.TimeTrackC(t.Name())()

apps, err := ddevapp.GetProjects(false)
assert.NoError(err)
_ = apps
}

// TestGetProjectsMissingApp exercises GetProjects() with a missing project from filesystem
func TestGetProjectsMissingApp(t *testing.T) {
tmpDir := testcommon.CreateTmpDir(t.Name())
badApp, err := ddevapp.NewApp(tmpDir, false)
require.NoError(t, err)

t.Cleanup(func() {
_ = badApp.Stop(true, false)
_ = os.RemoveAll(tmpDir)
})

err = badApp.Start()
require.NoError(t, err)
// Make sure the new badApp is listed
l, err := ddevapp.GetProjects(false)
require.NoError(t, err)
m := ddevapp.AppSliceToMap(l)
require.NotEmptyf(t, m[badApp.Name], "app not found when it should be")
// Remove the badApp from the filesystem and verify
// that it is no longer listed
err = badApp.Stop(false, false)
require.NoError(t, err)
_ = os.RemoveAll(badApp.AppRoot)
l, err = ddevapp.GetProjects(false)
require.NoError(t, err)
m = ddevapp.AppSliceToMap(l)
require.Emptyf(t, m[badApp.Name], "map should no longer have badapp")
}

// TestCustomCerts makes sure that added custom certificates are respected and used
Expand Down
19 changes: 18 additions & 1 deletion pkg/ddevapp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,15 @@ func GetProjects(activeOnly bool) ([]*DdevApp, error) {

app, err := NewApp(info.AppRoot, true)
if err != nil {
util.Warning("Unable to create project at project root '%s': %v", info.AppRoot, err)
if os.IsNotExist(err) {
util.Warning("The project '%s' no longer exists in the filesystem, removing it from registry", info.AppRoot)
err = globalconfig.RemoveProjectInfo(name)
if err != nil {
util.Warning("unable to RemoveProjectInfo(%s): %v", name, err)
}
} else {
util.Warning("Something went wrong with %s: %v", info.AppRoot, err)
}
continue
}

Expand Down Expand Up @@ -513,3 +521,12 @@ func (app *DdevApp) CanUseHTTPOnly() bool {
// Default case is OK to use https
return false
}

// Turn a slice of *DdevApp into a map keyed by name
func AppSliceToMap(appList []*DdevApp) map[string]*DdevApp {
nameMap := make(map[string]*DdevApp)
for _, app := range appList {
nameMap[app.Name] = app
}
return nameMap
}