Help us shape the future of Go! Take the 2026 Go Developer Survey

runner

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

relationships:

references: [runner, ../../lifecycle/lifecycle]

Package runner executes a pkg/lifecycle.Plan against an already-running container, abstracted from the transport that talks to that container.

The transport — docker exec, the apiserver pod-exec subresource, an SSH session, an in-process shim for tests — is supplied by the embedder as an Execer. The runner owns the parts that are identical across transports: marker-file gating (WhenOnce / WhenStart / WhenAttach), Shell / Args / Parallel command-form dispatch, per-parallel-command output buffering, and error formatting. Those behaviors mirror upstream's src/spec-common/injectHeadless.ts so a container created by either host stays compatible with the upstream devcontainer CLI.

Both pkg/host/docker and pkg/host/kubernetes use this package internally — the docker host adapts the Execer onto docker exec, the kubernetes host adapts it onto the apiserver pod-exec subresource.

Embedding

An operator that wants to drive lifecycle commands itself can plug in any Execer:

plan := lifecycle.Plan(dc, features)
rt := runner.Runtime{
    User:           dc.RemoteUser,
    Workdir:        containerWorkspace,
    CreatedAt:      pod.CreationTimestamp.UTC().Format(time.RFC3339Nano),
    StartedAt:      pod.Status.StartTime.UTC().Format(time.RFC3339Nano),
    UserDataFolder: lifecycle.UserDataFolder(homeDir, ""),
}
err := runner.Run(ctx, myK8sExecer{client: cs, pod: pod}, plan, rt, runner.Options{
    Logger: klog.Infof,
    Stdout: os.Stdout,
    Stderr: os.Stderr,
})

relationships:

references: [env, ../../../reference/cli/src/spec-common/injectHeadless]

relationships:

references: [probe, ../../../reference/cli/src/spec-common/injectHeadless]

Index

Constants

View Source
const DefaultDotfilesTargetPath = "~/dotfiles"

DefaultDotfilesTargetPath is upstream's --dotfiles-target-path default.

View Source
const DefaultUserEnvProbe = "loginInteractiveShell"

Variables

This section is empty.

Functions

func DetectContainerEnv

func DetectContainerEnv(ctx context.Context, execer Execer) (map[string]string, error)

DetectContainerEnv reads /proc/1/environ inside the container to find the effective environment available to lifecycle commands. The result is suitable for ${containerEnv:VAR} substitution via pkg/substitute.ContainerSubstitute.

Always reads as root since /proc/<pid>/environ is mode 0400.

func DetectHome

func DetectHome(ctx context.Context, execer Execer, user string) (string, error)

DetectHome runs `getent passwd <user>` (or returns "/root" for root) to find a user's home directory inside the container. The home directory is the basis for the marker file paths under ~/.devcontainer.

Falls back to "/home/<user>" when getent isn't available or the user isn't in /etc/passwd — matching the behavior in pkg/host/docker so containers built from minimal base images (alpine, distroless variants) still get marker files at a predictable path.

func EnvFromList

func EnvFromList(values []string) map[string]string

EnvFromList decodes the inspected container environment, as upstream's envListToObj does. PID 1 can add variables that are not container settings.

func LifecycleEnv

func LifecycleEnv(probed, remote, config, secrets map[string]string) map[string]string

LifecycleEnv layers the environment of exec, lifecycle commands and the dotfiles install in upstream's order: probed shell environment, caller remote environment (--remote-env), configuration remoteEnv, then secrets (--secrets-file). A later layer wins for the same name. Lifecycle callers keep the result in Runtime.Env only, so secrets reach exec environments without being stored.

func PatchSystemEnv

func PatchSystemEnv(ctx context.Context, execer Execer, folder string, env map[string]string) error

PatchSystemEnv applies upstream's setupInContainer patches in order. Each marker is created before its edit, with noclobber, so a container is patched only once. A marker that cannot be created skips the edit, as upstream does.

func ProbeUserEnv

