Skip to content

Commit 269272a

Browse files
Fix status command failing with non-default port (#132)
1 parent df59674 commit 269272a

File tree

6 files changed

+101
-4
lines changed

6 files changed

+101
-4
lines changed

internal/container/status.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ func Status(ctx context.Context, rt runtime.Runtime, containers []config.Contain
3939
return output.NewSilentError(fmt.Errorf("%s is not running", name))
4040
}
4141

42-
host, _ := endpoint.ResolveHost(c.Port, localStackHost)
42+
// status makes direct HTTP calls to LocalStack, so it needs the actual host port.
43+
// Ask Docker rather than trusting the config: the user may have changed the config
44+
// port while the container still runs on the old one.
45+
port := c.Port
46+
if containerPort, err := c.ContainerPort(); err == nil {
47+
if actualPort, err := rt.GetBoundPort(ctx, name, containerPort); err == nil {
48+
port = actualPort
49+
}
50+
}
51+
host, _ := endpoint.ResolveHost(port, localStackHost)
4352

4453
var uptime time.Duration
4554
if startedAt, err := rt.ContainerStartedAt(ctx, name); err == nil {

internal/runtime/docker.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,18 @@ func (d *DockerRuntime) StreamLogs(ctx context.Context, containerID string, out
284284
return nil
285285
}
286286

287+
func (d *DockerRuntime) GetBoundPort(ctx context.Context, containerName string, containerPort string) (string, error) {
288+
inspect, err := d.client.ContainerInspect(ctx, containerName)
289+
if err != nil {
290+
return "", fmt.Errorf("failed to inspect container: %w", err)
291+
}
292+
bindings, ok := inspect.NetworkSettings.Ports[nat.Port(containerPort)]
293+
if !ok || len(bindings) == 0 {
294+
return "", fmt.Errorf("no binding found for port %s on container %s", containerPort, containerName)
295+
}
296+
return bindings[0].HostPort, nil
297+
}
298+
287299
func (d *DockerRuntime) GetImageVersion(ctx context.Context, imageName string) (string, error) {
288300
inspect, err := d.client.ImageInspect(ctx, imageName)
289301
if err != nil {

internal/runtime/mock_runtime.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/runtime/runtime.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,7 @@ type Runtime interface {
5555
Logs(ctx context.Context, containerID string, tail int) (string, error)
5656
StreamLogs(ctx context.Context, containerID string, out io.Writer, follow bool) error
5757
GetImageVersion(ctx context.Context, imageName string) (string, error)
58+
// GetBoundPort returns the host port bound to the given container port (e.g. "4566/tcp").
59+
GetBoundPort(ctx context.Context, containerName string, containerPort string) (string, error)
5860
SocketPath() string
5961
}

test/integration/main_test.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/docker/docker/api/types/container"
2222
"github.com/docker/docker/api/types/image"
2323
"github.com/docker/docker/client"
24+
"github.com/docker/go-connections/nat"
2425
"github.com/localstack/lstk/test/integration/env"
2526
"github.com/stretchr/testify/require"
2627
"github.com/zalando/go-keyring"
@@ -169,18 +170,34 @@ const (
169170
testImage = "alpine:latest"
170171
)
171172

172-
func startTestContainer(t *testing.T, ctx context.Context) {
173+
// startTestContainer starts the test container with no port bindings by default.
174+
// Pass hostPort to bind 4566/tcp to a specific host port (e.g. to test that lstk status
175+
// uses the actual bound port rather than the port from config).
176+
func startTestContainer(t *testing.T, ctx context.Context, hostPort ...string) {
173177
t.Helper()
174178

175179
reader, err := dockerClient.ImagePull(ctx, testImage, image.PullOptions{})
176180
require.NoError(t, err, "failed to pull test image")
177181
_, _ = io.Copy(io.Discard, reader)
178182
_ = reader.Close()
179183

180-
resp, err := dockerClient.ContainerCreate(ctx, &container.Config{
184+
cfg := &container.Config{
181185
Image: testImage,
182186
Cmd: []string{"sleep", "infinity"},
183-
}, nil, nil, nil, containerName)
187+
}
188+
var hostCfg *container.HostConfig
189+
if len(hostPort) > 0 {
190+
const containerPort = nat.Port("4566/tcp")
191+
cfg.ExposedPorts = nat.PortSet{containerPort: struct{}{}}
192+
hostCfg = &container.HostConfig{
193+
PortBindings: nat.PortMap{
194+
// 127.0.0.2 avoids conflicting with the mock HTTP server on 127.0.0.1:hostPort.
195+
containerPort: []nat.PortBinding{{HostIP: "127.0.0.2", HostPort: hostPort[0]}},
196+
},
197+
}
198+
}
199+
200+
resp, err := dockerClient.ContainerCreate(ctx, cfg, hostCfg, nil, nil, containerName)
184201
require.NoError(t, err, "failed to create test container")
185202
err = dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{})
186203
require.NoError(t, err, "failed to start test container")

test/integration/status_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package integration_test
22

33
import (
44
"fmt"
5+
"net"
56
"net/http"
67
"net/http/httptest"
8+
"os"
9+
"path/filepath"
710
"strings"
811
"testing"
912

@@ -66,6 +69,45 @@ func TestStatusCommandShowsResourcesWhenRunning(t *testing.T) {
6669
assert.Contains(t, stdout, "my-function")
6770
}
6871

72+
func TestStatusCommandWorksWithNonDefaultPort(t *testing.T) {
73+
requireDocker(t)
74+
cleanup()
75+
t.Cleanup(cleanup)
76+
77+
ctx := testContext(t)
78+
79+
// The mock server is assigned a random free port (guaranteed not to conflict).
80+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81+
switch r.URL.Path {
82+
case "/_localstack/health":
83+
w.Header().Set("Content-Type", "application/json")
84+
_, _ = fmt.Fprintln(w, `{"version": "4.14.1", "services": {}}`)
85+
case "/_localstack/resources":
86+
w.Header().Set("Content-Type", "application/x-ndjson")
87+
default:
88+
w.WriteHeader(http.StatusNotFound)
89+
}
90+
}))
91+
defer server.Close()
92+
93+
// Extract the port so we can bind it to the container.
94+
_, mockPort, err := net.SplitHostPort(server.Listener.Addr().String())
95+
require.NoError(t, err)
96+
97+
// Simulates starting LocalStack on a non-default host port.
98+
startTestContainer(t, ctx, mockPort)
99+
100+
// Write a config with the default port 4566
101+
// Simulates the user changing the config port after starting the container
102+
configContent := "[[containers]]\ntype = \"aws\"\ntag = \"latest\"\nport = \"4566\"\n"
103+
configFile := filepath.Join(t.TempDir(), "config.toml")
104+
require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0644))
105+
106+
stdout, stderr, err := runLstk(t, ctx, "", nil, "--config", configFile, "status")
107+
require.NoError(t, err, "lstk status failed: %s", stderr)
108+
assert.Contains(t, stdout, "4.14.1")
109+
}
110+
69111
func TestStatusCommandShowsNoResourcesWhenEmpty(t *testing.T) {
70112
requireDocker(t)
71113
cleanup()

0 commit comments

Comments
 (0)