Skip to content

Commit

Permalink
fix(db): do not remove backup volume when start fails
Browse files Browse the repository at this point in the history
  • Loading branch information
sweatybridge committed Jan 8, 2024
1 parent ed7e872 commit ca9983d
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 203 deletions.
10 changes: 6 additions & 4 deletions internal/db/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func Run(ctx context.Context, fsys afero.Fs) error {
utils.Config.Analytics.Enabled = false
err := StartDatabase(ctx, fsys, os.Stderr)
if err != nil {
utils.DockerRemoveAll(context.Background())
if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
return err
}
Expand Down Expand Up @@ -126,8 +128,8 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
}
// Creating volume will not override existing volume, so we must inspect explicitly
_, err := utils.Docker.VolumeInspect(ctx, utils.DbId)
noBackupVolume := client.IsErrNotFound(err)
if noBackupVolume {
utils.NoBackupVolume = client.IsErrNotFound(err)
if utils.NoBackupVolume {
fmt.Fprintln(w, "Starting database...")
} else {
fmt.Fprintln(w, "Starting database from backup...")
Expand All @@ -139,7 +141,7 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
return errors.New(ErrDatabase)
}
// Initialize if we are on PG14 and there's no existing db volume
if noBackupVolume {
if utils.NoBackupVolume {
if err := setupDatabase(ctx, fsys, w, options...); err != nil {
return err
}
Expand Down
12 changes: 1 addition & 11 deletions internal/db/start/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ func TestInitBranch(t *testing.T) {
}

func TestStartDatabase(t *testing.T) {
teardown := func() {
utils.Containers = []string{}
utils.Volumes = []string{}
}

t.Run("initialize main branch", func(t *testing.T) {
defer teardown()
utils.Config.Db.MajorVersion = 15
utils.Config.Db.Image = utils.Pg15Image
utils.DbId = "supabase_db_test"
Expand Down Expand Up @@ -108,7 +102,6 @@ func TestStartDatabase(t *testing.T) {
})

t.Run("recover from backup volume", func(t *testing.T) {
defer teardown()
utils.Config.Db.MajorVersion = 14
utils.Config.Db.Image = utils.Pg15Image
utils.DbId = "supabase_db_test"
Expand Down Expand Up @@ -145,7 +138,6 @@ func TestStartDatabase(t *testing.T) {
})

t.Run("throws error on start failure", func(t *testing.T) {
defer teardown()
utils.Config.Db.MajorVersion = 15
utils.Config.Db.Image = utils.Pg15Image
utils.DbId = "supabase_db_test"
Expand Down Expand Up @@ -241,9 +233,7 @@ func TestStartCommand(t *testing.T) {
Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json").
ReplyError(errors.New("network error"))
// Cleanup resources
gock.New(utils.Docker.DaemonHost()).
Post("/v" + utils.Docker.ClientVersion() + "/networks/prune").
Reply(http.StatusOK)
apitest.MockDockerStop(utils.Docker)
// Run test
err := Run(context.Background(), fsys)
// Check error
Expand Down
5 changes: 4 additions & 1 deletion internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
_ "embed"
"fmt"
"io"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -71,7 +72,9 @@ func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignore
if ignoreHealthCheck && errors.Is(err, reset.ErrUnhealthy) {
fmt.Fprintln(os.Stderr, err)
} else {
utils.DockerRemoveAll(context.Background())
if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return err
}
}
Expand Down
70 changes: 6 additions & 64 deletions internal/stop/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import (
_ "embed"
"fmt"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/errdefs"
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
)
Expand All @@ -33,67 +28,14 @@ func Run(ctx context.Context, backup bool, projectId string, fsys afero.Fs) erro
}

fmt.Println("Stopped " + utils.Aqua("supabase") + " local development setup.")
if backup {
listVolume := fmt.Sprintf("docker volume ls --filter label=%s=%s", utils.CliProjectLabel, utils.Config.ProjectId)
utils.CmdSuggestion = "Local data are backed up to docker volume. You may list them with " + utils.Aqua(listVolume)
}
return nil
}

func stop(ctx context.Context, backup bool, w io.Writer) error {
args := utils.CliProjectFilter()
containers, err := utils.Docker.ContainerList(ctx, types.ContainerListOptions{
All: true,
Filters: args,
})
if err != nil {
return errors.Errorf("failed to list containers: %w", err)
}
// Gracefully shutdown containers
var ids []string
for _, c := range containers {
if c.State == "running" {
ids = append(ids, c.ID)
}
}
fmt.Fprintln(w, "Stopping containers...")
result := utils.WaitAll(ids, func(id string) error {
if err := utils.Docker.ContainerStop(ctx, id, container.StopOptions{}); err != nil {
return errors.Errorf("failed to stop container: %w", err)
}
return nil
})
if err := errors.Join(result...); err != nil {
return err
}
if _, err := utils.Docker.ContainersPrune(ctx, args); err != nil {
return errors.Errorf("failed to prune containers: %w", err)
}
// Remove named volumes
if backup {
fmt.Fprintln(os.Stderr, "Postgres database saved to volume:", utils.DbId)
fmt.Fprintln(os.Stderr, "Postgres config saved to volume:", utils.ConfigId)
fmt.Fprintln(os.Stderr, "Storage directory saved to volume:", utils.StorageId)
fmt.Fprintln(os.Stderr, "Functions cache saved to volume:", utils.EdgeRuntimeId)
fmt.Fprintln(os.Stderr, "Inbucket emails saved to volume:", utils.InbucketId)
} else {
// TODO: label named volumes to use VolumesPrune for branch support
volumes := []string{
utils.ConfigId,
utils.DbId,
utils.StorageId,
utils.EdgeRuntimeId,
utils.InbucketId,
}
result = utils.WaitAll(volumes, func(name string) error {
if err := utils.Docker.VolumeRemove(ctx, name, true); err != nil && !errdefs.IsNotFound(err) {
return errors.Errorf("Failed to remove volume %s: %w", name, err)
}
return nil
})
if err := errors.Join(result...); err != nil {
return err
}
}
// Remove networks.
if _, err = utils.Docker.NetworksPrune(ctx, args); err != nil {
return errors.Errorf("failed to prune networks: %w", err)
}
return nil
utils.NoBackupVolume = !backup
return utils.DockerRemoveAll(ctx, w)
}
28 changes: 1 addition & 27 deletions internal/stop/stop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,33 +108,7 @@ func TestStopServices(t *testing.T) {
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
defer gock.OffAll()
gock.New(utils.Docker.DaemonHost()).
Get("/v" + utils.Docker.ClientVersion() + "/containers/json").
Reply(http.StatusOK).
JSON([]types.Container{})
gock.New(utils.Docker.DaemonHost()).
Post("/v" + utils.Docker.ClientVersion() + "/containers/prune").
Reply(http.StatusOK).
JSON(types.ContainersPruneReport{})
gock.New(utils.Docker.DaemonHost()).
Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.ConfigId).
Reply(http.StatusOK)
gock.New(utils.Docker.DaemonHost()).
Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
Reply(http.StatusOK)
gock.New(utils.Docker.DaemonHost()).
Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.StorageId).
Reply(http.StatusNotFound)
gock.New(utils.Docker.DaemonHost()).
Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.EdgeRuntimeId).
Reply(http.StatusNotFound)
gock.New(utils.Docker.DaemonHost()).
Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.InbucketId).
Reply(http.StatusNotFound)
gock.New(utils.Docker.DaemonHost()).
Post("/v" + utils.Docker.ClientVersion() + "/networks/prune").
Reply(http.StatusOK).
JSON(types.NetworksPruneReport{})
apitest.MockDockerStop(utils.Docker)
// Run test
err := stop(context.Background(), false, io.Discard)
// Check error
Expand Down
48 changes: 32 additions & 16 deletions internal/testing/apitest/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"gopkg.in/h2non/gock.v1"
Expand Down Expand Up @@ -39,6 +40,11 @@ func MockDockerStart(docker *client.Client, image, containerID string) {
Post("/v" + docker.ClientVersion() + "/networks/create").
Reply(http.StatusCreated).
JSON(types.NetworkCreateResponse{})
gock.New(docker.DaemonHost()).
Post("/v" + docker.ClientVersion() + "/volumes/create").
Persist().
Reply(http.StatusCreated).
JSON(volume.Volume{})
gock.New(docker.DaemonHost()).
Post("/v" + docker.ClientVersion() + "/containers/create").
Reply(http.StatusOK).
Expand All @@ -48,31 +54,31 @@ func MockDockerStart(docker *client.Client, image, containerID string) {
Reply(http.StatusAccepted)
}

// Ref: internal/utils/docker.go::DockerRunOnce
func MockDockerLogs(docker *client.Client, containerID, stdout string) error {
var body bytes.Buffer
writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
_, err := writer.Write([]byte(stdout))
// Ref: internal/utils/docker.go::DockerRemoveAll
func MockDockerStop(docker *client.Client) {
gock.New(docker.DaemonHost()).
Get("/v"+docker.ClientVersion()+"/containers/"+containerID+"/logs").
Get("/v" + docker.ClientVersion() + "/containers/json").
Reply(http.StatusOK).
SetHeader("Content-Type", "application/vnd.docker.raw-stream").
Body(&body)
JSON([]types.Container{})
gock.New(docker.DaemonHost()).
Get("/v" + docker.ClientVersion() + "/containers/" + containerID + "/json").
Post("/v" + docker.ClientVersion() + "/containers/prune").
Reply(http.StatusOK).
JSON(types.ContainerJSONBase{State: &types.ContainerState{ExitCode: 0}})
JSON(types.ContainersPruneReport{})
gock.New(docker.DaemonHost()).
Delete("/v" + docker.ClientVersion() + "/containers/" + containerID).
Reply(http.StatusOK)
return err
Post("/v" + docker.ClientVersion() + "/volumes/prune").
Reply(http.StatusOK).
JSON(types.VolumesPruneReport{})
gock.New(docker.DaemonHost()).
Post("/v" + docker.ClientVersion() + "/networks/prune").
Reply(http.StatusOK).
JSON(types.NetworksPruneReport{})
}

// Ref: internal/utils/docker.go::DockerRunOnce
func MockDockerLogsExitCode(docker *client.Client, containerID string, exitCode int) error {
func setupDockerLogs(docker *client.Client, containerID, stdout string, exitCode int) error {
var body bytes.Buffer
writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout)
_, err := writer.Write([]byte(""))
_, err := writer.Write([]byte(stdout))
gock.New(docker.DaemonHost()).
Get("/v"+docker.ClientVersion()+"/containers/"+containerID+"/logs").
Reply(http.StatusOK).
Expand All @@ -81,13 +87,23 @@ func MockDockerLogsExitCode(docker *client.Client, containerID string, exitCode
gock.New(docker.DaemonHost()).
Get("/v" + docker.ClientVersion() + "/containers/" + containerID + "/json").
Reply(http.StatusOK).
JSON(types.ContainerJSONBase{State: &types.ContainerState{ExitCode: exitCode}})
JSON(types.ContainerJSONBase{State: &types.ContainerState{
ExitCode: exitCode,
}})
gock.New(docker.DaemonHost()).
Delete("/v" + docker.ClientVersion() + "/containers/" + containerID).
Reply(http.StatusOK)
return err
}

func MockDockerLogs(docker *client.Client, containerID, stdout string) error {
return setupDockerLogs(docker, containerID, stdout, 0)
}

func MockDockerLogsExitCode(docker *client.Client, containerID string, exitCode int) error {
return setupDockerLogs(docker, containerID, "", exitCode)
}

func ListUnmatchedRequests() []string {
result := make([]string, len(gock.GetUnmatchedRequests()))
for i, r := range gock.GetUnmatchedRequests() {
Expand Down
Loading

0 comments on commit ca9983d

Please sign in to comment.