func ProbeUserEnv(ctx context.Context, execer Execer, configured, fallback, user string, containerEnv map[string]string, logf func(string, ...any), options ProbeOptions) map[string]string

ProbeUserEnv reads the user's shell environment inside the container. Configuration wins over the caller's default. Failures are non-fatal, as in the upstream CLI.

func Run

func Run(ctx context.Context, execer Execer, plan []lifecycle.Phase, rt Runtime, opts Options) error

Run executes plan against execer in order, observing markers to skip phases that have already run. The first phase that returns a non-nil error short-circuits with a wrapped error identifying the hook and origin.

func ValidUserEnvProbe

func ValidUserEnvProbe(mode string) bool

Types

type Dotfiles

type Dotfiles struct {
	// Repository is a Git URL or container path. A value without ":" that
	// does not start with "/", "./" or "../" is a GitHub owner/repository.
	Repository string
	// InstallCommand runs in the clone. Empty runs the first file of
	// install.sh, install, bootstrap.sh, bootstrap, script/bootstrap,
	// setup.sh, setup, script/setup; without one, dotfiles are linked to ~.
	InstallCommand string
	// TargetPath is the clone location; empty means DefaultDotfilesTargetPath.
	// The shell expands it, so "~" names the remote user's home.
	TargetPath string
}

Dotfiles configures the personal dotfiles install that upstream runs after postCreateCommand and before postStartCommand. A zero Repository installs nothing.

type DotfilesOutcome

type DotfilesOutcome string

DotfilesOutcome reports what the dotfiles step did in one run.

const (
	// DotfilesNotRun means no repository was configured or a lifecycle
	// command failed before the dotfiles step.
	DotfilesNotRun DotfilesOutcome = ""
	// DotfilesInstalled means the clone and install command succeeded. If
	// the marker could not be written, a warning says the next run installs
	// again.
	DotfilesInstalled DotfilesOutcome = "installed"
	// DotfilesSkipped means the completion marker of an earlier successful
	// install exists.
	DotfilesSkipped DotfilesOutcome = "skipped"
	// DotfilesFailed means the install failed. Like upstream, the failure
	// does not stop the lifecycle; the runner logs a warning.
	DotfilesFailed DotfilesOutcome = "failed"
)

type ExecRequest

type ExecRequest struct {
	// TerminalSize allocates a lifecycle PTY of [rows, columns]; nil uses pipes.
	TerminalSize *[2]uint
	// Cmd is the argv passed to the container. Lifecycle string
	// commands are wrapped with /bin/sh -c by the runner before they
	// reach the Execer.
	Cmd []string
	// User is the in-container user to run as ("uid[:gid]" or a
	// username). Empty means the container's default.
	User string
	// WorkDir is the cwd inside the container. Empty means the
	// container's default.
	WorkDir string
	// Env is layered on top of the container's existing environment
	// for this exec.
	Env map[string]string
	// Stdin / Stdout / Stderr are forwarded to/from the exec session.
	// nil stdin sends EOF immediately. nil stdout/stderr signals the
	// Execer that the runner wants captured output in the result.
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

ExecRequest is the transport-agnostic exec call.

type ExecResult

type ExecResult struct {
	ExitCode int
	// Stdout / Stderr are populated only when the corresponding writer
	// in the request was nil.
	Stdout []byte
	Stderr []byte
}

ExecResult reports the outcome.

type Execer

type Execer interface {
	Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)
}

Execer is the per-container transport. Implementations bind a "where to run" (a docker container ID, a pod+container name, an SSH host) and translate ExecRequest into the appropriate underlying call.

Implementations must:

  • return ExecResult.ExitCode = 0 only when the command succeeded;
  • capture stdout/stderr into the ExecResult buffers when the corresponding Stdout/Stderr writer in the request is nil (the runner relies on captured stdout for the home-directory and container-env probes);
  • stream to Stdout/Stderr when the writers are non-nil (the runner uses this for lifecycle command output, where the user wants to see progress live).

