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
- func DetectContainerEnv(ctx context.Context, execer Execer) (map[string]string, error)
- func DetectHome(ctx context.Context, execer Execer, user string) (string, error)
- func EnvFromList(values []string) map[string]string
- func LifecycleEnv(probed, remote, config, secrets map[string]string) map[string]string
- func PatchSystemEnv(ctx context.Context, execer Execer, folder string, env map[string]string) error
- func ProbeUserEnv(ctx context.Context, execer Execer, configured, fallback, user string, ...) map[string]string
- func Run(ctx context.Context, execer Execer, plan []lifecycle.Phase, rt Runtime, ...) error
- func ValidUserEnvProbe(mode string) bool
- type Dotfiles
- type DotfilesOutcome
- type ExecRequest
- type ExecResult
- type Execer
- type LifecycleControls
- type Options
- type PhaseResult
- type ProbeOptions
- type RunResult
- type Runtime
- type StopReason
Constants ¶
const DefaultDotfilesTargetPath = "~/dotfiles"
DefaultDotfilesTargetPath is upstream's --dotfiles-target-path default.
const DefaultUserEnvProbe = "loginInteractiveShell"
Variables ¶
This section is empty.
Functions ¶
func DetectContainerEnv ¶
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 ¶
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 ¶
EnvFromList decodes the inspected container environment, as upstream's envListToObj does. PID 1 can add variables that are not container settings.
func LifecycleEnv ¶
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 ¶
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 ¶
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 ¶
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.
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" )