Skip to content

Commit

Permalink
Add --list and --cleanup options to snapshot command, for #1163 (#2701)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmuench committed Jan 1, 2021
1 parent 67f99f5 commit 85e656f
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 5 deletions.
74 changes: 70 additions & 4 deletions cmd/ddev/cmd/snapshot.go
@@ -1,20 +1,31 @@
package cmd

import (
"fmt"
"github.com/drud/ddev/pkg/ddevapp"
"github.com/drud/ddev/pkg/util"
"github.com/spf13/cobra"
"strings"
)

var snapshotAll bool
var snapshotCleanup bool
var snapshotList bool
var snapshotName string

// noConfirm: If true, --yes, we won't stop and prompt before each deletion
var snapshotCleanupNoConfirm bool

// DdevSnapshotCommand provides the snapshot command
var DdevSnapshotCommand = &cobra.Command{
Use: "snapshot [projectname projectname...]",
Short: "Create a database snapshot for one or more projects.",
Long: `Uses mariabackup or xtrabackup command to create a database snapshot in the .ddev/db_snapshots folder. These are compatible with server backups using the same tools and can be restored with "ddev restore-snapshot".`,
Example: `ddev snapshot
ddev snapshot --name some_descriptive_name
ddev snapshot --cleanup
ddev snapshot --cleanup -y
ddev snapshot --list
ddev snapshot --all`,
Run: func(cmd *cobra.Command, args []string) {
apps, err := getRequestedProjects(args, snapshotAll)
Expand All @@ -26,17 +37,72 @@ ddev snapshot --all`,
}

for _, app := range apps {
if snapshotNameOutput, err := app.Snapshot(snapshotName); err != nil {
util.Failed("Failed to snapshot %s: %v", app.GetName(), err)
} else {
util.Success("Created snapshot %s", snapshotNameOutput)

switch {
case snapshotList:
listAppSnapshots(app)
case snapshotCleanup:
deleteAppSnapshot(app)
default:
createAppSnapshot(app)
}
}
},
}

func listAppSnapshots(app *ddevapp.DdevApp) {
if snapshotNames, err := app.ListSnapshots(); err != nil {
util.Failed("Failed to list snapshots %s: %v", app.GetName(), err)
} else {
util.Success("Snapshots of project %s: %s", app.GetName(), strings.Join(snapshotNames, ", "))
}
}

func createAppSnapshot(app *ddevapp.DdevApp) {
if snapshotNameOutput, err := app.Snapshot(snapshotName); err != nil {
util.Failed("Failed to snapshot %s: %v", app.GetName(), err)
} else {
util.Success("Created snapshot %s", snapshotNameOutput)
}
}

func deleteAppSnapshot(app *ddevapp.DdevApp) {
var snapshotsToDelete []string
var prompt string
var err error

if !snapshotCleanupNoConfirm {
if snapshotName == "" {
prompt = fmt.Sprintf("OK to delete all snapshots of %s.", app.GetName())
} else {
prompt = fmt.Sprintf("OK to delete the snapshot %s of %s.", snapshotName, app.GetName())
}
if !util.Confirm(prompt) {
return
}
}

if snapshotName == "" {
snapshotsToDelete, err = app.ListSnapshots()
if err != nil {
util.Failed("Failed to detect snapshots %s: %v", app.GetName(), err)
}
} else {
snapshotsToDelete = append(snapshotsToDelete, snapshotName)
}

for _, snapshotToDelete := range snapshotsToDelete {
if err := app.DeleteSnapshot(snapshotToDelete); err != nil {
util.Failed("Failed to delete snapshot %s: %v", app.GetName(), err)
}
}
}

func init() {
DdevSnapshotCommand.Flags().BoolVarP(&snapshotAll, "all", "a", false, "Snapshot all running projects")
DdevSnapshotCommand.Flags().BoolVarP(&snapshotList, "list", "l", false, "List snapshots")
DdevSnapshotCommand.Flags().BoolVarP(&snapshotCleanup, "cleanup", "C", false, "Cleanup snapshots")
DdevSnapshotCommand.Flags().BoolVarP(&snapshotCleanupNoConfirm, "yes", "y", false, "Yes - skip confirmation prompt")
DdevSnapshotCommand.Flags().StringVarP(&snapshotName, "name", "n", "", "provide a name for the snapshot")
RootCmd.AddCommand(DdevSnapshotCommand)
}
46 changes: 46 additions & 0 deletions cmd/ddev/cmd/snapshot_test.go
@@ -0,0 +1,46 @@
package cmd

import (
"fmt"
"github.com/drud/ddev/pkg/ddevapp"
"github.com/drud/ddev/pkg/exec"
asrt "github.com/stretchr/testify/assert"
"os"
"testing"
)

// TestCmdSnapshot runs `ddev snapshot` on the test apps
func TestCmdSnapshot(t *testing.T) {
assert := asrt.New(t)

testDir, _ := os.Getwd()
fmt.Println(testDir)
site := TestSites[0]
cleanup := site.Chdir()
app, err := ddevapp.NewApp(site.Dir, false, "")
assert.NoError(err)
defer func() {
// Make sure all databases are back to default empty
_ = app.Stop(true, false)
_ = app.Start()
cleanup()
}()

// Ensure that a snapshot can be created
args := []string{"snapshot", "--name", "test-snapshot"}
out, err := exec.RunCommand(DdevBin, args)
assert.NoError(err)
assert.Contains(string(out), "Created snapshot test-snapshot")

// Try to delete a not existing snapshot
args = []string{"snapshot", "--name", "not-existing-snapshot", "--cleanup", "--yes"}
out, err = exec.RunCommand(DdevBin, args)
assert.Error(err)
assert.Contains(string(out), "Failed to delete snapshot")

// Ensure that an existing snapshot can be deleted
args = []string{"snapshot", "--name", "test-snapshot", "--cleanup"}
out, err = exec.RunCommand(DdevBin, args)
assert.NoError(err)
assert.Contains(string(out), "Deleted database snapshot test-snapshot")
}
7 changes: 6 additions & 1 deletion docs/users/cli-usage.md
Expand Up @@ -670,7 +670,12 @@ Restored database snapshot: /Users/rfay/workspace/d8git/.ddev/db_snapshots/d8git
Snapshots are stored in the project's .ddev/db_snapshots directory, and the directory can be renamed as necessary. For example, if you rename the above d8git_20180801132403 directory to "working_before_migration", then you can use `ddev restore-snapshot working_before_migration`.
To delete a snapshot, delete its folder from the .ddev/db_snapshots directory. Snapshots are not removed from the filesystem by `ddev delete`. It is safe to remove all the snapshots with `rm -r .ddev/db_snapshots` if you no longer need the snapshots.
To delete a snapshot, delete its folder from the .ddev/db_snapshots directory. Snapshots are not removed from the filesystem by `ddev delete`. It is safe to remove all the snapshots with `rm -r .ddev/db_snapshots` if you no longer need the snapshots.
All snapshots of a project can be removed with `ddev snapshot --cleanup`. A single snapshot can be removed by `ddev snapshot --cleanup --name <snapshot-name>`.
To see all existing snapshots of a project use `ddev snapshot --list`.
All existing snapshots of your system can be listed by adding the `--all` option to the command (`ddev snapshot --list --all`).
There are some interesting consequences of restoring huge multi-gigabyte databases. Ddev may show the project as ready and started when in reality tables are still being loaded. You can see this behavior with `ddev logs -s db -f`.
Expand Down
54 changes: 54 additions & 0 deletions pkg/ddevapp/ddevapp.go
Expand Up @@ -1528,6 +1528,60 @@ func (app *DdevApp) Snapshot(snapshotName string) (string, error) {
return snapshotName, nil
}

// DeleteSnapshot removes the snapshot directory inside a project
func (app *DdevApp) DeleteSnapshot(snapshotName string) error {
var err error
err = app.ProcessHooks("pre-delete-snapshot")
if err != nil {
return fmt.Errorf("Failed to process pre-delete-snapshot hooks: %v", err)
}

snapshotDir := path.Join("db_snapshots", snapshotName)
hostSnapshotDir := filepath.Join(filepath.Dir(app.ConfigPath), snapshotDir)

if err = fileutil.PurgeDirectory(hostSnapshotDir); err != nil {
return fmt.Errorf("Failed to purge contents of snapshot directory: %v", err)
}

if err = os.Remove(hostSnapshotDir); err != nil {
return fmt.Errorf("Failed to delete snapshot directory: %v", err)
}

util.Success("Deleted database snapshot %s in %s", snapshotName, hostSnapshotDir)
err = app.ProcessHooks("post-delete-snapshot")
if err != nil {
return fmt.Errorf("Failed to process post-delete-snapshot hooks: %v", err)
}

return nil

}

// ListSnapshots returns a list of the names of all project snapshots
func (app *DdevApp) ListSnapshots() ([]string, error) {
var err error
var snapshots []string

snapshotDir := filepath.Join(filepath.Dir(app.ConfigPath), "db_snapshots")

if !fileutil.FileExists(snapshotDir) {
return snapshots, nil
}

files, err := ioutil.ReadDir(snapshotDir)
if err != nil {
return snapshots, err
}

for _, f := range files {
if f.IsDir() {
snapshots = append(snapshots, f.Name())
}
}

return snapshots, nil
}

// RestoreSnapshot restores a mariadb snapshot of the db to be loaded
// The project must be stopped and docker volume removed and recreated for this to work.
func (app *DdevApp) RestoreSnapshot(snapshotName string) error {
Expand Down
41 changes: 41 additions & 0 deletions pkg/ddevapp/ddevapp_test.go
Expand Up @@ -1367,6 +1367,47 @@ func TestDdevFullSiteSetup(t *testing.T) {
fmt.Print()
}

// TestDdevSnapshotCleanup tests creating a snapshot and deleting it.
func TestDdevSnapshotCleanup(t *testing.T) {
assert := asrt.New(t)
app := &ddevapp.DdevApp{}
site := TestSites[0]
switchDir := site.Chdir()
defer switchDir()

runTime := util.TimeTrack(time.Now(), fmt.Sprintf("TestDdevSnapshotCleanup"))

testcommon.ClearDockerEnv()
err := app.Init(site.Dir)
assert.NoError(err)

err = app.StartAndWait(0)
assert.NoError(err)

// Make a snapshot of d7 tester test 1
backupsDir := filepath.Join(app.GetConfigPath(""), "db_snapshots")
snapshotName, err := app.Snapshot("d7testerTest1")
assert.NoError(err)

assert.True(fileutil.FileExists(filepath.Join(backupsDir, snapshotName, "xtrabackup_info")), "Expected that file xtrabackup_info in snapshot exists")

err = app.Init(site.Dir)
require.NoError(t, err)

err = app.Start()
require.NoError(t, err)
//nolint: errcheck
defer app.Stop(true, false)

err = app.DeleteSnapshot("d7testerTest1")
assert.NoError(err)

// Snapshot data should be deleted
assert.False(fileutil.FileExists(filepath.Join(backupsDir, snapshotName, "xtrabackup_info")), "Expected that file of snapshot is deleted during cleanup")

runTime()
}

// TestDdevRestoreSnapshot tests creating a snapshot and reverting to it. This runs with Mariadb 10.2
func TestDdevRestoreSnapshot(t *testing.T) {
assert := asrt.New(t)
Expand Down

0 comments on commit 85e656f

Please sign in to comment.