Skip to content

Add snapshot save command (local)#210

Open
anisaoshafi wants to merge 23 commits into
mainfrom
drg-764-migrate-state-commands-from-cli-v1-export
Open

Add snapshot save command (local)#210
anisaoshafi wants to merge 23 commits into
mainfrom
drg-764-migrate-state-commands-from-cli-v1-export

Conversation

@anisaoshafi
Copy link
Copy Markdown
Collaborator

@anisaoshafi anisaoshafi commented Apr 28, 2026

Added a new snapshot subcommand with an initial save action that saves the current LocalStack state to a snapshot file.
Scope for this PR is saving a snapshot locally.
Saving remotely coming next.

@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch 4 times, most recently from f6bbfbf to 6a24cdf Compare April 28, 2026 13:42
Comment thread cmd/snapshot.go Outdated
Comment thread internal/snapshot/destination.go Outdated
Comment thread internal/snapshot/client.go Outdated
Comment thread internal/snapshot/client.go Outdated
_ = resp.Body.Close()
return nil, fmt.Errorf("LocalStack returned status %d", resp.StatusCode)
}
return resp.Body, nil
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: instead of returning io.ReadCloser can we instead pass the io.Writer destination as argument?
This way we're sure the caller won't forget to close the returned body.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anisaoshafi were you able to explore this idea?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed this. I gave it a try here 🤞🏼 2bf9e13

Comment thread internal/snapshot/destination.go Outdated
@gtsiolis
Copy link
Copy Markdown
Member

I've posted a comment[1] in PRO-212 to discuss some potential changes, comments welcome!

@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch from 19cab09 to cc8b494 Compare April 29, 2026 16:45
@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch 5 times, most recently from e2a83f0 to b09a963 Compare May 11, 2026 17:02
@anisaoshafi anisaoshafi marked this pull request as ready for review May 11, 2026 17:27
@anisaoshafi anisaoshafi requested a review from silv-io as a code owner May 11, 2026 17:27
@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch from 36170a5 to 7e45925 Compare May 11, 2026 17:47
@anisaoshafi anisaoshafi changed the title Add snapshot save command Add snapshot save command (local) May 11, 2026
anisaoshafi and others added 7 commits May 12, 2026 15:29
Without this, a failed io.Copy left a corrupt/partial ZIP on disk.
The user had no indication the file was incomplete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Passing nil to runLstk inherited the developer's real $HOME, which
could write to ~/.config/lstk/lstk.log and the file-keyring fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verify that ErrorEvent is routed to errOut and all other events go to
out, and that nil writer arguments fall back safely without panicking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch from 7e45925 to cc13c98 Compare May 12, 2026 13:30
@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch from cc13c98 to c680041 Compare May 12, 2026 13:48
Comment thread internal/snapshot/save_test.go Outdated
return nil, f.err
}
return io.NopCloser(bytes.NewReader(f.body)), nil
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use gomock to generate this mock exporter?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: b16938c

Comment thread internal/snapshot/client.go Outdated
// StateExporter retrieves state from the running LocalStack instance.
type StateExporter interface {
ExportState(ctx context.Context, host string) (io.ReadCloser, error)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the go convention is rather to define an interface where it is used, that would be directly in save.go

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, done here: 8bdf86e

Comment thread internal/snapshot/client.go Outdated
_ = resp.Body.Close()
return nil, fmt.Errorf("LocalStack returned status %d", resp.StatusCode)
}
return resp.Body, nil
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anisaoshafi were you able to explore this idea?

Comment thread internal/snapshot/destination_test.go Outdated
got, err := snapshot.ParseDestination("", now)
require.NoError(t, err)
assert.Equal(t, filepath.Join(wd, "snapshot-2026-05-11T21-04-32.zip"), got)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can't this test case go as a new row in TestParseDestination?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, addressed here: 832bbf8

Comment thread internal/snapshot/destination_test.go Outdated
_, err := snapshot.ParseDestination(dir, now)
require.Error(t, err)
assert.Contains(t, err.Error(), "is a directory")
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: isn't this test case already covered in TestParseDestination?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed here: 832bbf8

Comment thread cmd/snapshot.go

Pass [destination] as an absolute or relative path for the exported file:

lstk snapshot save # saves to ./snapshot-<YYYY-MM-DDTHH-mm-ss>.zip
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: spec says:

Omitting REF generates snapshot-- and writes to CWD.

So it looks like we're missing a hash?
Not sure if it makes sense since it is unlikely to conflict with the timestamp being precise to the second.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gtsiolis we agreed to remove the hash, but forgot to remove one reference in the spec. Is that correct?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we can safely skip it, the timestamp should be quite unique on every generation, right? Plus, system will place a second file next to each other in the rare case you try to generate one in the same second, correct?

snapshot-2026-05-12T20-07-31.zip
snapshot-2026-05-12T20-07-31 (1).zip

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we don't handle the double saving on the same second and overwrite the existing one, but sounds like an edge case and ok to leave as is and skip the over-engineering. Could we continue skipping the hash and easily append the (1) on the filename?

Fix AWS-only guard to correctly reject non-AWS emulator configs, align
container lookup with the for-loop pattern used in aws.go, route
ErrorEvents to stderr via NewPlainSinkSplit, and minor code clarity
improvements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@anisaoshafi anisaoshafi force-pushed the drg-764-migrate-state-commands-from-cli-v1-export branch from 570047a to e5dfa92 Compare May 12, 2026 16:07
@anisaoshafi
Copy link
Copy Markdown
Collaborator Author

@gtsiolis would really appreciate your UX input when you have time to test this 🙏🏼

@gtsiolis
Copy link
Copy Markdown
Member

Looking at this now! 👀

Copy link
Copy Markdown
Member

@gtsiolis gtsiolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX LGTM on first pass. 🏁

Comment thread cmd/snapshot.go

Pass [destination] as an absolute or relative path for the exported file:

lstk snapshot save # saves to ./snapshot-<YYYY-MM-DDTHH-mm-ss>.zip
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we don't handle the double saving on the same second and overwrite the existing one, but sounds like an edge case and ok to leave as is and skip the over-engineering. Could we continue skipping the hash and easily append the (1) on the filename?

Comment thread internal/snapshot/save.go
defer func() {
sink.Emit(output.SpinnerStop())
if retErr == nil {
sink.Emit(output.MessageEvent{Severity: output.SeveritySuccess, Text: fmt.Sprintf("Snapshot saved to %s", dest)})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Nice! 😋 Adding some screens for future reference.

Saving Snapshot Saved Snapshot
Image Image

Comment thread internal/snapshot/save.go
defer func() {
sink.Emit(output.SpinnerStop())
if retErr == nil {
sink.Emit(output.MessageEvent{Severity: output.SeveritySuccess, Text: fmt.Sprintf("Snapshot saved to %s", dest)})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: There's some hard text wrapping on long file path. Any quick wins to make this render better on small width terminals? Like ...

  • If it's under CWD → ./snapshot-….zip
  • If it's under $HOME → ~/snapshot-….zip
  • Otherwise leave the absolute path
Text wrapping for saved snapshot on a small terminal width
Image

Copy link
Copy Markdown
Member

@gtsiolis gtsiolis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed one important question before.

// ParseDestination resolves the user-supplied path to an absolute local path.
// When dest is empty, a default name based on now (UTC) is used, e.g.
// "snapshot-2026-05-11T21-04-32.zip", saved in the current working directory.
// The returned path always has a .zip extension.
Copy link
Copy Markdown
Member

@gtsiolis gtsiolis May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Can we drop the .zip filename later, following the relevant discussion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants