Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions internal/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/localstack/lstk/internal/auth"
"github.com/localstack/lstk/internal/awsconfig"
"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/emulator/snowflake"
"github.com/localstack/lstk/internal/endpoint"
"github.com/localstack/lstk/internal/log"
"github.com/localstack/lstk/internal/output"
Expand Down Expand Up @@ -202,16 +203,17 @@ func runPostStartSetups(ctx context.Context, sink output.Sink, containers []conf
}
}
for _, t := range uniqueEmulatorTypes {
c := firstByType[t]
resolvedHost, dnsOK := endpoint.ResolveHost(c.Port, localStackHost)
if !dnsOK {
sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: endpoint.DNSRebindNote})
}
if setup, ok := setups[t]; ok {
resolvedHost, dnsOK := endpoint.ResolveHost(firstByType[t].Port, localStackHost)
if !dnsOK {
sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: endpoint.DNSRebindNote})
}
if err := setup(ctx, sink, interactive, resolvedHost); err != nil {
return err
}
emitPostStartPointers(sink, resolvedHost, webAppURL, true)
}
emitPostStartPointers(sink, t, resolvedHost, webAppURL)
}
return nil
}
Expand All @@ -222,21 +224,37 @@ func emitAlreadyRunning(sink output.Sink, c runtime.ContainerConfig, localStackH
if !dnsOK {
sink.Emit(output.MessageEvent{Severity: output.SeverityNote, Text: endpoint.DNSRebindNote})
}
emitPostStartPointers(sink, resolvedHost, webAppURL, c.EmulatorType == config.EmulatorAWS)
emitPostStartPointers(sink, c.EmulatorType, resolvedHost, webAppURL)
}

func emitPostStartPointers(sink output.Sink, resolvedHost, webAppURL string, showTip bool) {
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: fmt.Sprintf("• Endpoint: %s", resolvedHost)})
func emitPostStartPointers(sink output.Sink, emulatorType config.EmulatorType, resolvedHost, webAppURL string) {
if sfHost := snowflake.Endpoint(resolvedHost); emulatorType == config.EmulatorSnowflake && sfHost != "" {
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: fmt.Sprintf("• Snowflake endpoint: %s", sfHost)})
} else {
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: fmt.Sprintf("• Endpoint: %s", resolvedHost)})
}
if webAppURL != "" {
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: fmt.Sprintf("• Web app: %s", strings.TrimRight(webAppURL, "/"))})
}
if showTip {
tips := []string{
if tips := tipsForType(emulatorType); len(tips) > 0 {
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: tips[rand.IntN(len(tips))]})
}
}

func tipsForType(t config.EmulatorType) []string {
switch t {
case config.EmulatorAWS:
return []string{
"> Tip: View emulator logs: lstk logs --follow",
"> Tip: View deployed resources: lstk status",
}
sink.Emit(output.MessageEvent{Severity: output.SeveritySecondary, Text: tips[rand.IntN(len(tips))]})
case config.EmulatorSnowflake:
return []string{
"> Tip: View emulator logs: lstk logs --follow",
"> Tip: Check emulator status: lstk status",
}
}
return nil
}

