Skip to content
Closed
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
26 changes: 14 additions & 12 deletions internal/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,12 @@ func pullImages(ctx context.Context, rt runtime.Runtime, sink output.Sink, conta
return fmt.Errorf("failed to remove existing container %s: %w", c.Name, err)
}

output.EmitStatus(sink, "pulling", c.Image, "")
progress := make(chan runtime.PullProgress)
go func() {
for p := range progress {
output.EmitProgress(sink, c.Image, p.LayerID, p.Status, p.Current, p.Total)
}
}()
if err := rt.PullImage(ctx, c.Image, progress); err != nil {
output.EmitSpinnerStart(sink, fmt.Sprintf("Pulling %s", c.Image))
if err := rt.PullImage(ctx, c.Image, nil); err != nil {
output.EmitSpinnerStop(sink)
return fmt.Errorf("failed to pull image %s: %w", c.Image, err)
}
output.EmitSpinnerStop(sink)
}
return nil
}
Expand All @@ -119,19 +115,21 @@ func validateLicenses(ctx context.Context, rt runtime.Runtime, sink output.Sink,

func startContainers(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []runtime.ContainerConfig) error {
for _, c := range containers {
output.EmitStatus(sink, "starting", c.Name, "")
output.EmitSpinnerStart(sink, "Starting LocalStack")
containerID, err := rt.Start(ctx, c)
if err != nil {
output.EmitSpinnerStop(sink)
return fmt.Errorf("failed to start %s: %w", c.Name, err)
}

output.EmitStatus(sink, "waiting", c.Name, "")
healthURL := fmt.Sprintf("http://localhost:%s%s", c.Port, c.HealthPath)
if err := awaitStartup(ctx, rt, sink, containerID, c.Name, healthURL); err != nil {
output.EmitSpinnerStop(sink)
return err
}

output.EmitStatus(sink, "ready", c.Name, fmt.Sprintf("containerId: %s", containerID[:12]))
output.EmitSpinnerStop(sink)
output.EmitStatus(sink, "ready", c.Name, "")
}
return nil
}
Expand Down Expand Up @@ -169,7 +167,7 @@ func validateLicense(ctx context.Context, rt runtime.Runtime, sink output.Sink,
version = actualVersion
}

output.EmitStatus(sink, "validating license", containerConfig.Name, version)
output.EmitSpinnerStart(sink, "Validating license")

hostname, _ := os.Hostname()
licenseReq := &api.LicenseRequest{
Expand All @@ -188,9 +186,13 @@ func validateLicense(ctx context.Context, rt runtime.Runtime, sink output.Sink,
}

if err := platformClient.GetLicense(ctx, licenseReq); err != nil {
output.EmitSpinnerStop(sink)
return fmt.Errorf("license validation failed for %s:%s: %w", containerConfig.ProductName, version, err)
}

output.EmitSpinnerStop(sink)
output.EmitStatus(sink, "valid license", containerConfig.Name, version)

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/output/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (f SinkFunc) emit(event any) {
}

type ContainerStatusEvent struct {
Phase string // e.g., "pulling", "starting", "waiting", "ready"
Phase string // e.g., "valid license", "ready"
Container string
Detail string // optional extra info (e.g., container ID)
}
Expand Down
28 changes: 6 additions & 22 deletions internal/output/plain_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func FormatEventLine(event any) (string, bool) {
case ContainerStatusEvent:
return formatStatusLine(e), true
case ProgressEvent:
return formatProgressLine(e)
return "", false
case UserInputRequestEvent:
return formatUserInputRequest(e), true
case ContainerLogLineEvent:
Expand All @@ -32,19 +32,14 @@ func FormatEventLine(event any) (string, bool) {
}
}

const successPrefix = "\033[32m✓\033[0m "

func formatStatusLine(e ContainerStatusEvent) string {
switch e.Phase {
case "pulling":
return fmt.Sprintf("Pulling %s...", e.Container)
case "starting":
return fmt.Sprintf("Starting %s...", e.Container)
case "waiting":
return fmt.Sprintf("Waiting for %s to be ready...", e.Container)
case "valid license":
return successPrefix + "License activated"
case "ready":
if e.Detail != "" {
return fmt.Sprintf("%s ready (%s)", e.Container, e.Detail)
}
return fmt.Sprintf("%s ready", e.Container)
return successPrefix + "LocalStack ready"
default:
if e.Detail != "" {
return fmt.Sprintf("%s: %s (%s)", e.Container, e.Phase, e.Detail)
Expand All @@ -53,17 +48,6 @@ func formatStatusLine(e ContainerStatusEvent) string {
}
}

func formatProgressLine(e ProgressEvent) (string, bool) {
if e.Total > 0 {
pct := float64(e.Current) / float64(e.Total) * 100
return fmt.Sprintf(" %s: %s %.1f%%", e.LayerID, e.Status, pct), true
}
if e.Status != "" {
return fmt.Sprintf(" %s: %s", e.LayerID, e.Status), true
}
return "", false
}

func formatUserInputRequest(e UserInputRequestEvent) string {
return FormatPrompt(e.Prompt, e.Options)
}
Expand Down
26 changes: 10 additions & 16 deletions internal/output/plain_format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,25 @@ func TestFormatEventLine(t *testing.T) {
wantOK: true,
},
{
name: "status pulling",
event: ContainerStatusEvent{Phase: "pulling", Container: "localstack/localstack:latest"},
want: "Pulling localstack/localstack:latest...",
name: "status ready",
event: ContainerStatusEvent{Phase: "ready", Container: "localstack"},
want: "\033[32m✓\033[0m LocalStack ready",
wantOK: true,
},
{
name: "status ready with detail",
event: ContainerStatusEvent{Phase: "ready", Container: "localstack", Detail: "abc123"},
want: "localstack ready (abc123)",
wantOK: true,
},
{
name: "progress with total",
name: "progress suppressed with total",
event: ProgressEvent{LayerID: "abc123", Status: "Downloading", Current: 50, Total: 100},
want: " abc123: Downloading 50.0%",
wantOK: true,
want: "",
wantOK: false,
},
{
name: "progress with status only",
name: "progress suppressed with status only",
event: ProgressEvent{LayerID: "abc123", Status: "Pull complete"},
want: " abc123: Pull complete",
wantOK: true,
want: "",
wantOK: false,
},
{
name: "progress ignored when empty",
name: "progress suppressed when empty",
event: ProgressEvent{LayerID: "abc123"},
want: "",
wantOK: false,
Expand Down
86 changes: 14 additions & 72 deletions internal/output/plain_sink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,9 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) {
expected string
}{
{
name: "pulling phase",
event: ContainerStatusEvent{Phase: "pulling", Container: "localstack/localstack:latest"},
expected: "Pulling localstack/localstack:latest...\n",
},
{
name: "starting phase",
event: ContainerStatusEvent{Phase: "starting", Container: "localstack"},
expected: "Starting localstack...\n",
},
{
name: "waiting phase",
event: ContainerStatusEvent{Phase: "waiting", Container: "localstack"},
expected: "Waiting for localstack to be ready...\n",
},
{
name: "ready phase with detail",
event: ContainerStatusEvent{Phase: "ready", Container: "localstack", Detail: "abc123"},
expected: "localstack ready (abc123)\n",
},
{
name: "ready phase without detail",
name: "ready phase",
event: ContainerStatusEvent{Phase: "ready", Container: "localstack"},
expected: "localstack ready\n",
expected: "\033[32m✓\033[0m LocalStack ready\n",
},
{
name: "unknown phase with detail",
Expand All @@ -90,54 +70,19 @@ func TestPlainSink_EmitsStatusEvent(t *testing.T) {
}
}

func TestPlainSink_EmitsProgressEvent(t *testing.T) {
tests := []struct {
name string
event ProgressEvent
expected string
}{
{
name: "with total (percentage)",
event: ProgressEvent{
Container: "localstack",
LayerID: "abc123",
Status: "Downloading",
Current: 50,
Total: 100,
},
expected: " abc123: Downloading 50.0%\n",
},
{
name: "without total (status only)",
event: ProgressEvent{
Container: "localstack",
LayerID: "abc123",
Status: "Pull complete",
Current: 0,
Total: 0,
},
expected: " abc123: Pull complete\n",
},
{
name: "no status and no total",
event: ProgressEvent{
Container: "localstack",
LayerID: "abc123",
},
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out bytes.Buffer
sink := NewPlainSink(&out)
func TestPlainSink_SuppressesProgressEvent(t *testing.T) {
var out bytes.Buffer
sink := NewPlainSink(&out)

Emit(sink, tt.event)
Emit(sink, ProgressEvent{
Container: "localstack",
LayerID: "abc123",
Status: "Downloading",
Current: 50,
Total: 100,
})

assert.Equal(t, tt.expected, out.String())
})
}
assert.Equal(t, "", out.String())
}

func TestPlainSink_EmitsContainerLogLineEvent(t *testing.T) {
Expand Down Expand Up @@ -222,8 +167,7 @@ func TestPlainSink_UsesFormatterParity(t *testing.T) {
AuthEvent{Code: "ABC123", URL: "https://example.com"},
SpinnerEvent{Active: true, Text: "Loading"},
ErrorEvent{Title: "Failed", Summary: "Something broke"},
ContainerStatusEvent{Phase: "starting", Container: "localstack"},
ProgressEvent{LayerID: "abc", Status: "Downloading", Current: 1, Total: 2},
ContainerStatusEvent{Phase: "ready", Container: "localstack"},
}

for _, event := range events {
Expand All @@ -241,8 +185,6 @@ func TestPlainSink_UsesFormatterParity(t *testing.T) {
Emit(sink, e)
case ContainerStatusEvent:
Emit(sink, e)
case ProgressEvent:
Emit(sink, e)
default:
t.Fatalf("unsupported event type in test: %T", event)
}
Expand Down