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
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ var rootCmd = &cobra.Command{
}

if err := runStart(cmd.Context(), rt); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
if !output.IsSilent(err) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
os.Exit(1)
}
},
Expand Down
5 changes: 4 additions & 1 deletion cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"

"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/runtime"
"github.com/spf13/cobra"
)
Expand All @@ -21,7 +22,9 @@ var startCmd = &cobra.Command{
}

if err := runStart(cmd.Context(), rt); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
if !output.IsSilent(err) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
os.Exit(1)
}
},
Expand Down
5 changes: 5 additions & 0 deletions internal/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import (
)

func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, platformClient api.PlatformAPI, interactive bool) error {
if err := rt.IsHealthy(ctx); err != nil {
rt.EmitUnhealthyError(sink, err)
return output.NewSilentError(fmt.Errorf("runtime not healthy: %w", err))
}

tokenStorage, err := auth.NewTokenStorage()
if err != nil {
return fmt.Errorf("failed to initialize token storage: %w", err)
Expand Down
29 changes: 29 additions & 0 deletions internal/container/start_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package container

import (
"context"
"errors"
"io"
"testing"

"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/runtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

func TestStart_ReturnsEarlyIfRuntimeUnhealthy(t *testing.T) {
ctrl := gomock.NewController(t)
mockRT := runtime.NewMockRuntime(ctrl)
mockRT.EXPECT().IsHealthy(gomock.Any()).Return(errors.New("cannot connect to Docker daemon"))
mockRT.EXPECT().EmitUnhealthyError(gomock.Any(), gomock.Any())

sink := output.NewPlainSink(io.Discard)

err := Start(context.Background(), mockRT, sink, nil, false)

require.Error(t, err)
assert.Contains(t, err.Error(), "runtime not healthy")
assert.True(t, output.IsSilent(err), "error should be silent since it was already emitted")
}
28 changes: 28 additions & 0 deletions internal/output/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package output

import "errors"

// SilentError wraps an error that has already been displayed to the user
// through the sink mechanism. Callers should check for this type and skip
// printing the error again.
type SilentError struct {
Err error
}

func (e *SilentError) Error() string {
return e.Err.Error()
}

func (e *SilentError) Unwrap() error {
return e.Err
}

func NewSilentError(err error) *SilentError {
return &SilentError{Err: err}
}

// IsSilent returns true if the error (or any error in its chain) is a SilentError.
func IsSilent(err error) bool {
var silent *SilentError
return errors.As(err, &silent)
}
4 changes: 2 additions & 2 deletions internal/output/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ func EmitNote(sink Sink, text string) {
Emit(sink, MessageEvent{Severity: SeverityNote, Text: text})
}

func EmitWarning(sink Sink, message string) {
Emit(sink, MessageEvent{Severity: SeverityWarning, Text: message})
func EmitWarning(sink Sink, text string) {
Emit(sink, MessageEvent{Severity: SeverityWarning, Text: text})
}

func EmitStatus(sink Sink, phase, container, detail string) {
Expand Down
29 changes: 29 additions & 0 deletions internal/runtime/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log"
stdruntime "runtime"
"strconv"
"strings"

Expand All @@ -15,6 +16,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/localstack/lstk/internal/output"
)

// DockerRuntime implements Runtime using the Docker API.
Expand All @@ -30,6 +32,33 @@ func NewDockerRuntime() (*DockerRuntime, error) {
return &DockerRuntime{client: cli}, nil
}

func (d *DockerRuntime) IsHealthy(ctx context.Context) error {
_, err := d.client.Ping(ctx)
if err != nil {
return fmt.Errorf("cannot connect to Docker daemon: %w", err)
}
return nil
}

func (d *DockerRuntime) EmitUnhealthyError(sink output.Sink, err error) {
actions := []output.ErrorAction{
{Label: "Install Docker:", Value: "https://docs.docker.com/get-docker/"},
}
switch stdruntime.GOOS {
case "darwin":
actions = append([]output.ErrorAction{{Label: "Start Docker Desktop:", Value: "open -a Docker"}}, actions...)
case "linux":
actions = append([]output.ErrorAction{{Label: "Start Docker:", Value: "sudo systemctl start docker"}}, actions...)
case "windows":
actions = append([]output.ErrorAction{{Label: "Start Docker Desktop:", Value: "Start-Process 'Docker Desktop'"}}, actions...)
}
output.EmitError(sink, output.ErrorEvent{
Title: "Docker is not available",
Summary: err.Error(),
Actions: actions,
})
}

func (d *DockerRuntime) PullImage(ctx context.Context, imageName string, progress chan<- PullProgress) error {
reader, err := d.client.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
Expand Down
185 changes: 185 additions & 0 deletions internal/runtime/mock_runtime.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package runtime
import (
"context"
"io"

"github.com/localstack/lstk/internal/output"
)

type ContainerConfig struct {
Expand All @@ -24,6 +26,8 @@ type PullProgress struct {

// Runtime abstracts container runtime operations (Docker, Podman, Kubernetes, etc.)
type Runtime interface {
IsHealthy(ctx context.Context) error
EmitUnhealthyError(sink output.Sink, err error)
PullImage(ctx context.Context, image string, progress chan<- PullProgress) error
Start(ctx context.Context, config ContainerConfig) (string, error)
Stop(ctx context.Context, containerName string) error
Expand Down