func pullImages(ctx context.Context, rt runtime.Runtime, sink output.Sink, tel *telemetry.Client, containers []runtime.ContainerConfig) (map[string]bool, error) {
Expand Down
37 changes: 33 additions & 4 deletions internal/container/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,54 @@ func TestEmitPostStartPointers_WithWebApp(t *testing.T) {
var out bytes.Buffer
sink := output.NewPlainSink(&out)

emitPostStartPointers(sink, "localhost.localstack.cloud:4566", "https://app.localstack.cloud/", true)
emitPostStartPointers(sink, config.EmulatorAWS, "localhost.localstack.cloud:4566", "https://app.localstack.cloud/")

got := out.String()
assert.Contains(t, got, "• Endpoint: localhost.localstack.cloud:4566\n")
assert.Contains(t, got, "• Web app: https://app.localstack.cloud\n")
assert.Contains(t, got, "> Tip:")
assert.NotContains(t, got, "• Snowflake endpoint:",
"AWS path must not show the snowflake-prefixed endpoint")
}

func TestEmitPostStartPointers_WithoutWebApp(t *testing.T) {
var out bytes.Buffer
sink := output.NewPlainSink(&out)

emitPostStartPointers(sink, "127.0.0.1:4566", "", true)
emitPostStartPointers(sink, config.EmulatorAWS, "127.0.0.1:4566", "")

got := out.String()
assert.Contains(t, got, "• Endpoint: 127.0.0.1:4566\n")
assert.Contains(t, got, "> Tip:")
}

func TestEmitPostStartPointers_Snowflake_ReplacesEndpointWithSnowflakeEndpoint(t *testing.T) {
var out bytes.Buffer
sink := output.NewPlainSink(&out)

emitPostStartPointers(sink, config.EmulatorSnowflake, "localhost.localstack.cloud:4566", "https://app.localstack.cloud/")

got := out.String()
assert.Contains(t, got, "• Snowflake endpoint: http://snowflake.localhost.localstack.cloud:4566\n")
assert.NotContains(t, got, "• Endpoint: localhost.localstack.cloud:4566",
"Snowflake should not show the bare endpoint — clients connect via the snowflake-prefixed host")
assert.Contains(t, got, "• Web app: https://app.localstack.cloud\n")
assert.Contains(t, got, "> Tip:")
}

func TestEmitPostStartPointers_Snowflake_FallsBackToBareEndpointForIPHost(t *testing.T) {
var out bytes.Buffer
sink := output.NewPlainSink(&out)

emitPostStartPointers(sink, config.EmulatorSnowflake, "127.0.0.1:4566", "")

got := out.String()
assert.Contains(t, got, "• Endpoint: 127.0.0.1:4566\n",
"falls back to bare endpoint when snowflake.<host> would be invalid")
assert.NotContains(t, got, "• Snowflake endpoint:")
assert.Contains(t, got, "> Tip:")
}

func TestSelectContainersToStart_AttachesWhenExternalContainerOnConfiguredPort(t *testing.T) {
ctrl := gomock.NewController(t)
mockRT := runtime.NewMockRuntime(ctrl)
Expand Down Expand Up @@ -175,11 +204,11 @@ func TestSelectContainersToStart_ErrorsOnEmulatorTypeMismatch(t *testing.T) {
assert.Contains(t, got, "docker stop localstack-aws")
}

func TestEmitPostStartPointers_NoTip(t *testing.T) {
func TestEmitPostStartPointers_UnknownEmulator_NoTip(t *testing.T) {
var out bytes.Buffer
sink := output.NewPlainSink(&out)

emitPostStartPointers(sink, "localhost.localstack.cloud:4566", "https://app.localstack.cloud/", false)
emitPostStartPointers(sink, config.EmulatorType("other"), "localhost.localstack.cloud:4566", "https://app.localstack.cloud/")

got := out.String()
assert.Contains(t, got, "• Endpoint: localhost.localstack.cloud:4566\n")
Expand Down
15 changes: 15 additions & 0 deletions internal/emulator/snowflake/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package snowflake

import "net"

func Endpoint(resolvedHost string) string {
host, _, err := net.SplitHostPort(resolvedHost)
if err != nil {
return ""
}
if net.ParseIP(host) != nil {
// Returns "" when resolvedHost is an IP, since prepending a subdomain to an IP is invalid.
return ""
}
return "http://snowflake." + resolvedHost
}
25 changes: 25 additions & 0 deletions internal/emulator/snowflake/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package snowflake

import "testing"

func TestEndpoint(t *testing.T) {
tests := []struct {
name string
resolvedHost string
want string
}{
{"hostname with port", "localhost.localstack.cloud:4566", "http://snowflake.localhost.localstack.cloud:4566"},
{"custom port", "localhost.localstack.cloud:4567", "http://snowflake.localhost.localstack.cloud:4567"},
{"ipv4 host", "127.0.0.1:4566", ""},
{"ipv6 host", "[::1]:4566", ""},
{"missing port", "localhost.localstack.cloud", ""},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Endpoint(tt.resolvedHost); got != tt.want {
t.Errorf("Endpoint(%q) = %q, want %q", tt.resolvedHost, got, tt.want)
}
})
}
}
9 changes: 8 additions & 1 deletion test/integration/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ func TestStartCommandSucceedsForSnowflake(t *testing.T) {
configFile := writeSnowflakeConfig(t, hostPort)

ctx := testContext(t)
_, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "--config", configFile, "start")
stdout, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "--config", configFile, "start")
require.NoError(t, err, "lstk start failed: %s", stderr)
requireExitCode(t, 0, err)

Expand All @@ -635,4 +635,11 @@ func TestStartCommandSucceedsForSnowflake(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() })
assert.Equal(t, http.StatusOK, resp.StatusCode)

assert.Contains(t, stdout, "• Snowflake endpoint: http://snowflake.",
"snowflake start should print the snowflake-prefixed endpoint hint")
assert.NotContains(t, stdout, "• Endpoint: localhost.localstack.cloud",
"snowflake start should not print the bare AWS-style endpoint line")
assert.Contains(t, stdout, "> Tip:",
"snowflake start should print a tip line like AWS does")
}
Loading