Exec must release its connection and stream readers before returning on context cancellation. ProbeUserEnv cancels its exec after the upstream 10-second limit; lifecycle commands use the caller's context without a limit.

type LifecycleControls

type LifecycleControls struct {
	SkipNonBlocking        bool
	Prebuild               bool
	StopForPersonalization bool
	WaitFor                string
}

LifecycleControls select upstream lifecycle stop points. Later commands are omitted, not started in the background. WaitFor comes from merged metadata.

type Options

type Options struct {
	// TerminalSize supplies the lifecycle PTY size [rows, columns].
	TerminalSize *[2]uint
	// Logger receives informational messages ("running postCreate
	// [from devcontainer.json]", "skipping ... marker up to date").
	// nil discards.
	Logger func(format string, args ...any)
	// Stdout / Stderr are the default sinks for lifecycle command
	// output. Per-parallel-command output is captured and replayed
	// here under a banner — no interleaving — matching upstream's
	// printMode='off' behavior.
	Stdout io.Writer
	Stderr io.Writer
}

Options configure logging and default output streams.

type PhaseResult

type PhaseResult struct {
	Phase lifecycle.Phase
	Ran   bool
}

PhaseResult reports whether a planned phase executed or was skipped by marker checks.

type ProbeOptions

type ProbeOptions struct {
	CanPatchSystemConfig bool
	// contains filtered or unexported fields
}

ProbeOptions identifies whether root execution and system configuration changes are available. Docker and Compose provide both, as upstream's CLI.

type RunResult

type RunResult struct {
	// Result is the terminal reason; empty if the lifecycle run failed.
	Result StopReason
	Phases []PhaseResult
	// Dotfiles reports the dotfiles step; a failure there is not an error.
	Dotfiles DotfilesOutcome
}

RunResult is the structured result of a lifecycle run.

func RunWithResult

func RunWithResult(ctx context.Context, execer Execer, plan []lifecycle.Phase, rt Runtime, opts Options) (*RunResult, error)

RunWithResult is Run plus executed/skipped phases and the successful stop reason.

func (RunResult) RanPhases

func (r RunResult) RanPhases() []lifecycle.Phase

RanPhases returns only phases whose lifecycle command actually executed.

type Runtime

type Runtime struct {
	// User is the lifecycle command user (typically dc.RemoteUser).
	// May be empty: the Execer falls back to the container's default.
	User string
	// Workdir is the cwd lifecycle commands run in.
	Workdir string
	// Env is layered on top of the container env for every lifecycle
	// exec.
	Env map[string]string
	// CreatedAt is the ISO-8601 timestamp marker for WhenOnce phases.
	// On docker, this is ContainerJSON.Created; on kubernetes,
	// pod.CreationTimestamp serialized to RFC3339Nano. Any stable
	// per-creation string works as long as it changes when the
	// container is recreated.
	CreatedAt string
	// StartedAt is the marker for WhenStart phases. Docker uses
	// State.StartedAt; kubernetes uses pod.Status.StartTime. Must
	// change every time the container restarts so postStart re-runs.
	StartedAt string
	// UserDataFolder is the in-container directory that holds the
	// per-hook marker files. Use lifecycle.UserDataFolder(homeDir, "")
	// to get the default.
	UserDataFolder string
	// Dotfiles, when its Repository is set, installs the user's dotfiles
	// after postCreateCommand, once per container. Leave it zero when
	// lifecycle commands are skipped, as upstream does.
	Dotfiles Dotfiles
	// Lifecycle selects stop points and prebuild marker behavior.
	Lifecycle LifecycleControls
}

Runtime is the per-container state the runner needs to gate phases idempotently. The host package fills it in once after start; the runner reads it.

type StopReason

type StopReason string

StopReason identifies the successful terminal boundary, independent of whether commands were configured or skipped by markers. Values match the upstream CLI.

const (
	SkipNonBlocking        StopReason = "skipNonBlocking"
	Prebuild               StopReason = "prebuild"
	StopForPersonalization StopReason = "stopForPersonalization"
	Done                   StopReason = "done"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL