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
3 changes: 3 additions & 0 deletions cmd/workspace/up/up_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ func (cmd *UpCmd) registerWorkspaceFlags(upCmd *cobra.Command) {
),
flags.Bool(&cmd.Reset, names.Reset, false,
"Remove and recreate existing containers, including their sources"),
flags.Bool(&cmd.NoAutoStart, names.NoAutoStart, false,
"Do not auto-start a stopped provider backend (e.g. Podman machine); "+
"report it instead"),
flags.StringSlice(&cmd.PrebuildRepositories, names.PrebuildRepo, nil,
"Docker repository hosting prebuilds for this workspace"),
flags.StringArray(&cmd.WorkspaceEnv, names.WorkspaceEnv, nil,
Expand Down
5 changes: 3 additions & 2 deletions pkg/apple/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type AppleHelper struct {
// EnsureSystemRunning starts the container system service if it is not running;
// it must be running before any container operation.
func (h *AppleHelper) EnsureSystemRunning(ctx context.Context) error {
if h.systemRunning(ctx) {
if h.SystemRunning(ctx) {
return nil
}

Expand Down Expand Up @@ -358,7 +358,8 @@ func (h *AppleHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd {
return cmd
}

func (h *AppleHelper) systemRunning(ctx context.Context) bool {
// SystemRunning reports whether the container system service is running.
func (h *AppleHelper) SystemRunning(ctx context.Context) bool {
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
out, err := h.buildCmd(cctx, "system", "status").CombinedOutput()
Expand Down
7 changes: 7 additions & 0 deletions pkg/devcontainer/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func NewRunner(
return nil, err
}

preflightOpts := driver.PreflightOptions{
DisableAutoStart: workspaceConfig.CLIOptions.NoAutoStart || driver.AutoStartDisabledByEnv(),
}
if err := driver.DriverPreflight(ctx, drv, preflightOpts); err != nil {
return nil, err
}

return &runner{
driver: drv,
agentPath: agentPath,
Expand Down
37 changes: 37 additions & 0 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,43 @@ func (r *DockerHelper) ClientVersion(ctx context.Context) string {
return strings.TrimSpace(string(out))
}

// podmanMachineStartTimeout bounds a Podman machine boot, which spins up a VM.
const podmanMachineStartTimeout = 90 * time.Second

// Ping reports whether the runtime daemon is reachable, returning its own
// message (e.g. "Cannot connect to Podman") on failure. It runs a bare `info`
// and judges reachability by exit status: `--format` field names differ
// between docker (.ServerVersion) and podman/nerdctl, so a shared template
// would falsely fail non-docker runtimes.
func (r *DockerHelper) Ping(ctx context.Context) error {
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

out, err := r.buildCmd(cctx, "info").CombinedOutput()
if err != nil {
if msg := strings.TrimSpace(string(out)); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
return err
}
return nil
}

// StartPodmanMachine starts the default Podman machine, which must already exist.
func (r *DockerHelper) StartPodmanMachine(ctx context.Context) error {
cctx, cancel := context.WithTimeout(ctx, podmanMachineStartTimeout)
defer cancel()

out, err := r.buildCmd(cctx, "machine", "start").CombinedOutput()
if err != nil {
if msg := strings.TrimSpace(string(out)); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
return err
}
return nil
}

func (r *DockerHelper) FindDevContainer(
ctx context.Context,
labels []string,
Expand Down
2 changes: 2 additions & 0 deletions pkg/driver/apple/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
// appleClient is the subset of *apple.AppleHelper the driver depends on, so its
// orchestration can be unit-tested with a mock instead of the real CLI.
type appleClient interface {
EnsureSystemRunning(ctx context.Context) error
SystemRunning(ctx context.Context) bool
EnsureBuilderRunning(ctx context.Context) error
FindDevContainer(ctx context.Context, labels []string) (*config.ContainerDetails, error)
FindContainerByID(ctx context.Context, ids []string) (*config.ContainerDetails, error)
Expand Down
45 changes: 37 additions & 8 deletions pkg/driver/apple/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package apple

import (
"context"
"errors"
"fmt"
"os/exec"
"runtime"

"github.com/devsy-org/devsy/pkg/apple"
Expand All @@ -30,12 +32,15 @@ type appleDriver struct {
containerID string // set when the workspace source is an existing container
}

var _ driver.ImageDriver = (*appleDriver)(nil)
var (
_ driver.ImageDriver = (*appleDriver)(nil)
_ driver.Preflighter = (*appleDriver)(nil)
)

// NewAppleDriver verifies the host is supported and the container system service
// is running (ctx bounds the potentially-slow `container system start`).
// NewAppleDriver verifies the host is supported and constructs the driver;
// Preflight validates the container system service.
func NewAppleDriver(
ctx context.Context,
_ context.Context,
workspaceInfo *provider.AgentWorkspaceInfo,
) (driver.ImageDriver, error) {
if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
Expand All @@ -55,10 +60,6 @@ func NewAppleDriver(
Environment: makeEnvironment(workspaceInfo.Agent.Apple.Env),
}

if err := helper.EnsureSystemRunning(ctx); err != nil {
return nil, err
}

rosetta, err := workspaceInfo.Agent.Apple.Rosetta.Bool()
if err != nil {
log.Warnf(
Expand All @@ -78,6 +79,34 @@ func NewAppleDriver(
}, nil
}

// Preflight checks the `container` binary is installed and the system service
// running, starting it unless auto-start is disabled.
func (d *appleDriver) Preflight(ctx context.Context, opts driver.PreflightOptions) error {
if _, err := exec.LookPath(d.command); err != nil {
return &driver.PreflightError{
Provider: provider.AppleDriver,
Err: fmt.Errorf("%s is not installed or not on PATH: %w", d.command, err),
}
}

if opts.DisableAutoStart {
if !d.Apple.SystemRunning(ctx) {
return &driver.PreflightError{
Provider: provider.AppleDriver,
Err: errors.New(
"container system service is not running (run `container system start`)",
),
}
}
return nil
}

if err := d.Apple.EnsureSystemRunning(ctx); err != nil {
return &driver.PreflightError{Provider: provider.AppleDriver, Err: err}
}
return nil
}

func makeEnvironment(env map[string]string) []string {
if len(env) == 0 {
return nil
Expand Down
4 changes: 4 additions & 0 deletions pkg/driver/apple/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ type mockClient struct {
calls []string // ordered event log, e.g. "stop:c1", "remove:c1"
ranArgs [][]string // exec/run argument lists, in call order
runWithDir string // dir passed to the last RunWithDir call
systemDown bool // SystemRunning reports false
ensureErr error // error returned by EnsureSystemRunning
}

func (m *mockClient) EnsureSystemRunning(context.Context) error { return m.ensureErr }
func (m *mockClient) SystemRunning(context.Context) bool { return !m.systemDown }
func (m *mockClient) EnsureBuilderRunning(context.Context) error { return nil }
func (m *mockClient) FindDevContainer(context.Context, []string) (*config.ContainerDetails, error) {
return m.found, m.foundErr
Expand Down
39 changes: 39 additions & 0 deletions pkg/driver/apple/preflight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package apple

import (
"context"
"errors"
"testing"

"github.com/devsy-org/devsy/pkg/driver"
)

func TestApplePreflightBinaryMissing(t *testing.T) {
d := &appleDriver{command: "devsy-nonexistent-container-binary", Apple: &mockClient{}}

err := d.Preflight(context.Background(), driver.PreflightOptions{})
var perr *driver.PreflightError
if !errors.As(err, &perr) {
t.Fatalf("expected *driver.PreflightError, got %v (%T)", err, err)
}
}

func TestApplePreflightAutoStart(t *testing.T) {
m := &mockClient{systemDown: true}
d := &appleDriver{command: "sh", Apple: m}

if err := d.Preflight(context.Background(), driver.PreflightOptions{}); err != nil {
t.Fatalf("Preflight with auto-start = %v, want nil", err)
}
}

func TestApplePreflightOptOutSystemDown(t *testing.T) {
m := &mockClient{systemDown: true}
d := &appleDriver{command: "sh", Apple: m}

err := d.Preflight(context.Background(), driver.PreflightOptions{DisableAutoStart: true})
var perr *driver.PreflightError
if !errors.As(err, &perr) {
t.Fatalf("expected *driver.PreflightError when opted out and system down, got %v", err)
}
}
53 changes: 53 additions & 0 deletions pkg/driver/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package docker
import (
"context"
"fmt"
"os/exec"
"runtime"

"github.com/devsy-org/devsy/pkg/compose"
Expand Down Expand Up @@ -100,8 +101,60 @@ var (
_ driver.ImageDriver = (*dockerDriver)(nil)
_ driver.ComposeDriver = (*dockerDriver)(nil)
_ driver.DockerHelperProvider = (*dockerDriver)(nil)
_ driver.Preflighter = (*dockerDriver)(nil)
)

// Preflight checks the runtime binary is installed and its daemon reachable,
// starting a stopped Podman machine unless auto-start is disabled.
func (d *dockerDriver) Preflight(ctx context.Context, opts driver.PreflightOptions) error {
return runPreflight(ctx, opts, dockerProbe{
command: d.Docker.DockerCommand,
runtime: d.Docker.GetRuntime().Name(),
lookPath: exec.LookPath,
ping: d.Docker.Ping,
start: d.Docker.StartPodmanMachine,
})
}

// dockerProbe bundles the operations runPreflight depends on so the branching
// can be exercised with fakes, without abstracting the whole DockerHelper.
type dockerProbe struct {
command string
runtime docker.RuntimeName
lookPath func(string) (string, error)
ping func(context.Context) error
start func(context.Context) error
}

func runPreflight(ctx context.Context, opts driver.PreflightOptions, p dockerProbe) error {
runtimeName := string(p.runtime)

if _, err := p.lookPath(p.command); err != nil {
return &driver.PreflightError{
Provider: runtimeName,
Err: fmt.Errorf("%s is not installed or not on PATH: %w", p.command, err),
}
}

err := p.ping(ctx)
if err == nil {
return nil
}

if p.runtime == docker.RuntimePodman && !opts.DisableAutoStart {
log.Infof(
"Podman machine is not running, attempting to start it (this may take a while)...",
)
if startErr := p.start(ctx); startErr != nil {
log.Warnf("failed to start Podman machine: %v", startErr)
} else if err = p.ping(ctx); err == nil {
return nil
}
}

return &driver.PreflightError{Provider: runtimeName, Err: err}
}

func (d *dockerDriver) TargetArchitecture(ctx context.Context, workspaceId string) (string, error) {
return runtime.GOARCH, nil
}
Expand Down
Loading
Loading