Skip to content
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.
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
var found bool
for _, c := range appConfig.Containers {
if c.Type == config.EmulatorAWS {
awsContainer = c
found = true
break
}
}
if !found {
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()

if interactive {
return ui.RunReset(cmd.Context(), rt, []config.ContainerConfig{awsContainer}, resetter, host, force)
}
return reset.Reset(cmd.Context(), rt, []config.ContainerConfig{awsContainer}, resetter, host, force, output.NewPlainSink(os.Stdout))
},
}

cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt")
return cmd
}
Comment thread
gtsiolis marked this conversation as resolved.
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
}

Comment thread
gtsiolis marked this conversation as resolved.
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
80 changes: 80 additions & 0 deletions internal/emulator/aws/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,83 @@ func TestExportState(t *testing.T) {
})
}

func TestResetState(t *testing.T) {
t.Parallel()

t.Run("posts to state reset endpoint on 200", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/_localstack/state/reset", r.URL.Path)
assert.Equal(t, http.MethodPost, r.Method)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()

c := NewClient()
err := c.ResetState(context.Background(), srv.Listener.Addr().String())
require.NoError(t, err)
})

t.Run("returns error on 500", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()

c := NewClient()
err := c.ResetState(context.Background(), srv.Listener.Addr().String())
require.Error(t, err)
assert.Contains(t, err.Error(), "500")
})

t.Run("returns error on 404", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()

c := NewClient()
err := c.ResetState(context.Background(), srv.Listener.Addr().String())
require.Error(t, err)
assert.Contains(t, err.Error(), "404")
})

t.Run("returns error on connection refused", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
addr := srv.Listener.Addr().String()
srv.Close()

c := NewClient()
err := c.ResetState(context.Background(), addr)
require.Error(t, err)
assert.Contains(t, err.Error(), "connect to LocalStack")
})

t.Run("returns error on context cancellation", func(t *testing.T) {
t.Parallel()
started := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
close(started)
<-r.Context().Done()
}))
defer srv.Close()

ctx, cancel := context.WithCancel(context.Background())
c := NewClient()

errCh := make(chan error, 1)
go func() {
errCh <- c.ResetState(ctx, srv.Listener.Addr().String())
}()

<-started
cancel()

err := <-errCh
require.Error(t, err)
})
}

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