-
Notifications
You must be signed in to change notification settings - Fork 2
Add volume path and clear subcommands #183
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
Open
gtsiolis
wants to merge
4
commits into
main
Choose a base branch
from
des-198-add-a-volume-path-subcommand
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8be1081
Add volume path command to print resolved volume directory
gtsiolis abda2a8
Add volume clear subcommand to reset emulator volume data
gtsiolis 22c0db4
Fix Windows TOML parsing by escaping backslashes in test volume paths
gtsiolis e5137df
Simplify volume clear branching and clean up test comments
gtsiolis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/localstack/lstk/internal/config" | ||
| "github.com/localstack/lstk/internal/env" | ||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/localstack/lstk/internal/telemetry" | ||
| "github.com/localstack/lstk/internal/ui" | ||
| "github.com/localstack/lstk/internal/volume" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func newVolumeCmd(cfg *env.Env, tel *telemetry.Client) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "volume", | ||
| Short: "Manage emulator volume", | ||
| } | ||
| cmd.AddCommand(newVolumePathCmd(cfg, tel)) | ||
| cmd.AddCommand(newVolumeClearCmd(cfg, tel)) | ||
| return cmd | ||
| } | ||
|
|
||
| func newVolumePathCmd(cfg *env.Env, tel *telemetry.Client) *cobra.Command { | ||
| return &cobra.Command{ | ||
| Use: "path", | ||
| Short: "Print the volume directory path", | ||
| PreRunE: initConfig, | ||
| RunE: commandWithTelemetry("volume path", tel, func(cmd *cobra.Command, args []string) error { | ||
| appConfig, err := config.Get() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get config: %w", err) | ||
| } | ||
|
|
||
| for _, c := range appConfig.Containers { | ||
| volumeDir, err := c.VolumeDir() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| _, err = fmt.Fprintln(cmd.OutOrStdout(), volumeDir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| func newVolumeClearCmd(cfg *env.Env, tel *telemetry.Client) *cobra.Command { | ||
| var force bool | ||
| var containerName string | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "clear", | ||
| Short: "Clear emulator volume data", | ||
| Long: "Remove all data from the emulator volume directory. This resets cached state such as certificates, downloaded tools, and persistence data.", | ||
| PreRunE: initConfig, | ||
| RunE: commandWithTelemetry("volume clear", tel, func(cmd *cobra.Command, args []string) error { | ||
| appConfig, err := config.Get() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get config: %w", err) | ||
| } | ||
|
|
||
| containers := appConfig.Containers | ||
| if containerName != "" { | ||
| containers, err = filterContainers(appConfig.Containers, containerName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| if !isInteractiveMode(cfg) && !force { | ||
| return fmt.Errorf("volume clear requires confirmation; use --force to skip in non-interactive mode") | ||
| } | ||
|
|
||
| if !isInteractiveMode(cfg) || force { | ||
| sink := output.NewPlainSink(os.Stdout) | ||
| return volume.Clear(cmd.Context(), sink, containers, true) | ||
| } | ||
|
|
||
| return ui.RunVolumeClear(cmd.Context(), containers) | ||
| }), | ||
| } | ||
|
|
||
| cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt") | ||
| cmd.Flags().StringVar(&containerName, "container", "", "Target a specific emulator container name") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func filterContainers(containers []config.ContainerConfig, name string) ([]config.ContainerConfig, error) { | ||
| for _, c := range containers { | ||
| if c.Name() == name { | ||
| return []config.ContainerConfig{c}, nil | ||
| } | ||
| } | ||
| var names []string | ||
| for _, c := range containers { | ||
| names = append(names, c.Name()) | ||
| } | ||
| return nil, fmt.Errorf("container %q not found in config; available: %v", name, names) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package ui | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "os" | ||
|
|
||
| tea "github.com/charmbracelet/bubbletea" | ||
| "github.com/localstack/lstk/internal/config" | ||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/localstack/lstk/internal/volume" | ||
| ) | ||
|
|
||
| func RunVolumeClear(parentCtx context.Context, containers []config.ContainerConfig) error { | ||
| ctx, cancel := context.WithCancel(parentCtx) | ||
| defer cancel() | ||
|
|
||
| app := NewApp("", "", "", cancel, withoutHeader()) | ||
| p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) | ||
| runErrCh := make(chan error, 1) | ||
|
|
||
| go func() { | ||
| err := volume.Clear(ctx, output.NewTUISink(programSender{p: p}), containers, false) | ||
| runErrCh <- err | ||
| if err != nil && !errors.Is(err, context.Canceled) { | ||
| p.Send(runErrMsg{err: err}) | ||
| return | ||
| } | ||
| p.Send(runDoneMsg{}) | ||
| }() | ||
|
|
||
| model, err := p.Run() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if app, ok := model.(App); ok && app.Err() != nil { | ||
| return output.NewSilentError(app.Err()) | ||
| } | ||
|
|
||
| runErr := <-runErrCh | ||
| if runErr != nil && !errors.Is(runErr, context.Canceled) { | ||
| return runErr | ||
| } | ||
|
|
||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| package volume | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io/fs" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/localstack/lstk/internal/config" | ||
| "github.com/localstack/lstk/internal/output" | ||
| ) | ||
|
|
||
| func Clear(ctx context.Context, sink output.Sink, containers []config.ContainerConfig, force bool) error { | ||
| type target struct { | ||
| name string | ||
| path string | ||
| size int64 | ||
| } | ||
|
|
||
| var targets []target | ||
| for _, c := range containers { | ||
| volumeDir, err := c.VolumeDir() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| size, err := dirSize(volumeDir) | ||
| if err != nil && !os.IsNotExist(err) { | ||
| return fmt.Errorf("failed to read volume directory %s: %w", volumeDir, err) | ||
| } | ||
| targets = append(targets, target{name: c.DisplayName(), path: volumeDir, size: size}) | ||
| } | ||
|
|
||
| for _, t := range targets { | ||
| output.EmitInfo(sink, fmt.Sprintf("%s: %s (%s)", t.name, t.path, formatSize(t.size))) | ||
| } | ||
|
|
||
| if !force { | ||
| responseCh := make(chan output.InputResponse, 1) | ||
| output.EmitUserInputRequest(sink, output.UserInputRequestEvent{ | ||
| Prompt: "Clear volume data? This cannot be undone", | ||
| Options: []output.InputOption{ | ||
| {Key: "y", Label: "Yes"}, | ||
| {Key: "n", Label: "NO"}, | ||
| }, | ||
| ResponseCh: responseCh, | ||
| }) | ||
|
|
||
| select { | ||
| case resp := <-responseCh: | ||
| if resp.Cancelled || resp.SelectedKey != "y" { | ||
| output.EmitNote(sink, "Cancelled") | ||
| return nil | ||
| } | ||
| case <-ctx.Done(): | ||
| return ctx.Err() | ||
| } | ||
| } | ||
|
|
||
| for _, t := range targets { | ||
| if err := clearDir(t.path); err != nil { | ||
| return fmt.Errorf("failed to clear %s: %w", t.path, err) | ||
| } | ||
| } | ||
|
|
||
| output.EmitSuccess(sink, "Volume data cleared") | ||
| return nil | ||
| } | ||
|
|
||
| func clearDir(dir string) error { | ||
| entries, err := os.ReadDir(dir) | ||
| if err != nil { | ||
| if os.IsNotExist(err) { | ||
| return nil | ||
| } | ||
| return err | ||
| } | ||
| for _, entry := range entries { | ||
| if err := os.RemoveAll(filepath.Join(dir, entry.Name())); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func dirSize(path string) (int64, error) { | ||
| var size int64 | ||
| err := filepath.WalkDir(path, func(_ string, d fs.DirEntry, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !d.IsDir() { | ||
| info, err := d.Info() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| size += info.Size() | ||
| } | ||
| return nil | ||
| }) | ||
| return size, err | ||
| } | ||
|
|
||
| func formatSize(bytes int64) string { | ||
| const ( | ||
| kb = 1024 | ||
| mb = 1024 * kb | ||
| gb = 1024 * mb | ||
| ) | ||
| switch { | ||
| case bytes >= gb: | ||
| return fmt.Sprintf("%.1f GB", float64(bytes)/float64(gb)) | ||
| case bytes >= mb: | ||
| return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb)) | ||
| case bytes >= kb: | ||
| return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb)) | ||
| default: | ||
| return fmt.Sprintf("%d B", bytes) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gtsiolis Is
containerthe right word for the flag or should we useemulatorfor consistency?