Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions cmd/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"errors"
"fmt"
"os"

"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/emulator/aws"
"github.com/localstack/lstk/internal/endpoint"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/reset"
"github.com/localstack/lstk/internal/runtime"
"github.com/localstack/lstk/internal/ui"
"github.com/spf13/cobra"
)

func newResetCmd(cfg *env.Env) *cobra.Command {
var force bool

cmd := &cobra.Command{
Use: "reset",
Short: "Reset emulator state",
Long: `Reset the running emulator's in-memory state.

All resources created in the emulator (S3 buckets, Lambda functions, etc.) are
discarded. The emulator keeps running; only its state is cleared.

To wipe the on-disk volume (certificates, persistence data, cached tools)
instead, stop the emulator and run "lstk volume clear".`,
PreRunE: initConfig(nil),
RunE: func(cmd *cobra.Command, args []string) error {
appConfig, err := config.Get()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

var awsContainer *config.ContainerConfig
for i, c := range appConfig.Containers {
if c.Type == config.EmulatorAWS {
awsContainer = &appConfig.Containers[i]
break
}
}
if awsContainer == nil {
return errors.New("reset is only supported for the AWS emulator")
}

interactive := isInteractiveMode(cfg)
if !interactive && !force {
return errors.New("reset requires confirmation; use --force to skip in non-interactive mode")
}

rt, err := runtime.NewDockerRuntime(cfg.DockerHost)
if err != nil {
return err
}
host, _ := endpoint.ResolveHost(cmd.Context(), awsContainer.Port, cfg.LocalStackHost)
resetter := aws.NewClient()

containers := []config.ContainerConfig{*awsContainer}

if interactive {
return ui.RunReset(cmd.Context(), rt, containers, resetter, host, force)
}
return reset.Reset(cmd.Context(), rt, containers, resetter, host, force, output.NewPlainSink(os.Stdout))
},
}

cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt")
return cmd
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func NewRootCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.C
newDocsCmd(),
newAWSCmd(cfg),
newSnapshotCmd(cfg),
newResetCmd(cfg),
)

return root
Expand Down
19 changes: 19 additions & 0 deletions internal/emulator/aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ func (c *Client) FetchResources(ctx context.Context, host string) ([]emulator.Re
return rows, nil
}

func (c *Client) ResetState(ctx context.Context, host string) error {
url := fmt.Sprintf("http://%s/_localstack/state/reset", host)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}

resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("connect to LocalStack: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("LocalStack returned status %d", resp.StatusCode)
}
return nil
}

func (c *Client) ExportState(ctx context.Context, host string, dst io.Writer) error {
url := fmt.Sprintf("http://%s/_localstack/pods/state", host)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Expand Down
55 changes: 55 additions & 0 deletions internal/reset/mock_state_resetter_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions internal/reset/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package reset

//go:generate mockgen -source=reset.go -destination=mock_state_resetter_test.go -package=reset_test

import (
"context"
"fmt"

"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/container"
"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/runtime"
)

// StateResetter clears state in the running LocalStack instance.
type StateResetter interface {
ResetState(ctx context.Context, host string) error
}

func Reset(ctx context.Context, rt runtime.Runtime, containers []config.ContainerConfig, resetter StateResetter, host string, force bool, sink output.Sink) (retErr error) {
if err := rt.IsHealthy(ctx); err != nil {
rt.EmitUnhealthyError(sink, err)
return output.NewSilentError(fmt.Errorf("runtime not healthy: %w", err))
}

runningContainers, err := container.RunningEmulators(ctx, rt, containers)
if err != nil {
return fmt.Errorf("checking emulator status: %w", err)
}
if len(runningContainers) == 0 {
sink.Emit(output.ErrorEvent{
Title: "LocalStack is not running",
Actions: []output.ErrorAction{
{Label: "Start LocalStack:", Value: "lstk"},
{Label: "See help:", Value: "lstk -h"},
},
})
return output.NewSilentError(fmt.Errorf("LocalStack is not running"))
}

if !force {
responseCh := make(chan output.InputResponse, 1)
sink.Emit(output.UserInputRequestEvent{
Prompt: "Reset emulator state? All resources will be lost",
Options: []output.InputOption{
{Key: "y", Label: "Yes"},
{Key: "n", Label: "NO"},
},
ResponseCh: responseCh,
})

select {
case resp := <-responseCh:
if resp.Cancelled || resp.SelectedKey != "y" {
sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: "Cancelled"})
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}

sink.Emit(output.SpinnerStart("Resetting state..."))
defer func() {
sink.Emit(output.SpinnerStop())
if retErr == nil {
sink.Emit(output.MessageEvent{Severity: output.SeveritySuccess, Text: "Emulator state reset"})
}
}()

if err := resetter.ResetState(ctx, host); err != nil {
return fmt.Errorf("reset state: %w", err)
}
return nil
}
Loading
Loading