-
Notifications
You must be signed in to change notification settings - Fork 0
Add stop command #21
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
base: main
Are you sure you want to change the base?
Add stop command #21
Changes from all commits
e44914e
0408af1
1f8e3eb
0601e91
5f61b8c
cdd5997
a55d50a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/localstack/lstk/internal/container" | ||
| "github.com/localstack/lstk/internal/runtime" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var stopCmd = &cobra.Command{ | ||
| Use: "stop", | ||
| Short: "Stop LocalStack", | ||
| Long: "Stop the LocalStack emulator.", | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| rt, err := runtime.NewDockerRuntime() | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| onProgress := func(msg string) { | ||
| fmt.Println(msg) | ||
| } | ||
|
|
||
| if err := container.Stop(cmd.Context(), rt, onProgress); err != nil { | ||
| fmt.Fprintln(os.Stderr, err) | ||
| os.Exit(1) | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(stopCmd) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package container | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/localstack/lstk/internal/config" | ||
| "github.com/localstack/lstk/internal/runtime" | ||
| ) | ||
|
|
||
| func Stop(ctx context.Context, rt runtime.Runtime, onProgress func(string)) error { | ||
| cfg, err := config.Get() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get config: %w", err) | ||
| } | ||
|
|
||
| for _, c := range cfg.Containers { | ||
| name := c.Name() | ||
| onProgress(fmt.Sprintf("Stopping %s...", name)) | ||
| if err := rt.Stop(ctx, name); err != nil { | ||
| return fmt.Errorf("failed to stop %s: %w", name, err) | ||
| } | ||
| onProgress(fmt.Sprintf("%s stopped", name)) | ||
| } | ||
|
|
||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,17 @@ func (d *DockerRuntime) Start(ctx context.Context, config ContainerConfig) (stri | |
| return resp.ID, nil | ||
| } | ||
|
|
||
| func (d *DockerRuntime) Stop(ctx context.Context, containerName string) error { | ||
| if err := d.client.ContainerStop(ctx, containerName, container.StopOptions{}); err != nil { | ||
| return err | ||
| } | ||
| return d.client.ContainerRemove(ctx, containerName, container.RemoveOptions{}) | ||
| } | ||
|
|
||
| func (d *DockerRuntime) Remove(ctx context.Context, containerName string) error { | ||
| return d.client.ContainerRemove(ctx, containerName, container.RemoveOptions{}) | ||
|
Comment on lines
+99
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In
What happens when
Engine default stop timeout (when neither API timeout nor per-container
Sources Citations:
🌐 Web query:
💡 Result: In This matches the Docker Engine API: So, to remove volumes you must opt in, e.g.:
Sources: [1] [2] [3] Clarify whether volumes should be removed in the Stop operation. The container.RemoveOptions{RemoveVolumes: true}Otherwise, document the volume retention behavior if it is intentional. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func (d *DockerRuntime) IsRunning(ctx context.Context, containerID string) (bool, error) { | ||
| inspect, err := d.client.ContainerInspect(ctx, containerID) | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package integration_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "os/exec" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/docker/docker/api/types/container" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func startTestContainer(t *testing.T, ctx context.Context) { | ||
| t.Helper() | ||
| resp, err := dockerClient.ContainerCreate(ctx, &container.Config{ | ||
| Image: "alpine:latest", | ||
| Cmd: []string{"sleep", "infinity"}, | ||
| }, nil, nil, nil, containerName) | ||
| require.NoError(t, err, "failed to create test container") | ||
| err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}) | ||
| require.NoError(t, err, "failed to start test container") | ||
|
Comment on lines
+14
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the test file
find . -name "stop_test.go" -path "*/test/integration/*" | head -5Repository: localstack/lstk Length of output: 90 🏁 Script executed: # Read the test file to see the current implementation
cat -n test/integration/stop_test.goRepository: localstack/lstk Length of output: 3076 🏁 Script executed: # Check the imports already present in the file
head -20 test/integration/stop_test.goRepository: localstack/lstk Length of output: 563 🏁 Script executed: # Search for any CI configuration or workflow files that might show failures
find . -name "*.yml" -o -name "*.yaml" | grep -E "(workflow|ci|github)" | head -10Repository: localstack/lstk Length of output: 85 🏁 Script executed: # Check the CI workflow for test execution and any indicators of failures
cat -n ./.github/workflows/ci.yml | head -100Repository: localstack/lstk Length of output: 3019 🏁 Script executed: # Search for any other references to alpine or docker image pulling in test files
rg "ImagePull|PullOptions" test/Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Check if there's a setup function or init in the test file that might handle image pulling
rg -B5 -A5 "func.*dockerClient" test/integration/stop_test.goRepository: localstack/lstk Length of output: 41 🏁 Script executed: # Look at the entire stop_test.go file to understand the test setup
tail -n +1 test/integration/stop_test.go | head -100Repository: localstack/lstk Length of output: 2502 🏁 Script executed: # Find where dockerClient is defined
rg "var dockerClient|dockerClient\s*=" test/integration/Repository: localstack/lstk Length of output: 120 🏁 Script executed: # Search for any test setup or init functions
rg "func init|func setup|func Setup" test/integration/Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Check if there are other files in the integration test directory
ls -la test/integration/Repository: localstack/lstk Length of output: 759 🏁 Script executed: # Look for the dockerClient initialization
rg -B10 "dockerClient" test/integration/stop_test.go | head -30Repository: localstack/lstk Length of output: 1013 🏁 Script executed: # Check the main_test.go file for dockerClient initialization and setup
cat -n test/integration/main_test.goRepository: localstack/lstk Length of output: 3051 🏁 Script executed: # Search for docker image pulling in the entire test directory
rg "ImagePull|PullOptions" test/Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Check if alpine image is pre-pulled or if there's a makefile target that sets it up
cat -n Makefile | grep -A10 "test-integration"Repository: localstack/lstk Length of output: 1018 🏁 Script executed: # Check if there's any docker image pre-pulling or setup in CI or any other helper scripts
rg "pull.*alpine|docker.*pull" .github/ Makefile scripts/ 2>/dev/null || echo "No docker pull found in CI/Makefile"Repository: localstack/lstk Length of output: 94 🏁 Script executed: # Verify the docker client API - check if ImagePull is the correct method
rg "ImagePull" test/integration/ || echo "ImagePull not used yet"Repository: localstack/lstk Length of output: 81 🏁 Script executed: # Check start_test.go to see if it has a similar pattern
cat -n test/integration/start_test.go | grep -A20 "startTestContainer"Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Check git history to see if there were recent test failures or commits related to this
git log --oneline test/integration/stop_test.go 2>/dev/null | head -10Repository: localstack/lstk Length of output: 82 🏁 Script executed: # Look for any docker-compose files that might pre-pull images
find . -name "docker-compose*" -o -name ".dockerignore" | head -10Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Verify the docker client library version and check if ImagePull behavior is as described
rg "github.com/docker/docker" test/integration/go.modRepository: localstack/lstk Length of output: 105 🏁 Script executed: # Verify the Docker Go client ImagePull API signature
rg -A5 "func.*ImagePull" test/integration/ || echo "Checking Docker client docs..."Repository: localstack/lstk Length of output: 89 🏁 Script executed: # Check if there are any recent test runs or logs that show failures
find . -name "*test*results*" -o -name "*junit*" 2>/dev/null | head -5Repository: localstack/lstk Length of output: 41 🏁 Script executed: # Verify the proposed fix compiles by checking if the imports are correct for this version
rg "github.com/docker/docker/api/types/image" test/integration/ || echo "image package not yet imported"Repository: localstack/lstk Length of output: 89 Pull the test image before creating the container.
Proposed fix@@
import (
"context"
+ "io"
"os/exec"
"testing"
"time"
"github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/image"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@
func startTestContainer(t *testing.T, ctx context.Context) {
t.Helper()
+ reader, err := dockerClient.ImagePull(ctx, "alpine:latest", image.PullOptions{})
+ require.NoError(t, err, "failed to pull test image")
+ defer func() {
+ _ = reader.Close()
+ }()
+ _, err = io.Copy(io.Discard, reader)
+ require.NoError(t, err, "failed to read image pull output")
+
resp, err := dockerClient.ContainerCreate(ctx, &container.Config{
Image: "alpine:latest",
Cmd: []string{"sleep", "infinity"},
}, nil, nil, nil, containerName)
require.NoError(t, err, "failed to create test container")
err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{})
require.NoError(t, err, "failed to start test container")
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func TestStopCommandSucceeds(t *testing.T) { | ||
| requireDocker(t) | ||
| cleanup() | ||
| t.Cleanup(cleanup) | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| startTestContainer(t, ctx) | ||
|
|
||
| stopCmd := exec.CommandContext(ctx, binaryPath(), "stop") | ||
| output, err := stopCmd.CombinedOutput() | ||
| require.NoError(t, err, "lstk stop failed: %s", output) | ||
|
|
||
| outputStr := string(output) | ||
| assert.Contains(t, outputStr, "Stopping", "should show stopping message") | ||
| assert.Contains(t, outputStr, "stopped", "should show stopped message") | ||
|
|
||
| _, err = dockerClient.ContainerInspect(ctx, containerName) | ||
| assert.Error(t, err, "container should not exist after stop") | ||
| } | ||
|
|
||
| func TestStopCommandFailsWhenNotRunning(t *testing.T) { | ||
| requireDocker(t) | ||
| cleanup() | ||
| t.Cleanup(cleanup) | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := exec.CommandContext(ctx, binaryPath(), "stop") | ||
| output, err := cmd.CombinedOutput() | ||
|
|
||
| require.Error(t, err, "expected lstk stop to fail when container not running") | ||
| assert.Contains(t, string(output), "is not running") | ||
| } | ||
|
|
||
| func TestStopCommandIsIdempotent(t *testing.T) { | ||
| requireDocker(t) | ||
| cleanup() | ||
| t.Cleanup(cleanup) | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| startTestContainer(t, ctx) | ||
|
|
||
| stopCmd := exec.CommandContext(ctx, binaryPath(), "stop") | ||
| output, err := stopCmd.CombinedOutput() | ||
| require.NoError(t, err, "first lstk stop failed: %s", output) | ||
|
|
||
| _, err = dockerClient.ContainerInspect(ctx, containerName) | ||
| require.Error(t, err, "container should not exist after first stop") | ||
|
|
||
| stopCmd2 := exec.CommandContext(ctx, binaryPath(), "stop") | ||
| _, err = stopCmd2.CombinedOutput() | ||
| assert.Error(t, err, "second lstk stop should fail since container already removed") | ||
| } | ||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: localstack/lstk
Length of output: 1912
🏁 Script executed:
Repository: localstack/lstk
Length of output: 630
🏁 Script executed:
Repository: localstack/lstk
Length of output: 364
🏁 Script executed:
Repository: localstack/lstk
Length of output: 41
🏁 Script executed:
Repository: localstack/lstk
Length of output: 91
🏁 Script executed:
Repository: localstack/lstk
Length of output: 113
🏁 Script executed:
Repository: localstack/lstk
Length of output: 111
🏁 Script executed:
Repository: localstack/lstk
Length of output: 749
🏁 Script executed:
Repository: localstack/lstk
Length of output: 1448
🏁 Script executed:
Repository: localstack/lstk
Length of output: 840
🏁 Script executed:
Repository: localstack/lstk
Length of output: 1236
🏁 Script executed:
Repository: localstack/lstk
Length of output: 41
🏁 Script executed:
Repository: localstack/lstk
Length of output: 3568
Replace
containerd/errdefs.IsNotFound()withdocker/errdefs.IsNotFound()for Docker error compatibility.The
Remove()method inDockerRuntimereturns errors directly from the Docker client'sContainerRemove(), which returns Docker-specific error types from thedocker/errdefspackage. However, the error check at line 55 usescontainerd/errdefs.IsNotFound(), which cannot recognize Docker errors. This causes the code to fail when a container doesn't exist instead of gracefully skipping the removal, breaking fresh starts.Import
github.com/docker/docker/errdefsand useerrdefs.IsNotFound(err)from the Docker package instead of the containerd version.🤖 Prompt for AI Agents