diff --git a/cmd/logout.go b/cmd/logout.go index 00e73024..aca4b3f5 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -7,8 +7,11 @@ import ( "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" + "github.com/localstack/lstk/internal/config" + "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/env" "github.com/localstack/lstk/internal/output" + "github.com/localstack/lstk/internal/runtime" "github.com/localstack/lstk/internal/ui" "github.com/spf13/cobra" ) @@ -20,8 +23,16 @@ func newLogoutCmd(cfg *env.Env) *cobra.Command { PreRunE: initConfig, RunE: func(cmd *cobra.Command, args []string) error { platformClient := api.NewPlatformClient(cfg.APIEndpoint) + appConfig, err := config.Get() + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + var rt runtime.Runtime + if dockerRuntime, err := runtime.NewDockerRuntime(); err == nil { + rt = dockerRuntime + } if isInteractiveMode(cfg) { - return ui.RunLogout(cmd.Context(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring) + return ui.RunLogout(cmd.Context(), rt, platformClient, cfg.AuthToken, cfg.ForceFileKeyring, appConfig.Containers) } sink := output.NewPlainSink(os.Stdout) @@ -36,6 +47,12 @@ func newLogoutCmd(cfg *env.Env) *cobra.Command { } return fmt.Errorf("failed to logout: %w", err) } + + if rt != nil { + if running, err := container.AnyRunning(cmd.Context(), rt, appConfig.Containers); err == nil && running { + output.EmitNote(sink, "LocalStack is still running in the background") + } + } return nil }, } diff --git a/cmd/logs.go b/cmd/logs.go index 5cfc2c67..40fac90f 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -1,8 +1,10 @@ package cmd import ( + "fmt" "os" + "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/runtime" @@ -24,7 +26,11 @@ func newLogsCmd() *cobra.Command { if err != nil { return err } - return container.Logs(cmd.Context(), rt, output.NewPlainSink(os.Stdout), follow) + appConfig, err := config.Get() + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + return container.Logs(cmd.Context(), rt, output.NewPlainSink(os.Stdout), appConfig.Containers, follow) }, } cmd.Flags().BoolP("follow", "f", false, "Follow log output") diff --git a/cmd/root.go b/cmd/root.go index 103bca04..0efc8758 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,12 +81,19 @@ func runStart(ctx context.Context, rt runtime.Runtime, cfg *env.Env, tel *teleme // TODO: replace map with a typed payload struct once event schema is finalised tel.Emit(ctx, "cli_cmd", map[string]any{"cmd": "lstk start", "params": []string{}}) + appConfig, err := config.Get() + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + opts := container.StartOptions{ PlatformClient: api.NewPlatformClient(cfg.APIEndpoint), AuthToken: cfg.AuthToken, ForceFileKeyring: cfg.ForceFileKeyring, WebAppURL: cfg.WebAppURL, LocalStackHost: cfg.LocalStackHost, + Containers: appConfig.Containers, + Env: appConfig.Env, } if isInteractiveMode(cfg) { return ui.Run(ctx, rt, version.Version(), opts) diff --git a/cmd/stop.go b/cmd/stop.go index f8aa46d1..c867a7c9 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -1,8 +1,10 @@ package cmd import ( + "fmt" "os" + "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/env" "github.com/localstack/lstk/internal/output" @@ -22,13 +24,16 @@ func newStopCmd(cfg *env.Env) *cobra.Command { if err != nil { return err } + appConfig, err := config.Get() + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } if isInteractiveMode(cfg) { - return ui.RunStop(cmd.Context(), rt) + return ui.RunStop(cmd.Context(), rt, appConfig.Containers) } - return container.Stop(cmd.Context(), rt, output.NewPlainSink(os.Stdout)) + return container.Stop(cmd.Context(), rt, output.NewPlainSink(os.Stdout), appConfig.Containers) }, } } - diff --git a/internal/container/logs.go b/internal/container/logs.go index a557f86c..8e0665a0 100644 --- a/internal/container/logs.go +++ b/internal/container/logs.go @@ -11,17 +11,13 @@ import ( "github.com/localstack/lstk/internal/runtime" ) -func Logs(ctx context.Context, rt runtime.Runtime, sink output.Sink, follow bool) error { - cfg, err := config.Get() - if err != nil { - return fmt.Errorf("failed to get config: %w", err) - } - if len(cfg.Containers) == 0 { +func Logs(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []config.ContainerConfig, follow bool) error { + if len(containers) == 0 { return fmt.Errorf("no containers configured") } // TODO: handle logs per container - c := cfg.Containers[0] + c := containers[0] pr, pw := io.Pipe() errCh := make(chan error, 1) diff --git a/internal/container/running.go b/internal/container/running.go new file mode 100644 index 00000000..7decfce0 --- /dev/null +++ b/internal/container/running.go @@ -0,0 +1,23 @@ +package container + +import ( + "context" + "fmt" + + "github.com/localstack/lstk/internal/config" + "github.com/localstack/lstk/internal/runtime" +) + +func AnyRunning(ctx context.Context, rt runtime.Runtime, containers []config.ContainerConfig) (bool, error) { + for _, c := range containers { + running, err := rt.IsRunning(ctx, c.Name()) + if err != nil { + return false, fmt.Errorf("checking %s running: %w", c.Name(), err) + } + if running { + return true, nil + } + } + + return false, nil +} diff --git a/internal/container/start.go b/internal/container/start.go index 60a14e2f..9b489f58 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -29,6 +29,8 @@ type StartOptions struct { ForceFileKeyring bool WebAppURL string LocalStackHost string + Containers []config.ContainerConfig + Env map[string]map[string]string } func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts StartOptions, interactive bool) error { @@ -48,17 +50,12 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start return err } - cfg, err := config.Get() - if err != nil { - return fmt.Errorf("failed to get config: %w", err) - } - - if hasDuplicateContainerTypes(cfg.Containers) { + if hasDuplicateContainerTypes(opts.Containers) { output.EmitWarning(sink, "Multiple emulators of the same type are defined in your config; this setup is not supported yet") } - containers := make([]runtime.ContainerConfig, len(cfg.Containers)) - for i, c := range cfg.Containers { + containers := make([]runtime.ContainerConfig, len(opts.Containers)) + for i, c := range opts.Containers { image, err := c.Image() if err != nil { return err @@ -72,7 +69,7 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start return err } - resolvedEnv, err := c.ResolvedEnv(cfg.Env) + resolvedEnv, err := c.ResolvedEnv(opts.Env) if err != nil { return err } @@ -115,7 +112,7 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start setups := map[config.EmulatorType]postStartSetupFunc{ config.EmulatorAWS: awsconfig.Setup, } - return runPostStartSetups(ctx, sink, cfg.Containers, interactive, opts.LocalStackHost, setups) + return runPostStartSetups(ctx, sink, opts.Containers, interactive, opts.LocalStackHost, setups) } func runPostStartSetups(ctx context.Context, sink output.Sink, containers []config.ContainerConfig, interactive bool, localStackHost string, setups map[config.EmulatorType]postStartSetupFunc) error { diff --git a/internal/container/stop.go b/internal/container/stop.go index fa5048f1..52c88c05 100644 --- a/internal/container/stop.go +++ b/internal/container/stop.go @@ -9,13 +9,8 @@ import ( "github.com/localstack/lstk/internal/runtime" ) -func Stop(ctx context.Context, rt runtime.Runtime, sink output.Sink) error { - cfg, err := config.Get() - if err != nil { - return fmt.Errorf("failed to get config: %w", err) - } - - for _, c := range cfg.Containers { +func Stop(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []config.ContainerConfig) error { + for _, c := range containers { name := c.Name() running, err := rt.IsRunning(ctx, name) if err != nil { diff --git a/internal/ui/run.go b/internal/ui/run.go index 0881f482..afca580d 100644 --- a/internal/ui/run.go +++ b/internal/ui/run.go @@ -6,7 +6,6 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" - "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/endpoint" "github.com/localstack/lstk/internal/output" @@ -32,10 +31,10 @@ func Run(parentCtx context.Context, rt runtime.Runtime, version string, opts con // FIXME: This assumes a single emulator; revisit for proper multi-emulator support emulatorName := "LocalStack Emulator" host := endpoint.Hostname - if cfg, err := config.Get(); err == nil && len(cfg.Containers) > 0 { - emulatorName = cfg.Containers[0].DisplayName() - if cfg.Containers[0].Port != "" { - host, _ = endpoint.ResolveHost(cfg.Containers[0].Port, opts.LocalStackHost) + if len(opts.Containers) > 0 { + emulatorName = opts.Containers[0].DisplayName() + if opts.Containers[0].Port != "" { + host, _ = endpoint.ResolveHost(opts.Containers[0].Port, opts.LocalStackHost) } } diff --git a/internal/ui/run_logout.go b/internal/ui/run_logout.go index 11e8e409..fdec68be 100644 --- a/internal/ui/run_logout.go +++ b/internal/ui/run_logout.go @@ -8,11 +8,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/localstack/lstk/internal/api" "github.com/localstack/lstk/internal/auth" + "github.com/localstack/lstk/internal/config" + "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/output" + "github.com/localstack/lstk/internal/runtime" ) -func RunLogout(parentCtx context.Context, platformClient api.PlatformAPI, authToken string, forceFileKeyring bool) error { - _, cancel := context.WithCancel(parentCtx) +func RunLogout(parentCtx context.Context, rt runtime.Runtime, platformClient api.PlatformAPI, authToken string, forceFileKeyring bool, containers []config.ContainerConfig) error { + ctx, cancel := context.WithCancel(parentCtx) defer cancel() app := NewApp("", "", "", cancel, withoutHeader()) @@ -28,8 +31,14 @@ func RunLogout(parentCtx context.Context, platformClient api.PlatformAPI, authTo return } - a := auth.New(output.NewTUISink(programSender{p: p}), platformClient, tokenStorage, authToken, "", false) + sink := output.NewTUISink(programSender{p: p}) + a := auth.New(sink, platformClient, tokenStorage, authToken, "", false) err = a.Logout() + if err == nil && rt != nil { + if running, runningErr := container.AnyRunning(ctx, rt, containers); runningErr == nil && running { + output.EmitNote(sink, "LocalStack is still running in the background") + } + } runErrCh <- err if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, auth.ErrNotLoggedIn) { diff --git a/internal/ui/run_stop.go b/internal/ui/run_stop.go index 3de944e4..f24f3cc9 100644 --- a/internal/ui/run_stop.go +++ b/internal/ui/run_stop.go @@ -6,12 +6,13 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" + "github.com/localstack/lstk/internal/config" "github.com/localstack/lstk/internal/container" "github.com/localstack/lstk/internal/output" "github.com/localstack/lstk/internal/runtime" ) -func RunStop(parentCtx context.Context, rt runtime.Runtime) error { +func RunStop(parentCtx context.Context, rt runtime.Runtime, containers []config.ContainerConfig) error { ctx, cancel := context.WithCancel(parentCtx) defer cancel() @@ -20,7 +21,7 @@ func RunStop(parentCtx context.Context, rt runtime.Runtime) error { runErrCh := make(chan error, 1) go func() { - err := container.Stop(ctx, rt, output.NewTUISink(programSender{p: p})) + err := container.Stop(ctx, rt, output.NewTUISink(programSender{p: p}), containers) runErrCh <- err if err != nil && !errors.Is(err, context.Canceled) { p.Send(runErrMsg{err: err}) diff --git a/test/integration/logout_test.go b/test/integration/logout_test.go index 650f047f..c3d8e204 100644 --- a/test/integration/logout_test.go +++ b/test/integration/logout_test.go @@ -40,3 +40,22 @@ func TestLogoutCommandWithEnvVarToken(t *testing.T) { require.NoError(t, err, "lstk logout should succeed: %s", stderr) assert.Contains(t, stdout, "LOCALSTACK_AUTH_TOKEN") } + +func TestLogoutCommandNotesWhenEmulatorStillRunning(t *testing.T) { + requireDocker(t) + cleanup() + t.Cleanup(cleanup) + t.Cleanup(func() { + _ = DeleteAuthTokenFromKeyring() + }) + + ctx := testContext(t) + startTestContainer(t, ctx) + + err := SetAuthTokenInKeyring("test-token") + require.NoError(t, err, "failed to store token in keyring") + + stdout, stderr, err := runLstk(t, ctx, "", nil, "logout") + require.NoError(t, err, "lstk logout failed: %s", stderr) + assert.Contains(t, stdout, "LocalStack is still running in the background") +}